← Back to Courses
module
31

A Game You Can Lose

Introduction

The game from S29 works, and it is not finished. You cannot start it - it is already running the moment it loads. You cannot lose it: a rock reaching the ship flashes the screen red and play carries on. You cannot leave it, either, because the loop ends JR main and nothing in it ever stops.

That is the difference between a program that draws a game and a game. A game has a beginning, a way to fail, and a way to go round again, and none of those are about graphics. They are about the program knowing which part of itself it is in.

One loop is not enough

Right now there is one loop and it does one thing:

main:   CALL waitframe
        CALL input
        CALL animate
        ...
        JR main

A title screen needs a different loop - draw some words, watch for a key, do nothing else. A game over screen needs a third. And the program has to be able to move between them.

The wrong way to do this is a pile of flags: LD A,(playing), OR A, JR Z,..., repeated at the top of everything. It works for two states and collapses at four.

The right way is a state number and a table.

The state, and the table

One byte says where the program is:

state:  DEFB 0          ; 0 title, 1 playing, 2 game over

And a table says what to run for each:

states: DEFW title
        DEFW playing
        DEFW over

The loop becomes two lines, and never changes again however many states you add:

main:   CALL waitframe
        CALL dispatch
        JR main

You have written dispatch before. It is S14's table of routines, unchanged: double the index, add it to the table's address, fetch the two bytes there, and jump.

dispatch:
        LD A,(state)
        ADD A,A
        LD L,A
        LD H,0
        LD DE,states
        ADD HL,DE
        LD E,(HL)
        INC HL
        LD D,(HL)
        EX DE,HL
        JP (HL)

S14 left one problem open. JP (HL) is a jump, not a call, so the routine it lands on has no way back - its RET goes wherever the stack was pointing before. The fix S14 describes is the one used here: reach the dispatcher with a CALL. The state routine's own RET then returns through the dispatcher's caller, which is main, and the loop carries on.

That is worth sitting with for a moment. CALL dispatch pushes a return address; JP (HL) jumps without pushing another; the state's RET pops the one that is there. Two instructions in different routines, cooperating through the stack, and the result is a game loop that runs whichever state is current and comes back afterwards.

Adding a state is now: write the routine, add a DEFW, use the number.

Doing a thing once, and doing it every frame

Every state has two different jobs, and separating them is most of what makes this work.

On the way in, once: clear the screen, draw whatever words belong there, set the backdrop, decide what the sprites are doing.

Every frame afterwards: watch for a key, move things, publish them.

One byte keeps them apart:

entered: DEFB 0         ; has this state drawn its screen yet?

and one routine reads it:

; --- Z, once, on the first frame of a state. NZ every frame after.
once:   LD A,(entered)
        OR A
        RET NZ
        LD A,1
        LD (entered),A
        XOR A
        RET

So a state opens CALL once / JR NZ,<the every-frame part>, and the setup sits between them. Changing state clears the flag, so the next state sets itself up on its first frame:

gostate:
        LD (state),A
        LD A,0
        LD (entered),A
        RET

Each state owns the whole screen

The rule that keeps this from becoming a mess: when a state is running, everything on screen is its responsibility. Not just the part it cares about.

That means the backdrop. S24 made the point for a collision light - a display you only write on some passes is a display you do not own - and it applies here one level up. flash writes the backdrop every frame while you are playing, so the playing state owns it. The title and game over states are not running flash, so they must set it themselves, or they inherit whatever the last state happened to leave:

; --- the backdrop colour in A
backdrop:
        OUT (9),A
        LD A,135
        OUT (9),A
        RET

It means the sprites, too. There is no rocket on a title screen, and the cheapest way to have none is S19's terminator - one byte into the first sprite's Y and the VDP stops reading the list entirely:

hide:   LD A,0
        OUT (9),A
        LD A,123
        OUT (9),A
        LD A,208
        OUT (8),A
        RET

Nothing has to put them back. place writes sprite 0's Y every frame while you are playing, so the moment the playing state runs again the terminator is overwritten and the ship reappears.

Text anywhere on the screen

S29 borrows the font into bank 0 only, which is why its score line sits in the top row: bank 0 is the top eight rows and there are no letters anywhere else. A title in the middle of the screen needs the same patterns in all three banks, and white ink in all three colour banks to go with them.

That is two loops and three calls each:

        LD A,65         ; bank 0 patterns, 0100h
        CALL fontto
        LD A,73         ; bank 1, 0900h
        CALL fontto
        LD A,81         ; bank 2, 1100h
        CALL fontto

        LD A,97         ; bank 0 colours, 2100h
        CALL colto
        LD A,105        ; bank 1, 2900h
        CALL colto
        LD A,113        ; bank 2, 3100h
        CALL colto

The addresses are S17's and S18's, one page in from each bank's base because the printable characters start at code 32 and 32 patterns is 256 bytes.

With that done, a string goes anywhere:

; --- the zero-terminated string at HL, into the name table at DE
text:   LD A,E
        OUT (9),A
        LD A,D
        ADD A,64
        OUT (9),A
tx:     LD A,(HL)
        OR A
        RET Z
        OUT (8),A
        INC HL
        JR tx

and the caller can say where in a form you can read:

        LD HL,msgname
        LD DE,3800h+9*32+12
        CALL text

Row 9, column 12. The assembler works the address out; you never write 392Ch and wonder later what it meant.

Three lives

A life is a byte, and losing one happens where the rock already reaches the ship. S29 ends that path by flashing the screen and spawning the next rock. Now it costs you something:

        LD A,25         ; the rock reached the ship: half a second of red
        LD (flashT),A
        CALL spawn
        LD A,(lives)    ; and it costs you one of three
        DEC A
        LD (lives),A
        CALL dlives
        OR A
        RET NZ
        LD A,2          ; that was the last one
        JP gostate

There is a trap in those nine lines, and it is worth pointing at because it is the oldest lesson in the course wearing new clothes. dlives draws the lives digit, and to do that it loads lives and adds 48 to make a character - so it comes back with A holding 51, not 3. Test A after calling it and the test is meaningless.

The fix is S12's, applied to a routine of your own rather than a ROM call:

dlives: PUSH AF         ; the caller is about to test A - S12's habit
        LD A,26
        OUT (9),A
        LD A,120
        OUT (9),A
        LD A,(lives)
        ADD A,48
        OUT (8),A
        POP AF
        RET

Two bytes, and the routine can now be called from anywhere without the caller having to know what it does to the accumulator. That is what S8 meant by saying you cannot assume a register survives a call - and here you are on the other side of it, writing the call somebody else has to trust.

The three states

With the machinery in place each state is short. The title:

title:  CALL once
        JR NZ,tpoll
        LD A,244                ; this screen owns the backdrop too
        CALL backdrop
        CALL hide               ; no sprites on this screen
        CALL clearall
        LD HL,msgname
        LD DE,3800h+9*32+12
        CALL text
        LD HL,msgstart
        LD DE,3800h+12*32+10
        CALL text
tpoll:  CALL spacedown
        RET NZ                  ; space not down - keep waiting
        CALL newgame
        LD A,1
        JP gostate

Playing is S29's loop with its furniture drawn on the way in:

playing:
        CALL once
        JR NZ,play
        CALL clearall
        LD HL,msgscore          ; the furniture this screen needs
        LD DE,3800h+2
        CALL text
        LD HL,msglives
        LD DE,3800h+20
        CALL text
        CALL dlives
play:   CALL input
        CALL animate
        CALL bullet
        CALL fall
        CALL hits
        CALL place
        CALL apub
        CALL dscore
        CALL flash
        RET

And game over, with one thing the other two do not need:

over:   CALL once
        JR NZ,opoll
        LD A,246                ; red, chosen rather than left over
        CALL backdrop
        CALL hide
        CALL clearall
        LD HL,msgover
        LD DE,3800h+10*32+11
        CALL text
        LD HL,msgstart
        LD DE,3800h+13*32+10
        CALL text
        LD A,50                 ; a second before the key counts - the
        LD (hold),A             ; space that killed you is probably still down
opoll:  LD A,(hold)
        OR A
        JR Z,opoll2
        DEC A
        LD (hold),A
        RET
opoll2: CALL spacedown
        RET NZ
        LD A,0
        JP gostate

That lockout is not fussiness. You die with your finger on the fire button, and without it the game over screen is dismissed by the same press that killed you - on screen for a fiftieth of a second, which reads as a bug. It is the same one-byte timer as the collision flash, spent on an input problem instead of a display one.

Starting again

The last piece is the one that makes a second game possible. Starting is not "go to the playing state" - it is putting everything back:

newgame:
        LD A,3
        LD (lives),A
        LD A,0
        LD (scoreU),A
        LD (scoreT),A
        LD (flashT),A
        LD (bullA),A
        LD A,224
        LD (bullY),A
        LD A,124
        LD (rockX),A
        LD A,160
        LD (rockY),A
        JP spawn

Every variable the game changes while it runs has to appear in that list. Miss one and the second game starts with the first game's leftovers - a score that carries over, a bullet already in flight, a ship parked where it died. This is the routine to come back to whenever you add a variable to the game.

The code

Everything above goes into S29's program. The rest of it - the rocket, the bullet, the rock, the collisions, the score, the frame wait - is unchanged. Three edits to what is already there:

  1. The loop becomes CALL waitframe / CALL dispatch / JR main, with dispatch and the states table after it.
  2. The font copy in setup writes to all three banks through fontto, with colto for the colours. The SCORE label that setup used to draw comes out - the playing state draws its own now.
  3. The ship collision in hits ends with the nine lines above instead of JR spawn.

Plus the new data:

state:  DEFB 0          ; 0 title, 1 playing, 2 game over
entered: DEFB 0         ; has this state drawn its screen yet?
lives:  DEFB 3
hold:   DEFB 0

msgscore: DEFM "SCORE ",0
msglives: DEFM "LIVES ",0
msgname:  DEFM "ROCKFALL",0
msgstart: DEFM "PRESS SPACE",0
msgover:  DEFM "GAME OVER",0

What you should see

It loads to a title screen: ROCKFALL and PRESS SPACE, white on blue, centred, with nothing else on the display.

The title screen

Press space and the game begins, with SCORE 00 on the left of the top row and LIVES 3 on the right.

Playing, with the score and lives on the top row

Let three rocks reach the ship. The third one takes the last life and the screen turns red: GAME OVER, PRESS SPACE. Press it and you are back at the title, on blue, with a fresh three lives and a score of zero waiting.

Game over

Change one thing

  • Delete the CALL once and JR NZ,tpoll from title, so its setup runs every frame. Run it. Nothing changes - the title screen is identical down to the pixel. Now do the same to playing: delete its CALL once and JR NZ,play. The score and lives never appear at all, while the ship, the rock and the bullet carry on exactly as before. The redraw is not failing; work out where in the loop the screen is being looked at.
  • Change LD A,3 in newgame to LD A,1. One life. Does anything else in the program need to know?
  • Take CALL hide out of over and lose a game. What is still on the screen, and why does nothing move it?
  • Swap the first and third entries of the states table, so it reads DEFW over / DEFW playing / DEFW title. Predict what happens before you run it. What does that tell you about where the wiring of this program actually lives?

Exercises

31.1 - A pause state. Add a fourth state that freezes the game: the screen stays as it is, nothing moves, and a key returns to playing. Where does it go in the table, and what must it not do on the way in?

31.2 - Tell them the score. Put the final score on the game over screen. The digits are already kept as two bytes; text will not help you directly, so work out what will.

31.3 - A high score. Keep the best score so far, show it on the title screen, and update it when a game beats it. It survives a new game but not a reset of the machine - say why, and what it would take to change that.

31.4 - Waves. Make the rocks fall faster every time the score passes a multiple of ten. fall gates on every other frame; that gate is where to start.

31.5 - Attract mode. After a few seconds on the title screen, let the game play itself - rocks falling, nothing shooting - until a key is pressed. You will find this is much easier than it sounds, and the reason why is the point of the exercise.

When it goes wrong

Symptom Cause
The lives count goes past zero and the game never ends A routine you called destroyed the value you were about to test. dlives returns a character code, not a count - PUSH AF / POP AF round it.
A screen keeps the previous screen's backdrop Only the playing state writes register 7, through flash. Every state that does not run flash has to set the backdrop itself.
Sprites from the last game sit on the title screen Nothing hid them. One 208 into sprite 0's Y stops the VDP reading the list.
The game over screen flashes past before you can read it The key that fired your last shot is still down when the state changes. Lock the input out for a moment on the way in.
The second game starts with the first game's score A variable that changes during play is missing from newgame.
A state's text never appears Its setup is running every frame, so the screen spends nearly all its time cleared. Guard the setup with once.

Summary

  • A game is several loops, not one. A state byte says which is running.
  • The loop dispatches through a table of routines - S14's mechanism, reached with a CALL so the state's own RET comes back.
  • Separate entering a state from running it. One flag does it.
  • A state owns the whole screen while it runs: backdrop, sprites and text, not only the parts it cares about.
  • Starting a game is a routine, and every variable play changes belongs in it.
  • A routine that alters A is a routine its callers have to trust. Push what you clobber.

Next

S32. Three Events, Three Noises. The game has a beginning and an end and makes no sound at all - S27 taught the chip and nothing has used it since. The shot, the hit and the flash that is already counting down are three events looking for three noises, and the trick is fitting them into a loop that cannot stop to listen.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.