tankgame
Press up moves firing angle upwards, press down moves it down.
Press left reduces power, press right increases power.
Press SPACE or left shift to fire.
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