tankgame

The game screen appears here if your browser supports the Canvas API.

Attribution

Licensed under GNU GENERAL PUBLIC LICENSE Version 3.

Original Python code


import math
import random
import pygame

TANK_COLOR_P1 = (216, 216, 153)
TANK_COLOR_P2 = (219, 163, 82)
SHELL_COLOR = (255,255,255)

class Tank:

    def __init__(self, left_right, tank_color):
        self.left_right = left_right
        self.tank_color = tank_color
        self.position = (0,0)
        # Angle that the gun is pointing (degrees relative to horizontal)
        if (left_right == "left"):
            self.gun_angle = 20
        else :
            self.gun_angle = 50
        # Amount of power to fire with - is divided by 40 to give scale 10 to 100
        self.gun_power = 25

    def set_position (self, position):
        self.position = position

    def get_position (self):
        return self.position

    def set_gun_angle (self, angle):
        self.gun_angle = angle

    def change_gun_angle (self, amount):
        self.gun_angle += amount
        if self.gun_angle > 85:
            self.gun_angle = 85
        if self.gun_angle < 0:
            self.gun_angle = 0

    def get_gun_angle (self):
        return self.gun_angle

    def set_gun_power (self, power):
        self.gun_power = power

    def change_gun_power (self, amount):
        self.gun_power += amount
        if self.gun_power > 100:
            self.gun_power = 100
        if self.gun_power < 10:
            self.gun_power = 10

    def get_gun_power (self):
        return self.gun_power



    # Draws tank (including gun - which depends upon direction and aim)
    # self.left_right can be "left" or "right" to depict which position the tank is in
    # tank_start_pos requires x, y co-ordinates as a tuple
    # angle is relative to horizontal - in degrees
    def draw (self, screen):
        (xpos, ypos) = self.position

        # The shape of the tank track is a polygon
        # (uses list of tuples for the x and y co-ords)
        track_positions = [
            (xpos+5, ypos-5),
            (xpos+10, ypos-10),
            (xpos+50, ypos-10),
            (xpos+55, ypos-5),
            (xpos+50, ypos),
            (xpos+10, ypos)
        ]
        # Polygon for tracks (pygame not pygame zero)
        pygame.draw.polygon(screen.surface, self.tank_color, track_positions)

        # hull uses a rectangle which uses top right co-ords and dimensions
        hull_rect = pygame.Rect((xpos+15,ypos-20),(30,10))
        # Rectangle for tank body "hull" (pygame zero)
        screen.draw.filled_rect(hull_rect, self.tank_color)

        # Despite being an ellipse pygame requires this as a rect
        turret_rect = pygame.Rect((xpos+20,ypos-25),(20,10))
        # Ellipse for turret (pygame not pygame zero)
        pygame.draw.ellipse(screen.surface, self.tank_color, turret_rect)

        # Gun position involves more complex calculations so in a separate function
        gun_positions = self.calc_gun_positions ()
        # Polygon for gun barrel (pygame not pygame zero)
        pygame.draw.polygon(screen.surface, self.tank_color, gun_positions)

    # Calculate the polygon positions for the gun barrel
    def calc_gun_positions (self):
        (xpos, ypos) = self.position
        # Set the start of the gun (top of barrel at point it joins the tank)
        if (self.left_right == "right"):
            gun_start_pos_top = (xpos+20, ypos-20)
        else:
            gun_start_pos_top = (xpos+40, ypos-20)

        # Convert angle to radians (for right subtract from 180 deg first)
        relative_angle = self.gun_angle
        if (self.left_right == "right"):
            relative_angle = 180 - self.gun_angle
        angle_rads = relative_angle * (math.pi / 180)
        # Create vector based on the direction of the barrel
        # Y direction *-1 (due to reverse y of screen)
        gun_vector =  (math.cos(angle_rads), math.sin(angle_rads) * -1)

        # Determine position bottom of barrel
        # Create temporary vector 90deg to existing vector
        if (self.left_right == "right"):
            temp_angle_rads = math.radians(relative_angle - 90)
        else:
            temp_angle_rads = math.radians(relative_angle + 90)
        temp_vector =  (math.cos(temp_angle_rads), math.sin(temp_angle_rads) * -1)

        # Add constants for gun size
        GUN_LENGTH = 20
        GUN_DIAMETER = 3
        gun_start_pos_bottom = (gun_start_pos_top[0] + temp_vector[0] * GUN_DIAMETER, gun_start_pos_top[1] + temp_vector[1] * GUN_DIAMETER)

        # Calculate barrel positions based on vector from start position
        gun_positions = [
            gun_start_pos_bottom,
            gun_start_pos_top,
            (gun_start_pos_top[0] + gun_vector[0] * GUN_LENGTH, gun_start_pos_top[1] + gun_vector[1] * GUN_LENGTH),
            (gun_start_pos_bottom[0] + gun_vector[0] * GUN_LENGTH, gun_start_pos_bottom[1] + gun_vector[1] * GUN_LENGTH),
        ]

        return gun_positions

class Shell:

    def __init__ (self, shell_color):
        self.shell_color = shell_color
        self.start_position = (0,0)
        self.set_position = (0,0)
        self.power = 1
        self.angle = 0
        self.time = 0

    def set_start_position(self, position):
        self.start_position = position

    def set_current_position(self, position):
        self.current_position = position

    def get_current_position(self):
        return self.current_position

    def set_angle(self, angle):
        self.angle = angle

    def set_power(self, power):
        self.power = power

    def set_time(self, time):
        self.time = time

    def draw (self, screen):
        (xpos, ypos) = self.current_position
        # Create rectangle of the shell
        shell_rect = pygame.Rect((math.floor(xpos),math.floor(ypos)),(5,5))
        pygame.draw.ellipse(screen.surface, self.shell_color, shell_rect)

    def update_shell_position (self, left_right):

        init_velocity_y = self.power * math.sin(self.angle)

        # Direction - multiply by -1 for left to right
        if (left_right == 'left'):
            init_velocity_x = self.power * math.cos(self.angle)
        else:
            init_velocity_x = self.power * math.cos(math.pi - self.angle)

        # Gravity constant is 9.8 m/s^2 but this is in terms of screen so instead use a sensible constant
        GRAVITY_CONSTANT = 0.004
        # Constant to give a sensible distance on x axis
        DISTANCE_CONSTANT = 1.5
        # Wind is not included in this version, to implement then decreasing wind value is when the wind is against the fire direction
        # wind > 1 is where wind is against the direction of fire. Wind must never be 0 or negative (which would make it impossible to fire forwards)
        wind_value = 1

        # time is calculated in update cycles
        shell_x = self.start_position[0] + init_velocity_x * self.time * DISTANCE_CONSTANT
        shell_y = self.start_position[1] + -1 * ((init_velocity_y * self.time) - (0.5 * GRAVITY_CONSTANT * self.time * self.time * wind_value))

        self.current_position = (shell_x, shell_y)

        self.time += 1

# Creates the land for the tanks to go on.
# Also positions the tanks - which can be retrieved using get_tank_position method

# How big a chunk to split up x axis
LAND_CHUNK_SIZE = 20
# Max that land can go up or down within chunk size
LAND_MAX_CHG = 20
# Max height of ground
LAND_MIN_Y = 200

class Land:

    def __init__ (self, ground_color, screen_size):
        self.ground_color = ground_color
        self.screen_size = screen_size

        # Setup landscape (these positions represent left side of platform)
        # Choose a random position (temp values - to be stored in tank object)
        # The complete x,y co-ordinates will be saved in a tuple in left_tank_rect and right_tank_rect
        left_tank_x_position = random.randint (10,300)
        right_tank_x_position = random.randint (500,750)

        # Sub divide screen into chunks for the landscape
        # store as list of x positions (0 is first position)
        current_land_x = 0
        current_land_y = random.randint (300,400)
        self.land_positions = [(current_land_x,current_land_y)]
        while (current_land_x < self.screen_size[0]):
            if (current_land_x == left_tank_x_position):
                # handle tank platform
                self.tank1_position = (current_land_x, current_land_y)
                # Add another 50 pixels further along at same y position (level ground for tank to sit on)
                current_land_x += 60
                self.land_positions.append((current_land_x, current_land_y))
                continue
            elif (current_land_x == right_tank_x_position):
                # handle tank platform
                self.tank2_position = (current_land_x, current_land_y)
                # Add another 50 pixels further along at same y position (level ground for tank to sit on)
                current_land_x += 60
                self.land_positions.append((current_land_x, current_land_y))
                continue
            # Checks to see if next position will be where the tanks are
            if (current_land_x < left_tank_x_position and current_land_x + LAND_CHUNK_SIZE >= left_tank_x_position):
                # set x position to tank position
                current_land_x = left_tank_x_position
            elif (current_land_x < right_tank_x_position and current_land_x + LAND_CHUNK_SIZE >= right_tank_x_position):
                # set x position to tank position
                current_land_x = right_tank_x_position
            elif (current_land_x + LAND_CHUNK_SIZE > self.screen_size[0]):
                current_land_x = self.screen_size[0]
            else:
                current_land_x += LAND_CHUNK_SIZE
            # Set the y height
            current_land_y += random.randint(0-LAND_MAX_CHG,LAND_MAX_CHG)
            # check not too high or too lower (note the reverse logic as high y is bottom of screen)
            if (current_land_y > self.screen_size[1]):   # Bottom of screen
                current_land_y = self.screen_size[1]
            if (current_land_y < LAND_MIN_Y):
                current_land_y = LAND_MIN_Y
            # Add to list
            self.land_positions.append((current_land_x, current_land_y))
        # Add end corners
        self.land_positions.append((self.screen_size[0],self.screen_size[1]))
        self.land_positions.append((0,self.screen_size[1]))

    def get_tank1_position(self):
        return self.tank1_position

    def get_tank2_position(self):
        return self.tank2_position



    def draw (self, screen):
        pygame.draw.polygon(screen.surface, self.ground_color, self.land_positions)

WIDTH=800
HEIGHT=600

# States are:
# start - timed delay before start
# player1 - waiting for player to set position
# player1fire - player 1 fired
# player2 - player 2 set position
# player2fire - player 2 fired
# game_over_1 / game_over_2 - show who won 1 = player 1 won etc.
game_state = "player1"

# Colour constants
SKY_COLOR = (165, 182, 209)
SKY_COLOR = (165, 182, 209)
GROUND_COLOR = (9,84,5)
# Different tank colors for player 1 and player 2
# These colors must be unique as well as the GROUND_COLOR
TANK_COLOR_P1 = (216, 216, 153)
TANK_COLOR_P2 = (219, 163, 82)
SHELL_COLOR = (255,255,255)
TEXT_COLOR = (255,255,255)

# Timer used to create delays before action (prevent accidental button press)
game_timer = 0

# Tank 1 = Left
tank1 = Tank("left", TANK_COLOR_P1)
# Tank 2 = Right
tank2 = Tank("right", TANK_COLOR_P2)

# Only fire one shell at a time, a single shell object can be used for both player 1 and player 2
shell = Shell(SHELL_COLOR)

ground = Land(GROUND_COLOR, (WIDTH, HEIGHT))

# Get positions of tanks from ground generator
tank1.set_position(ground.get_tank1_position())
tank2.set_position(ground.get_tank2_position())

def draw():
    global game_state
    screen.fill(SKY_COLOR)
    ground.draw(screen)
    tank1.draw (screen)
    tank2.draw (screen)
    if (game_state == "player1" or game_state == "player1fire"):
        screen.draw.text("Player 1\nPower "+str(tank1.get_gun_power())+"%", fontsize=30, topleft=(50,50), color=(TEXT_COLOR))
    if (game_state == "player2" or game_state == "player2fire"):
        screen.draw.text("Player 2\nPower "+str(tank2.get_gun_power())+"%", fontsize=30, topright=(WIDTH-50,50), color=(TEXT_COLOR))
    if (game_state == "player1fire" or game_state == "player2fire"):
        shell.draw(screen)
    if (game_state == "game_over_1"):
        screen.draw.text("Game Over\nPlayer 1 wins!", fontsize=60, center=(WIDTH/2,200), color=(TEXT_COLOR))
    if (game_state == "game_over_2"):
        screen.draw.text("Game Over\nPlayer 2 wins!", fontsize=60, center=(WIDTH/2,200), color=(TEXT_COLOR))

def update():
    global game_state, game_timer
    # Delayed start (prevent accidental firing by holding start button down)
    if (game_state == 'start'):
        game_timer += 1
        if (game_timer == 30):
            game_timer = 0
            game_state = 'player1'
    # Only read keyboard in certain states
    if (game_state == 'player1'):
        player1_fired = player_keyboard("left")
        if (player1_fired == True):
            # Set shell position to end of gun
            # Use gun_positions so we can get start position
            gun_positions = tank1.calc_gun_positions ()
            shell.set_start_position(gun_positions[3])
            shell.set_current_position(gun_positions[3])
            game_state = 'player1fire'
            shell.set_angle(math.radians (tank1.get_gun_angle()))
            shell.set_power(tank1.get_gun_power() / 40)
            shell.set_time(0)
    if (game_state == 'player1fire'):
        shell.update_shell_position ("left")
        # shell value is whether the shell is inflight, hit or missed
        shell_value = detect_hit("left")
        # shell_value 20 is if other tank hit
        if (shell_value >= 20):
            game_state = 'game_over_1'
        # 10 is offscreen and 11 is hit ground, both indicate missed
        elif (shell_value >= 10):
            game_state = 'player2'
    if (game_state == 'player2'):
        player2_fired = player_keyboard("right")
        if (player2_fired == True):
            # Set shell position to end of gun
            # Use gun_positions so we can get start position
            gun_positions = tank2.calc_gun_positions ()
            shell.set_start_position(gun_positions[3])
            shell.set_current_position(gun_positions[3])
            game_state = 'player2fire'
            shell.set_angle(math.radians (tank2.get_gun_angle()))
            shell.set_power(tank2.get_gun_power() / 40)
            shell.set_time(0)
    if (game_state == 'player2fire'):
        shell.update_shell_position ("right")
        # shell value is whether the shell is inflight, hit or missed
        shell_value = detect_hit("right")
        # shell_value 20 is if other tank hit
        if (shell_value >= 20):
            game_state = 'game_over_2'
        # 10 is offscreen and 11 is hit ground, both indicate missed
        elif (shell_value >= 10):
            game_state = 'player1'
    if (game_state == 'game_over_1' or game_state == 'game_over_2'):
        # Allow space key or left-shift (picade) to continue
        if (keyboard.space or keyboard.lshift):
            game_state = 'start'
            # Reset position of tanks and terrain
            setup()




# Detects if the shell has hit something.
# Simple detection looks at colour of the screen at the position
# uses an offset to not detect the actual shell
# Return 0 for in-flight,
# 1 for offscreen temp (too high),
# 10 for offscreen permanent (too far),
# 11 for hit ground,
# 20 for hit other tank
def detect_hit (left_right):
    (shell_x, shell_y) = shell.get_current_position()
    # Add offset (3 pixels)
    # offset left/right depending upon direction of fire
    if (left_right == "left"):
        shell_x += 3
    else:
        shell_x -= 3
    shell_y += 3
    offset_position = (math.floor(shell_x), math.floor(shell_y))

    # Check whether it's off the screen
    # temporary if just y axis, permanent if x
    if (shell_x > WIDTH or shell_x <= 0 or shell_y >= HEIGHT):
        return 10
    if (shell_y < 1):
        return 1

    # Get colour at position
    color_pixel = screen.surface.get_at(offset_position)
    if (color_pixel == GROUND_COLOR):
        return 11
    if (left_right == 'left' and color_pixel == TANK_COLOR_P2):
        return 20
    if (left_right == 'right' and color_pixel == TANK_COLOR_P1):
        return 20

    return 0

# Handles keyboard for players
# If player has hit fire key (space) then returns True
# Otherwise changes angle of gun if applicable and returns False
def player_keyboard(left_right):
    global shell_start_position

    # get current angle
    if (left_right == 'left'):
        this_gun_angle = tank1.get_gun_angle()
        this_gun_power = tank1.get_gun_power()
    else:
        this_gun_angle = tank2.get_gun_angle()
        this_gun_power = tank2.get_gun_power()

    # Allow space key or left-shift (picade) to fire
    if (keyboard.space or keyboard.lshift):
        return True
    # Up moves firing angle upwards, down moves it down
    if (keyboard.up):
        if (left_right == 'left'):
            tank1.change_gun_angle(1)
        else:
            tank2.change_gun_angle(1)
    if (keyboard.down):
        if (left_right == 'left'):
            tank1.change_gun_angle(-1)
        else:
            tank2.change_gun_angle(-1)
    # left reduces power, right increases power
    if (keyboard.right):
        if (left_right == 'left'):
            tank1.change_gun_power(1)
        else:
            tank2.change_gun_power(1)
    if (keyboard.left):
        if (left_right == 'left'):
            tank1.change_gun_power(-1)
        else:
            tank2.change_gun_power(-1)

    return False