.png)
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.
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.
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.
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.
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.
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.
; --- 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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.