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)
Thank you very much for your work reorganising the Tutorial and making it better to read an follow.
BR
You are welcome!
I have tried the spectrum tutorial ‘ZX Spectrum Machine Code in 30 Minutes chapter one and I have followed the instructions the program assembles and puts everything it should into memeory starting at 33000. However, when i try to run the program with the pRINT command it says:
2 variable not found, 10,1
why would it not find the variable?
Assuming your assembly program reads like this if you’ve followed the instructions (I removed the comments and the labels here for clarity):
org 33000
ld bc, 0
ret
This BASIC command works well enough. I just tried it.
10 PRINT AT 10,0;”Your score is: “;USR 33000
If you’re sure you’ve done the above then something else is going wrong and I’ll need more details:
a) Which spectrum model are you using to enter the program? (I tried it in 48k)
b) What does your assembly program look like?
c) What does your BASIC program look like?
Screenshots will also help.
Many thanks for your reply,
In the end I discovered what the problem was: being unfamiliar with the spectrum I was typing out the USR rather than tapping the key, so the code was not being read.
Hi I am attemping to follow your tutorial, it might be me being stupid but I am struggling to see which lines need inserting where e.g. in Chapter 2 you say Insert line 6, ld a,8. This puts a blue PAPER colour into the A register etc but the line numbers dont match up to the line numbers in the listing you provide at the start, do i just count lines with code or include blank lines too?
Am i just being stupid?
Thanks
Paul
Hi Paul,
Ignore the blank lines. There’s a full listing at the end of the article, so you can refer to it if you’re ever confused about what goes where.
Nice tutorial! One very minor observation; you have repeatedly written the value 65355 instead of 65535
Lol. This has now been corrected in the text. Thanks!
This is in some serious need of expanding and cleaning up to make it easier for people like me (completely new to z80 programming) to follow. Other than that, it’s awesomely great!
Oh, nevermind about the cleanup – the tutorial becomes much easier to understand if you delete the blank lines in ZX Spin after you copy-paste.
It would be great if you explain it by a video on youtube.. I’m getting “2 variable not found, 10,1” error, I’m very on zx, Thanks!
This is brilliant. After 30 years, I’m finally beginning to grasp machine code. Thank you.
Should this code work with Zasm? I don’t have windows so don’t use Spin, would be nice if I could use Zasm instead.
Yes, the code should work with any assembler as it doesn’t use any non-standard assembler specific directives.
The references to line numbers do make this difficult to follow. My line numbers don’t seem to be correct so I have to just make a best guess and which line you meant.
Yes, I see what you mean. Perhaps a text editor with line numbering might help to type in the assembly code which could perhaps be imported to Zasm? I haven’t used ZASM so I don’t know if it’s even possible. There is a full assembly listing of the source code at the end of the article. You can also refer to it in case you get stuck and wonder which line goes where.
I am still in the beginning of the tutorial, in the part where it is said:
”
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
”
So… I have:
1. Copy and pasted the code in the 1st box with code into the Spin Assembler (that I called in ZX Spin in Tools -> Z80 Assembler…).
2. In the assembler, I did File -> Save to a file named mc30mintut.asm
3. I have Clicked st the end of line 15, press enter to create line 16 and typeed ret
4. At the beginning of line 4, I typed ld bc,0
5. I did File -> Save, then File -> Assemble. Typed 33000 into the Start Address box and clicked OK.
6. At the bottom window of the assembler I have seen (as expected) a report saying “No errors in 16 lines. 4 bytes generated”.
7. I have seen the four bytes at memory address 33000 not by clicking in the main Spin display window on Tools -> Debugger (as indicated – I think by mistake) but in Program -> Debug.
Now the problem…
Where exactly in the SPIN debugger should I write???
10 PRINT AT 0,0; “Your score was “; USR 33000
If I put the above on the assembler code, it returns error… so I suppose that this should be placed somewhere where I can place commands to be run in real-time with the program I just typed in memory… but where is this?
Hi,
You have to enter it in the Basic editor. The main emulator window.
Cheers!
Arjun
Hello.
I’m glad to see spectrum is still alive :). I startet with it in good old 1981 ( ZX81). In a short while I became a master programmer in BASIC code. I never got to know assembly code. I tried a few times, but lost my grip on it. ( I was 10 years old at the time lol ). Time went by and forgot about the spectrum and now when I look at this code it gets so clear and easy. Well, in the years I got to know pretty much every language ( cobol, fortran, C, C#, C++, Java, PHP, HTML…) so I guess I get the idea about this language. Anyway, I’m glad my childhood dreams are still alive. Keep it up, nevertheless thats the begining of personal computer age as we know it.
Regards, Andrej
Hi Andrej,
Glad to hear from another specchum! I hope to keep the blog updated as and when I can. Thanks for the encouragement!
Arjun
Thanks for this, truly useful for the nostalgic minds such as mine. I was having trouble getting the program to run (variable not found) but realised you can’t just type in “USR”, you need to specify the command using the correct key press (doh!). The keyboard helper built into SPIN helped me to get the code running. Thought that was worth a mention in case anyone else is having trouble. Thanks again, i intend to work through many chapters!
You are welcome!
good times lol. my score was 2242. nice tutorial. was great fun. time to dust off my 48k beast and show my kids dad can program lol. thanks
Just awesome! was struggling for so long with the assembler (I think zeus if I remember correctly) in my first computer – a ZX spectrum replica (around 14 years old) and lacked any real documentation on it so never fully understood it back then. Your tutorial makes it look so easy, who thought I would get the hang of it after 20 years!
Very nicely explained, just fantastic!
Thank you!
Machine type in Spin was set to 48k and reading port 65278 and 32766 returned $ff if the keyboard untouched, so the car did not moved at all. Changed machine type to 48k+ and the ports started to return 191 and everithing was OK. Nice tutorial.
RATOS, Romania – Senior Z80 Programmer.
Great post!
Will this work on the new Spectrum remake?
Not sure I quite follow. Which Specrum remake do you mean? The Vega? It should work just fine as far as I know.
http://www.recreatedzxspectrum.com
My bad, the recreated Spectrum is just a wireless keyboard that looks like the original Spectrum. The code actually runs on an emulator, so I assume it should work as you say.
I’ll give it a go and find out!
Thanks for this! Enjoyed getting it running, and managed to add braking. I’m trying to get sound now, but it stops the keyboard working for some reason. I’m doing this at the start of the principal loop:
ld hl,497 ; load pitch into hl
ld de,2 ; load duration into de;
call 949 ; call the process to play the sound
Anything wrong with that? I don’t think the prior values in hl and de were needed but could be wrong.
Hi James,
I tried out what you did and faced no issues whatsoever. Both sound and keyboard are working fine. Are you sure you didn’t change anything else and forgot about it?
Thanks for looking, but it seems to be working for me too now!
It may have been an issue with the emulator – I was using Fuse on mac before, but in RealSpec on Windows it seems to work fine.
It has been around 33 years since I first experienced ownership of a ZX81 then upgraded to a Spectrum 48K Rubber keyboard version… and eventually upgraded again to a SAM Coupe. I still have my 48K Speccy and the Sam Coupe. I was quite good with BASIC on all machines, but Assembler code seemed way too difficult to understand… now I am considering trying to learn Z80 machine code programming via PC emulator. I hope to use your tutorial as a good starting area to help me understand the commands etc. Thank you. I’m from New Zealand
Good luck with assembly! Looking forward to seeing what you come up with.