Ok, this is it - the one and only part 3, the final chapter. We looked at pygame in
part 1, we deciphered the BASIC code in
part 2,and now we're gonna produce a python version of this simple little 1K ZX81 game.
Why, oh why am I doing this?!
If you've sat through the previous 2 parts, then that's a question you're probably asking...
Is it purely to learn about converting old 1980's programs to python? Well, in a way yes - its something I've wanted to do given how much python reminded me of my youth. I am enthralled with the fact I can code games and have fun just as much as I did back at age 11.
But one thing I am hoping to instill on everybody who reads this article is that
writing games is a great way to learn to program. Programming is a a
technical art form that has mostly disappeared these days. Lets face it, nobody really goes and buys a high specced PC or Mac to sit and write programs - its mainly about playing the latest games, running business applications, browsing the web and making media. And that's not a bad thing, but its also a
sad thing.
If you're just getting into programming and need a challenge to help you learn, consider looking at the past (I've provided links at the bottom of this page). This article is about using that 80's approach of
learn through example. By reverse-engineering a game from another language, you really can learn a
LOT. Concepts that were used back in that era to make the most of the hardware (ie. tricks like using boolean logic for scores), the way game logic works and of course you pick up a lot about the programming language you're using by trying to work out how to create code that works the same way.
Don't forget - its a case of
practise makes perfect. In the 80's, typing in games and understanding how they worked was how I learned to create my own code... Today, the same concept is just as relevant - the more you do, the more you learn - and the more your expertise grows.
Its these reasons I've written this 3-part project for. To try and bring a little of the past to the current day, and just show how much fun it can be to code games rather then click-drag them.
That said, programming is a skill that's started being taught again in schools - and its great to see young people getting this opportunity. In fact, I'm off to a secondary school in a couple of weeks to teach kids some python! Very exciting.
So, lets get on with it.
Step 1 - make sure we have those things we can't program...
While the game can be coded, to really achieve that
classic look we need to have the classic font and graphics! There are some great free ZX-style fonts that people have created - I grabbed
this one, and unzipped the files into the folder where I was writing my python script.
|
These are not the droids you are looking for, but definitely that font is... |
When files sit in the same folder as your python project, we can load them easily by just specifying the filename - so put those fonts there. There are two .ttf files -
zx81.ttf and
zxspectr.ttf... Great font, and does a great job too.
If you don't want to use a custom font, for a project like this you'll need to consider using a
monospaced font - one where all the characters (including space) are identical widths.
Courier for instance works just fine.
However, as great as the font is, we still need those clunky graphic characters. The best way to handle this is to simply these draw up yourself. 8 pixels high by 16 pixels wide... The thickness of the black blocks are 4 pixels.
Get the files here...
You can download the extremely tiny graphic (along with the source code for this game)
here... Obviously for permission reasons, I can't re-distribute the font - but here's
that link to it again for convenience.
OK - enough natter, lets start to code this game and see how we fare. I'll be showing code here in this article without my usual python comments in it - but don't fear as I'll be explaining what each part does below the code itself.
Modules
To start off, we'll need to import our modules of course.
import pygame
import pygame.time
import sys
from pygame.locals import *
from random import randint
In this block of code, its worth noting that I've loaded all (indicated by the
*) of
pygame.locals into the
root namespace. What's meant by this is that rather then referring to a command using its module namespace we can now refer to it directly. I did this because it saves me having to write
pygame.locals before each constant. Now I can just refer to
pygame.locals.QUIT simply as
QUIT.
You don't need to do this, mind you. In the end you can just load them as you would otherwise if you prefer, but its worth considering.
Of course, we also need to bring in
randint - a function that lets us ask for a random integer (used to determine where the coin would fall). This I'm also loading into the root namespace from the
random module.
Constants
So that our code will be easier to understand, and to save us repeating ourselves (plus in case we wanted to change a value used throughout a program) we will define a few constants here that we can use everywhere.
BLACK = (0,0,0)
WHITE = (255,255,255)
TXT = 16
BLACK and
WHITE are defining the RGB values that we can use for drawing text, wiping the screen, etc. The
TXT constant is for text scaling - for instance, using the original pixel size of the ZX81 on a PC with a 1920x1080 resolution will be tiny and hard to look at. This value will be used to let us change the size of all of our character elements. In this case, I've used a size of 16 pixels per character.
Getting pygame set up
pygame.init()
fpsClock = pygame.time.Clock()
Obviously before we can use pygame, we need to set it up (see
part 1
for an explanation about how this works). This includes not just the
pygame system, but loading in fonts and graphics that we will be using.
gameWindow = pygame.display.set_mode(( 32 * TXT, 22 * TXT ))
The ZX81 screen is 32 characters wide, 22 characters high (each
character taking up 8 x 8 pixels). We therefore need to set up our
window appropriately, using our TXT scale constant.
Fonts
We want to import our
zx81.ttf font. Providing you've placed it into the same folder, we can refer to it directly by file name. If you've instead installed the font into your system - or you want to use an alternate installed font - then you'll need to request the path to the font file itself. That's fairly easy to do by asking for it using
pygame.font.match_font('nameoffont').
bFont = pygame.font.Font('zx81.ttf', TXT)
For now, lets just load up the ZX81 font, and set its pixel size to TXT
Player graphic
pGfx = pygame.transform.scale(pygame.image.load('ZX81_block.png'),
(2*TXT,TXT))
We can import our nifty 8 x 16 pixel graphic and then resize it to match
the rest of the game as follows. In this line of code, I'm using
pygame's
transform.scale
command to resize the file that I'm loading from disk... You could of
course break this into two lines - load image, and then scale - if you
prefer.
As the width of the players graphic is double the height, we set the scale values to ( 2 * TXT, TXT)
Note : If you are unfamiliar with python, long code that is encapsulated between brackets (such as our parameters for the pygame.transform.scale command) can be broken apart into multiple lines. This is termed implicit line continuation (python regards anything inside brackets, strings or parentheses as continuous until the brackets/etc are closed. This includes code that is broken up over multiple lines)
Functions - its where the fun begins...
def bPRINT ( cRow, cCol, cGFX ):
gameWindow.blit(cGFX, (cCol * TXT, cRow * TXT))
We'll now define some functions... For our first one, lets define a function that takes care of
simulating BASIC's
PRINT AT
command. This will make life a lot easier for us. If we can pass the
same settings as the BASIC code, we'll end up making this code a real
doddle to complete.
In the case of pygame, text and graphics are treated the same way. Text, as we'll see in our game code, is rendered to an image first before its drawn (
blitted) to the gameWindow.
The Game - a function?
The game itself - that part of the BASIC listing that does 10 rounds of coin catching - is being defined as a function. Why, you may ask, would we want our game in a function?
Its simple... Our game loop (which is the infinite loop that we write at the end of the program
after the function) needs to be able to restart the game. By making the game a function, we can call it from our main game loop. When the game is over, if we then have the function return back to our main game loop (passing the score back as well), we can print the score and wait for a key press to restart the game (ie. call the game function).
playDropout - where BASIC and Python meet...
Get ready because this is where we translate that old BASIC code into its python equivalent. We'll stick to using the same variable names as well as the
FOR loops as the original code so that we can see the similarities...
def playDropout():
T,P = 0,0
We start by defining our score, and our players position in the game. In Python we can do it in one line, rather then then two like BASIC.
for Z in range(10):
gameWindow.fill(WHITE)
bText = bFont.renders("%d" % T, True, BLACK, WHITE)
bPRINT(12,0,bText)
As in the original version, we have a
for loop that gives us our 10 rounds of coin catching. the
gameWindow.fill does the equivalent of the CLS command by filling the display with white.
After that, the score is rendered to an image that is stored in the variable
bText, then we draw it to our display using our handy bPRINT function
R = randint(0,16)
Next we choose our random value we'll use to position our coin at. And then we're ready to catch that coin with our Y loop...
The coin falling...
for Y in range(10):
Obviously we add in our Y loop. The coin drops 10 characters downwards from the top of the screen. Amusingly this does make it impossible to catch coins that fall over 10 characters away from the player... But hey, we'll keep to the original here and retain that particular retro-feature.
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
Before we jump into the game code, the first thing I've added is a
test to see if the user has quit the game. Quitting the game is called
when the user closes the window... We want to make sure we clean up
nicely in that case. Obviously this isn't something that we needed in
BASIC - you'd never quit a program since it was the only thing running
on the computer of course...
You'd notice that by importing the
pygame.locals into the root namespace, I only needed to type the constant name QUIT here. It looks cleaner, and its easier to type.
N = P+(pygame.key.getpressed()[K_4])- \
(pygame.key.getpressed()[K_1])
For the key press - the code is literally the same. The only difference is that
INKEY$ is replaced by pygame's
pygame.key.getpressed() function. As expected, both of these also return a True (1) or False (0) result.
Note : much like the transform.scale code, we've broken up this line with a line continuation character (back slash). If your code isn't continuously encapsulated between brackets, parentheses or a string, we need this character to indicate the line continues.
if N < 0 or N > 15:
N = P
bText = bFont.render("O", True, BLACK, WHITE)
bPRINT(Y,R,bText)
Again, almost exactly the same as the original code - we check for our bounds, and then we print the coin (the letter "O") to the screen. This is just too easy! (which is a good thing)
bPRINT(11,N,pGfx)
We draw our player graphic...
pygame.display.update()
Now that we've drawn our graphics, we simply need to update it to show it on screen.
bText = bFont.render(" ", True, BLACK, WHITE)
bPRINT(Y,R,bText)
bText = bFont.render(" ", True, BLACK, WHITE)
bPRINT(11,N,bText)
Finally we delete our graphics (the coin, and the player) - we can do
this by simply printing a space character (or in the case of the
players graphic, two spaces). We can only do this, of course, as long
as we are using a monospace font... Any other type of font tends to
have thinner spaces, which will only partially blank out half (or less)
of the graphic.
P = N
fpsClock.tick(6)
We update our position to use the new position we calculated, and we
set the frames per second to delay the game to keep it running at the
right speed. On my PC, I found a frame rate of 6 fps felt the most
accurate to embrace the processing power of the ZX81
And that's the end of the Y loop.
T = T + (P == R or P+1 == R)
Once the coin has dropped all the way down (10 characters), the loop
will end and continue into the next line... This is where we'll update
our score (again using the same logic from the original program) before
the Z loop repeats.
return T
Once the Z loop is complete, the function exits. As we'll want to print
the final score, this function simply needs to return T...
On to the main event, eh, loop
This is the end - our game is written as a function, all our initialisation is finished so we simply need to now put it all together in our game loop...
while True:
fSc = playDropout()
We call the playDropout() function which will run the game, and then exit on completion of 10 rounds of coin dropping.
bText = bFont.render("YOU SCORED %d/10" % fSc, True,BLACK, WHITE)
bPRINT(12,0,bText)
We returned the score back, which will be stored in fSc. Lets print that back to the player...
pygame.display.update()
We won't see this until we refresh the display.
replay = False
while not replay:
for event in pygame.event.get():
if event.type == KEYDOWN:
if event.key == K_SPACE:
replay = True
if event.type == QUIT:
pygame.quit()
sys.exit()
Finally, we want to pause until we press a key - well, in this case
I'm going to set it to be the space bar, simply because its possible we
may still be holding down a key from playing the game - which of course
will jump us straight back into the game. We also want to make sure we
check for the user quitting the application.
This can all be done with a while loop that waits for the space bar to be pressed.
And that's it! Once the space bar is pressed, the code will jump back into our while loop, running the playDropout() function once more. Game on!
Final Comparison
If we remove all that additional Python code for rendering text, event catching, etc and look at the
core code that actually is the game, we can see the way that the two languages compare - and its really not that different!
Congratulations!
If your catching the letter O and experiencing the excitement of doing that 10 times then you've successfully managed to write your own recreation of an old BASIC game in python! In fact, what the aim of doing this was to demonstrate that you can code games very easily with python, as easily as kids did 33 years ago using BASIC.
Keep going!
If you don't write games at all - or want to learn more about how to develop your skills as both a programmer and a game developer then don't dismiss the opportunity presented before you. Learn through typing in others games.
Of course, BASIC was a language found on most home computers. While the dialect may be different, the logic behind how the language worked is no different - and there were just as many amazing games on other platforms as there were on just the ZX81. Challenge yourself to find some of the amazing games that were around and learn to convert them into python. You'll be amazed how much you will discover from looking at how people developed their code.
Resources online
There are
lots of BASIC listings to be found online thanks the those people who are passionately preserving the past. Magazine's are preserved and available from web archives such as
retropdfs, the computer magazines archive, Atarimagazines, DPLibrary, etc.
Books, including some that explain how to program in a particular flavor of BASIC can also be found online. Some other archives and sites worth looking into include
Atariarchives,
Folkscanomy, and the original publisher of well-loved books -
Usborne (who have released a lot of their old 80's computer programming books for free - I wrote an article
here). You can actually
read the ZX81 programming guide online if you want to understand the language more.
...and surprisingly the excellent collection of cassette software at
ZX81stuff also display basic listings on screen when you click on them. Back in the day, a lot of early 'commercial' games software was in fact written in BASIC.
So...
I'm definitely happy to be able to see how easy it is to code games the same way I did 33 years ago... And no - I've no real plans to continue just re-writing old BASIC code in python. Roll on new, modern games!