0% found this document useful (0 votes)
81 views13 pages

Pygame Zero: e e R Z o

This document provides code for a Light Cycle minigame inspired by the arcade classic Tron. The summary includes: - The code uses Pygame Zero to create a 2D matrix to track the player's trail as they move around the screen avoiding walls and other trails. - Each update, the player's position is incremented based on their direction and speed. Collisions are detected if their new position has been visited within the last 15 updates. - When a collision occurs, the game state changes to trigger an explosion animation sequence before resetting. Controls use arrow keys to change direction and space to restart.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
81 views13 pages

Pygame Zero: e e R Z o

This document provides code for a Light Cycle minigame inspired by the arcade classic Tron. The summary includes: - The code uses Pygame Zero to create a 2D matrix to track the player's trail as they move around the screen avoiding walls and other trails. - Each update, the player's position is incremented based on their direction and speed. Collisions are detected if their new position has been visited within the last 15 updates. - When a collision occurs, the game state changes to trigger an explosion animation sequence before resetting. Controls use arrow keys to change direction and space to restart.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 13

z

e r
o

Pygame Zero

• Wireframe54 - How to write your own emulator in Python


• Wireframe54 - Code a Spectrum-style Crazy Golf game
• Wireframe47 - Code a Light Cycle Arcade Mini Game
Toolbox
Source Code

The TRON cab had


two game controllers:


a rotar w eel and
a jo stick

 attle a ainst
enemies in t e ori inal
arcade classic Source Code

Code a Light Cycle


AUTHOR
arcade minigame
MARK VANSTONE Speed around an arena, avoiding walls and deadly trails

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

Light Cycles in Python Download


the code
from GitHub:
Here’s Mark’s code for a Light Cycle minigame straight out of TRON. To get it wfmag.cc/
working on your system, you’ll need to install Pygame Zero – full instructions are wfmag47
available at wfmag.cc/pgzero.

# 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

was released in t e same screen.draw.text(“SCORE : “+ str(score), center=(400, 588), owidth=0.5,


ear alt isne ocolor=(0,255,255), color=(0,0,255) , fontsize=28)
rod ctions motion ict re
TRON was released
def update():
global matrix,gamestate,score
if gamestate == 0:
the angle the player is pointing, and another bike.angle = dirs[bike.direction]
list of x and y increments for each direction. bike.x += moves[bike.direction][0]*speed
Each update we add x and y coordinates to bike.y += moves[bike.direction][1]*speed
score += 10
the Cycle actor to move it in the direction
if matrix[int(bike.x/10)][int(bike.y/10)] < 15 :
that it’s pointing multiplied by our speed
matrix[int(bike.x/10)][int(bike.y/10)] += 1
variable. We have an on_key_down() function else:
defined to handle changing the direction of gamestate = 1
the Cycle actor with the arrow keys. if bike.x < 60 or bike.x > 750 or bike.y < 110 or bike.y > 525:
We need to wait a while before checking gamestate = 1
for collisions on the current position, as the else:
if gamestate < 18:
ycle won’t have moved away for several
bike.image = “bike”+str(int(gamestate/2))
updates, so each screen position in the bike.angle = dirs[bike.direction]
matrix is actually a counter of how many gamestate += 1
updates it’s been there for. We can then test
to see if 15 updates have happened before def on_key_down(key):
testing the square for collisions, which gives if key == keys.LEFT:
bike.direction += 1
our Cycle enough time to clear the area.
snapBike()
If we do detect a collision, then we can if bike.direction == 4 : bike.direction = 0
start the game-end sequence. We set the if key == keys.RIGHT:
gamestate variable to 1, which then means bike.direction -= 1
the update() function uses that variable snapBike()
as a counter to run through the frames of if bike.direction == -1 : bike.direction = 3
if key == keys.SPACE and gamestate == 18:
animation for the ycle’s explosion. nce it
init()
reaches the end of the sequence, the game
stops. We have a key press defined the def snapBike():
SPACE bar) in the on_key_down() function to bike.x = int(bike.x/10)*10
call our init() function, which will not only bike.y = int(bike.y/10)*10
set up variables when the game starts but
sets things back to their starting state. def init():
global bike,matrix,gamestate,score
So that’s the fundamentals of the
bike = Actor(‘bike1’, center=(400, 500))
player Light Cycle movement and collision bike.direction = 0
checking. To make it more like the original matrix = [[0 for y in range(60)] for x in range(80)]
arcade game, why not try experimenting gamestate = score = 0
with the code and adding a few computer-
controlled rivals? init()

wfmag.cc \ 65
Toolbox
Write your own emulator in Python

How to write your own


Download
the code
emulator in Python
from GitHub:
wfmag.cc/ Ever wanted to create your own computer? Here’s how to do
wfmag54
just that – and even get it to run some classic games

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.

“this module defines the chip 8 cpu”

class Cpu:
“””this class represents the CHIP 8 cpu”””

# game ram begins at address 0x200 / 512


PROGRAM_START_ADDRESS = 0x200
 All being well, your pychip8
# the chip 8 works with 16 bit/2 byte opcodes
folder should look like this
when you’re finished. WORD_SIZE_IN_BYTES = 2

48 / wfmag.cc
Toolbox
Write your own emulator in Python

# V[15/0xF] is used as a carry/no borrow flag


for certain ops
ARITHMETIC_FLAG_REGISTER_ADDRESS = 0xF
FRAME_BUFFER_WIDTH = 64
FRAME_BUFFER_HEIGHT = 32

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

self.stack = [] def get_rom_bytes(rom_name):


self.stack_pointer = 0 “””This method loads the bytes from a rom from
the roms folder”””
self.keys = set() folder = os.path.dirname(os.path.realpath(__
file__))
self.frame_buffer = [[bool()] * 32 for i rom_folder = os.path.join(folder, “roms”)
in range(64)] rom_file_path = os.path.join(rom_folder,
rom_name)
self._load_font()
with open(rom_file_path, “rb”) as file:
self._current_word = 0 file_bytes = file.read()
self._current_operation = None return file_bytes

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

self.word = word & 0xFFFF

# we just want the most significant bits/


nibble
# here so we bitshift right
self.a = (word & 0xF000) >> 12

self.nnn = word & 0x0FFF


self.nn = word & 0x00FF
self.n = word & 0x000F

# Where don’t use the lower nibbles,


bitshift
# right to get just the raw value
self.x = (word & 0x0F00) >> 8

# Eg. we want 0x4 not 0x40


 e sure to get the full
B self.y = (word & 0x00F0) >> 4

CHIP-8 code from wfmag.


cc/wfmag54, where strange, but it’s just half a byte. A byte is 8 bits
you’ll find the operations.
py module, and more
and can be represented with two hex digits so Don’t worry if you don’t understand all this
detailed comments that 0xFF means 255. A nibble can be represented code at once. The important thing to know is
we couldn’t fit in here.
with one hex digit (e.g. 0xF for 15). This will come that we’re parsing a 16-bit value and storing it
in handy later when we’re manipulating our in different fields of the class. opcode.a is the
opcode data. One way leftmost nibble, x the
to solve this problem is “Don’t worry if you next one along, y the
to abstract it into a class, one after that and n
which is exactly what
don’t understand all the very last. You can
we’re going to do. this code at once” remember this with the
Our next piece of hex-like pattern 0xAXYN.
Python code is going to be an Opcode class, as opcode.word, opcode.nn, and opcode.nnn are ways
shown below. Again, save this into your folder, of accessing these nibbles in groups. This will
this time with the file name operation_code.py. come in handy later on.
Now we have a way to read the ROM and parse
class Opcode: the contents of it, but what can we actually do
“””This class represents the instructions and with all this data? The CHIP-8 has 35 opcodes,
data of an opcode””” but we shall only be using 33 of them. For
simplicity, we're ignoring sound and any machine
def __init__(self, word): code operations. You can find a list of them on
“”” Wikipedia at wfmag.cc/opcode.
This class takes in a 2 byte value/word First, let’s define a module that turns
and parses the bytes these opcodes into readable functions we
to store them in different attributes for can reference. Hex is fun, but it’s not easy to
later use understand what the raw numbers mean at
times. The following bit of code is too long to
Args: run in full here, but you’ll find it on our GitHub
word: a 2 byte/16 bit value that at wfmag.cc/wfmag54. It’s the file named
represents an opcode. operations.py – here’s a small snippet:
“””
def add_to_x(opcode, cpu):
# We use bitwise AND with a mask to pass
extract specific nibbles.
def add_x_to_i(opcode, cpu):
# a word should be no more than 16 bits pass

50 / wfmag.cc
Toolbox
Write your own emulator in Python

def add_y_to_x(opcode, cpu):


pass

Don’t worry that all these functions contain a pass


statement at the moment; the important thing is
that each one has the same signature and a clear
name. Now we need to make another module,
called operation_mapping.py, that lets us map
the raw binary/opcode to one of the functions
we’ve defined above. Modelling code without
a full implementation like this is often known  nce it’s complete, you can
O


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

if opcode.nn == 0x07: screen, loading fonts, and input handling, so we


return operations.set_x_to_delay_timer need to define code that can manage this logic
if opcode.nn == 0x0A: for us. Let’s start with the internal font of the
return operations.wait_for_key_press machine. The CHIP-8 has a single font that is
if opcode.nn == 0x15: hard-coded into memory, and usually stored in
return operations.set_delay_timer the first 512 bytes of memory (0x000–0x200). The
if opcode.nn == 0x18: font is basically a pattern of bits that describes
return operations.set_sound_timer the dots that make up each letter. Let’s make a
if opcode.nn == 0x1E: module with this pattern as shown below, and
return operations.add_x_to_i save it as font.py.
if opcode.nn == 0x29:
return operations.load_character_ “””
address Chars are 4x5. Each line must be a byte wide so
if opcode.nn == 0x33: each line value is padded with 0x0.
return operations.save_x_as_bcd Eg:
if opcode.nn == 0x55: 0:
return operations.save_registers_zero_ #### = 1111 = F0
to_x # # = 1001 = 90
if opcode.nn == 0x65: # # = 1001 = 90
return operations.load_registers_zero_ # # = 1001 = 90
to_x #### = 1111 = F0
“””
raise KeyError(f”Opcode {word:#06x} not
present in list of valid operations”) # each char is 5 bytes long, so to get
# char ‘B’ you’d use DATA[CHAR_SIZE_IN_BYTES *
Now we’re cooking! This code will take a block of 0xB]
16 bits we load from a ROM file, parse this into CHAR_SIZE_IN_BYTES = 5
our custom Opcode class and use the properties
of that class to figure out what corresponding DATA = [0xF0, 0x90, 0x90, 0x90, 0xF0, # 0
functionality the opcode is referencing. If you 0x20, 0x60, 0x20, 0x20, 0x70, # 1
refer to the table on the previous pages, you 0xF0, 0x10, 0xF0, 0x80, 0xF0, # 2
should be able to see exactly what our functions 0xF0, 0x10, 0xF0, 0x10, 0xF0, # 3
are going to actually do by looking up the 0x90, 0x90, 0xF0, 0x10, 0x10, # 4
related opcode. 0xF0, 0x80, 0xF0, 0x10, 0xF0, # 5
Before we can wire this up further, we’re going 0xF0, 0x80, 0xF0, 0x90, 0xF0, # 6
to need to add some extra functionality. There 0xF0, 0x10, 0x20, 0x40, 0x40, # 7
 es, it’s a bit blocky, but
Y

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

The code above is a layout of a series of nibbles


in a list format that we can access, and each 5
bytes represents the raw pixels of a character.
Each “1” equals a lit pixel for each character; each
character is 4 bits across and consists of 5 rows.
As we only use a nibble, only the most significant
bits of each byte are used – that is why each
value ends with 0.

52 / wfmag.cc
Toolbox
Write your own emulator in Python

Next, we’re going to need some way to handle


input. The CHIP-8 had a very simple input
mechanism – 16 keys from 0–F. This fits exactly
into a nibble. Let’s define a module to map our
keyboard keys to those values. Save the following
as keyboard_input.py:

“””This module contains the keyboard input


handling logic”””

import sys CHIP-8 Noughts and


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

“This method sets a key as released”


if key in self.keys:
self.keys.remove(key)

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_

ROM shows this message,


then congratulations: you BYTES
have a functioning CHIP-8 pygame.draw.rect(
emulator, and you’re ready
to play some games! screen, def load_rom(self, rom_bytes):
WHITE, for i, byte_value in enumerate(rom_bytes):
(x * SCALE, y * SCALE, SCALE, self.ram[Cpu.PROGRAM_START_ADDRESS +
SCALE)) i] = byte_value

# Go ahead and update the screen with what def set_arithmetic_flag(self):


we’ve drawn. self.general_purpose_registers[self.
# This MUST happen after all the other drawing ARITHMETIC_FLAG_REGISTER_ADDRESS] = 1
commands.
pygame.display.update() def clear_arithmetic_flag(self):
self.general_purpose_registers[self.
The code above takes the frame buffer of the CPU ARITHMETIC_FLAG_REGISTER_ADDRESS] = 0
and iterates through it, drawing a 10×10 white
square for every pixel that should be set. This gives def emulate_cycle(self):
us a larger window to see the emulator running, self._current_word = self.fetch_word()
and will be easier to use on a larger, modern
display. A frame buffer is just a section of memory opcode = Opcode(self._current_word)
that stores things you want to show on a display self._current_operation = operation_
monitor or TV. In our case, it’s just a 2D list of mapping.find_operation(self._current_word)
boolean values representing ‘on’ or ‘off’ for a pixel
that we defined in our Cpu class earlier. self.move_to_next_instruction()
We’re getting closer to having something self._current_operation(opcode, self)
work as all the core components of our system
are now wired in. There’s a bit more code we def fetch_word(self):
need to add to our CPU now we’ve defined all its word = self.ram[self.program_counter] << 8
dependencies. Let’s go back and add some extra | self.ram[self.program_counter + 1]
imports to the top of our CPU module, cpu.py:
return word
from operation_code import Opcode
import operation_mapping def update_timers(self):
import font if self.delay_timer > 0:
self.delay_timer -= 1
Now append the code below to the end of the
CPU module: def _load_font(self):
offset = 0x0
def key_down(self, key): for item in font.DATA:
“This method sets a key as pressed” self.ram[offset] = item
if key not in self.keys: offset += 1
self.keys.add(key)
If you read through the code above, you should
def key_up(self, key): be able to understand what each method is doing

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

bc_test.ch8 ROM gets you a


# The CHIP-8 is reported to run best at message like this, then you
can troubleshoot your
around 500 hz
emulator with the error list
# The update loop runs at 60 fps. 60 * 8 = at wfmag.cc/error-codes.

wfmag.cc \ 55
Toolbox
Source Code

The pointer’s angle is


rotated using degrees, but


we’ll use radians for our
ball direction as it will
simplify our movement
and bounce calculations.

 he game was called Crazy


T


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.

# Crazy Golf shots += 1


import pgzrun
import math def moveBall():
from pygame import image, Color ball.x += ball.speed * math.sin(ball.dir)
ball.y += ball.speed * math.cos(ball.dir)
collisionMap = image.load(‘images/collision.png’)
pointer = Actor(‘pointer’,center=(90,85)) def checkBounce():
pointer.angle = 0 global gamestate
ball = Actor(‘ball’, center=(100,150)) rgb = collisionCheck()
ball.speed = ball.dir = 0 if rgb == Color(“black”):
gamestate = shots = 0 gamestate = 1
shotrange = 300
def collisionCheck():
def draw(): r = 4
screen.blit(“background”, (0, 0)) cl = [(0,-r),(r,0),(0,r),(-r,0)]
ball.draw() for t in range(4):
pointer.draw() rgb = collisionMap.get_at((int(ball.x)+cl[t]
screen.draw.filled_ [0],int(ball.y)+cl[t][1]))
rect(Rect((180,5),(shotrange,10)),(255,0,0)) if rgb != Color(“white”):
screen.draw.text(“SHOT RANGE:”, topleft = (20, if rgb == Color(“blue”):
2),color=(0,0,0) , fontsize=28) ball.dir = (2*math.pi - ball.dir)%(2*math.pi)
if gamestate == 1 : screen.draw.text(“YOU SUNK THE BALL IN if rgb == Color(“red”):
“ + str(shots) + “ STROKES”, center = (400, 300), owidth=0.5, ball.dir = (3*math.pi - ball.dir)%(2*math.pi)
ocolor=(255,255,0), color=(255,0,0) , fontsize=50) return rgb

def update(): def limit(n, minn, maxn):


global shotrange return max(min(maxn, n), minn)
if gamestate == 0:
if keyboard.left: pgzrun.go()
pointer.angle += 5
if keyboard.right:
pointer.angle -= 5
if keyboard.up:
shotrange = limit(shotrange + 10, 0, 600)
if keyboard.down:
shotrange = limit(shotrange - 10, 0, 600)
checkBounce()
moveBall()
ball.speed = limit(ball.speed-0.01, 0, 10)

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

ball.dir = math.radians(a) Golf. The early games industry was strange.

wfmag.cc \ 65

You might also like