2 player text artillery game

The objective of this little piece of code was to play with classes some more, whilst doing something ‘fun’. There is indeed much better ways to code the same piece, ie working the bearing could possibly be done easier using modulo % and that might (would) reduce the giant switch statement. Also, there is no error handling, no validation of inputs, it’s just a straight hacky piece, however, the code is reusable so it wouldn’t take much to improve or to add new bits and pieces – ie if you wanted to fire multiple rounds at once, or add additional attributes such as weather it would be quite easy.

What does it do? Enter bearing and range to target, if you get within (x) metres, it’s a kill. If not, the bearing and range from the round location to the target, is sent back to you, using this information you need to compute your next shot (hint: search for artillery bracketing method). Of course, your opponent can use this information to their benefit too – but just pretendsie they’re advantage comes from having an AN TPQ 36

The Code – Walkthrough

Set it up

import random
import math

# Set map extents
BOUNDARY_SW = [100.0, 100.0]
BOUNDARY_NE = [1000.0, 1000.0]
DEATH_RANGE = 10.0

This first bit just sets up the constants and imports a couple of required libraries.

Game and Player

# The game board
class Game:
    def __init__(self):
        self.player_turn = 0
        self.round_e = 0.0
        self.round_n = 0.0
        self.round_el = 0.0
        self.players = []


# Define a player
class Player:
    def __init__(self):
        self.position_e = random.randrange(BOUNDARY_SW[0], BOUNDARY_NE[0], 1)
        self.position_n = random.randrange(BOUNDARY_SW[1], BOUNDARY_NE[1], 1)
        self.position_el = 0.0

The Game class, simply stores attributes required in game. Whose turn is it, where did their round land? And an array players.  When the game starts, two instances of the Player class (holding nothing more than their coordinates for this game) are created and each instance is stored in the array so I can cycle through for updates, select the correct player throughout the game and so on. I could store them in a dictionary and utilise player names as well.

CalculateCoordinates

Set up the class

# Do the math
class CalculateCoordinates:
    def __init__(self):
        self.round_val = 4
        self.working_coords = []
        self.bearing = 0.0
        self.distance_2d = 0.0
        self.distance_3d = 0.0
        self.delta_e = 0.0
        self.delta_n = 0.0
        self.delta_el = 0.0

We just establish some initial class attributes (<-- hopefully my terminology is correct at this point!) that will be used for calculations, and set them all to nothings.

Seek Deltas

def seek_deltas(self):
        # difference between both sets of coordinates.
if len(self.working_coords) > 1:
            self.delta_e = self.working_coords[1][0] - self.working_coords[0][0]
            self.delta_n = self.working_coords[1][1] - self.working_coords[0][1]
            self.delta_el = self.working_coords[1][2] - self.working_coords[0][2]
        else:
            quit("Not enough coords dude.")

This first method simply calculates the differences between provided eastings, northings, and elevations.

Create Deltas

def create_deltas(self):
        # Creates deltas from bearing and distance
self.delta_e = self.distance_2d * (math.sin(math.radians(self.bearing)))
        self.delta_n = self.distance_2d * (math.cos(math.radians(self.bearing)))

When we don’t have two sets of coordinates, we must have a bearing and distance from one set of coordinates, and then we can create the deltas from this to later compute the second set of coordinates.

Create Coordinates

def create_coordinates(self):
        # Create new coords from deltas.
new_coord = [self.working_coords[0][0] + self.delta_e,\
                  self.working_coords[0][1] + self.delta_n,\
                  self.working_coords[0][2]]
        self.working_coords.append(new_coord)

See ^ we simply add the deltas. If one of the deltas is a minus, it will add the negative (ie, it’ll subtract it) so this is a pretty simple method.

Seek Bearing

def seek_bearing(self):
        # A great big switch statement to determine the correct direction.
if self.delta_e == 0 and self.delta_n > 0:
            self.bearing = 0.0
        elif self.delta_e == 0 and self.delta_n < 0:
            self.bearing = 180.0
        elif self.delta_e > 0 and self.delta_n == 0:
            self.bearing = 90.0
        elif self.delta_e < 0 and self.delta_n == 0:
            self.bearing = 270.0

# Check for 45 degree variations.
elif abs(self.delta_e) == abs(self.delta_n):
            if self.delta_e > 0 and self.delta_n > 0:
                self.bearing = 45.0
            elif self.delta_e > 0 > self.delta_n:
                self.bearing = 135.0
            elif self.delta_e < 0 and self.delta_n < 0:
                self.bearing = 225.0
            elif self.delta_n > 0 > self.delta_e:
                self.bearing = 315.0

        # Compute it out.
elif self.delta_e > 0:
            self.bearing = math.degrees(math.atan(self.delta_e / self.delta_n))
        elif self.delta_e < 0 and self.delta_n < 0:
            self.bearing = math.degrees(math.atan(self.delta_e / self.delta_n)) + 180
        elif self.delta_n > 0 > self.delta_e:
            self.bearing = math.degrees(math.atan(self.delta_e / self.delta_n)) + 360

# The final piece.
if self.bearing < 0:
            self.bearing += 180

here’s the giant switch statement :/ Given two sets of coordinates, it is simple math to find the bearing, but firstly you need to see if any math is actually required. If the deltas are the same (▲E: 500, ▲N: 500) then the horizontal bearing has to be in increments of 45°. If there is zero movement on one axis (▲E: 0.00, ▲N: 1000) then the bearing is in increments of 90° – so checking for those easy possibilities first, leaves us with some very simple math at the end to determine our final corrected bearing.

Distance 2D and Distance 3D

def dist_2d(self):
# Determine the distance. (2D)
self.distance_2d = math.sqrt((self.delta_e ** 2 + self.delta_n ** 2))

def dist_3d(self):
# Determine the distance. (3D)
self.distance_3d = math.sqrt(self.delta_e ** 2 + self.delta_n ** 2 + self.delta_el ** 2)

Distances are easy, you remember Pythagoras? Well that’s all that is going on here. Twice for the 3D distance (which isn’t used as I decided not to use elevations in this yet)

List the Coordinates

def list_coords(self):
        # Send back the values.
for i in self.working_coords:
            print("E: {e} N: {n} El: {el} ".format(
                e=round(i[0], self.round_val),
                n=round(i[1], self.round_val),
                el=round(i[2], self.round_val),
            ))

Real simple method to list the coordinates, not used except when I was writing the code and testing it out.

BDC and CBD

def bdc(self):
        # Return bearing and distance derived from two sets of coordinates.
self.seek_deltas()
        self.seek_bearing()
        self.dist_2d()
        self.dist_3d()

    def cbd(self):
        self.create_deltas()
        self.create_coordinates())

The last two methods use the previous methods to calculate either Bearing and Distance from Coordinates, or, Coordinates from Bearing and Distance. Remembering, BDC requires two sets of coordinates, CBD requires one set and a bearing and a distance.

The Game Code

So now we have the stuff we need for the game, let’s put it together. The below code can be optimised, i think there’s a couple of breaches of DRY in there as well as a complete lack of validation / error checking.

new_game = Game()
calcs = CalculateCoordinates()
number_players = 2
play_away = True
while number_players:
    new_game.players.append(Player())
    number_players -= 1

Instantiate an instance of the Game,  and CalculateCoordinates, and create two players – dropping each one in tho the Game players array (right at the start)

while play_away:
    calcs.working_coords = []
    player_now = new_game.player_turn
    if player_now == 0:
        player_next = player_now + 1
    else:
        player_next = player_now - 1

    print("player turn " + str(new_game.player_turn + 1))
    # new_game.player_turn = player_next
player_now_coords = []
    player_next_coords = []

    player_now_coords.append(new_game.players[player_now].position_e)
    player_now_coords.append(new_game.players[player_now].position_n)
    player_now_coords.append(new_game.players[player_now].position_el)
    # print(player_now_coords)
    # Calculate round fall.
fire_bearing = float(input("fire at bearing: "))
    fire_range = float(input("range: "))
    calcs.distance_2d = fire_range
    calcs.bearing = fire_bearing
    calcs.working_coords.append(player_now_coords)

Determine whose turn it is, and whose turn is next (who is firing, who is the target), set the working coordinates, take the bearing and range as inputs, store all of these in the CalculateCoordinates instance.

calcs.cbd()
    round_coords = [calcs.working_coords[1][0], calcs.working_coords[1][1], calcs.working_coords[1][2]]

    # Calculate bearing distance to target
calcs.working_coords = []
    calcs.working_coords.append(round_coords)

    player_next_coords.append(new_game.players[player_next].position_e)
    player_next_coords.append(new_game.players[player_next].position_n)
    player_next_coords.append(new_game.players[player_next].position_el)

    calcs.working_coords.append(player_next_coords)

    calcs.bdc()

Compute CBD (Coordinates from Bearing and Distance) from the previous inputs based on the firer coordinates and their input range and bearing, this will let us know where the round landed, from this, calculate BDC (Bearing and Distance from Coordinates) from the round landing position to the target position.

    print("round landed: ")
    print(str(round(calcs.distance_2d, 3)) + "m from target")
    print("at bearing " + str(round(calcs.bearing, 2)))

    if calcs.distance_2d <= DEATH_RANGE:
        quit("CONGRATS YOU WON!")
    else:
        new_game.player_turn = player_next

Wrap it up with some displayed text to help the user on their next turn, and do a check to see if they actually nailed the target or not.

Complete code below for a copy paste and play or use the github version to be sure of no format issues, let me know in the comments below or via Twitter (@hopBuddyHop) or Facebook if you liked this or have any questions. Plenty of improvements can be made, including play by net!

import random
import math

# Set map extents
BOUNDARY_SW = [100.0, 100.0]
BOUNDARY_NE = [1000.0, 1000.0]
DEATH_RANGE = 10.0


# The game board
class Game:
    def __init__(self):
        self.player_turn = 0
        self.round_e = 0.0
        self.round_n = 0.0
        self.round_el = 0.0
        self.players = []


# Define a player
class Player:
    def __init__(self):
        self.position_e = random.randrange(BOUNDARY_SW[0], BOUNDARY_NE[0], 1)
        self.position_n = random.randrange(BOUNDARY_SW[1], BOUNDARY_NE[1], 1)
        self.position_el = 0.0


# Do the math
class CalculateCoordinates:
    def __init__(self):
        self.round_val = 4
        self.working_coords = []
        self.bearing = 0.0
        self.distance_2d = 0.0
        self.distance_3d = 0.0
        self.delta_e = 0.0
        self.delta_n = 0.0
        self.delta_el = 0.0

    def seek_deltas(self):
        # difference between both sets of coordinates.
if len(self.working_coords) > 1:
            self.delta_e = self.working_coords[1][0] - self.working_coords[0][0]
            self.delta_n = self.working_coords[1][1] - self.working_coords[0][1]
            self.delta_el = self.working_coords[1][2] - self.working_coords[0][2]
        else:
            quit("Not enough coords dude.")

    def create_deltas(self):
        # Creates deltas from bearing and distance
self.delta_e = self.distance_2d * (math.sin(math.radians(self.bearing)))
        self.delta_n = self.distance_2d * (math.cos(math.radians(self.bearing)))

    def create_coordinates(self):
        # Create new coords from deltas.
new_coord = [self.working_coords[0][0] + self.delta_e,\
                  self.working_coords[0][1] + self.delta_n,\
                  self.working_coords[0][2]]
        self.working_coords.append(new_coord)

    def seek_bearing(self):
        # A great big switch statement to determine the correct direction.
if self.delta_e == 0 and self.delta_n > 0:
            self.bearing = 0.0
        elif self.delta_e == 0 and self.delta_n < 0:
            self.bearing = 180.0
        elif self.delta_e > 0 and self.delta_n == 0:
            self.bearing = 90.0
        elif self.delta_e < 0 and self.delta_n == 0:
            self.bearing = 270.0

        # Check for 45 degree variations.
elif abs(self.delta_e) == abs(self.delta_n):
            if self.delta_e > 0 and self.delta_n > 0:
                self.bearing = 45.0
            elif self.delta_e > 0 > self.delta_n:
                self.bearing = 135.0
            elif self.delta_e < 0 and self.delta_n < 0:
                self.bearing = 225.0
            elif self.delta_n > 0 > self.delta_e:
                self.bearing = 315.0

        # Compute it out.
elif self.delta_e > 0:
            self.bearing = math.degrees(math.atan(self.delta_e / self.delta_n))
        elif self.delta_e < 0 and self.delta_n < 0:
            self.bearing = math.degrees(math.atan(self.delta_e / self.delta_n)) + 180
        elif self.delta_n > 0 > self.delta_e:
            self.bearing = math.degrees(math.atan(self.delta_e / self.delta_n)) + 360

        # The final piece.
if self.bearing < 0:
            self.bearing += 180

    def dist_2d(self):
        # Determine the distance. (2D)
self.distance_2d = math.sqrt((self.delta_e ** 2 + self.delta_n ** 2))

    def dist_3d(self):
        # Determine the distance. (3D)
self.distance_3d = math.sqrt(self.delta_e ** 2 + self.delta_n ** 2 + self.delta_el ** 2)

    def list_coords(self):
        # Send back the values.
for i in self.working_coords:
            print("E: {e} N: {n} El: {el} ".format(
                e=round(i[0], self.round_val),
                n=round(i[1], self.round_val),
                el=round(i[2], self.round_val),
            ))

    def bdc(self):
        # Return bearing and distance derived from two sets of coordinates.
self.seek_deltas()
        self.seek_bearing()
        self.dist_2d()
        self.dist_3d()

    def cbd(self):
        self.create_deltas()
        self.create_coordinates()


new_game = Game()
calcs = CalculateCoordinates()
number_players = 2
play_away = True
while number_players:
    new_game.players.append(Player())
    number_players -= 1

while play_away:
    calcs.working_coords = []
    player_now = new_game.player_turn
    if player_now == 0:
        player_next = player_now + 1
    else:
        player_next = player_now - 1

    print("player turn " + str(new_game.player_turn + 1))
    # new_game.player_turn = player_next
player_now_coords = []
    player_next_coords = []

    player_now_coords.append(new_game.players[player_now].position_e)
    player_now_coords.append(new_game.players[player_now].position_n)
    player_now_coords.append(new_game.players[player_now].position_el)
    # print(player_now_coords)
    # Calculate round fall.
fire_bearing = float(input("fire at bearing: "))
    fire_range = float(input("range: "))
    calcs.distance_2d = fire_range
    calcs.bearing = fire_bearing

    calcs.working_coords.append(player_now_coords)
    calcs.cbd()
    round_coords = [calcs.working_coords[1][0], calcs.working_coords[1][1], calcs.working_coords[1][2]]

    # Calculate bearing distance to target
calcs.working_coords = []
    calcs.working_coords.append(round_coords)

    player_next_coords.append(new_game.players[player_next].position_e)
    player_next_coords.append(new_game.players[player_next].position_n)
    player_next_coords.append(new_game.players[player_next].position_el)

    calcs.working_coords.append(player_next_coords)

    calcs.bdc()

    print("round landed: ")
    print(str(round(calcs.distance_2d, 3)) + "m from target")
    print("at bearing " + str(round(calcs.bearing, 2)))

    if calcs.distance_2d <= DEATH_RANGE:
        quit("CONGRATS YOU WON!")
    else:
        new_game.player_turn = player_next

Author: JR

Leave a Reply