Pygame Zero: e e R Z o
Pygame Zero: e e R Z o
e r
o
Pygame Zero
attle a ainst
enemies in t e ori inal
arcade classic Source Code
A
t the beginning of the 1980s, the Light Cycle section of the movie, where screen. There are various ways to code this
Disney made plans for an players speed around an arena on high- with Pygame ero. In this sample, we’ll focus
entirely new kind of animated tech motorbikes, which leave a deadly trail on the movement of the player Light Cycle
movie that used cutting- of light in their wake. If competitors hit any and creating the trails that are left behind as
edge computer graphics. walls or cross the path of any trails, then it moves around the screen. We could use
The resulting film was ’s TRON, and it’s game over. Players progress through line drawing functions for the trail behind
it inevitably sparked one of the earliest the twelve levels which were all named the bike, or go for a system like Snake, where
tie-in arcade machines. The game featured after programming languages. In the Light blocks are added to the trail as the player
several minigames, including one based on Cycle game, the players compete against moves. In this example, though, we’re going
AI players who drive yellow Light Cycles to use a two-dimensional list as a matrix of
around the arena. As the levels progress, positions on the screen. This means that
more AI Players are added. wherever the player moves on the screen,
The TRON game, distributed by Bally we can set the position as visited or check
Midway, was well-received in arcades, and to see if it’s been visited before and, if so,
even won lectronic ames aga ine’s trigger an end-game event.
(presumably) coveted Coin-operated Game For the main draw() function, we first blit
of the Year gong. Although the arcade game our background image which is the cross-
wasn’t ported to home computers at the hatched arena, then we iterate through our
time, several similar games – and outright two-dimensional list of screen positions
clones – emerged, such as the unsubtly (each 10 pixels square) displaying a square
named Light Cycle for the BBC Micro, Oric, anywhere the Cycle has been. The Cycle is
and ZX Spectrum. then drawn and we can add a display of the
The Light Cycle minigame is essentially a score. The update() function contains code to
Our homage to the
variation on Snake, with the player leaving a move the Cycle and check for collisions. We
TRON i t C cle
classic arcade ame trail behind them as they move around the use a list of directions in degrees to control
64 / wfmag.cc
Toolbox
Source Code
# TRON
speed = 3
dirs = [0,90,180,270]
moves = [(0,-1),(-1,0),(0,1),(1,0)]
def draw():
screen.blit(“background”, (0, 0))
for x in range(0, 79):
for y in range(0, 59):
if matrix[x][y] > 0:
matrix[x][y] += 1
screen.blit(“dot”,((x*10)-5,(y*10)-5))
bike.draw()
The TRON arcade game
wfmag.cc \ 65
Toolbox
Write your own emulator in Python
AUTHOR
EDWIN JONES
A software engineer at video game developer Mediatonic, Edwin
Jones has been working in the games industry for around 16 years.
Find out more about him here: edwinjones.me.uk
H
ow can we make our own computer simple specification to implement. The CHIP-8
with Python? Don’t computers need only has 35 opcodes, which is low compared
lots of physical things like circuit to other computers. You still have all the usual
boards and electronics? Normally problems to solve – loading binary files, parsing
yes, they do, but we’re going to opcodes, and then running them on virtual
create something known as a ‘virtual machine’ – a hardware – but not the other baggage that comes
computer defined completely by software, not with trying to write emulators for other systems.
hardware. You’ve probably used virtual machines You also get to finish the project by playing some
many times without realising it. Many modern classic games – what’s not to like?
games consoles run older, classic games via We’ll be using Python 3 and Pygame for the
virtual machines, also known as emulators. project because CHIP-8 isn’t very demanding
There are many kinds of emulator, but one and can be easily emulated. This means it’s a
of the most fun to start with is the CHIP-8. The good fit for Python, as we don’t need to worry
CHIP-8 was never a real computer, but that’s so much about performance and can use a
what makes it fascinating: long before most other language that makes solving the problems at
games systems, it existed as a program for 8-bit hand a bit easier – we don’t need to focus as
computers in the 1970s. This means there’s no much on smaller details.
hardware to attempt to emulate and quite a A simple way to build such a device in code
is with a class representing the CPU, so let’s go
ahead and create a Cpu class. Create a folder
called pychip8 on your machine and save our
code below there with the file name cpu.py. We’ll
be using this folder for every module we create.
class Cpu:
“””this class represents the CHIP 8 cpu”””
48 / wfmag.cc
Toolbox
Write your own emulator in Python
def __init__(self):
# 4k of RAM
self.ram = [0] * 4096
self.program_counter = self.PROGRAM_START_ here are all kinds of simple
T
games available for CHIP-8,
ADDRESS from puzzlers to dinky
for us to use later. You can see how this works in Space Invaders clones.
You’ll find a good selection
self.index_register = 0 the code below – save this into your folder with at wfmag.cc/chip-roms.
self.general_purpose_registers = [0] * 16 the file name rom_loader.py.
self.delay_timer = 0 import os
Can you see how we’ve defined the registers Here we’re opening and reading the file in binary
and buffers from the system’s specification into mode with the “rb” options string from the given
simple Python types and collections? Don’t worry file path and returning it from the get_rom_bytes
about understanding all this code right now, as method as a bytes object. This means we can
we’ll be plugging in more parts of the computer then iterate over each byte in turn to decode
as we go. This is just the ‘heart’ of the machine each instruction for the CHIP-8.
and where our core logic will run. How can we parse the raw binary from a file
The CHIP-8, like almost all computers, works and turn that into instructions for the CPU? The
by following a sequence CHIP-8 has a strange
of instructions. We’re “There are many emulators, opcode definition, in that
going to load up files each opcode is a 16 bit/2
representing these
but one of the most fun byte value that contains
instructions in a binary to start with is CHIP-8” the instruction and the
format (called ROMs) and data for that instruction.
process each instruction one by one in our CPU. This would be simple enough were it not for
The name for such an instruction is sometimes the fact that apart from the first four bits, each
known as – you guessed it – an opcode! following nibble can be part of an instruction or
The problem we face now is how can we get data – depending on the instruction in question.
the data from a file into memory so our code can For instance, the opcode 0x00E0 means ‘clear
work with it? We can do this with a simple ‘ROM the screen’, but opcode 0x1234 means ‘jump to
loader‘ module that reads all the binary into a list memory address 0x234’. A nibble might sound
wfmag.cc \ 49
Toolbox
Write your own emulator in Python
50 / wfmag.cc
Toolbox
Write your own emulator in Python
run CHIP-8 straight from
as stubbing, and can be a useful approach to your command line, like this.
working on a problem when you aren’t entirely return operations.set_x_to_y
sure what the solution will be, but do need to if opcode.n == 0x1:
have some idea of the interface you’ll be using to return operations.bitwise_or
solve it. if opcode.n == 0x2:
return operations.bitwise_and
from operation_code import Opcode if opcode.n == 0x3:
import operations return operations.bitwise_xor
if opcode.n == 0x4:
def find_operation(word): return operations.add_y_to_x
opcode = Opcode(word) if opcode.n == 0x5:
return operations.take_y_from_x
if word == 0x00E0: if opcode.n == 0x6:
return operations.clear_display return operations.shift_x_right
if opcode.n == 0x7:
if word == 0x00EE: return operations.take_x_from_y TECH SPECS
return operations.return_from_function if opcode.n == 0xE: The CHIP-8 has the following
return operations.shift_x_left specifications:
if opcode.a == 0x1:
• 4 kB of memory
return operations.goto if opcode.a == 0x9:
return operations.skip_if_x_y_not_equal
• 1 6 general-purpose 8-bit
registers (the 16th is
if opcode.a == 0x2: also used as a special
return operations.call_function if opcode.a == 0xA: arithmetic flag at times)
return operations.set_i
if opcode.a == 0x3:
• A 64 pixel-wide by 32 pixel-
high frame buffer
return operations.skip_if_equal if opcode.a == 0xB:
return operations.goto_plus
• A stack that stores return
addresses for function
if opcode.a == 0x4: calls and nothing else
return operations.skip_if_not_equal if opcode.a == 0xC:
return operations.generate_random
• S ome way to store
the pressed state of a
if opcode.a == 0x5: 16-button keypad (0–F)
return operations.skip_if_x_y_equal if opcode.a == 0xD:
return operations.draw_sprite
• A n 8-bit index register
if opcode.a == 0x6: • A n 8-bit program counter
return operations.set_x if opcode.a == 0xE: • A n 8-bit stack pointer
if opcode.nn == 0x9E: • A n 8-bit delay timer
if opcode.a == 0x7: return operations.skip_if_key_pressed register
return operations.add_to_x if opcode.nn == 0xA1: • A n 8-bit sound timer
return operations.skip_if_key_not_pressed register (when above zero,
if opcode.a == 0x8: a beep is made)
if opcode.n == 0x0: if opcode.a == 0xF:
wfmag.cc \ 51
Toolbox
Write your own emulator in Python
it’s a tiny, functioning are a few opcodes that handle drawing to the 0xF0, 0x90, 0xF0, 0x90, 0xF0, # 8
version of Space
0xF0, 0x90, 0xF0, 0x10, 0xF0, # 9
Invaders. Pretty neat.
0xF0, 0x90, 0xF0, 0x90, 0x90, # A
0xE0, 0x90, 0xE0, 0x90, 0xE0, # B
0xF0, 0x80, 0x80, 0x80, 0xF0, # C
0xE0, 0x90, 0x90, 0x90, 0xE0, # D
0xF0, 0x80, 0xF0, 0x80, 0xF0, # E
0xF0, 0x80, 0xF0, 0x80, 0x80] # F
52 / wfmag.cc
Toolbox
Write your own emulator in Python
Crosses. Perfect if you
import pygame don’t have a pen and
if event.key in keys: piece of paper handy.
keys = {} cpu.key_up(keys[event.key])
keys[pygame.K_0] = 0x0
keys[pygame.K_1] = 0x1 This code defines a dictionary to map keyboard
keys[pygame.K_2] = 0x2 input into and a function we can use to check
keys[pygame.K_3] = 0x3 which keys are pressed. We then pass that data
keys[pygame.K_4] = 0x4 into our CPU. Handy!
keys[pygame.K_5] = 0x5 The last missing part of our emulator is the
keys[pygame.K_6] = 0x6 renderer. A renderer is simply some code that
keys[pygame.K_7] = 0x7 can take raw data and instructions and turn
keys[pygame.K_8] = 0x8 those into some form of visible output. We’ll use
keys[pygame.K_9] = 0x9 Pygame Zero to draw to our screen and we can
keys[pygame.K_a] = 0xA define our renderer like so, remembering that
keys[pygame.K_b] = 0xB the CHIP-8 was limited to a 64×32 resolution with
keys[pygame.K_c] = 0xC black and white output. The following needs to be
keys[pygame.K_d] = 0xD saved with the file name renderer.py:
keys[pygame.K_e] = 0xE
keys[pygame.K_f] = 0xF “””This module defines the renderer object and
related methods”””
def handle_input(cpu=None):
“”” import pygame
This function handles control input for this
program. WHITE = pygame.Color(255, 255, 255)
“”” BLACK = pygame.Color(0, 0, 0)
for event in pygame.event.get():
SCALE = 10
# quit if user presses exit or closes the
window # The CHIP-8 display ran at only 64 * 32 pixels.
if event.type == pygame.QUIT: # this value scales the framebuffer
sys.exit() # so it’s easier to view the emulator on modern
displays
# check cpu registers and inject key input screen = pygame.display.set_mode((640, 320))
if cpu:
if event.type == pygame.KEYDOWN: def render(frame_buffer):
if event.key == pygame.K_ESCAPE: “””This method draws everything to the screen”””
sys.exit()
screen.fill(BLACK)
if event.key in keys:
cpu.key_down(keys[event.key]) for x in range(64):
for y in range(32):
if event.type == pygame.KEYUP: if frame_buffer[x][y]:
wfmag.cc \ 53
Toolbox
Write your own emulator in Python
def move_to_next_instruction(self):
self.program_counter += Cpu.WORD_SIZE_IN_
BYTES
def move_to_previous_instruction(self):
igure 1: If the bc_test.ch8
F self.program_counter -= Cpu.WORD_SIZE_IN_
54 / wfmag.cc
Toolbox
Write your own emulator in Python
as there is a comment in each one to help you 480, which is close enough.
understand the code. We’ll need this for the next for _ in range(8):
step as we’re going to revisit our operation class cpu.emulate_cycle()
and fill in the blanks we left earlier. This is going
to be quite a lot of code – as before, it's too much cpu.update_timers()
to print here, so download the file operations.py renderer.render(cpu.frame_buffer)
from our GitHub at wfmag.cc/wfmag54. Don’t
worry too much about what each method does, # delay until next frame.
focus more on what opcode it relates to and what clock.tick(60)
functionality it achieves for our emulator. There
are several comments to help you understand The code above sets up all the dependencies,
each opcode, if you’re interested. loads the ROM data, and runs the core loop of
We only need to write one more module to get the CHIP-8 CPU, which will read and evaluate
our emulator working and to see our hard work instruction by instruction. Don’t worry about
pay off. We need an entry point to our code that “running out”, as almost every ROM loops; they're
initialises and runs it in the right order. Let’s go not designed to terminate without user input.
ahead and create a file called __main__.py (NB: The last thing we’ll need is a ROM to test this
there are two underscores either side of the out with. First, let’s use a debug ROM – the best
word ‘main’ that will handle all this for us.) I’ve found is called bc_test created by BestCoder.
Create a folder called roms in your working
“””This is the main entry point for the program””” directory and copy bc_test.ch8 into it from this
repository: wfmag.cc/chiptest.
import argparse With the ROM in place, open the directory that
import keyboard_input contains your pychip8 folder in your terminal
import renderer of choice and run the command python pychip8
import rom_loader --rom=”bc_test.ch8”. You should see something
import pygame like the output in Figure 1. If you do, have a round
of applause – you have a working emulator! If you
from cpu import Cpu see nothing on the screen, the error’s likely in
the renderer module; if you see a screen like the
if __name__ == “__main__”: one in Figure 2, then you’ll be able to work out
what’s gone wrong by checking the error codes at
parser = argparse.ArgumentParser() wfmag.cc/error-codes.
parser.add_argument(“-r”, “--rom”, Once you’re sure the test passes, it’s time for
required=True, type=str, help=”the name of the rom real games! There’s plenty of ROMs available
to run in the emulator”) for the CHIP-8 – you’ll find several at wfmag.cc/
args = parser.parse_args() chip-roms. Just copy the files into your roms
folder and run the command, replacing the test
cpu = Cpu() ROM name with the name of the ROM you want
clock = pygame.time.Clock() to run, and bingo: you’re playing games on a
rom_bytes = rom_loader.get_rom_bytes(args.rom) computer that you wrote yourself!
cpu.load_rom(rom_bytes)
pygame.display.set_caption(“pychip8”)
pygame.init()
# main loop
while True:
keyboard_input.handle_input(cpu)
igure 2: If running the
F
wfmag.cc \ 55
Toolbox
Source Code
Golf on the cover…
Source Code
Code a Spectrum-style
AUTHOR
Crazy Golf game
MARK VANSTONE Putt the ball around irrational obstacles in our take on golf
F
irst released by Mr. Micro in bounce angles. Here, we’re only going to collided with a surface by sampling the
1983 – then under the banner have horizontal and vertical walls, so we can colours of the pixels from the collision map.
of Sinclair Research – Krazy Golf use some fairly straightforward maths to If the pixel’s blue, we know that the ball
was, confusingly, also called calculate more precisely the new angle as has hit a vertical wall; if it’s red, the wall’s
Crazy Golf. The loading screen the ball bounces off a surface. In the original horizontal. We then calculate the new angle
featured the Krazy spelling, but on the game, the ball was limited to only 16 angles, for the ball. If we mark the hole as black,
cover, it was plain old Crazy Golf. Designed and the ball moved at the same speed then we can also test for collision with that –
for the ZX Spectrum, the game provided regardless of the strength of the shot. We’re if the ball’s in the hole, the game ends.
nine holes and a variety of obstacles to putt going to improve on this a bit so that there’s We have our ball bouncing mechanism,
the ball around. Crazy Golf was released at more flexibility around the shot angle; we’ll so now we need our user interaction
a time when dozens of other games were also get the ball to start moving fast and system. We’ll use the left and right arrow
hitting the Spectrum market, and although then reduce its speed until it stops. keys to rotate our pointer, which designates
it was released under the Sinclair name To make this work, we need to have a the direction of the next shot. We also need
and reviewed in magazines such as Crash, way of defining whether an obstruction is a range-setting gizmo, which will be shown
it didn’t make much impact. The game horizontal or vertical, as the calculation is as a bar at the top of the screen. We can
itself employed a fairly rudimentary control different for each. We’ll have a background make that grow and shrink with the up and
system, whereby the player selects the angle graphic showing the course and obstacles, down arrows. Then when we press the
of the shot at the top left of the screen, sets but we’ll also need another map to check RETURN key, we transfer the pointer angle
the range via a bar along the top, and then our collisions. We need to make a collision and the range to the ball and watch it go.
presses the RETURN key to take the shot. map that just has the obstacles on it, so We ought to count each shot so that we
If you’ve been following our Source Code we need a white background; mark all the can display a tally to the player once they’ve
articles each month, you will have seen horizontal surfaces red and all the vertical putted the ball into the hole. From this
the pinball game where a ball bounces surfaces blue. As we move the ball around point, it’s a simple task to create another
off various surfaces. In that example, we the screen (in much the same way as our eight holes – and then you’ll have a full crazy
used a few shortcuts to approximate the pinball game) we check to see if it has golf game!
64 / wfmag.cc
Toolbox
Source Code
Download
Crazy Golf in Python the code
from GitHub:
wfmag.cc/
Here’s Mark’s code for a nifty top-down golf game. To get it running on your system, you’ll need to wfmag54
install Pygame Zero. Full instructions are available at wfmag.cc/pgzero.
def on_key_down(key):
if gamestate == 0:
if key.name == “RETURN”: hitBall(pointer.
angle,shotrange/100)
def hitBall(a,s):
global shots
ball.speed = s
…but weirdly, the loading screen spelled the name as Krazy
wfmag.cc \ 65