Fire tank

chengxi Li & siyu liu

Introduction

 Fire Tanks is a real-time strategy game in which each player controls its own tank to fight with others, try to survive the bullets of others and be the winner. Players take turns to act, in a single turn, only one player can take actions and others cannot do anything. In detail, a player can, in its own turn, move the tank, adjust the angle, and launch a bullet. A fixed number of fuel will be added automatically at the end of each turn. Moving the tank and launch the bullet will certainly cost a little fuel, while adjust the angle does not have any costs. Note that a player can only launch one bullet in its turn. A turn is finished if either of the following happens. (1) the player launches a bullet or (2) the player cannot do anything due to empty fuel or (3) the player volunteers to skip. Typically, the key to winning the game is to use bullet to hit other tanks to reduce their HP. A far off hit will not deduct the HP of any tanks, only if the bullet hits within a fixed range(should be small) of tank, will the HP be deducted. But it’s not that easy, an accurate hit requires a perfect combination of angle and power, players should explore the combinations on their own. The game will be based on Raspberry Pi, Python is the programming language we will use, and Pygame is the library which will be largely involved.

Objective

 Software level: implement the minimum function of Fire tank stated above, two tanks can shoot and compete with each other, and add more features if time permits. Hardware level: using buttons, resistance and capacitor to form a control board, and connect the control board to Raspberry Pi.

Design

1.Software

 

1) Main framework

In the main framework, we first needed to perform initialization. We needed to initialize the piTFT as the output and the pygame library. As we needed to use the pygame.mixer( ) method for playing the music, we needed to initialize it. Then we needed to define all the GPIOs and their callbacks as well as their event-detect functions. For the implementation of the game, we first realized a main menu. There would be a background picture with the name of the game as well as four options shown on it.These four options appear to be START, MODE, HELP and EXIT. Each one will be linked to a new page. In the main framework, we needed to set a global variable to indicate which option we chose. The main framework program should be divided into two parts, the initialization parts and the loop parts. In initialization part, all global variables should be assigned and all classes should be implemented. Plus, the game should be able to load corresponding materials and have everything done. The loop part’s job is to maintain the status of each class as well as that of the menu. Typically three things should be done within a single loop: get the changes, update the status, and display the new status.

 

1.1) GPIO initialization

We needed six external buttons as well as GPIO17 on the PiTFT. As a result, we needed to initialize and define all of these 7 GPIOs. We chose GPIO 12 and GPIO 5 as up and down buttons. GPIO 16 and GPIO 13 are used as left and right buttons. GPIO 19 and GPIO 26 are used as the shoot and skip buttons which we will cover in more details later. We set them to be pull-up mode and would detect both the rising edge as well as the falling edge. In the GPIO.add_event_detect( ) method, we chose the flag to be GPIO.BOTH to detect both edges. To distinguish these two edges, we added an if-else statement in each callback’s routine. As we set the GPIOs to be pull-up input, if the GPIO.input( ) of each GPIO is 0, which indicates the input is low-level, then a falling edge appears and the button is pressed. Otherwise the button is released and the GPIO input is high-level. In the callback routine of each GPIO, we used pygame.event.post( ) method to post the events of keyboard up and down to the external button up and down. In this way, we can easily debug using keyboard buttons rather than external buttons.

 

1.2) Global Variable Initialization

To realize the functionality of the game, we needed to initialize a set of global variables. The use of global variables is highly related to each module and the initialization is usually accomplished in the process of development. We will cover the initialization and definition of each global variable in later discussion. For piTFT display, we initialized global variables WIDTH and HEIGHT as the piTFT screen size, BLACK and WHITE as font or line colors, FONT, FONT_SMALL and TITLE by using pygame.font.Font( ) as different font size and style.

 

1.3) Class Instance Initialization

In the unit module, we would define class tank, bullet and player to control each unit and realize the overall function. As a result, we needed to initialize each class’s instance in the main framework. We needed to initialize two tanks, two players and two bullets. We also needed to put the two players in a list named players to reference later. Also, we needed to initialize a group object. The group object is used to update the sprite in this group which would be used to update the players.

 

1.4) Main Loop

As mentioned above, the main loop should maintain the status of each class and each menu option. Each class was packaged into module in each menu option. We would discuss each menu option first. For the menu option, we needed to declare 3 global variables. MENU_LIST is used to show the four options and their locations on the screen. Each option and its location were stored as a tuple in the list. MODE was initialized to be -1 which indicated the menu itself. MODE had value of 0 to 3 indicating starting the game, choosing modes, checking game rules and exiting game respectively. Also, we had a variable of CURRENT which was used to show the current selection and appeared to be an arrow pointing to the selection on the screen. CURRENT had a value ranging from 0 to 3 as there were four options in menu. In the main loop, we used the pooling routine to detect the variable changes. To be specific, the value of MODE was consistently checked and would enter the corresponding module. If MODE was equal to -1, menu would show up. In the interrupt routine of menu module, when up or down button was detected to be pressed, the value of CURRENT would be changed accordingly to point to the correct option. Then the title of the game and the menu options would be displayed. The arrow pointing to the current selection would be displayed as well according to CURRENT’s value and would be placed in front of each menu option. CURRENT can also be used to indicate the MODE. If Space on the keyboard was detected to be pressed, MODE would be assigned to the value of current and would enter the corresponding mode in the next pooling. If Return button on the keyboard was detected, nothing would be done. The main menu looks like the following on the piTFT:

If MODE was equal to 1, the program would enter the Mode option to choose different mode. Mode was a new page and all selective modes were shown horizontally. In that case, we needed another global variable named CURRENT_ to indicate which option we were current choosing. The logic of this MODE was quite similar to that of the menu MODE. The changes were that we needed to change the value of CURRENT_ in each interrupt routine and in the interrupt routine of Space button we needed to set the PLAY_MODE global variable to CURRENT_ to indicate different play modes then set the MODE to be -1 to return to the main menu. Also, when the Return button was hit, MODE would be set to -1 to return to the main menu. In this MODE, we also needed to show the name of the game and the play modes’ names and the selection arrow. The Mode selection page looks like the following on the piTFT.

If the MODE was equal to 2, the program would enter the Help MODE which would display an explanation of game rules. The interrupt routine in this mode would just be connected to the Return button. When the return button was hit, MODE would be set to -1 and program would return to the main menu. On the screen, a new page would be displayed. The arrows used the same technique as before by calling a helper function named show_arrow and the explanations were shown by calling the helper function show_font. The other two boxes were shown by pygame functions FONT.render( ) and text_surface.get_rect( ). The layout of Help is shown below.

If the MODE was equal to 3, the sys.exit( ) method would be called and would exit the game and went back to the command line. If the MODE was equal to 0, the game would be entered and we would cover the modules in game later.

 

1.5) Helper Functions

Besides several global variables, we created some helper functions.

1.5.1) show_arrow(angle, position, color, factor)

This function was used to draw an arrow on the display. The angle in its signature was used to show which direction it pointed to as we have vertical main menu and horizontal mode menu. It called pygame.draw.polygon( ) to realize drawing arrows.

1.5.2) reset()

This function was used to reset the game when one player lost or wanted to quit. In this function, we reset every global value to its initial status and MODE to -1 to return to the main menu.

1.5.3) show_font(font, s, position, color)

This function was used to show any texts on the piTFT. It called font.render( ) and surface.get_rect( ) and then blit on the workspace. This function was basically the same with what we used in previous labs.

1.5.4) bgm()

This function was used to play the background music. It first read the music file in the format of “.ogg” and then loaded it to playback by method pygame.mixer.Sound( ). We set the output channel for background music as Channel 0 and used pygame.mixer.play( music filename ) to play the music source file. The flag “loop” was used to decide how many times the music would be played. “loop = -1” means it would loop infinitely. Then we needed to use the method pygame.mixer.get_busy( ) as the loop condition to hold the program and let the music play.

 

1.6) Multi-threading programming

Before the main loop started, we needed to do several things first to configure the game settings. First, we needed to load a background image. If we loaded the image in the main loop, it could take up too many cpu usage and made the program sluggish. Then, we needed to create new threads and ran background music and sound effect. For background music, we can just create new thread by using threading.Thread( ) method and assign bgm( ) to its parameter. Then we called threading.start( ) to start this thread. But we needed to define a class Sound to play the sound effect as it needed an event to trigger. We would cover that part later.

 

2) Game module The game module needed to deal with the following things: (1) changed the layout of the display according to the mode we selected before. (2) realized the control over tanks (3) simulated the real-world bullet tracks when players shot against each other. (4) realized the game effect of deducting HP or Fuel once hit or moved or shot. (5) realized the control-right switches between players (6) showed the congratulation words when one player wins and gave hint on the next step. (7) dealt with any corner cases appeared during the game. We’d like to show the game layout first. The basic mode looks like the following.

2.1) Layout

The layout will be different from each other when in different modes. Basically, every mode has three colored bars, red representing HP, blue representing fuel, and grey/yellow representing wind. In normal mode, there will only be pictures of tank displayed. In wind mode, and barrier mode, wind and barrier(those black rectangle) gets involved respectively. Also, in hard mode, all elements are combined together, challenging players to win. After a player wins the game, a line of congratulations will be displayed on the screen, another line guide you back to main menu will also be seen.

2.2) Control system

To connect the action of players to the actual move of tank is important. We first convert the actions of players to events in pygame, like KEYUP and KEYDOWN. Then, according to the events in the event queue, move the tank to the specific direction.

2.3) Bullet track

Bullet track is a challenging part compared to others. The essence to the implementation is the use of rect.move() which gives the user an easy way to move a Rect object. All the moves and changes should obey the physics laws: the change of location is proportional to speed, and the change of speed is proportional to acceleration, while acceleration is constant. In each time interval, we modified the speed by acceleration, and move the object by the speed.

2.4)HP and fuel deduction

HP deduction is triggered by bullet hits, and bullet hits is detected by the location of the bullet, all of which are done in the main loop.Fuel deduction is triggered whenever the tank moves or shoots. To detect a move or shoot of tank, it is equivalent to detect a player action.

2.5) Control-right

The alternation of control-right is a little bit complex. Whenever those cases happen, the turn should be alternated. (1) A player shoots its bullet, (2) a player’s fuel goes 0, (3) a player chooses to skip. We detect all those occurrences in the main loop.

2.6) Congratulations words

Once a player’s HP reaches 0, the game should be over. At this time, two lines of word come into appear, one is the congratulations words, another is the guide back to main menu. In addition, the latter one is made blink to notice the player, those displays are implemented by using font.draw, and the blink is implemented by time.sleep()

2.7) Corner cases

Corner cases is an annoying part, behaviours of players are unpredictable, thus it requires high quality in fault tolerance in our code. For example, we never expect players to move after they shoot a bullet, but the fact is that, in the prototype, they can move and even catch their own bullet after they shoot. To avoid those corner cases, we added many cases detection to the program and narrow the space where players can take actions.

 

3) Unit Module for each class

We developed several classes in the unit module to call them in the game module.

3.1) class Tank

In class Tank, we defined the following methods for tank: __init__ ( ) to create tank object, move_up( ) and move_down( ) to control the shooting angle, move_left( ) and move_right( ) to move the tank around, update( ) to update and draw the tank.

3.1.1) __init__(self, screen, x, y, speed)

This method took in the parameter screen which was used to draw the tank on, (x,y) to represent the coordinates of tank, speed to decide the moving speed of tank. Those were important attributes to draw the tank. In this method, we also needed to define the angle_speed and angle_limit attributes of the tank’s barrel. We defined tank_points list to represent the tank’s shape.

3.1.2) update(self)

In this method, we drew the tank according to its attributes. We mainly used the pygame.draw.line( ) to draw the tank’s barrel and used pygame.draw.lines( ) to draw the tank’s body. We also used pygame.draw.circle( ) to draw the red point to represent the connection point of barrel and body.

3.1.3) move_up(self) and move_down(self)

These two methods were used to move the tank’s barrel when up or down button was pressed. Their implementation was basically the same just had one difference of the moving direction. In move_up(self) method, tank’s attribute of angle should plus angle_speed while minus angle speed in move_down(self) method. angle_limit were used to restrict the moving range.

3.1.4) move_left(self) and move_right(self)

These two methods were used to move the tank’s body when left or right button was pressed. The position attribute of tank should increase or decrease according to the speed attribute. Attribute of direction should also be changed and attribute points should be changed according to the direction attribute and position attribute.

3.2) class Bullet

In class Bullet, we defined the following methods for bullet: __init__ ( ) to create bullet object, update( ) to update and draw the bullet, set( ) to set the bullet to visible and update its status.

3.2.1) __init__(self, screen)

In the initialization method, we created a bullet object and set its attributes. We created the bullet by loading a photo of bullet. We needed to scale it and get its rect for further collision determination. We initialized the bullet’s position to be (0,0) and used an attribute visible to indicate whether it’s visible. At first it’s invisible. We set the attribute of speed of direction x and y to be 0 and acceleration of direction x to be 2 and that of y to be 0.

3.2.2) update(self)

In this method, we needed to draw the bullet according to its speed, acceleration and position attribute and call the rect.move( ) method to move it. Also, we needed to check if the bullet hit something. If the bullet hit something, this method should return its position.

3.2.3) set(self, x, y, speed_x, speed_y)

This method was called when the player shot a bullet. We needed to update its status and set the attribute of visible to be True.

 

3.3) class Player

In class Player, we defined the following methods for player: __init__ ( ) to create player object, move_up( ) and move_down( ) to control the tank to adjust the shooting angle, move_left( ) and move_right( ) to move the tank around, update( ) to draw the tank and the force bar, turn( ) to draw a purple circle to indicate whose turn it was, shoot( ) to draw the bullet and deduct the fuel, hold( ) to be called when button was pressed and release( ) to be called when button was released.

3.3.1) __init__(self, screen, tank, bullet)

Player instance had its attributes of HP, Fuel, tank and bullet. Also, it had an attribute of bar_width to determine the width of the HP and Fuel bar. The initial HP was set to be 100 and initial Fuel was set to 50. Player also had an attribute of t to record the press time.

3.3.2) update(self)

In this method, we needed to draw the HP and Fuel bars according to this player’s attribute. Also, we needed to call tank.update( ) to draw this player’s tank.

3.3.3) turn(self)

In this method, we needed to draw a circle to indicate whose turn it’s right now.

3.3.4) shoot(self, speed)

In this method, we needed to call the bullet.set( ) method to draw the bullet. But first, we needed to determine if the Fuel was greater than 10. If not, the tank cannot shoot. Also, we needed to deduct the Fuel by 10 after a bullet was shot.

3.3.5) move_up(self) and move_down(self)

These two methods just called tank’s move_up( ) and move_down( ) method to manipulate the tank’s barrel.

3.3.6) move_left(self) and move_right(self)

These two methods just called tank’s move_left( ) and move_right( ) method to manipulate the tank’s body to move right or left. Also, before the tank can move, we needed to check if the Fuel was greater than 0. If not, the tank cannot move. After every move, Fuel should be deducted by 3 but its minimum value was 0.

3.3.7) hold(self)

This method was called when the button was pressed and it called time.time( ) to record the current time and saved it to attribute t.

3.3.8) release(self)

This method was called when the button was released and returned the time for how long the button was pressed.

 

3.4) class Sound

In class Sound, we defined the following methods for sound: __init__ ( ) to create new thread for the sound effect and load the music file and add lock, play( ) to release the lock, exit( ) to kill the thread and release the lock, run( ) to play the music and acquire the lock.

3.4.1) __init__(self, filename)

In this method, we called threading.Thread.__init__( ) to create a new thread. We initialized the attribute of filename to the input filename, the attribute of _end to be True to indicate that the music shouldn’t end, the attribute of lock to be threading.lock( ) to create a lock.

3.4.2) play(self)

In this method, we just released the lock.

3.4.3) exit(self)

In this method, we set the attribute _end to be False and released the lock.

3.4.4) run(self)

In this method, we played the music file if attribute _end was True and just played it for once as the music was just sound effect. Also, we needed to acquire the lock to indicate that there was a file being played.

 

 

Hardware

The hardware plays a role to send the actions of players to the computer. For the game Fire Tank, a player will have the access to adjust the power and angle of the bullet, the movement of the tank, and the start, pause, exit of the game. To implement those functions, we will have a few buttons, and each button will have a corresponding function. For example, button A is to upward angle, button B is to downward the angle, button C is to move tank left, button D is to move it right, etc. Apparently, in this case all the operations will be done by buttons, and the cursor is set invisible. Hardware design We used OMRON B3F tactile switch as button whose inner design is shown below. The important thing is that this fancy button has 4 pins and they are connected as shown below. Compared to raw button, this one is fancier but is a little bit complex to connect.

To have it work properly, we design a circuit, according to lab2, using the above 4-pins switch instead of the 2-pins switch. The following graph shows our circuit design. We chose pull up resistor as we did in Lab2, thus when the button is released, the GPIO should have an input of logic one, and logic zero when pressed.

Two resistors are used to protect GPIO from exceeding its current limit, while the capacitor is used for debouncing. Then we make six buttons like above to meet our requirements, and are connected to 12, 5, 16, 13, 19, 26. The GPIO input/output image is shown below, remember to choose those pins without multiplex.

The final connection should look like the graph below. The two yellow buttons are used to vertically move cursor in menu, and adjust shooting angle in play mode. Two blue buttons are used to horizontally move cursor in menu, and horizontally move the player’s tank. The red button, in menu, is used to select option, and shoot the bullet in play mode. Lastly, the green button is for exit and skip for menu and play mode respectively.

In addition to software design and hardware design, we also make a case for RPi and the breadboard. The case is made of paper, and is designed to perfectly cover all the stuff. The eventual product looks like this:

Testing

We have several testing phases and ran into many issues when developing.

1.Test the game on PC without RPi Due to the reason that we firstly developed our game on PC, so the first testing phase is on our own PC, and all things worked well on this stage.

 

2.Test the game using keyboard and monitor Then we transfer the game from our PC to the RPi, some little things came out there. The resolution of the game on our PC is 1024*768, and the screen window is quite large. But when transferred to PiTFT, the whole window is restricted to 320*240, which is smaller than the previous one. As a result, many elements became disproportionate. For example, the tank is supposed to be a small sprites in the screen but it turned out to be very huge for we didn’t set a proportion factor to it, so it remained its original size when run in PC. Another example is that some elements became way tiny than we expected, we set the proportion size but it’s not correct.

 

3.Test the game using keyboard and PiTFT A bad point about PiTFT is that it won’t give any information about where goes wrong unless syntax error. We really expected that there is no difference between using PiTFT and monitor to display until we ran our game using PiTFT. All things went well using monitor, but when using PiTFT, there was something wrong. Somethings more terrible is we didn't know where the bug located. To figure out where the bug was, we tried to use print() to print the information. But again PiTFT won’t give out the standard output. At this point we were really desperate for the deadline is two days later. Then we came out a beautiful idea to keep track of where bug happens. Instead of printing something, we used write() to write something into a file, which cannot be eliminated by PiTFT. It really worked for us, we tried a bunch of places, and finally got that an API went wrong here: pygame.draw.aalines() works perfectly on monitor but cannot work in PiTFT, we suspect it is due to the version of pygame used. To address the problem, we used pygame.draw.lines() instead, and it is fine now. This problem takes us near three hours to solve. Analysis: we are still wondering why the PiTFT won’t give any traceback of bug and any information about bugs. One possible explanation is that when we redirect the display from monitor to PiTFT, we do not redirect the file 1 and 2 to right place, namely we do not change the place to display standard output and standard error. Thus the PiTFT cannot give the information we want to see.

 

4.Test the game using PiTFT and buttons After we succeed running our game on PiTFT with keyboard, it is time to get buttons involved. To create a connection between buttons and keys, every time a button is pressed or released, an event KEYUP or KEYDOWN will be post using event mechanism is pygame. The pygame event is good, but the hardware has something wrong: when we press button1, and then release it, we are supposed to have two events posted, a KEYDOWN and a KEYUP. But the fact is that when we press button1, several events posted, which means that button1 has some interference with other buttons. We turned to Joe for help, and we checked together for a long time. It turned out that the use of GPIO4 led to the interference. As is shown in documentation, GPIO4 is multiplexed with GCLK, which may interfere with other keys. We then got rid of GPIO4, and all things went well again. This problem takes us about 3 hours.

 

 

 

Results 

By the end of the fall semester, we successfully designed a multi-player game Fire Tank with multiple modes. The control board works well with six buttons, and the PiTFT can display our game quite fluently.  

 


 

Work Distribution

Chengxi Li (right): game & module design, software development, hardware development

Siyu Liu(left): software development, hardware testing  

Budget

 Total: $116

 

 Components  Number  Cost($)
 Raspberry Pi Model 3B+  1  50
 PiTFT  1  30
 Omron B3F Buttons  6  6
 Bread Board  1  5
 Speaker   1  10
 Power bank  1  15

Code Appendix

 
*

'''

Author: Chengxi Li, Siyu Liu

cl2535 sl3282

'''

# Screen Size 320 240

#(key,corresponding number) up273, down274,left276,right275,space32

import pygame

import sys

import time

import random

import unit

import os

import threading

from RPi import GPIO

from unit import *

os.putenv('SDL_VIDEODRIVER','fbcon')

os.putenv('SDL_FBDEV','/dev/fb1')

os.putenv('SDL_MOUSEDRV','TSLIB')

os.putenv('SDL_MOUSEDEV','/dev/input/touchscreen')

pygame.init()

pygame.mixer.init()

pygame.mouse.set_visible(False)

GPIO.setmode(GPIO.BCM)

GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP)

GPIO.setup(12, GPIO.IN, pull_up_down=GPIO.PUD_UP) # up

GPIO.setup(5, GPIO.IN, pull_up_down=GPIO.PUD_UP) # down

GPIO.setup(16, GPIO.IN, pull_up_down=GPIO.PUD_UP) # left

GPIO.setup(13, GPIO.IN, pull_up_down=GPIO.PUD_UP) # right

GPIO.setup(19, GPIO.IN, pull_up_down=GPIO.PUD_UP) # shoot

GPIO.setup(26, GPIO.IN, pull_up_down=GPIO.PUD_UP) # skip

# set GPIO callbacks

def GPIO17_callback(channel):

    reset()

    

def GPIO12_callback(channel):

    if GPIO.input(12) == 0:

        pygame.event.post(pygame.event.Event(pygame.KEYDOWN, {'key':273}))

        print("down1")

    else:

        pygame.event.post(pygame.event.Event(pygame.KEYUP, {'key':273}))

        print("up1")

def GPIO5_callback(channel):

    if GPIO.input(5) == 0:

        pygame.event.post(pygame.event.Event(pygame.KEYDOWN, {'key':274}))

        print("down2")

    else:

        pygame.event.post(pygame.event.Event(pygame.KEYUP, {'key':274}))

        print("up2")

def GPIO16_callback(channel):

    if GPIO.input(16) == 0:

        pygame.event.post(pygame.event.Event(pygame.KEYDOWN, {'key':276}))

        print("down3")

    else:

        pygame.event.post(pygame.event.Event(pygame.KEYUP, {'key':276}))

        print("up3")

def GPIO13_callback(channel):

    if GPIO.input(13) == 0:

        pygame.event.post(pygame.event.Event(pygame.KEYDOWN, {'key':275}))

        print("down4")

    else:

        pygame.event.post(pygame.event.Event(pygame.KEYUP, {'key':275}))

        print("up4")

def GPIO19_callback(channel):

    if GPIO.input(19) == 0:

        pygame.event.post(pygame.event.Event(pygame.KEYDOWN, {'key':32}))

        print("down5")

    else:

        pygame.event.post(pygame.event.Event(pygame.KEYUP, {'key':32}))

        print("up5")

def GPIO26_callback(channel):

    if GPIO.input(26) == 0:

        pygame.event.post(pygame.event.Event(pygame.KEYDOWN, {'key':13}))

        print("down6")

    else:

        pygame.event.post(pygame.event.Event(pygame.KEYUP, {'key':13}))

        print("up6")

    

GPIO.add_event_detect(17, GPIO.FALLING, callback=GPIO17_callback, bouncetime=100)

GPIO.add_event_detect(12, GPIO.BOTH, callback=GPIO12_callback, bouncetime=50)

GPIO.add_event_detect(5, GPIO.BOTH, callback=GPIO5_callback, bouncetime=50)

GPIO.add_event_detect(16, GPIO.BOTH, callback=GPIO16_callback, bouncetime=50)

GPIO.add_event_detect(13, GPIO.BOTH, callback=GPIO13_callback, bouncetime=50)

GPIO.add_event_detect(19, GPIO.BOTH, callback=GPIO19_callback, bouncetime=50)

GPIO.add_event_detect(26, GPIO.BOTH, callback=GPIO26_callback, bouncetime=50)


SCREEN = pygame.display.set_mode((WIDTH, HEIGHT))

BLACK = (0, 0, 0)

WHITE = (255, 255, 255)

MENU_LIST = [("Start", (WIDTH//2, HEIGHT//2)), ("Mode", (WIDTH//2, HEIGHT//8*5)), ("Help",(WIDTH//2, HEIGHT//8*6)),("Exit",(WIDTH//2, HEIGHT//8*7))]

MODE_LIST = [("Norm", (WIDTH//5, HEIGHT//8*5)),("Wind", (WIDTH//5*2, HEIGHT//8*5)),("Barrier", (WIDTH//5*3, HEIGHT//8*5)),("Hard",(WIDTH //5*4, HEIGHT//8*5))]

FONT = pygame.font.Font('freesansbold.ttf', WIDTH//16)

FONT_SMALL = pygame.font.Font('freesansbold.ttf', WIDTH//32)

TITLE = pygame.font.Font('freesansbold.ttf', WIDTH//8)

# Flag used for detect if key is pressed

LEFT_PRESSED = False

RIGHT_PRESSED = False

UP_PRESSED = False

DOWN_PRESSED = False

SPACE_PRESSED = False

ENTER_PRESSED = False

# Flag used for player switch

MUSIC = True

WIND = 0

MODE = -1

PLAY_MODE = 0

TURN = 0

CURRENT = CURRENT_ = 0

INIT = True

CLOCK = pygame.time.Clock()


# Initialize tank, bullet and player

tank1 = Tank(SCREEN, WIDTH // 6, HEIGHT * 4 // 5, 10)

tank2 = Tank(SCREEN, WIDTH * 5 // 6, HEIGHT * 4 // 5, 10)

bullet1 = Bullet(SCREEN)

bullet2 = Bullet(SCREEN)

player1 = Player(SCREEN, tank1, bullet1)

player2 = Player(SCREEN, tank2, bullet2)

players = [player1, player2]

# Group object is used to update the sprite in this group

# In this case: group.update() call player.update()

group = pygame.sprite.Group(player1, player2)



player = player1

bullet = bullet1


def show_arrow(angle, position, color, factor):

    points = [(0, 10), (0, 20), (20, 20), (20, 30), (30, 15), (20, 0), (20, 10)]

    points = [((point[0]*factor), (point[1])*factor) for point in points]

    surf = pygame.Surface((30*factor, 30*factor))

    pygame.draw.polygon(surf, color, points)

    surf = pygame.transform.rotate(surf, angle)

    surf.set_colorkey(BLACK)

    rect = surf.get_rect(center=position)

    SCREEN.blit(surf, rect)


def reset():

    global MODE, tank1, tank2, bullet1, bullet2, player1, PLAY_MODE

    global player2, players, group, bullet, player, TURN, WIND

    MODE = -1

    PLAY_MODE = 0

    tank1 = Tank(SCREEN, WIDTH // 6, HEIGHT * 4 // 5, 10)

    tank2 = Tank(SCREEN, WIDTH * 5 // 6, HEIGHT * 4 // 5, 10)

    bullet1 = Bullet(SCREEN)

    bullet2 = Bullet(SCREEN)

    player1 = Player(SCREEN, tank1, bullet1)

    player2 = Player(SCREEN, tank2, bullet2)

    players = [player1, player2]

    group = pygame.sprite.Group(player1, player2)

    player = player1

    bullet = bullet1

    TURN = 0

    WIND = 0

def show_font(font, s, position, color):

    text_surface = font.render(s, True, color)

    rect= text_surface.get_rect(center=position)

    SCREEN.blit(text_surface, rect)

    

def bgm():

    file = "So_Be_It.ogg"

    s = pygame.mixer.Sound(file)

    pygame.mixer.Channel(0).play(s,loops=-1)

    while pygame.mixer.Channel(0).get_busy() and MUSIC:

        pygame.time.Clock().tick(10)

    

#def write(s):

#    with open("test.txt", 'w') as f:

#        f.write(s)

t = 0

img = pygame.image.load("bg.jpg")

surf = pygame.transform.scale(img,(WIDTH,HEIGHT))

th1 = threading.Thread(target=bgm)

th1.start()

MUSIC = False

sound = Sound("shoot.ogg")

sound.start()

while True:

    CLOCK.tick(60)

    time.sleep(0.04)

    if MODE == -1: # Menu screen

        SCREEN.fill(WHITE)

        for event in pygame.event.get():

            if event.type == pygame.QUIT:

                MUSIC = False

                sound.exit()

                sound.join()

                th1.join()

                sys.exit()

            if event.type == pygame.KEYUP: #(key,corresponding number) up273, down274,left276,right275,space32

                if event.dict['key'] == 273: # up

                    CURRENT = (CURRENT-1)%len(MENU_LIST)

                elif event.dict['key'] == 274: # down

                    CURRENT = (CURRENT+1)%len(MENU_LIST)

                elif event.dict['key'] == 32: # space

                    MODE = CURRENT

                elif event.dict['key'] == 13: # enter

                    pass

        

        SCREEN.blit(surf,surf.get_rect())

        show_font(TITLE, "FirE tAnk", (WIDTH//2, HEIGHT//4), WHITE)

        for s in MENU_LIST:

            show_font(FONT, s[0], s[1], WHITE)

        show_arrow(0, (MENU_LIST[CURRENT][1][0]-WIDTH//8, MENU_LIST[CURRENT][1][1]), WHITE, 1)

    elif MODE == 1: # Choose Mode

        

        SCREEN.fill(WHITE)

        for event in pygame.event.get():

            if event.type == pygame.QUIT: 

                MUSIC =False

                sound.exit()

                sound.join()

                th1.join()

                sys.exit()

            if event.type == pygame.KEYUP: #(key,corresponding number) up273, down274,left276,right275,space32

                if event.dict['key'] == 275: # right

                    CURRENT_ = (CURRENT_+1)%len(MODE_LIST)

                elif event.dict['key'] == 276: # left

                    CURRENT_ = (CURRENT_-1)%len(MODE_LIST)

                elif event.dict['key'] == 32: # space

                    PLAY_MODE = CURRENT_

                    MODE = -1

                elif event.dict['key'] == 13: # enter

                    MODE = -1

        

        SCREEN.blit(surf,surf.get_rect())

        show_font(TITLE, "FirE tAnk", (WIDTH//2, HEIGHT//4), WHITE)

        for s in MODE_LIST:

            show_font(FONT, s[0], s[1], WHITE)

        show_arrow(90, (MODE_LIST[CURRENT_][1][0], MODE_LIST[CURRENT_][1][1]+HEIGHT//8), WHITE, 1)

    elif MODE == 2: # Help 

        SCREEN.fill(BLACK)

        for event in pygame.event.get():

            if event.type == pygame.QUIT: 

                MUSIC =False

                sound.exit()

                sound.join()

                th1.join()

                sys.exit()

            if event.type == pygame.KEYUP: #(key,corresponding number) up273, down274,left276,right275,space32

                if event.dict['key'] == 13: # up

                    MODE = -1

        show_arrow(90, (WIDTH//9, HEIGHT//8), (255,255,0), 1)

        show_arrow(-90, (WIDTH//9*2, HEIGHT//8), (255,255,0), 1)

        show_font(FONT, "Adjust shooting angle", (WIDTH//5*3, HEIGHT//8), WHITE)

        show_arrow(0, (WIDTH//9, HEIGHT//8*3), (0,0,255), 1)

        show_arrow(180, (WIDTH//9*2, HEIGHT//8*3), (0,0,255), 1)

        show_font(FONT, "Move your tank", (WIDTH//5*3, HEIGHT//8*3), WHITE)

        text_surface = FONT.render("red", True, WHITE, (255,0,0))

        rect= text_surface.get_rect(center=(WIDTH//6, HEIGHT//8*5))

        SCREEN.blit(text_surface, rect)

        show_font(FONT, "Shoot Bullet", (WIDTH//5*3, HEIGHT//8*5), WHITE)

        text_surface = FONT.render("green", True, WHITE, (0,255,0))

        rect= text_surface.get_rect(center=(WIDTH//6, HEIGHT//8*7))

        SCREEN.blit(text_surface, rect)

        show_font(FONT, "Skip your turn", (WIDTH//5*3, HEIGHT//8*7), WHITE)

    elif MODE == 3: # exit

        MUSIC =False

        sound.exit()

        sound.join()

        th1.join()

        sys.exit()

    elif MODE == 0: # play

        

        if player.Fuel == 0:

            player.Fuel += 20

            TURN = 1 - TURN

            if PLAY_MODE == 1 or PLAY_MODE == 3:

                WIND += random.randint(-30,30)/100.0

                if WIND > 2:

                    WIND = 2

                elif WIND < -2:

                    WIND = -2

        

        if TURN:

            player = player2

            bullet = bullet2

        else:

            player = player1

            bullet = bullet1

        

        

            

        

        SCREEN.fill(WHITE)

        if INIT and (PLAY_MODE == 2 or PLAY_MODE == 3):

            INIT = not INIT

            rect1 = pygame.Rect(WIDTH//12*5, HEIGHT//20*11, WIDTH//6, HEIGHT//8)

            rect2 = pygame.Rect(WIDTH//12*2, HEIGHT//10*4, WIDTH//6, HEIGHT//10)

            rect3 = pygame.Rect(WIDTH//12*8, HEIGHT//10*3, WIDTH//6, HEIGHT//10)

            barriers = [rect1, rect2, rect3]

        

        

        pygame.draw.rect(SCREEN, (200,200,200), pygame.Rect(WIDTH//8*3, HEIGHT//10, WIDTH//4, HEIGHT//20))

        pygame.draw.rect(SCREEN, (200,200,0), pygame.Rect(WIDTH//2, HEIGHT//10, WIND*WIDTH//16, HEIGHT//20))

        show_font(FONT_SMALL, "%.2f"%WIND, (WIDTH//2, HEIGHT//12), BLACK)

        bullet.acc_x = WIND

        

        for event in pygame.event.get():

            

            if event.type == pygame.QUIT: 

                MUSIC =False

                sound.exit()

                sound.join()

                th1.join()

                sys.exit()

            if bullet.visible:

                continue

            if not all([p.visible for p in players]):

                continue

            if event.type == pygame.KEYDOWN: #(key,corresponding number) up273, down274,left276,right275,space32

                if event.dict['key'] == 276:

                    LEFT_PRESSED = True

                elif event.dict['key'] == 275:

                    RIGHT_PRESSED = True

                elif event.dict['key'] == 273:

                    UP_PRESSED = True

                elif event.dict['key'] == 274:

                    DOWN_PRESSED = True

                elif event.dict['key'] == 32:

                    player.hold()

                    SPACE_PRESSED = True

                elif event.dict['key'] == 13:

                    ENTER_PRESSED = True

                

            elif event.type == pygame.KEYUP:

                

                if event.dict['key'] == 276:

                    LEFT_PRESSED = False

                elif event.dict['key'] == 275:

                    RIGHT_PRESSED = False

                elif event.dict['key'] == 273:

                    UP_PRESSED = False

                elif event.dict['key'] == 274:

                    DOWN_PRESSED = False

                elif event.dict['key'] == 32:

                    # timing means how long space is pressed

                    #timing = player.release()

                    # shoot the bullet whose speed is proportional to timing

                    player.shoot(t*15)

                    sound.play()

                    SPACE_PRESSED = False

                elif event.dict['key'] == 13:

                    player.Fuel += 20

                    TURN = 1 - TURN

                    if PLAY_MODE == 1 or PLAY_MODE == 3:

                        WIND += random.randint(-30,30)/100

                        if WIND > 2:

                            WIND = 2

                        elif WIND < -2:

                            WIND = -2

                    ENTER_PRESSED = False

                    

        if SPACE_PRESSED: # if space is pressed draw the speed bar 

            t = time.time()-player.t

            t = min(t, 4)

            pygame.draw.rect(SCREEN, BLACK, pygame.Rect(WIDTH // 6, HEIGHT * 7 // 8, t*WIDTH//6, HEIGHT//15))

        elif LEFT_PRESSED: # otherwise when other keys are pressed call the corresponding functions

            player.move_left()

        elif RIGHT_PRESSED:

            player.move_right()

        elif UP_PRESSED:

            player.move_up()

        elif DOWN_PRESSED:

            player.move_down()

        # draw the line which means ground, this is a pseudo-ground

        pygame.draw.line(SCREEN, BLACK, (0, HEIGHT * 4 // 5), (WIDTH, HEIGHT * 4 // 5))

        # update and draw the bullet, return the location of the hitting position if it has

        # see class Bullet

        if PLAY_MODE == 2 or PLAY_MODE == 3:

            for rect in barriers:

                if bullet.rect.colliderect(rect):

                    bullet.visible = False

                    bullet.rect.x = 0 

                    bullet.rect.y = 0 

                    player.Fuel += 20

                    TURN = 1 - TURN

                    if PLAY_MODE == 1 or PLAY_MODE == 3:

                        WIND += random.randint(-30,30)/100

                        if WIND > 2:

                            WIND = 2

                        elif WIND < -2:

                            WIND = -2

                    break

        loc = bullet.update()

        if loc and bullet.visible:

            # once hit set it to invisible

            bullet.visible = False

            # player's HP is deducted

            for p in players:

                if abs(p.tank.X-loc) < WIDTH/16:

                    p.HP -= -600/WIDTH*(abs(p.tank.X-loc) - WIDTH/16) + 5*random.random()

            # switch the turn

            player.Fuel += 20

            TURN = 1 - TURN

            if PLAY_MODE == 1 or PLAY_MODE == 3:

                WIND += random.randint(-30,30)/100

                if WIND > 2:

                    WIND = 2

                elif WIND < -2:

                    WIND = -2

                    

        if PLAY_MODE == 2 or PLAY_MODE == 3:

            for rect_ in barriers:

                pygame.draw.rect(SCREEN, (BLACK), rect_)

        # when HP goes below 0, game over

        # if players[0].HP <= 0 and players[1].HP <= 0:

        #   players[0].visible = False

        #   players[1].visible = False

        # elif players[0].HP <= 0:

        #   players[0].visible = False

        #   show_font(FONT, "Player2 wins!", (WIDTH//2, HEIGHT//2), BLACK)

        # elif players[1].HP <= 0:

        #   players[1].visible = False

        #   show_font(FONT, "Player1 wins!", (WIDTH//2, HEIGHT//2), BLACK)

        # when HP goes below 0, game over

        if players[0].HP <= 0 and players[1].HP <= 0:

            players[0].visible = False

            players[1].visible = False

        elif players[0].HP <= 0:

            players[0].visible = False

            #show_font(FONT, "Player2 wins!", (WIDTH//2, HEIGHT//2), BLACK)

        elif players[1].HP <= 0:

            players[1].visible = False

            #show_font(FONT, "Player1 wins!", (WIDTH//2, HEIGHT//2), BLACK)

            #show_font(FONT, "Press green button to return to main menu...", (WIDTH//2, HEIGHT * 2//3), BLACK)

        show = True

        #show_font(FONT_SMALL, "Press green button to return to main menu...", (WIDTH//2, HEIGHT * 2//3), BLACK)

        while not all([p.visible for p in players]):

            SCREEN.fill(WHITE)

            pygame.draw.line(SCREEN, BLACK, (0, HEIGHT * 4 // 5), (WIDTH, HEIGHT * 4 // 5))

            if players[0].visible == False and players[1].visible == False:

                show_font(FONT, "Tie game!", (WIDTH//2, HEIGHT//2), BLACK)

            elif players[0].visible == False:

                show_font(FONT, "Player2 wins!", (WIDTH//2, HEIGHT//2), BLACK)

                group.update()

            elif players[1].visible == False:

                show_font(FONT, "Player1 wins!", (WIDTH//2, HEIGHT//2), BLACK)

                group.update()

            

            show = not show

            if show:

                show_font(FONT_SMALL, "Press green button to return to main menu...", (WIDTH//2, HEIGHT * 2//3), BLACK)

                #time.sleep(0.5)


            for event in pygame.event.get():

                if event.type == pygame.KEYUP:

                    if event.dict['key'] == 13:

                        reset()

                        break

                        

            if MODE == -1:

                break

            pygame.display.update()


        # update the sprites in group, and display

        if MODE != -1:

            group.update()

#            player1.update()

#            player2.update()

            player.turn()

    pygame.display.update()


        # update the sprites in group, and display

    #   group.update()

    #   player.turn()

    # pygame.display.update()


# unit.py

import pygame

import math

import sys

import time

import threading

WIDTH = 320

HEIGHT = 240

FACTOR = 0.7

class Tank(pygame.sprite.Sprite):

    # class tank holds information of a tank

    def __init__(self, screen, x, y, speed):

        pygame.sprite.Sprite.__init__(self)

        self.screen = screen

        self.X = x

        self.Y = y

        self.direction = 1 # tank's direction, 1 = to the right, -1 = to the left

        self.speed = speed # tank's speed

        

        self.angle_speed = 5 # angle change's speed

        self.angle_limit = [20,90] # angle limit

        self.angle = self.angle_limit[0] # initialize to the lower bound

        # the template points used to draw a tank, regardless of direction

        self.tank_points = [(0,-32),(2,-30),(30,-30),(30,-26),(10,-26),(14,-20),(32,-20),(34,-18),

                        (6,-18),(32,-18),(28,-14),(2,-14),(10,-6),(20,-4),(20,0),(-14,0),(-4,-8),

                        (-12,-12),(-12,-26),(-12,-32)]

        # the actual points to draw a tank, with direction

        self.points = [(point[0]*FACTOR*self.direction+self.X, point[1]*FACTOR+self.Y) for point in self.tank_points]

    def move_up(self): # change the angle

        self.angle += self.angle_speed

        if self.angle > self.angle_limit[1]:

            self.angle = self.angle_limit[1]

        elif self.angle < self.angle_limit[0]:

            self.angle = self.angle_limit[0]

    def move_down(self):

        self.angle -= self.angle_speed

        if self.angle > self.angle_limit[1]:

            self.angle = self.angle_limit[1]

        elif self.angle < self.angle_limit[0]:

            self.angle = self.angle_limit[0]

    def move_left(self): # change position

        self.X -= self.speed

        self.direction = -1

        self.points = [(point[0]*FACTOR*self.direction+self.X, point[1]*FACTOR+self.Y) for point in self.tank_points]


    def move_right(self):

        self.X += self.speed

        self.direction = 1

        self.points = [(point[0]*FACTOR*self.direction+self.X, point[1]*FACTOR+self.Y) for point in self.tank_points]


    def update(self): # draw this tank according to its attributes

        pygame.draw.line(self.screen, (255,100,255), [self.X, self.Y-20], 

            [self.X + 30*math.cos((self.direction*(self.angle-90)+90)/180*math.pi), 

            self.Y-20-30*math.sin((self.direction*(self.angle-90)+90)/180*math.pi)], 2)

        pygame.draw.lines(self.screen, (0,0,0), True, self.points)

        pygame.draw.circle(self.screen, (200,0,0), (self.X+2, self.Y-20), 2)


class Bullet(pygame.sprite.Sprite):

    # the class of bullet

    def __init__(self, screen):

        pygame.sprite.Sprite.__init__(self)

        img = pygame.image.load("bullet.png")

        self.size = (10,10)

        self.surf = pygame.transform.scale(img, self.size)

        self.surf.set_colorkey((255,255,255))

        self.rect = self.surf.get_rect()

        self.screen = screen

        # set the position of bullet to (0,0), and not visible

        self.rect.x = 0

        self.rect.y = 0

        self.visible = False

        # set the speed

        self.speed_x = 0

        self.speed_y = 0

        # set the acceleration

        self.acc_y = 2

        self.acc_x = 0

        

    # set the bullet to visible and update its status

    def set(self, x, y, speed_x, speed_y):

        self.rect.x = x

        self.rect.y = y

        self.speed_x = speed_x

        self.speed_y = speed_y

        self.visible = True

    # draw the bullet return its hitting position if any

    def update(self):

        if self.rect.y <= HEIGHT * 4 // 5 -2*self.speed_y and self.visible:

            self.speed_y += self.acc_y

            self.speed_x += self.acc_x

            self.rect = self.rect.move([self.speed_x, self.speed_y])

            self.screen.blit(self.surf,self.rect)

        else:

            return self.rect.x

class Player(pygame.sprite.Sprite): 

    def __init__(self, screen, tank, bullet):

        pygame.sprite.Sprite.__init__(self)

        self.screen = screen

        # player's HP is set to 100, fuel 50

        self.HP = 100

        self.Fuel = 50

        self.tank = tank

        self.bullet = bullet

        # bar width means the width of HP bar and Fuel bar

        self.bar_width = 6

        self.visible = True

        self.t = None

    # draw the bars and its tank

    def update(self):

        if self.visible:

            self.Fuel = min(100, self.Fuel)

            self.HP = max(0, self.HP)

            pygame.draw.rect(self.screen, (255,0,0), 

                pygame.Rect(self.tank.X-25, self.tank.Y+4, 

                    self.HP/2, self.bar_width))

            pygame.draw.rect(self.screen, (0,0,255), 

                pygame.Rect(self.tank.X-25, self.tank.Y+4+self.bar_width, 

                    self.Fuel/2, self.bar_width))

            self.tank.update()

    def turn(self):

        pygame.draw.arc(self.screen, (100,100,255), pygame.Rect(self.tank.X - 30, self.tank.Y - 50, 60, 60), 0, 360)

    # draw the bullet on and deduct fuel

    def shoot(self, speed):

        if self.Fuel > 10:

            self.bullet.set(self.tank.X, self.tank.Y-20,

             speed*math.cos((self.tank.direction*(self.tank.angle-90)+90)/180*math.pi),

             -speed*math.sin((self.tank.direction*(self.tank.angle-90)+90)/180*math.pi))

            self.Fuel = self.Fuel-10

    def move_up(self):

        self.tank.move_up()

    def move_down(self):

        self.tank.move_down()

    def move_left(self):

        if self.Fuel > 0:

            self.tank.move_left()

        self.Fuel = max(self.Fuel-3, 0)

    def move_right(self):

        if self.Fuel > 0:

            self.tank.move_right()

        self.Fuel = max(self.Fuel-3, 0)

    # call hold() when key is pressed

    def hold(self):

        self.t = time.time()

    # call release() when key is released and return how long the key is pressed

    def release(self):

        if not self.t:

            return 0

        ans = time.time() - self.t

        self.t = None

        return ans

    

    

class Sound(threading.Thread):

    def __init__(self, filename):

        threading.Thread.__init__(self)

        self.filename = filename

        self._end = True

        self.lock = threading.Lock()

        self.lock.acquire()

    def play(self):

        try:

            self.lock.release()

        except:

            pass

    def exit(self):

        self._end = False

        self.lock.release()

    def run(self):

        while self._end:

            pygame.mixer.music.load(self.filename)

            pygame.mixer.music.play(0)

            while pygame.mixer.music.get_busy(): 

                pygame.time.Clock().tick(10)

            self.lock.acquire()

Contact

Chengxi Li: cl2535@cornell.edu

Siyu Liu: sl3282@cornell.edu 

W3C+Hates+Me Valid+CSS%21 Handcrafted with sweat and blood Runs on Any Browser Any OS