Game Playing - Programming a video game


In this project we shall develop small video arcade game. It is modeled on a game my family enjoyed in the 1980’s called Lode Runner. We could play for hours, having a good time and wearing out keyboards. But Lode Runner, at least the version we had, assumed your cpu ran at a certain speed and once we had processors faster than about 8Mhz, the game went too fast to be played.

Lode runner consisted of a vertical “board” containing ladders and “catwalks” between the ladders. In the original game there were about 100 different boards or levels, each one just a bit harder than the last. “You” were represented by a little person going up and down the ladders and running across the catwalks gathering the “lodes” of treasure. You used the arrow keys to indicate a change of direction. While doing this you were pursued by what I always thought of as robots.

You won a level by retrieving all the lodes and returning to the top of the board before being tagged by a robot. You had one weapon at your disposal. You could burn a hole in a catwalk that a robot would fall into and get stuck. The robot would take a couple of seconds to get back out. And in a few seconds more the catwalk would self repair. If you fell into the hole you could not get out and the repair brought tragic.

We are going to develop a game a bit simpler, using simplified graphics and keyboard input. The code has been redone and works fine on Python 2.7 and Python 3.0 The Zelle graphics package will be used to let us control the keyboard.

The nature of game programs

Game programs, actually games in general whether Lode Runner or Chess, have a certain structure. A game is a set of moves. There must be a way to visualize the game in progress (screen), a way to get input from the player(s), rules that determine what moves are possible and to determine when the game is over.

At the center of any game program is the “game loop” that repeatably performs the above steps until the game is terminated, that is, won, lost or otherwise interrupted.

Moves in chess require each player to examine the board, decide on the best next move and take it. Moves in Lode Runner are a little more dynamic. Each player (you and the robots) move one position in each time window. Each player can change direction. A time windows are long enough for you to keep up, and short enough to not be boring. Finally, it needs to be determined when the game is over. This will happen if a robot tags you or if you fall off the board (into oblivion). You will have won if you gathered all the lodes before this happens.

We will build the program in stages so we can concentrate one aspect at a time. we shall start with some unusual I/O to the terminal.

Dealing with the board and screen.

The game will run in a terminal window and to keep everything simple we shall just use character “graphics” to represent the board and players. Here is the board layout we shall use.

01 #
02 #     b o a r d s . p y
03 #
04 #   Each board is a list of strings - The easier to index by
05 #
06
07 board0 = ['','','','',
08 '                                __*___|_             ',
09 '                          |           |              ',
10 '                          |           |              ',
11 '                         _|__*________|______*___    ',
12 '                          |           |              ',
13 '                  ___*____|___________|_             ',
14 '                          |           |              ',
15 '                          |           |              ',
16 '                          |           |              ',
17 '    ____*_________________|___________|______*____   ']
18
19 boards = [board0]

The module boards.py contains our board as a list of python strings, each string representing one row on the screen. This format is convenient. We can view the layout in the code as it appears on the screen. And it is easy to edit the board since the rows are aligned vertically. Using an editor with a “replace mode” makes the creation of new boards easy.

We use 3 characters in the layout. “_” (underscores) form horizonal walkways. Asteriks are the lodes to be gathered. The vertical bars are ladders. You will be represnted with the “hat” “^” and the robots as ampersands “&”. Reading the board into the program is done by simply importing the module.

The list “boards” references board0 in a single item. This is meant to leave room for expansion to allow you to have several boards and let the game switch between them. In the traditional Lode Runner game winning at one level automatically advanced you to the next.

Looking at the Code

Before looking at the main part of the program let us look at the display module to handle output to the screen. Basically, we want to send text to the screen at a certain row and column and clear the screen.

We keep a copy in board of what should be on the screen. The board is a list of rows and each row a list of characters. The board is updated with each move in the game and then sent to the screen

01 #
02 #  d i s p l a y . p y
03 #
04 import sys, boards, copy
05 from   graphics import *
06
07 win = GraphWin("x", 1, 1) # virtually invisible
08

The Zelle graphics module is imported and a invisibled window one pixel square is created. This is used to only to get keyboard input from YOU the player. The keys you will use are the four arrow keys and two keys to shoot holes in the floor.

09 def setBoard(n) :
10    global board
11    board = copy.copy(boards.boards[n])
12
13 def getSpot (row,col) :
14    'return board value ("_","|"," ") for row,col'
15    try     : return board[row][col]
16    except  : return ' '
17
18 def setSpot (row,col,newchar) :
19    'update board value at row,col with newchar'
20    try :
21        l = board[row]
22        l = l[:col]+newchar+l[col+1:]
23        board[row] = l
24    except : pass

Function setBoard makes copy of a board from the boards module. The copy is modified during game play but the original board is kept secure.

Functions getSpot and setSpot read a write a single character to the board. The try and except clauses are there to make any off-board access to simply emulate empty space.

25
26 def writeBoard() :
27    'write entire board to the screen'
28    global board
29    sys.stdout.write('\x1b[1;1H\x1b[J') # clear screen
30    for row in range(len(board)) :
31        writeScreen(row,0,board[row])
32
33 def writeScreen(row,col,char) :
34    'write char to screen at row,col'
35    sys.stdout.write('\x1b[%d;%dH%s' % (row+1,col+1,char))
36    sys.stdout.flush()

The function writeBoard is used to send the board to the screen and only happens at the start of the game. After that, updates are done with writeScreen which send a string of characters to the screen starting at a given row and column.

Positioning the cursor on the screen and erasing the screen are done with a pair of ANSI control sequences that start with the ESC char 27 (‘x1b’). At line 29 the screen is cleared by setting the cursor to the upper left corner (‘x1b[1;1H’) and then clearing to the end of the screen with (‘x1b[J’). The screen is sys.stdout and after writing each string the output is flushed, that is sent immediately rather than being cached in memory.

This works fine in the Linux Terminal and also with the Mac. Windows support is somewhat more problematic. Check out the topic ANSI.SYS with Google.

The Main code

At this point it would be a good idea to download the Zip file, unpack it with unzip and run the program.

$python lode.py

You can also use python3 if you want.

The Players

In the file lode.py the players, both You and the Robots, are objects. The class Player provides the common logic of how the players move. Remember, the screen (and board) have slots for single characters in rows and columns. Players move one slot in each time window.

Here is the common logic in Player.move

08 class Player :
09     def setDirection (self, ch) : pass
10
11     def move (self) :
12         global inPlay, you, players
13         spot  = getSpot(self.row,self.col)
14         lspot = getSpot(self.row,self.col-1)
15         rspot = getSpot(self.row,self.col+1)
16
17         horz,vert = self.dir
18         writeScreen(self.row,self.col,spot)
19         if   spot == '_' : self.col += horz  # catwalk left/right
20         elif spot == '|' :
21             self.row -= vert             # by up/down arrow
22             self.col += horz             # may jump off the ladder
23         elif spot == ' ' : self.row += 1 # always fall in air
24         writeScreen(self.row,self.col,self.face)
25         if self.row > 23 :
26             if self.face == '^' : inPlay = False  # You have plunged
27             else : players.remove(self)

In line 12 inPlay is True until the game ends, you is the player controlled by the keyboard. Others are robots. players is list containing all active ones.

spot will contain what underlies the player. Only the board holds players. The player being moved will update only the screen in line 24. lspot and rspot are what is to the left and right accordingly. a dir (direction of travel) is a (horz,vert) tuple where horz and vert can take a value from -1, 0, or 1. A dir of (0,0) is standing still, (1,0) is moving to the right, (0,-1) is moving up. Only four directions exist. Diagonal movement is not supported.

So, with all of that in mind, here we go. Line 18, we take the player off the screen. Later in line 24 we put him back on, probably one spot away. Line 19, if we are on a catwalk we can step to the left or the right. Line 20, on a ladder, we may continue up or down or jump off the ladder to the left or right. Finally, in line 23, being in the air just means you fall.

Both You and the robots use player.move with the same rules.

Setting direction.

The methods You.setDirection and Robot.setDirection are very different. You are trying to gather lodes (avoiding the robots) and the robots are trying to gather You.

29 class You(Player) :
30     def __init__ (self, row=1, col=10, face='^') :
31         self.row = row; self.col = col; self.face=face
32         self.dir = (0,0); self.score=0; self.cmd =None
33
34     def setDirection (self, ch) :
35         here = getSpot(self.row,self.col)
36         if here == '*' :
37             self.score += 10
38             setSpot(self.row,self.col,"_")
39             writeScreen(self.row,self.col,"_")   # update the screen
40
41         if ch == 'Up' and here == '|' : self.dir=( 0, 1)
42         if ch == 'Down' : self.dir=( 0,-1)
43         if ch == 'Right': self.dir=( 1, 0)
44         if ch == 'Left' : self.dir=(-1, 0)
45         if ch == 'a' : burn(self.row, self.col-1); self.dir=(0,0)
46         if ch == 's' : burn(self.row, self.col+1); self.dir=(0,0)

Class You has attributes for position (row,col), picture for the screen, direction and score. There is also an attribute for a pending command. The game moves quickly and if you are, say, heading for a ladder you want to climb you may hit the “up” arrow in anticipation. The “up” command will be held. Line 41 will use it when it can.

The same works for the other arrow keys. We talked above about how direction tuples work. Two other commands are used to burn holes in the catwalk. You stop then. You might jump through the hole to another catwalk below.

At line 36 you can gather a lode of treasure and bump your score. Once the lode is gone the spot is replaced by ordinary catwalk.

48 class Robot(Player) :
49     def __init__ (self, row=1, col=12, Face='&') :
50         self.row = row; self.col = col; self.face=Face
51         self.dir = (0,0)
52
53     def move (self) :
54         global clock
55         if clock%2 == 0 : Player.move(self)
56
57     def setDirection (self, ch) :
58         global inPlay, you
59         # did we tag him?
60         if you.row == self.row and you.col == self.col : inPlay=False
61         # same level. run toward you
62         if self.row == you.row :
63             if self.col > you.col : self.dir=(-1,0) # left
64             if self.col < you.col : self.dir=( 1,0) # right
65         else :
66             me = getSpot(self.row,self.col)  # where am I
67             if me == "|" : # on a ladder
68                 if self.row > you.row : self.dir=(0, 1) # up
69                 if self.row < you.row : self.dir=(0,-1) # down

The attributes of a Robot instance are almost the same as You except that it lacks score and cmd

A robot moves at half YOUR speed. At line 55 it moves every other clock tick.

Its logic is very simple and could be improved. If it is on the same level with you it will head your way (line 62). If it is on a ladder it moves to match your vertical positon. (line 67)

The Game Loop

Once the above makes sense the rest is pretty straight-forward.

83 def playGame() :
84     global clock, inPlay, you, players
85     writeBoard ()
86     you = You()
87     players = [you]
88     clock = 0
89     cmd   = None
90     inPlay = True
91     while inPlay :
92         clock += 1
93         if clock > 40 and len(players) < 3 :
94             players.append(Robot(col=int(random.random()*40+5)))
95         time.sleep(.2)
96         keys = win.checkKey()
97         if keys : cmd = keys
98         for player in players :
99             player.setDirection(cmd)
100            player.move()
101        writeScreen (20,0,'Score: %d. Cmd: %s    ' % (you.score,cmd))
102    writeScreen (21,0,'Game over. Score: %d\n'     %  you.score)

Function playGame is called just once. writeBoard is called to display the initial screen. You are created and are set as the first player in the players list (87). Then the clock and command are reset and the game declared inPlay. The game loop (91) begins.

With each click of the clock (200ms) the loop is run. After YOU have 40 click headstart (93) 2 robots appear. If lost, a robot will be replaced. If you hit a key then it is saved in cmd. Finally, each player calculates a changed of direction and moves. Once inPlay turns False, the loop is exited and the final score sent (102).

The function main simply calls playGame after setting a board. We have just one, but if you make more you can perhaps pass the name on the command line. The call to playGame is inside a try clause so that if the program crashes for some reason the graphic window created for keyboard input is closed before exit. The finally clause is also executed if the exit is normal.

def main ():
    try :
        setBoard(0)
        playGame()
    finally:
        win.close()

Some ideas for further exploration.

The best thing about programming your own games is that you can modify and extend them. Here are a few ideas.

If you have comments or suggestions You can email me at mail me

Copyright © 2003-2021 Chris Meyers and Fred Obermann

* * *