Two player Artillery shooter

Two player Artillery shooter
Photo by John Torcasio / Unsplash

This is a neat little game I wrote to demonstrate one particular way to use the survey-toolbox. I have a comprehensive code walkthrough below, and right at the bottom is the full code you can paste into a python file (.py) and you're good to go.

The concept of this game is pretty simple, two players are created at random points on a 'map' - in this case a predetermined game grid. The players have 10 rounds each with which to zero in and kill the opponent.

They're not flying blind though, I have provided the courtesy of a gun computer that will give an estimate of the bearing and distance to the target - it's not accurate so don't rely on it to save your life! Take notes, use your head - get rounds on target before your enemy finds you!

This is not the first time I've built this game (I've coded a few Python versions, and at least one in Java) but it is the first time I've built it using the survey-toolbox. It was great to use this because it:

  1. Made it so much faster to code and
  2. Highlighted a few bugs in the survey-toolbox for me to get to fixing straight away for a v 0.0.4 release.

As far as games go, this is pretty simple, you could definitely tweak it a bit more if you wanted, some suggestions are:

  • Add damage to the players, the closer you are the more damage you do. Currently it's a straight up kill within 100m
  • Add inaccuracies to the players, a little drift on the projectiles wouldn't kill anybody (ha, great pun!)
  • Really go all out and add a web frontend to it. This game is text based, but there is no reason at all the same calculations couldn't be used in a web based game (by for example; tacking it into a Django setup, or, running it on its own server and exporting coordinates in JSON format via API)
  • Add some bonus targets randomly within the gamespace, if you hit them - you might get better ammunition, more lives etc.

One last thing before we get into it, I'm hoping to show you next week how to implement the survey-toolbox in an ArcGIS environment so make sure you subscribe for that!

pip install survey-toolbox
Remember to install the survey-toolbox
from surveytoolbox.config import EASTING, NORTHING, ELEVATION, BEARING, DIST_2D
from surveytoolbox.bdc import bearing_distance_from_coordinates
from surveytoolbox.cbd import coordinates_from_bearing_distance
from surveytoolbox.PointStore import NewPointStore
from surveytoolbox.SurveyPoint import NewSurveyPoint
from surveytoolbox.dms import to_deg_min_sec

from random import uniform
Import the functions we'll use. A bunch from the survey toolbox, and uniform from random (it's cool)
# Set the bounds of our game.
GAME_MIN_E = 1000
GAME_MAX_E = 10000
GAME_MIN_N = 1000
GAME_MAX_N = 10000
GAME_MIN_EL = 250
GAME_MAX_EL = 300
This is the lower left and upper right corner of our game space. Everything is created in here.
# We'll round to x decimals just for the hell of it.
ROUND_TO_DEC = 3

# Set the kill zone of our Artillery rounds in metres
KILL_ZONE = 100

# Set the players - you can choose to do this based on user input if you prefer.
PLAYER_1 = "player 1"
PLAYER_2 = "player 2"
PLAYERS = [PLAYER_1, PLAYER_2]
We have some pretty deadly artillery there! Also adding two players / player names.
def calc_errors(err_percent, original_value):
    """Calculates and returns a value with an error."""
    calc_error = original_value - original_value * err_percent
    calc_lower = - calc_error / 2
    calc_upper = calc_error / 2
    calc_error = uniform(calc_lower, calc_upper)
    return original_value + calc_error
A function that will degrade the reliability of the gun computer - otherwise the game will be over in one shot every time.
# Set the error of our estimations
error_bg = 0.95
error_dist = 0.95
The initial error is +/- 5% of the distance / bearing. This reduces by 1% (of the error) each round... so the game gets easier as it goes.
# Create a point store so we can store and retrieve our points. (or create a dictionary / array / connect to db instead)
game_players = NewPointStore()

# Create a new point object for each player and drop it in the point store.
for x in PLAYERS:
    game_players.set_new_point(NewSurveyPoint(x))
    # TODO should not be accessing the point store directly.
    # Assign some random coordinates, keeping it within our game boundaries.
    game_players.point_store[x].set_vertex(
        {
            EASTING: round(uniform(GAME_MIN_E, GAME_MAX_E), ROUND_TO_DEC),
            NORTHING: round(uniform(GAME_MIN_N, GAME_MAX_N), ROUND_TO_DEC),
            ELEVATION: round(uniform(GAME_MIN_EL, GAME_MAX_EL), ROUND_TO_DEC)
        }
    )
We create a point store for keeping our players tucked away in, we also assign some coordinates to our players - randomly and within the game bounds - so we can then hunt them down and bust them up!
# Let's make the game run in a loop
game_on = True
player_turn = PLAYER_1
next_player = PLAYER_2
round_number = 1
playable_rounds = 10
Some further game settings before we start the game logic itself.
while game_on and round_number < playable_rounds:
    # Whose turn is it?
    print(f"it is {player_turn}'s turn")

    # get an approximate bearing / distance from player to player
    # TODO build an error in here
    bd_p_to_p = bearing_distance_from_coordinates(
        game_players.point_store[player_turn].get_vertex(),
        game_players.point_store[next_player].get_vertex()
    )

    approx_dist = int(calc_errors(error_dist, bd_p_to_p[DIST_2D]))
    approx_bg = int(calc_errors(error_bg, bd_p_to_p[BEARING]))

    # don't forget to convert decimal degrees to dms before formatting it!
    bearing_dms = round(to_deg_min_sec(bd_p_to_p[BEARING]), 4)
    print(f"you are {approx_dist}m at bearing {approx_bg}°")
The first part of this loop announces whose turn it is, and then calculates the bearing and distance from that player, to the target - before degrading this information (using the earlier function) and displaying it to the player.
# create a temp array to store the input bearing and distance.
    bd_to_tgt = []
    for y in ["bearing", "distance"]:
        bd_to_tgt.append(float(input(f"please enter a {y} to target: ")))

    print("firing!")
    # Determine where the round lands.
    splash = coordinates_from_bearing_distance(
        game_players.point_store[player_turn].get_vertex(),
        bd_to_tgt[0],
        bd_to_tgt[1]
    )

    # Determine distance from splash to target
    did_we_hit = bearing_distance_from_coordinates(splash, game_players.point_store[next_player].get_vertex())
this looks complicated but it isn't. Simply asking the user to enter the bearing and distance to the target, determining where their round landed, and then further determining how far this is away from the target.
# Check for win condition. Exit game or switch to next player.
    if did_we_hit[DIST_2D] < KILL_ZONE:
        print("=============================================")
        print(f"{player_turn} just destroyed {next_player}!")
        game_on = False
        exit("win condition!")
        print("=============================================")
    else:
        print(f"{player_turn}'s round landed within {round(did_we_hit[DIST_2D], 0)} metres of {next_player}...")
        print("=============================================")
If the round landed within the kill zone, then it's game over. Otherwise, just give some general feedback and move on.
if player_turn == PLAYER_1:
            player_turn = PLAYER_2
            next_player = PLAYER_1
        else:
            player_turn = PLAYER_1
            next_player = PLAYER_2

            # End of round
            round_number += 1

            # Reduce the errors just a little
            if error_dist < 1:
                error_dist = error_dist * 1.01

            if error_bg < 1:
                error_bg = error_bg * 1.01
Switch players, end the round, reduce the errors by 1%

And that - my pals, is a simple text based two player Python game written utilising my survey-toolbox. I'm quite chuffed really :D Full code below, feedback always welcome.

from surveytoolbox.config import EASTING, NORTHING, ELEVATION, BEARING, DIST_2D
from surveytoolbox.bdc import bearing_distance_from_coordinates
from surveytoolbox.cbd import coordinates_from_bearing_distance
from surveytoolbox.PointStore import NewPointStore
from surveytoolbox.SurveyPoint import NewSurveyPoint
from surveytoolbox.dms import to_deg_min_sec

from random import uniform

# Set the bounds of our game.
GAME_MIN_E = 1000
GAME_MAX_E = 10000
GAME_MIN_N = 1000
GAME_MAX_N = 10000
GAME_MIN_EL = 250
GAME_MAX_EL = 300

# We'll round to x decimals just for the hell of it.
ROUND_TO_DEC = 3

# Set the kill zone of our Artillery rounds in metres
KILL_ZONE = 100

# Set the players - you can choose to do this based on user input if you prefer.
PLAYER_1 = "player 1"
PLAYER_2 = "player 2"
PLAYERS = [PLAYER_1, PLAYER_2]


def calc_errors(err_percent, original_value):
    """Calculates and returns a value with an error."""
    calc_error = original_value - original_value * err_percent
    calc_lower = - calc_error / 2
    calc_upper = calc_error / 2
    calc_error = uniform(calc_lower, calc_upper)
    return original_value + calc_error


# Set the error of our estimations
error_bg = 0.95
error_dist = 0.95

# Create a point store so we can store and retrieve our points. (or create a dictionary / array / connect to db instead)
game_players = NewPointStore()

# Create a new point object for each player and drop it in the point store.
for x in PLAYERS:
    game_players.set_new_point(NewSurveyPoint(x))
    # TODO should not be accessing the point store directly.
    # Assign some random coordinates, keeping it within our game boundaries.
    game_players.point_store[x].set_vertex(
        {
            EASTING: round(uniform(GAME_MIN_E, GAME_MAX_E), ROUND_TO_DEC),
            NORTHING: round(uniform(GAME_MIN_N, GAME_MAX_N), ROUND_TO_DEC),
            ELEVATION: round(uniform(GAME_MIN_EL, GAME_MAX_EL), ROUND_TO_DEC)
        }
    )

# Let's make the game run in a loop
game_on = True
player_turn = PLAYER_1
next_player = PLAYER_2
round_number = 1
playable_rounds = 10

while game_on and round_number < playable_rounds:
    # Whose turn is it?
    print(f"it is {player_turn}'s turn")

    # get an approximate bearing / distance from player to player
    # TODO build an error in here
    bd_p_to_p = bearing_distance_from_coordinates(
        game_players.point_store[player_turn].get_vertex(),
        game_players.point_store[next_player].get_vertex()
    )

    approx_dist = int(calc_errors(error_dist, bd_p_to_p[DIST_2D]))
    approx_bg = int(calc_errors(error_bg, bd_p_to_p[BEARING]))

    # don't forget to convert decimal degrees to dms before formatting it!
    bearing_dms = round(to_deg_min_sec(bd_p_to_p[BEARING]), 4)
    print(f"you are {approx_dist}m at bearing {approx_bg}°")

    # create a temp array to store the input bearing and distance.
    bd_to_tgt = []
    for y in ["bearing", "distance"]:
        bd_to_tgt.append(float(input(f"please enter a {y} to target: ")))

    print("firing!")
    # Determine where the round lands.
    splash = coordinates_from_bearing_distance(
        game_players.point_store[player_turn].get_vertex(),
        bd_to_tgt[0],
        bd_to_tgt[1]
    )

    # Determine distance from splash to target
    did_we_hit = bearing_distance_from_coordinates(splash, game_players.point_store[next_player].get_vertex())

    # Check for win condition. Exit game or switch to next player.
    if did_we_hit[DIST_2D] < KILL_ZONE:
        print("=============================================")
        print(f"{player_turn} just destroyed {next_player}!")
        game_on = False
        exit("win condition!")
        print("=============================================")
    else:
        print(f"{player_turn}'s round landed within {round(did_we_hit[DIST_2D], 0)} metres of {next_player}...")
        print("=============================================")
        if player_turn == PLAYER_1:
            player_turn = PLAYER_2
            next_player = PLAYER_1
        else:
            player_turn = PLAYER_1
            next_player = PLAYER_2

            # End of round
            round_number += 1

            # Reduce the errors just a little
            if error_dist < 1:
                error_dist = error_dist * 1.01

            if error_bg < 1:
                error_bg = error_bg * 1.01