Tutorial: ZX Spectrum Machine Code Game in 30 Minutes!
This tutorial by Jon Kingsman (bigjon) originally appeared in a thread on WoSF. Reproduced with permission.
Roadrace game by bigjon, incorporating suggestions from Dr Beep, skoolkid and Matt B
Hi folks, I’m a machine code novice who coded a very small roadrace game to help me learn.
I reckon you can learn the basics of machine code in half an hour by coding this game step by step.
This tutorial assumes you have a working knowledge of ZX Spectrum basic, and the ZX Spin emulator.
Make yourself a large cup of tea – by the time you’ve drunk it, you be able to program in machine code!
CHAPTER 1 – Create a machine code function that returns the score to BASIC
Machine code programs are a series of bytes in the Spectrum’s memory.
In this chapter we will
- – Use the Spin assembler to write a few bytes into the memory generating a score for our game.
- – Write a BASIC program to run the machine code and print the score we have obtained from it.
Open ZX Spin. Select Tools -> Z80 Assembler.
To run our roadrace game, we need to execute the following steps:
MAIN ;label for main section of program as opposed to graphics data etc ;arrange to put our machine code at free, fast-running (over 32768) and memorable address in RAM org 33000 ;initialise score ;initialise road, car PRINCIPALLOOP ;label for the loop in the game that will execute over and over ;read keyboard ;set new carposition ;crash? if so, go to GAMEOVER. ;print car ;scroll road ;random road left or right ;jump back to PRINCIPALLOOP GAMEOVER ;label for the cleaning up that needs to be done before returning to BASIC ;return score to BASIC
Copy and paste the paragraph above into the Spin Assembler.It will appear as 15 lines of mainly grey text.
The text is grey because text after a ; is a comment. The assembler ignores it but it’s there for our benefit.
You can TAB comments over towards the right-hand side of the assembler page to make your code more readable.
The labels are in pink.The assembler won’t put anything in RAM for them but will use them as entry points to jump to.
In the assembler, do File -> Save as and type something like mc30mintut.asm into the save box.
We’ll do the first and last of these steps in this chapter, starting with the last one.
The assembly language instruction for ‘return to calling program’ (in our case a BASIC routine) is ‘ret’.
Click on the end of line 15, press enter to create line 16 and type ret
The word ret‘ appears in blue. This is Spin’s colour code for an instruction.
When the Spin assembler gets to the instruction ret it writes the byte 201 into the memory at an address we choose.
The computer knows that the first byte it meets will be an instruction byte.
It does something different for each byte from 0 to 255. There’s a list in Appendix A of the Spectrum Manual.
Some instruction bytes, like 201 for ret, are complete as is – no further info is needed to complete the action.
Some instruction bytes need one or two bytes of data afterwards for the computer to know what to do.
Some other instruction bytes need a further instruction byte to clarify the action required.
Now we’ll initialise the score, ready for the mc program to report it back to BASIC at the end of the game.
The computer does most of its work using 7 temporary byte-sized addresses called registers.
The first register deals with single bytes only (numbers 0 to 255), the other six are in pairs to deal with 0 to 65535.
The first (single-byte) register is called the A register, sometimes also referred to as the accumulator.
The other three register pairs are called BC, DE, and HL (H is for High byte, L is for Low byte)
Any machine code function called from basic will return the value from 0 to 65535 in the BC register.
We will write the value 0 into the BC register, ready to increase it by 1 each time the game goes round its principal loop.
At the beginning of line 4, type ld bc,0. ld is the instruction for load a value into a register or register pair.
The instruction byte for ld is different for each register or register pair that is loaded.
The instruction byte for ld bc is 1. The computer then expects 2 data bytes to give it a number from 0 to 65535.
In our case the two data bytes will be 0,0. So the assembler will write 1,0,0,201 at address 33000 in RAM.
We’ll assemble this code now. Do File -> Save, then File -> Assemble. Type 33000 into the Start Address box and click OK.
At the bottom window of the assembler you should see a report that says “No errors in 16 lines. 4 bytes generated”.
You can see the four bytes are now at memory address 33000 by clicking in the main Spin display window on Tools -> Debugger.
To run these four bytes of machine code, enter this one-line program in the main Spin display window:
10 PRINT AT 0,0; “Your score was “; USR 33000
Now RUN the program. Did you get “Your score was 0”? Congratulations – you have coded your first machine code program!
Do File -> Save in the main Spin display window and save as something like mc30mintut.sna. Here ends Chapter 1!
CHAPTER 2 – Display material on the screen.
There are two areas of the Spectrum’s memory which have a direct effect on the screen display.
The complicated way is the display file, from addresses 16384 to 22527, which stores a dash of 8 pixels per byte.
Try POKE-ing 255 into bytes within this range to see the funny order in which this memory area is mapped onto the screen.
The simple way is the attribute file, from 22528 to 23296, which affects an 8×8 pixel block per byte, in logical order.
In this chapter we will
- Draw our ‘car’ by changing the paper colour of one character square to blue.
- Draw our ‘road’ by using a loop to create two vertical stripes of black paper colour down the screen.
In the spin assembler line 5, delete the word ‘road’ in the comments.
At the beginning of line 5, type ld hl,23278. This points HL to the middle of the bottom row in the display file.
Insert line 6, ld a,8. This puts a blue PAPER colour into the A register. Why 8? See the BASIC manual chapter 16.
Insert line 7, ld (hl),a. The brackets round hl mean the load will be to the address in RAM that hl is pointing to.
Insert line 8, ld (32900),hl ;save car posn. We’ll store the attribute file address of the ‘car’ in some free bytes in RAM.
Now for the road. Insert line 4, ld hl,22537 ;initialise road. This points to a third of the way along the top line.
To save the road position, which we’ll need frequently, we’ll let the computer choose where to store it, on its ‘stack’.
Chapter 24 of the manual has a diagram showing where the machine stack is in the RAM.
To write to the stack we use push. To write from the stack we use pop. What goes on the stack first will come off last.
Insert line 5, push hl ;save road posn. Insert line 21, pop hl ;empty stack
To print a black road we need 0 in the accumulator (ch16 of the BASIC manual).
We could do ld a, 0 but this takes 2 bytes whereas xor a takes only one. Insert line 6, xor a.
xor compares the chosen register to the A register and puts a 1 in the A register for each bit that is different.
We’ll print the top line of the road. Two double squares of black with a 9-square gap between them.
Insert line 7, then copy and paste the following code:
ld (hl),a inc hl ;inc increases the register by one, dec decreases it by one. ld (hl),a ld de,9 ;for a 9-space gap in the road. add hl,de ;add adds the registers together, so hl points to the right hand side of the road. ld (hl),a inc hl ld (hl),a
To get hl to point to the left hand verge on the next line, we need to move 21 bytes further in the attribute file.
Insert line 15, ld de, 21 ;point to left verge on next line
Insert line 16, add hl,de
To fill the screen with the road we will use machine code’s equivalent of a FOR-NEXT loop, djnz.
djnz stands for Decrement then Jump if Not Zero. We load the b register with the number of times we want to loop.
Insert line 7, ld b,24 ;print road verge on 24 lines.
Insert line 8, fillscreen – this is the label for our loop to jump back to.
Insert line 19, djnz fillscreen.
Because our routine will continue from the loop when b=0, we no longer need to initialise b as well as c to 0 in the next line.
Change line 20 ld bc, 0 to ld c,b. This is one byte shorter.
Assemble and save. If you want to see the blue ‘car’, you’ll need to add something like 20 PAUSE 0 to your basic program.
CHAPTER 3 – move the car, test for collision.
Time to start playing the game! First we need to erase the car ready to move it if the player wants to.
Insert line 26, then copy and paste the following code:
ld hl,(32900) ;retrieve car posn ld a,56 ;erase car ld (hl),a
Before we read the keyboard we will lock the keyboard for most of the game, and unlock it only when we want to read the keys.
The instruction to lock the keyboard is di = ‘disable interrupts. Its opposite is ei = ‘enable interrupts’.
Replace line 3 with di. Insert line 29, ei. Insert line 31, di. Insert line 41, ei
To read the keys we use the IN ports – see ch23 of the BASIC manual – to read the left and right half of the bottom row.
We load bc with the port number and use the instruction cp (compare) to see if the number has dropped to show a keypress.
Delete line30 and replace with the following code:
ld bc,65278 ;read keyboard caps to v in a,(c) cp 191 jr nz, moveright inc l moveright ld bc,32766 ;read keyboard space to b in a,(c) cp 191 jr nz, dontmove dec l dontmove
jr nz stands for jump relative if not zero. It skips over the instruction to increment / decrement the car position.
Replace line 43 with the following to see if we bump into the oncoming road 32 bytes (1 screen) down the attribute file:
ld (32900),hl ;store car posn ld de, 32 ;new carposn xor a ;set carry flag to 0 sbc hl,de ld a,(hl) ;crash? or a jr z,gameover ld a,8 ;print car ld (hl),a
We’d like to sub hl,de but there’s no such instruction so we use sbc, subtract with carry, and set the carry flag to zero.
or compares the register to the a register bit by bit and leaves a 1 in the a register for each bit that is 1 in either.
If all the digits are zero, then the zero flag will be set, so we can use or a to test for a black paper colour.
Delete line 53. Delete line 53 again!
To clean up the score at GAMEOVER insert line21, push bc; save score. Replace line 57 with pop bc;retrieve score
To cycle round the game before GAMEOVER change line 55 to jp PRINCIPALLOOP.
Assemble, save, and run. You’ll need to deliberately crash to get out!
CHAPTER 4 – scroll and move the road, keep score, adjust speed.
To scroll the road down the screen we copy the screen attribute bytes to the line beneath 736 times.
We use the instruction lddr, which stand for LoaD ((hl) to (de)),Decrement (hl and de) and Repeat (until bc is zero).
Replace line 53 with the following:
ld hl,23263 ;scroll road ld de,23295 ld bc,736 lddr pop bc ;retrieve score
To add 1 to the score and save it ready for GAMEOVER, insert the following into line 59:
inc bc ;add 1 to score
push bc ;save score
To move the road randomly left or right on the top line we use the following algorithm –
Choose a location in ROM where the are 256 random looking bytes and add the low byte of the score in bc to it.
If it is odd, lower the road position in hl by one. If it is even, increase by one.
(To test the last bit for odd and even we use ‘and 1’ which “masks” the last bit and sets the zero flag if it is 0).
Check to see if the road has reached the edge of the screen and bump it away if it has.
Print the new road top line like we did in chapter 2.
Replace line 58 with the following hefty chunk of code:
pop hl ;retrieve road posn push hl ;save road posn ld a,56 ;delete old road ld (hl),a inc hl ld (hl),a ld de,9 add hl,de ld (hl),a inc hl ld (hl),a ;random road left or right ld hl,14000 ;source of random bytes in ROM ld d,0 ld e,c add hl, de ld a,(hl) pop hl ;retrieve road posn dec hl ;move road posn 1 left and 1 jr z, roadleft inc hl inc hl roadleft ld a,l ;check left cp 255 jr nz, checkright inc hl inc hl checkright ld a,l cp 21 jr nz, newroadposn dec hl dec hl newroadposn push hl ;save road posn xor a ;print new road ld (hl),a inc hl ld (hl),a ld de,9 add hl,de ld (hl),a inc hl ld (hl),a
The last thing we need to do to have a playable game is slow down our blindingly fast machine code.
Insert the following into line 106 (as extension material, you could adjust the figure in bc with a keypress to ‘brake’):
;wait routine ld bc,$1fff ;max waiting time wait dec bc ld a,b or c jr nz, wait
Save, assemble, and run – and that’s it! Has your tea gone cold yet?
A full listing follows, with my email address at the end for your comments and suggestions:
main org 33000 di ld hl, 22537 ;initialise road push hl ;save road posn xor a ld b,24 fillscreen ld (hl),a inc hl ld (hl),a ld de,9 add hl,de ld (hl),a inc hl ld (hl),a ld de,21 add hl,de djnz fillscreen ld c,b ;initialise score push bc ;save score ld hl,23278 ;initialise car ld a,8 ld (hl),a ld (32900),hl ;save car posn principalloop ld hl,(32900) ;retrieve car posn ld a,56 ;erase car ld (hl),a ei ld bc,65278 ;read keyboard caps to v in a,(c) cp 191 jr nz, moveright inc l moveright ld bc,32766 ;read keyboard space to b in a,(c) cp 191 jr nz, dontmove dec l dontmove di ld (32900),hl ;store car posn ld de, 32 ;new carposn xor a ;set carry flag to 0 sbc hl,de ld a,(hl) ;crash? or a jr z,gameover ld a,8 ;print car ld (hl),a ld hl,23263 ;scroll road ld de,23295 ld bc,736 lddr pop bc ;retrieve score pop hl ;retrieve road posn push hl ;save road posn ld a,56 ;delete old road ld (hl),a inc hl ld (hl),a ld de,9 add hl,de ld (hl),a inc hl ld (hl),a ;random road left or right ld hl,14000 ;source of random bytes in ROM ld d,0 ld e,c add hl, de ld a,(hl) pop hl ;retrieve road posn dec hl ;move road posn 1 left and 1 jr z, roadleft inc hl inc hl roadleft ld a,l ;check left cp 255 jr nz, checkright inc hl inc hl checkright ld a,l cp 21 jr nz, newroadposn dec hl dec hl newroadposn push hl ;save road posn xor a ;print new road ld (hl),a inc hl ld (hl),a ld de,9 add hl,de ld (hl),a inc hl ld (hl),a inc bc ;add 1 to score push bc ;save score ;wait routine ld bc,$1fff ;max waiting time wait dec bc ld a,b or c jr nz, wait jp principalloop gameover pop bc ;retrieve score pop hl ;empty stack ei ret; game and tutorial written by Jon Kingsman ('bigjon', 'bj'). electronic mail gmail.com - atsign - jon.kingsman (reversed)