← Back to Courses
module
41

Appendix X (continued) - Worked solutions

The second half of the worked solutions, picking up where the course takes the screen over. If you are looking for S3 to S16, they are in Appendix X.

Everything there applies here: one solution rather than the solution, have a go first, and the Change one thing edits are deliberately left unanswered - running the edit is a better answer key than any appendix.

Every listing below was assembled and run on the machine before it was written down. One of them, 28.2, records a failure rather than a success: the program is right and the machine does not do what the exercise expects. It is written up as it happened.

S17. A screen of your own

Seven exercises, and all seven share the same three pieces: a bank loader, a map drawer, and a tile set. Rather than print them seven times, here they are once - and where a listing below says "the usual three helpers", these are what it means.

; --- copy 40 bytes from (HL) into pattern 0 onwards of one bank.
;     A holds the bank's high byte: 64, 72 or 80.
bank:   LD D,A
        LD A,0
        OUT (9),A
        LD A,D
        OUT (9),A
        LD B,40
copy:   LD A,(HL)
        OUT (8),A
        INC HL
        DJNZ copy
        RET

; --- draw the 768-byte map at HL into the name table
draw:   LD A,0
        OUT (9),A
        LD A,120
        OUT (9),A
        LD BC,768
dm:     LD A,(HL)
        OUT (8),A
        INC HL
        DEC BC
        LD A,B
        OR C
        JR NZ,dm
        RET

Turning the map write into a subroutine is worth doing straight away. 17.3 needs it, 17.5 and 17.6 need a blank screen to draw on, and a routine that takes a map address in HL is the whole of exercise 17.3 already written.

17.1 - Your own border

Only the forty bytes change. The map, the loader and the draw routine are untouched, which is the point of the exercise.

tiles:
        ; 0 - blank
        DEFB 00000000b,00000000b,00000000b,00000000b
        DEFB 00000000b,00000000b,00000000b,00000000b
        ; 1 - corner: a solid quarter with a notch
        DEFB 11111111b,11111111b,11111100b,11111000b
        DEFB 11110000b,11100000b,11000000b,10000000b
        ; 2 - horizontal: a double rule
        DEFB 00000000b,11111111b,00000000b,00000000b
        DEFB 00000000b,00000000b,11111111b,00000000b
        ; 3 - vertical: a double rule on its side
        DEFB 01000010b,01000010b,01000010b,01000010b
        DEFB 01000010b,01000010b,01000010b,01000010b
        ; 4 - interior: a small cross
        DEFB 00000000b,00011000b,00011000b,01111110b
        DEFB 01111110b,00011000b,00011000b,00000000b

What you should see: the same bordered screen, now with double rules round the edge and a field of small crosses inside it.

Forty bytes of data changed the entire display and not one instruction moved. That is the thing to notice: the program does not know what the tiles look like, and it does not need to.

17.2 - A room

Two new tiles - a faint dot for floor and a solid block - so the tile set is six patterns, 48 bytes, and LD B,40 in bank becomes LD B,48.

        ; 4 - floor: a faint dot
        DEFB 00000000b,00000000b,00000000b,00011000b
        DEFB 00011000b,00000000b,00000000b,00000000b
        ; 5 - solid
        DEFB 11111111b,11111111b,11111111b,11111111b
        DEFB 11111111b,11111111b,11111111b,11111111b

And the map. The top wall has two blank cells in it for a doorway, and four rows in the middle carry a block of tile 5:

TileMap:
        DEFB 1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1
        DEFB 3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3
        ; ... seven more floor rows ...
        DEFB 3,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,3
        DEFB 3,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,3
        DEFB 3,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,3
        DEFB 3,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,3
        ; ... ten more floor rows ...
        DEFB 1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1

What you should see: a room with a gap in the top wall and a solid slab sitting in the middle of the floor.

The exercise says this is a level format, and it is worth taking that seriously. Nothing about the program knows that 5 means solid and 4 means floor. That meaning only exists in whatever code later asks "can the player walk onto this cell?", and that code will do it by reading the map back - which is why keeping the map in your own memory, rather than only in VRAM, is the habit to start now.

17.3 - Two screens

The whole of this exercise is that draw takes its map address in HL, which it already did.

        LD HL,MapOne
        CALL draw

        RST 8
        DEFB 156        ; ZKEYIN - wait for a key

        LD HL,MapTwo
        CALL draw

        JP $

What you should see: the room from 17.2, then - as soon as you press a key - a different screen entirely.

Note what is not here: no clearing between the two. The second draw writes all 768 entries, so every cell is accounted for and there is nothing left of the first screen to clean up. A draw routine that covered only part of the screen would need the clear, and forgetting it is how you end up with two levels on top of each other.

RST 8 / DEFB 156 still works perfectly on a tile screen, by the way. It is only the printing call that the takeover breaks - reading the keyboard never touched the display.

17.4 - Economise

Rows 8 to 15 are all 3,4,...,4,3, so bank 1 is asked for tiles 3 and 4 and nothing else. Loading only those two means starting the copy 24 bytes into the tile data and landing it at pattern 3, so the loader needs a start address and a length as well as a bank:

        LD HL,tiles
        LD A,64
        LD C,40         ; all five tiles
        LD B,0          ; starting at pattern 0
        CALL bank

        LD HL,tiles+24  ; tiles 3 and 4 only
        LD A,72
        LD C,16         ; sixteen bytes
        LD B,24         ; landing at pattern 3, which is byte 24
        CALL bank

        LD HL,tiles
        LD A,80
        LD C,40
        LD B,0
        CALL bank

; copy C bytes from (HL) into the bank whose high byte is in A,
; starting at the low byte in B.
bank:   LD D,A
        LD A,B
        OUT (9),A
        LD A,D
        OUT (9),A
        LD B,C
copy:   LD A,(HL)
        OUT (8),A
        INC HL
        DJNZ copy
        RET

What you should see: exactly what 17.1 showed. Not similar - identical.

The saving is 24 bytes. And now the judgement the exercise actually wants.

Twenty-four bytes out of 16K is nothing, and the cost is a loader with four inputs instead of two plus a tiles+24 that has to stay correct every time the tile set is reordered. Reorder the tiles so that 3 and 4 are no longer consecutive and this silently loads the wrong two.

So: keep the general loader, because a loader that takes an offset and a length is more useful than one that does not. Do not keep the economy. The bytes you save in a bank are bytes you were never going to use, and the first time the tile set changes the saving costs you an afternoon.

That is a real lesson about optimisation and it is better learned on 24 bytes than on something that matters.

17.5 - Draw one cell

; --- one name table entry. B = row, C = column, A = tile number.
cell:   PUSH AF
        LD L,B
        LD H,0
        ADD HL,HL       ; row * 2
        ADD HL,HL       ; * 4
        ADD HL,HL       ; * 8
        ADD HL,HL       ; * 16
        ADD HL,HL       ; * 32 - no multiply instruction needed
        LD B,0
        ADD HL,BC       ; + the column
        LD DE,14336     ; 3800h - the name table
        ADD HL,DE
        LD A,L
        OUT (9),A
        LD A,H
        ADD A,64        ; 40h for a write
        OUT (9),A
        POP AF
        OUT (8),A
        RET

What you should see: with three calls - row 1 column 1, row 12 column 16, row 22 column 30 - three tiles, each exactly where you asked for it.

Test it in all three thirds, not just the top one. Row 1 is entry 33 and the high byte never changes, so it would pass with the address arithmetic wrong; row 22 column 30 is entry 734, address 3ADE, and getting that right means the carry out of ADD HL,BC reached the high byte properly.

Two details that matter more than they look.

PUSH AF / POP AF around the whole thing. The tile number arrives in A and A is needed for both halves of the address, so it has to be put somewhere. The stack is the natural place, and S12 is the reason you can use it without thinking about where.

LD B,0 before ADD HL,BC. The row came in in B and has already been copied into L, so B is free - and it has to be zeroed, or the column addition brings the row in a second time, 256 cells further along. This is S13's LD BC,n trap arriving from a different direction.

17.6 - A moving dot

sweep:  LD A,(colnum)
        LD C,A
        LD B,12         ; always row 12
        LD A,4          ; the dot
        CALL cell

        CALL delay

        LD A,(colnum)
        LD C,A
        LD B,12
        LD A,0          ; blank it again
        CALL cell

        LD A,(colnum)
        INC A
        LD (colnum),A
        CP 32
        JR NZ,sweep

        LD B,12         ; leave it showing at the last column
        LD C,31
        LD A,4
        CALL cell

        JP $

delay:  LD DE,0
d1:     DEC DE
        LD A,D
        OR E
        JR NZ,d1
        RET

colnum: DEFB 0

What you should see: a dot crossing row 12 from left to right, one cell at a time, taking a few seconds to get there.

The column counter lives in memory, and that is not laziness. cell uses A, B, C, D, E, H and L - every register there is - so there is nowhere in the processor to keep a loop counter across a call to it. This is the S8 lesson about call 181 destroying BC, except that this time the call is yours and you wrote the destruction yourself.

delay is 65536 turns of a four-instruction loop, which is the whole of timing in this course so far: count until enough has happened. S26 replaces it with something that actually knows what a frame is, and the difference is the difference between a dot that crawls and a game that runs.

And it does flicker, because the blank and the redraw happen whenever the program gets round to them rather than while the screen is looking away. Both problems are real and both are solved later; the point of this exercise is that you now have a thing on screen whose position is a number you can change.

17.7 - A score on the floor

Two changes from the section's example, exactly as the exercise says - and the arithmetic for both is worth writing out, because all three addresses are ones you have to work out rather than copy.

the glyphs:    6144 + 48*8 = 6528 = 1980h  -> low 80h, high 19h = 25, bare
bank 2's 48th: 1000h + 384 = 1180h         -> low 80h, high 11h + 64 = 81
row 23:        3800h + 23*32 = 3AE0h       -> low E0h, high 3Ah + 64 = 122
        ; --- read the ten digit glyphs out of the font
        LD A,80h        ; low byte of 6528
        OUT (9),A
        LD A,25         ; high byte, bare - a read
        OUT (9),A
        LD HL,buffer
        LD B,80         ; ten glyphs of eight bytes
rd:     IN A,(8)
        LD (HL),A
        INC HL
        DJNZ rd

        ; --- write them into bank 2 at pattern 48
        LD A,80h
        OUT (9),A
        LD A,81         ; 64 + 11h
        OUT (9),A
        LD HL,buffer
        LD B,80
wr:     LD A,(HL)
        OUT (8),A
        INC HL
        DJNZ wr

        ; --- print on the last row
        LD A,0E0h       ; low byte of 3AE0h
        OUT (9),A
        LD A,122        ; 64 + 3Ah
        OUT (9),A
        LD HL,message
        LD B,6
pr:     LD A,(HL)
        OUT (8),A
        INC HL
        DJNZ pr

        JP $

message:
        DEFM "000100"
buffer:
        DEFS 80

What you should see: 000100 along the bottom row of an otherwise empty screen, with the Einstein's diamond-shaped zero.

Eighty bytes instead of 768, and ten tile numbers instead of ninety-six. That is the shape of the deal in a real game: you are not copying a font, you are copying the characters your score line happens to use, into the bank the score line happens to sit in.

The reason the digits keep their own character codes as tile numbers - 48 to 57 rather than 0 to 9 - is that it makes printing free. Leave them where the codes put them and DEFM "000100" is already the tile numbers. Move them to 0-9 and every print needs a SUB 48 first, which is six subtractions you did not have to do.

The addresses are the whole exercise, and the one to get wrong is the middle one. 1180h is not 1000h + 48 and it is not 48 * 8 - it is the bank's base plus the pattern's offset within the bank, and the bank base is what makes the high byte 81 rather than 65.

S18. Colour

All five use S17's screen, so all five use S17's helpers. One change to bank earns its keep here, though: giving it a start offset and a length as well as a bank, exactly as 17.4 did, because these programs load sets of two and sixteen tiles rather than always five.

; copy C bytes from (HL) into the bank whose high byte is in A,
; starting at the low byte in B.
bank:   LD D,A
        LD A,B
        OUT (9),A
        LD A,D
        OUT (9),A
        LD B,C
copy:   LD A,(HL)
        OUT (8),A
        INC HL
        DJNZ copy
        RET

18.1 - Your own scheme

Only the forty colour bytes change:

colours:
        DEFB 0B0h,0B0h,0B0h,0B0h,0B0h,0B0h,0B0h,0B0h   ; 0 blank
        DEFB 0D0h,0D0h,0D0h,0D0h,0D0h,0D0h,0D0h,0D0h   ; 1 corner
        DEFB 0B0h,0B0h,0B0h,0B0h,0B0h,0B0h,0B0h,0B0h   ; 2 horizontal
        DEFB 0D0h,0D0h,0D0h,0D0h,0D0h,0D0h,0D0h,0D0h   ; 3 vertical
        DEFB 030h,030h,030h,030h,030h,030h,030h,030h   ; 4 spot

What you should see: a magenta frame with light yellow bars, and light green spots filling the middle, all on black.

And the second half of the exercise - a combination that is genuinely unreadable. This one:

colours:
        DEFB 0F0h,0F0h,0F0h,0F0h,0F0h,0F0h,0F0h,0F0h   ; 0 blank
        DEFB 010h,010h,010h,010h,010h,010h,010h,010h   ; 1 corner
        DEFB 010h,010h,010h,010h,010h,010h,010h,010h   ; 2 horizontal
        DEFB 010h,010h,010h,010h,010h,010h,010h,010h   ; 3 vertical
        DEFB 010h,010h,010h,010h,010h,010h,010h,010h   ; 4 spot

What you should see: absolutely nothing. A black screen.

Every tile is inked in colour 1, black, and the backdrop is black. The rule broken is the one the section spends half its length on: the backdrop decides whether your ink is visible, so it is chosen first. Note that this is not the same failure as the missing corners earlier - those were dark blue on a dark blue backdrop, close enough to be invisible. These are exactly the same colour, which is worse and easier to do by accident.

18.2 - One write, whole screen

Draw the screen once, then change nothing but register 7:

        LD HL,backs     ; four backdrops, one after another
        LD B,4
cyc:    LD A,(HL)
        OUT (9),A
        LD A,135
        OUT (9),A
        PUSH BC
        PUSH HL
        CALL delay
        POP HL
        POP BC
        INC HL
        DJNZ cyc

        JP $

delay:  LD DE,0
d1:     DEC DE
        LD A,D
        OR E
        JR NZ,d1
        LD DE,0
d2:     DEC DE
        LD A,D
        OR E
        JR NZ,d2
        RET

backs:  DEFB 0F0h,0F1h,0F6h,0FFh   ; transparent, black, dark red, white

What you should see: the same screen four times over, on black, then black again, then dark red, then white - the tiles unchanged and everything between them following the register.

Two things in the listing rather than the result.

PUSH BC / PUSH HL around the CALL. delay uses A, D and E, which is fine - but the loop's counter is in B and its pointer in HL, and the moment you add anything to a subroutine that touches them the loop breaks silently. Pushing them costs four bytes and removes the whole question. This is S12 being useful rather than educational.

The first two backdrops look the same. F0h puts 0 in the low nibble and F1h puts 1, and transparent behind everything comes out black, so the first change is invisible. That is worth seeing on purpose: it is the same fact as the section's spot-colour edit, from the backdrop's side instead of the tile's.

18.3 - The palette

Sixteen solid tiles, one per colour, in a row:

pats:
        DEFB 0FFh,0FFh,0FFh,0FFh,0FFh,0FFh,0FFh,0FFh   ; tile 0
        ; ... fifteen more identical solid patterns, tiles 1 to 15 ...

cols:
        DEFB 000h,000h,000h,000h,000h,000h,000h,000h   ; colour 0
        DEFB 010h,010h,010h,010h,010h,010h,010h,010h   ; colour 1
        DEFB 020h,020h,020h,020h,020h,020h,020h,020h   ; colour 2
        ; ... up to ...
        DEFB 0F0h,0F0h,0F0h,0F0h,0F0h,0F0h,0F0h,0F0h   ; colour F

names:  DEFB 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15

with the whole 128 bytes of patterns and 128 bytes of colours sent to all three banks, the name table cleared to tile 0, and names written into row 1.

What you should see: fourteen colour blocks in a row near the top of an otherwise empty screen, and a gap at the left where the first two should be.

That gap is the answer to the exercise. Colours 0 and 1 cannot be told apart on a black backdrop, because 0 is transparent and what it shows through to is black. Every other pair is distinguishable, and the table in the section is right about all sixteen.

Two details worth having.

Clearing the name table to tile 0 is what keeps the rest of the screen clean, and it works because tile 0 in this set is a solid pattern coloured transparent - solid ink that you cannot see. A blank pattern would have done the same job for a different reason, and knowing which reason applies is the difference between a screen you designed and a screen that happened.

And load all three banks even though the row you are writing is in the top third. The first version of this program did not, and the lower two thirds filled with whatever MOS had left in banks 1 and 2 - a screenful of repeated prompt characters that made the actual answer hard to find. S17's rule applies to every program from here on, including the ones testing something else.

18.4 - Colour by bank

One solid pattern in all three pattern banks, and three different colour entries for it:

col0:   DEFB 0F0h,0F0h,0F0h,0F0h,0F0h,0F0h,0F0h,0F0h
        DEFB 070h,070h,070h,070h,070h,070h,070h,070h   ; cyan in bank 0
col1:   DEFB 0F0h,0F0h,0F0h,0F0h,0F0h,0F0h,0F0h,0F0h
        DEFB 090h,090h,090h,090h,090h,090h,090h,090h   ; light red in bank 1
col2:   DEFB 0F0h,0F0h,0F0h,0F0h,0F0h,0F0h,0F0h,0F0h
        DEFB 030h,030h,030h,030h,030h,030h,030h,030h   ; light green in bank 2

Then eight cells of tile 1 at row 1, row 12 and row 22, using 17.5's arithmetic to find each name table entry.

What you should see: three strips of the same shape, cyan near the top, light red in the middle, light green near the bottom.

And the question the exercise asks: what does that let you do?

It gives you three times as many coloured tiles as you have patterns. The three banks are usually a nuisance - the same tile has to be loaded three times - and this is the one place where they pay you back. A sky that is lighter at the top, a cave that darkens as you descend, a scoreline in its own colour at the foot of the screen: one pattern, three colours, and the map does not have to know.

The catch is that it is decided by where on screen the cell is, not by anything your program chooses per cell. You get a horizontal band of colour eight rows deep and you get exactly three of them. Within a third, a pattern has one colour and that is that.

18.5 - Eight colours in one tile

pats:
        DEFB 00000000b,00000000b,00000000b,00000000b
        DEFB 00000000b,00000000b,00000000b,00000000b   ; 0 blank
        DEFB 0FFh,0FFh,0FFh,0FFh,0FFh,0FFh,0FFh,0FFh   ; 1 solid

cols:
        DEFB 0F0h,0F0h,0F0h,0F0h,0F0h,0F0h,0F0h,0F0h   ; 0 blank
        DEFB 050h,050h,070h,030h,020h,0C0h,0A0h,060h   ; 1 sky to ground

Eight cells of tile 1 at rows 1, 2 and 3:

        LD A,33         ; row 1, column 1
        CALL eight
        LD A,65         ; row 2, column 1
        CALL eight
        LD A,97         ; row 3, column 1
        CALL eight

; --- eight cells of tile 1, starting at the name table low byte in A
eight:  OUT (9),A
        LD A,120
        OUT (9),A
        LD B,8
e1:     LD A,1
        OUT (8),A
        DJNZ e1
        RET

What you should see: a block of cells banded top to bottom - light blue, light blue, cyan, light green, medium green, dark green, dark yellow, dark red - repeating every eight pixels down.

A solid pattern is the right choice here, and worth saying why. With every bit set there is nowhere for the backdrop to show through, so all eight bytes' high nibbles are on display and none of the low nibbles matter. Any other pattern and you are reading two colours per row against a third behind them, which is harder to judge and not what the exercise is asking you to look at.

The repetition every eight pixels is the limitation you have just run into. Eight colour bytes give a gradient exactly one cell tall, and stacking the same tile three deep repeats it three times rather than continuing it. A gradient down a whole screen needs one pattern per cell row - twenty-four of them - or the same pattern with twenty-four different colour entries, which the three banks will give you three of and no more.

Which is the honest summary of Graphics II's colour: astonishingly cheap per tile, and inflexible the moment you want something that is not made of tiles.

S19. Sprites

Four of these six need no pattern data at all, which is the section's point made in listings rather than prose. Where a shape is wanted it is this one:

face:   DEFB 00111100b
        DEFB 01000010b
        DEFB 10100101b
        DEFB 10000001b
        DEFB 10100101b
        DEFB 10011001b
        DEFB 01000010b
        DEFB 00111100b

and the four attribute bytes always go the same way: point port 9 at 3B00h with LD A,0 / LD A,123, then OUT (8),A four times per sprite, Y, X, pattern, colour. The VDP walks the address along, so a run of sprites is one setup and four writes each.

19.1 - Five sprites in a row

        ORG 256

        LD A,0
        OUT (9),A
        LD A,123        ; 40h + 3Bh - the attribute table
        OUT (9),A

        LD A,100        ; sprite 0 - A, white
        OUT (8),A
        LD A,60
        OUT (8),A
        LD A,65
        OUT (8),A
        LD A,15
        OUT (8),A

        ; ... four more, X stepping 76, 92, 108, 124,
        ;     patterns 66 to 69, colours 7, 9, 3, 11 ...

        JP $

What you should see: A B C D in white, cyan, light red and light green. Four sprites where you wrote five, and no sign at all of the fifth.

The number is four because only four sprites are drawn on any one scan line, and all five of these share every line they occupy. The fifth is not corrupted or misplaced - it is simply not drawn there. Move it to a different Y and it reappears, which is worth doing to convince yourself the entry was fine all along.

This is the limit that shapes how a game lays out its sprites, and it is per line, not per screen: thirty-two sprites are fine as long as no horizontal line has more than four of them crossing it.

19.2 - Move it

move:   LD A,0          ; point at sprite 0's Y again
        OUT (9),A
        LD A,123
        OUT (9),A
        LD A,100        ; Y unchanged
        OUT (8),A
        LD A,(xpos)     ; X
        OUT (8),A

        CALL delay

        LD A,(xpos)
        INC A
        LD (xpos),A
        CP 248
        JR NZ,move

        JP $

delay:  LD DE,2000
d1:     DEC DE
        LD A,D
        OR E
        JR NZ,d1
        RET

xpos:   DEFB 0

What you should see: the face crossing the whole screen, left to right, leaving nothing behind it.

Now the comparison the exercise asks for, against 17.6's moving tile.

The tile version needed 17.5's whole cell routine - the row times thirty-two, the column added, the carry into the high byte - and then two of those calls per step, one to blank the old cell and one to draw the new. It could only ever land on one of 768 positions, and it flickered because for part of every step there was nothing on screen at all.

This is four OUTs. No erasing, because the sprite is not in the background; no arithmetic, because the position is the number you store. And it moves one pixel at a time rather than eight.

That is the entire argument for sprites, and it is worth having written both to feel it.

19.3 - Diagonally, and bounce

        LD A,(xpos)     ; --- X
        LD B,A
        LD A,(xstep)
        ADD A,B
        LD C,A          ; the position it wants to move to
        CP 241
        JR NC,xbounce   ; off the right, or wrapped past zero
        CP 4
        JR C,xbounce    ; off the left
        LD A,C
        LD (xpos),A
        JR xdone
xbounce: LD A,(xstep)
        NEG
        LD (xstep),A
xdone:

with the same eight lines again for Y against 184, and:

xpos:   DEFB 100
ypos:   DEFB 100
xstep:  DEFB 3
ystep:  DEFB 2

What you should see: the face crossing the screen diagonally and turning at each edge. Three seconds in it was at (76, 66); eight seconds in, at (223, 139); and it never leaves the screen.

Three things in that listing are the exercise.

The step is applied to a copy first. C holds where the sprite wants to go, and the move is only committed if it is legal. The obvious alternative - move first, then notice you have gone too far and negate - leaves the sprite one step off the screen for one frame, and on a slow bounce you can see it.

NEG is the whole of "negate". It is one byte, it subtracts A from zero, and it is the instruction you were reaching for. A step of 3 becomes 253, and 253 added to a position is the same as subtracting 3 from it, because a byte wraps. Nothing needs to know which direction the number "really" means.

Both edges need testing, and one of the tests is not obvious. CP 241 / JR NC catches the right-hand edge - and it also catches a position that has wrapped past zero going left, because 4 minus 3 minus 3 is 254, not -2. So the high test does double duty and the low test only has to catch the last few pixels. Get this wrong and the sprite escapes leftwards, wraps, and comes back from the right, which looks like a different bug entirely.

19.4 - Two sprites, one shape

        LD A,100        ; sprite 0
        OUT (8),A
        LD A,100
        OUT (8),A
        LD A,0          ; pattern 0
        OUT (8),A
        LD A,15         ; white
        OUT (8),A

        LD A,100        ; sprite 1 - the same pattern
        OUT (8),A
        LD A,140
        OUT (8),A
        LD A,0          ; pattern 0 again
        OUT (8),A
        LD A,9          ; light red
        OUT (8),A

What you should see: two identical faces, one white and one light red.

And the answer: the second sprite cost four bytes - its attribute entry - and nothing else. No pattern data, no VRAM beyond the four bytes that were already reserved for sprite 1 whether you used it or not. In practice it cost nothing at all.

That is the same saving as a tile appearing in many cells, arriving from a different direction: a pattern is a shape, and how many things are wearing it is a separate question. Thirty-two sprites can share one eight-byte pattern.

Note that colour is per sprite, not per pattern - the opposite of tiles, where the colour belongs to the pattern and every cell using it is the same. That is why a formation of identical enemies in different colours is free here and is not free with tiles.

19.5 - Switch them off

Four sprites at one Y, with 208 written into the second one's Y byte:

        LD A,100        ; sprite 0 - A
        OUT (8),A
        LD A,60
        OUT (8),A
        LD A,65
        OUT (8),A
        LD A,15
        OUT (8),A

        LD A,208        ; sprite 1 - the terminator
        OUT (8),A
        LD A,90
        OUT (8),A
        LD A,66
        OUT (8),A
        LD A,7
        OUT (8),A

        ; ... sprites 2 and 3, both perfectly valid ...

What you should see: one A. Nothing else.

Which is the answer, and the exact reason matters: 208 does not switch off the sprite it is in - it stops the VDP reading the table. Sprite 0 was read and drawn. Sprite 1 said 208, so processing stopped there, and sprites 2 and 3 were never looked at, however correct their entries are.

So this is the cheap way to disable every sprite from some point onwards - write one byte, and thirty entries go away - and it is a trap worth respecting. Any sprite whose Y can be computed to 208 will take every sprite behind it in the table with it, and 208 is only just off the bottom of a 192-line screen. A falling object that runs past the bottom of the screen will pass through it.

Two ways to stay out of trouble: keep computed Y values clamped below 192, or put the sprites whose disappearance you would not notice at the end of the table, not the start.

19.6 - A sprite from the font

        LD A,100        ; sprite 0
        OUT (8),A
        LD A,80
        OUT (8),A
        LD A,94         ; the up arrow - Appendix I, code 94
        OUT (8),A
        LD A,11         ; light yellow
        OUT (8),A

        LD A,100        ; sprite 1
        OUT (8),A
        LD A,120
        OUT (8),A
        LD A,160        ; the solid block - Appendix I, code 160
        OUT (8),A
        LD A,3          ; light green
        OUT (8),A

        LD A,100        ; sprite 2
        OUT (8),A
        LD A,160
        OUT (8),A
        LD A,93         ; the right arrow - Appendix I, code 93
        OUT (8),A
        LD A,7          ; cyan
        OUT (8),A

What you should see: a yellow up arrow, a green block and a cyan right arrow. Three sprites, three colours, and not one byte of pattern data in the program.

The codes are in comments because you will not remember them, and finding a usable shape means looking through Appendix I rather than guessing.

One warning worth taking from a mistake made writing this. Codes 0 to 31 draw nothing. They are control codes with blank glyphs, which is exactly why the section recommends them as the safe range to overwrite - and it also means a sprite given one of those pattern numbers, with no pattern written, is invisible. Pick from 32 upwards when you want a shape that is already there, and from 0 to 31 when you are providing your own.

For a first game this is the shortcut to take. An up arrow is a perfectly good ship and a full stop is a perfectly good bullet, and you can design the real sprites once the game works.

S20. Bigger sprites

The character these four share is a sixteen-by-sixteen lander, and it is worth showing the way it was actually designed - as rows of text - because that is the step the exercise says you will do many times:

......####......
.....######.....
....##....##....
...##..##..##...
...##..##..##...
....##....##....
.....######.....
......####......
....########....
...##########...
..##..####..##..
.##...####...##.
##....####....##
#.....####.....#
......####......
.....#....#.....

Sixteen rows of sixteen. Converting that into DEFB lines is mechanical and it is where the mistakes live, so the order is worth writing on the drawing itself: the first sixteen bytes are the whole left half, top to bottom - the left eight characters of row 0, then of row 1, all the way to row 15, and only then the right eight of row 0.

20.1 - Your own character

        ORG 256

        LD A,0          ; the four quadrants into patterns 0-3
        OUT (9),A
        LD A,88
        OUT (9),A
        LD HL,lander
        LD B,32
sp:     LD A,(HL)
        OUT (8),A
        INC HL
        DJNZ sp

        LD A,226        ; 224 + 2: 16x16 sprites
        OUT (9),A
        LD A,129
        OUT (9),A

        LD A,0
        OUT (9),A
        LD A,123
        OUT (9),A
        LD A,100
        OUT (8),A
        LD A,120
        OUT (8),A
        LD A,0          ; group 0
        OUT (8),A
        LD A,7          ; cyan
        OUT (8),A

        JP $

lander:
        ; top-left
        DEFB 00000011b
        DEFB 00000111b
        DEFB 00001100b
        DEFB 00011001b
        DEFB 00011001b
        DEFB 00001100b
        DEFB 00000111b
        DEFB 00000011b
        ; bottom-left
        DEFB 00001111b
        DEFB 00011111b
        DEFB 00110011b
        DEFB 01100011b
        DEFB 11000011b
        DEFB 10000011b
        DEFB 00000011b
        DEFB 00000100b
        ; top-right
        DEFB 11000000b
        DEFB 11100000b
        DEFB 00110000b
        DEFB 10011000b
        DEFB 10011000b
        DEFB 00110000b
        DEFB 11100000b
        DEFB 11000000b
        ; bottom-right
        DEFB 11110000b
        DEFB 11111000b
        DEFB 11001100b
        DEFB 11000110b
        DEFB 11000011b
        DEFB 11000001b
        DEFB 11000000b
        DEFB 00100000b

What you should see: a cyan lander sixteen pixels square, a domed body on four spread legs.

The check that the split is right is that the picture is coherent. Get the quadrant order wrong and you do not get a slightly wrong lander, you get the scrambled mess S20's first edit produces - so if the shape reads at all, the order is right.

20.2 - Two frames

Frame two is the same lander with its legs tucked in, at group 4 - which means patterns 4 to 7, starting at 1800h + 32:

        LD A,32         ; frame two into patterns 4-7, at 1800h + 32
        OUT (9),A
        LD A,88
        OUT (9),A
        LD HL,frame2
        LD B,32
sp2:    LD A,(HL)
        OUT (8),A
        INC HL
        DJNZ sp2

and then the animation, which is the point:

flip:   LD A,2          ; point at the pattern byte only - 3B00h + 2
        OUT (9),A
        LD A,123
        OUT (9),A
        LD A,(group)
        OUT (8),A       ; one byte changes the whole frame

        CALL delay

        LD A,(group)    ; 0 becomes 4, 4 becomes 0 - with CP and JR,
        CP 0            ; because XOR is not until S22
        JR Z,setfour
        LD A,0
        JR store
setfour: LD A,4
store:  LD (group),A
        LD A,(count)
        DEC A
        LD (count),A
        JR NZ,flip

What you should see: the lander's legs spreading and tucking, about twice a second.

The address is 3B00h + 2, not 3B00h. The pattern number is the third of the four attribute bytes, and you can address it directly - so a frame change is one OUT (8), without touching Y, X or colour. That is the whole of animation on this machine: the shape a sprite wears is a number, and changing a number is free.

Sixty-four bytes of pattern data bought two frames. Eight frames would be 256 bytes and still one byte per change, which is why animation on this chip is cheap in code and expensive in VRAM - the opposite of the trade you might expect.

XOR 4 is the natural way to alternate 0 and 4 and it is three bytes shorter than what is written above. It arrives in S22; there is no harm in using the long form until then.

20.3 - A tall ship

Three 8x8 patterns - nose, body, exhaust - and one routine that places all three:

        LD B,100        ; place it once at (120, 100)
        LD C,120
        CALL place

        JP $

; --- place the whole ship from one position. B = Y, C = X.
; Three entries: nose at Y, body at Y+8, exhaust at Y+16.
place:  LD A,0
        OUT (9),A
        LD A,123
        OUT (9),A

        LD A,B          ; nose
        OUT (8),A
        LD A,C
        OUT (8),A
        LD A,0
        OUT (8),A
        LD A,15         ; white
        OUT (8),A

        LD A,B          ; body, eight lines lower
        ADD A,8
        OUT (8),A
        LD A,C
        OUT (8),A
        LD A,1
        OUT (8),A
        LD A,15         ; white
        OUT (8),A

        LD A,B          ; exhaust, eight lower again
        ADD A,16
        OUT (8),A
        LD A,C
        OUT (8),A
        LD A,2
        OUT (8),A
        LD A,9          ; light red
        OUT (8),A

        LD A,208        ; nothing after this
        OUT (8),A
        RET

What you should see: a white ship twenty-four pixels tall with a light red exhaust at its foot, and no seam anywhere.

Three things about that routine are the exercise rather than the picture.

One position in, three entries out. The caller knows one (x, y) and cannot get the parts out of step, because nothing else in the program is allowed to know where the exhaust goes. This is the rule the section states and it is the only way a composite survives being moved.

ADD A,8 and ADD A,16, not INC eight times. The offsets are constants of the design, and writing them as constants is how the listing stays readable when the ship grows a fourth part.

The trailing 208. One extra OUT (8),A after the third entry writes 208 into the fourth sprite's Y, so the VDP stops there and the twenty-eight unused entries are not processed at all. Safe here because the ship is the only object on screen - and exactly the thing S20 warns you about the moment there is anything else, because that one byte would switch it off too.

20.4 - Shade it

Two 16x16 groups. Group 0 is the hull with a four-by-two hole punched in its middle; group 4 is the highlight, which is nothing but that block:

        LD A,100        ; sprite 0 - the highlight, in front
        OUT (8),A
        LD A,120
        OUT (8),A
        LD A,4
        OUT (8),A
        LD A,11         ; light yellow
        OUT (8),A

        LD A,100        ; sprite 1 - the hull, behind
        OUT (8),A
        LD A,120
        OUT (8),A
        LD A,0
        OUT (8),A
        LD A,7          ; cyan
        OUT (8),A

What you should see: the cyan lander with a light yellow band across its middle, reading as one two-coloured object.

Which layer goes in front matters less than you would think, and that is worth understanding rather than memorising. If the layers are true complements - every pixel belonging to exactly one of them - the order makes no difference at all, because they never contend for a pixel. Priority only decides anything where both layers have ink, and a well-punched pair has nowhere like that. The section's alien edit shows the other case: stop punching the holes and priority starts to matter immediately.

So punch the holes properly and you can stop thinking about layer order. Leave them unpunched and the front layer's colour wins everywhere they overlap, which is either a shortcut or a bug depending on whether you meant it.

The bill: two sprites, 64 bytes of pattern data, and two of every scan line's four slots for as long as this character is on screen. One more character like it and the line is full. That is why the section says to spend layers on the hero.

S28. A clock of your own

S21 to S27 set no exercises, so this follows on from S20. All four here build on the section's own program - the vector table, the channel reset, IM 2, the handler - and change only what the exercise asks for.

28.1 - Pick a rate

The formula is rate = 4,070,000 / 256 / time constant, so for 200 a second:

        4,070,000 / 256 / 200  =  79.5,  so 80

and one line of the setup changes:

        LD A,165        ; A5h - interrupt enable, timer mode, prescaler 256
        OUT (40),A
        LD A,80         ; 4.07M / 256 / 80 = about 199 a second
        OUT (40),A

What you should see: the same HL = 01F4 as the section's program, but you wait about two and a half seconds for it instead of eight.

And the exercise's real instruction - confirm it by counting interrupts, not by watching a clock - is worth taking seriously. The count is the measurement: 500 interrupts at 199 a second is 2.5 seconds, so if the program takes about two and a half seconds to finish, the rate is right. Timing the program with a stopwatch measures the same thing twice as badly.

28.2 - A metronome

        LD A,(ticks)    ; 31 interrupts is about half a second
        INC A
        CP 31
        JR NZ,keep
        LD A,(colr)     ; half a second up - swap the backdrop
        XOR 2           ; F4h and F6h differ in one bit
        LD (colr),A
        OUT (9),A
        LD A,135
        OUT (9),A
        LD A,0          ; and start the half-second again
keep:   LD (ticks),A

dropped into the handler after the counting, with ticks and colr as two more bytes of data. 31 interrupts at 62.1 a second is 0.499 seconds, which is the N the exercise asks you to pick.

XOR 2 is the neat part: the two backdrop bytes are F4h and F6h, which differ in exactly one bit, so the whole swap is one instruction and no branches. Choosing colours that differ in one bit is worth doing deliberately whenever something has to alternate.

What you should see - and what actually happened here. The toggling definitely runs: adding a counter to it gives 19 toggles over 600 interrupts, which is 600/31 exactly. But on the machine as tested, the backdrop did not visibly alternate - it sat on one colour throughout, whichever way the program was arranged.

That is not a fault in this listing. Writing register 7 from a program that has interrupts running does not reliably reach the screen, and the same handler will happily drive a sprite instead - it is only this one register that misbehaves. It is an open question about the machine rather than about the code, so treat the metronome as written correctly and unconfirmed on screen, and if you want a visible half-second tick right now, move something rather than recolour something.

28.3 - Time a tune

This is the exercise the section is really for. The handler owns the tune; the main loop never learns that there is one.

        LD A,(sub)      ; sixteen interrupts to a note, about a quarter second
        INC A
        CP 16
        JR NZ,keepsub
        LD A,(note)     ; step to the next note in the tune
        INC A
        CP 4
        JR NZ,setnote
        LD A,0
setnote: LD (note),A
        LD HL,tune      ; look the period up and send it
        LD B,0
        LD C,A
        ADD HL,BC
        LD A,0
        OUT (2),A
        LD A,(HL)
        OUT (3),A
        LD A,0
keepsub: LD (sub),A

with the sound chip set up once before EI - tone A through the mixer, a fixed volume, coarse period zero - and the tune itself four bytes of data:

note:   DEFB 0
sub:    DEFB 0
tune:   DEFB 60,80,101,120   ; four periods - a little four-note phrase

What you should see: nothing, and hear a four-note phrase repeating at about four notes a second, while the main loop does exactly what it did before - watch a number climb.

Checked by reading the sound chip's registers back at two moments in one run:

2s into the run   register 0 = 0
4s into the run   register 0 = 120

Register 0 is tone A's period and 120 is one of the four in the table, so the handler is driving the tune with the main loop uninvolved. That is the whole argument of the section in one measurement.

Two notes on the shape of it. Sixteen interrupts to a note is the same divide-down as S22's animation clock and S26's frame counter - a fast clock and a counter is how you get any slower clock you like. And the tune is a table, S13 again: adding notes means adding bytes and changing the CP 4, not writing code.

28.4 - Two clocks at once

wait:   IN A,(9)        ; the frame flag - S26's clock, polled here
        BIT 7,A
        JR Z,nofr
        LD HL,(frames)
        INC HL
        LD (frames),HL
nofr:   LD HL,(count)
        LD DE,500
        OR A
        SBC HL,DE
        JR C,wait       ; two clocks counted in one loop

and at the end, both counts handed back:

        LD HL,(count)
        LD DE,(frames)
        RET             ; HL = CTC interrupts, DE = frames

What you should see: HL = 01F4 and DE = 019B - 500 CTC interrupts against 411 frames, from the same stretch of time.

Which is the answer. Two clocks, two counts, and no arithmetic that could turn one into the other: 500 against 411 is a ratio of 1.216, and 62.1 against 50 is 1.242. They are close because both clocks are honest and neither is derived from the other.

One change to the handler is needed and it is not obvious. The section's handler contains IN A,(9), and that read consumes the VDP's frame flag - the very flag this loop is trying to poll. Leave it in and the frame count comes out low, for a reason that has nothing to do with either clock. The section's fourth Change one thing edit has already established the line does nothing useful; this is the exercise where its absence starts to matter.

That is also why the measured ratio is a little under the predicted one: even with the handler's read gone, the loop can still be inside the interrupt when a frame flag goes up, and a flag not seen is a frame not counted. The two clocks are independent, and so are their misses.

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.