Einstein 256 Primer cover: Graphics 7's 256 colours on the Einstein 256's screen
← Back to Modules
module
19

A Game for the 256

Introduction

The assembly course ended with a game: S29's rocks fell, you flew, you shot or dodged, and S31 gave it a way to lose. It was built from everything the TC-01 could do: one colour a sprite, four sprites a line, a screen that stayed where it was, and a frame flag your loop had to watch for.

This section builds the same kind of game for the 256, and every part of it is something the TC-01 can't do. River Run flies you up a winding river. The river scrolls down the screen a line every frame, without end, and never the same twice. A score panel sits still underneath it. Your ship has three colours on every line, the rocks are shaded from light to dark, and a crash turns the water red for a moment. One interrupt drives it all.

Almost nothing here is new. The new part is fitting the pieces together, and three places where two pieces get in each other's way.

The Shape of It

Setup runs once: Graphics 4, the sprite tables, the panel, the first 161 lines of river, the ship in mid-river, then both interrupts on.

After that, the work is split in two, and the split is the most important idea in the program.

The frame interrupt does all the video work. onframe (S7) runs once a frame, straight after the picture is finished:

onframe:
        ...             ; R#2, R#23, R#19, R#8 for the top of the picture
        CALL fade       ; the water's colour
        CALL sprites    ; the sprites, placed for this scroll
        CALL clip       ; no sprite row on line 160
        ...             ; feed in the next line of river
        JP numbers      ; the panel's numbers, when they change

The main loop does the game, and never touches the video chip:

main:   CALL waitf
        RST 8
        DEFB 181        ; MOS: a key, or 0 (never from onframe)
        ...             ; Q quits; game over counts down
        CALL steer      ; the joystick
        CALL shoot      ; fire, and the bullet's flight
        CALL rocks      ; the rocks float down
        CALL hits       ; what touched what
        CALL banks      ; over the bank?
        JR main

Why so strict? The video chip has one address register. If the main loop is half way through setting an address when the interrupt arrives, and the interrupt sets one of its own, the main loop's next byte goes to the wrong place. S10's place got round this with DI and EI. Here that would be a mistake: a DI at the wrong moment holds up the line interrupt, and the panel's join lands late.

So the main loop only changes numbers in RAM: where the ship is, where each rock is, the score, a flag that says the panel needs redrawing. onframe reads them and does the drawing. Nothing else in the program writes to the video chip once the game is running.

It works the other way round for MOS. The main loop may call MOS, as it does for the keyboard, because frame.asm's stub pages the ROM out if the interrupt arrives during the call (S7). onframe must never call MOS.

The River

The playfield is the Graphics 4 page at 10000h, 256 lines of 128 bytes, as in S14. The screen shows 160 of them above the panel.

S14 scrolled up. River Run scrolls down, so you seem to fly up the river: each frame, onframe takes one off R#23, and the picture moves down a line. The line that appears at the top is the one above the old top, R#23 - 1, and until this frame it was at the bottom of the loop, out of sight. So onframe draws the river's next row there, ready for the next frame:

        LD A, (scroll)
        DEC A           ; the line above the top, out of sight
        CALL newrow

Scrolling down, you feed the line above the top; scrolling up, as in S14, the line just gone off it. Either way you draw on a line nobody can see.

S14's river was worked out from the row number, so it repeated. This one is made as it goes. newrow keeps the left bank's position in bank, in bytes. Every 16 rows it picks a new direction at random - left, straight on or right - and every other row it moves the bank a byte that way, turning back at the edges. The water is always 48 bytes (96 dots) wide. Then it builds the row in a 128-byte buffer in RAM - dark green bank, a byte of sand, blue water, sand, bank, and one random dot, a white glint in the water or a light green one on the bank, so you can see the banks move - and sends it with one OTIR.

newrow also writes the bank into a 256-byte table, BANKS, at the row's line number. The picture is in video memory, where the game can't easily look; the table keeps the one fact the game needs about each line, where its water starts, in RAM. A crash into the bank is then a look-up:

banks:  LD A, (scroll)
        ADD A, SHIPY+8  ; the VRAM line under the ship's middle
        LD L, A
        LD H, BANKS/256
        LD A, (HL)      ; that line's left bank

Screen line y is VRAM line y + R#23, so adding the scroll turns the ship's screen line into the right entry. With BANKS at 9300h, a whole page of RAM, the line number is simply the low byte of the address.

What it costs: 128 bytes to video memory a frame for the river, and a 256-byte table in RAM. The random numbers come from a 16-bit shift register, rnd, which takes the address of its seed. The river and the rocks have a seed each, because the river's numbers are made in the interrupt and the rocks' in the main loop, and one shared seed could be updated by both at once.

The Panel

The panel is S15's, unchanged in principle: 32 lines at the top of the page at 18000h, shown from screen line 160 by the line interrupt. online switches R#2 to the panel's page and R#23 to 96, and onframe puts both back for the top of the next picture. The words SCORE and LIVES are drawn once, at setup, from MOS's font (S15's glyph).

The numbers change, and drawing them is slow next to everything else here: each character is eight reads from the font and 32 bytes written. So the main loop never draws them. When it changes the score or the lives it sets dirty, and onframe redraws the numbers on the next frame, then clears the flag. Most frames nothing changes and the panel costs nothing at all.

S29 carried its score by hand, a counter per digit. This one is kept in BCD, binary-coded decimal: two digits a byte, one in each nibble. DAA (decimal adjust) after an ADD puts A right as a pair of decimal digits, so 09h plus 1 comes out as 10h, not 0Ah; ADC A, 0 and another DAA carry into the next byte. Each nibble then prints as one digit, by adding '0'. Four digits and a fixed 0 make the score go up in tens.

Sprites on a Scrolling Screen

This is the first of the three places where two features meet.

R#23 moves the sprites too. The chip counts a sprite's Y in lines of the scrolled picture, not lines of the screen: a sprite shows at screen line Y + 1 - R#23. Scroll the picture down a line and every sprite goes with it.

That is exactly right for something painted on the river, and wrong for a ship that should stay where it is on the screen. River Run keeps every object's position as a screen line, and converts as it sends the attribute table:

sp1:    LD A, (scroll)
        DEC A
        ADD A, (HL)     ; Y = line - 1 + scroll
        CP 216
        JR NZ, sp2
        INC A           ; 216 would end the list: a line lower instead

Y = 216 still ends the sprite list (S8), scroll or no scroll. Once you add R#23 to a sprite's Y, any sprite can come out at 216 for a frame, and then it and every sprite after it vanish. So sprites steps round it: 216 becomes 217, and that sprite is drawn one line low for one frame, which nobody will see.

The objects live in RAM as three bytes each - screen line, X, pattern - in sprite order: the ship, its overlay, the bullet, four rocks. Every frame sprites sends all seven and the end mark: 29 bytes. The overlay doesn't have a position of its own; sprites copies the ship's into it first.

Out of sight is a place. A sprite needs to be somewhere when it isn't in play. The screen shows 192 lines, so a sprite at screen line 208, PARK, is never drawn: that is where the bullet waits between shots, and where the ship goes at the end. The rocks have a longer wait. A rock below the playfield, at lines 160 to 239, is hidden too - by the panel, or below the picture - so a rock that floats off the bottom is given a random line in that range and simply carries on floating. When it reaches line 240 it is 16 lines above the top, and it comes in again at a new place, which spawn picks from BANKS at the top line. A crash sends all four rocks to 160, 180, 200 and 220, so the river is clear for a moment, then fills again.

The rocks move down a line a frame, the same as the river, so they float with the water.

No Sprites in the Panel

The second meeting: the panel and the sprites. The panel is a different page, scrolled to 96, and the chip would happily draw sprites over it, at whatever lines of the panel's scroll they happen to fall on. Turn them off. R#8 bit 1 (SPD) hides all the sprites (S8), and like any register the line handler writes, it takes effect from the next line. So online ends with R#8 = 0Ah, and onframe writes 08h, MOS's value, for the top of the next picture.

The SPD write has to come last. S15 found that the panel's R#2 and R#23 writes must go first and fast, or the panel's top line is wrong. And so the SPD write lands a line late: on line 160, the panel's white line, the chip has R#23 = 96 already, and sprites still on. It draws whatever sprite rows are on line 160 + 96 = 256, which is line 0 of the scrolled picture: a stray line of a rock or a bullet across the panel's top, at a place that has nothing to do with where the rock is on screen.

The cure uses sprite mode 2's colour table. A colour byte of 00h leaves that line of the sprite undrawn, so clip rewrites the colour table every frame, with the one row of each sprite that would land on scrolled line 0 set to 00h:

cl1:    LD A, (DE)      ; the Y this sprite was sent with
        INC A
        NEG
        LD C, A         ; C = the row on scrolled line 0, if 0-15

Row r of a sprite is on scrolled line Y + 1 + r, so the row on line 0 is r = -(Y + 1). If that comes out as 16 or more, the sprite isn't there and no row is blanked. sprites keeps the Y it sent for each sprite in ys for this.

What it costs: 112 bytes to the colour table a frame, for a line that would otherwise be wrong in nearly half the frames.

Sprites in Many Colours

The ship is S9's: sprite 0 with a colour on every line, cyan at the nose through light blue to green, and sprite 1 on top of it with CC set on every line, so its red is OR-ed into the colours underneath. Where the two overlap you get a third colour on the same line. The exhaust lines are marked IC, so a rock touching the flame doesn't count.

The rocks use a colour a line to look solid: white on the top two lines, as if lit from above, then grey, then dark red, then black underneath. That's sixteen bytes in the colour table, the same for all four rocks.

The bullet is sprite 2, lower-numbered than the rocks, so it is drawn in front of them.

The ship and two rocks, magnified: the ship in cyan, light blue and green with red pods and a yellow flame, the rocks white, grey, dark red and black

At most seven sprites share a line - the ship and its overlay, the bullet and four rocks - inside sprite mode 2's eight. On the TC-01 the ship alone would use half the line's four.

What Touched What

The collision bit is the assembly course's S24 again. frame.asm keeps the status the handler read in STATUS (S7), and bit 5 says that two sprites touched in the picture just shown. It doesn't say which. hits asks only when the bit is set, and then checks the coordinates: the bullet against each rock in sight, then the ship against each rock.

A rock that touches another rock sets the bit too. The coordinates find nothing near the ship or the bullet, and nothing happens, which is just what you want. The ship and its overlay never set it at all, because of CC, and the exhaust doesn't because of IC.

near does each check, one axis at a time: carry if the difference in A is less than C either way. The windows are generous, because the bit is from the last picture and everything has moved one step since. The bit has already said the sprites touched; the coordinates only have to say which.

The banks are not sprites, so the bit knows nothing about them. banks checks them by coordinates every frame, from the BANKS table.

The Water Turns Red

crash costs a life, clears the rocks away, puts the ship back in mid-river, and sets flash to 14. fade, in onframe, counts it down and rewrites one palette entry each frame (S6): entry 4, the water's blue, which is also the border colour. It goes from red back to blue in seven steps:

        SRL A           ; f = 6 down to 0
        LD C, A
        INC A           ; red 7 down to 1
        ...
        LD A, 7
        SUB C           ; blue 1 up to 7

The last step is red 1, green 1, blue 7: MOS's own colour 4. Nothing in the bitmap is redrawn. One palette entry recolours every dot of water on the screen, and the border with it, in the time it takes to send two bytes.

With no lives left, crash sets over, which puts GAME OVER in the panel and stops the main loop moving anything. The river keeps scrolling. After 150 frames, three seconds, the game goes back to MOS.

Leaving

Two ways out: the end of the game, or Q at any time, read through MOS's key call 181 from the main loop (the assembly course's S8). It returns at once, with 0 if no key is down, so it costs the loop nothing.

The way back is S15's, plus the two registers this game changed that the others didn't: R#0 without the line interrupt, under DI, first; R#8 back to 08h; R#6 back to 03h, MOS's sprite patterns at 1800h; then irqoff and tomos. tomos calls MOS's BE, which also puts the palette back, so a crash's red never outlives the game.

The Code

; S19: River Run - a game for the 256.
;
; A river winds through green banks, scrolling down the screen a line a
; frame: you are flying up it. Steer with joystick A, left and right; fire
; shoots. Rocks float down the river. Shoot one for 10 points. Touch one,
; or fly over a bank, and you lose a life: the water flashes red. With no
; lives left the panel says GAME OVER and, three seconds later, the game
; returns to MOS. Q quits at any time.
;
; What it uses, and where it was taught:
;   Graphics 4, the playfield page at 10000h, the panel at 18000h (S11, S13)
;   R#23 scrolls the playfield; a new line is fed in each frame     (S14)
;   a line interrupt switches to the panel at line 160              (S15)
;   sprite mode 2: a colour a line, CC, IC, 7 sprites on a line     (S8, S9)
;   the 50 Hz frame interrupt drives the game                       (S7)
;   joystick A on ports 30h and 20h                                 (S10)
;   palette entry 4 (the water) flashed on a crash                  (S6)
;
; Memory: frame.asm uses 9000h-9203h and s15-lineint.asm 9140h-; this
; program keeps its bank table at 9300h and a row buffer at 9400h.

SHIPY:  EQU 136         ; the ship's top line on screen
PARK:   EQU 208         ; a line below the picture: a sprite here is unseen
WAIT:   EQU 160         ; rocks at lines 160-239 are out of sight, waiting
WATER:  EQU 48          ; the river's width in bytes (96 dots)
BANKS:  EQU 9300h       ; the left bank, in bytes, for each of 256 lines
ROW:    EQU 9400h       ; one line of the river, built before it is sent

        ORG 256

        DI
        ; --- Graphics 4 first, showing the page at 10000h, not scrolled
        LD A, 06h
        LD B, 0
        CALL setreg
        LD A, 5Fh
        LD B, 2
        CALL setreg
        XOR A
        LD B, 23
        CALL setreg

        ; --- the sprite tables, at the top of the panel's page (S8):
        ;     patterns 1F000h, colours 1F800h, attributes 1FA00h
        LD A, 7
        LD B, 14
        CALL setreg     ; R#14 = 7: 1C000h-1FFFFh
        LD HL, 3000h
        CALL vwrite
        LD HL, pats
        LD BC, 128*256+8        ; 4 patterns of 32 bytes, to port 8
        OTIR
        LD HL, 3800h
        CALL vwrite
        LD HL, cols
        LD BC, 112*256+8        ; 7 sprites of 16 lines
        OTIR
        LD HL, 3A00h
        CALL vwrite
        LD A, 216               ; no sprites until the game starts
        OUT (8), A
        LD A, 0F7h
        LD B, 5
        CALL setreg     ; R#5 = F7h, R#11 = 03h: attributes 1FA00h
        LD A, 03h
        LD B, 11
        CALL setreg
        LD A, 3Eh
        LD B, 6
        CALL setreg     ; R#6 = 3Eh: patterns 1F000h

        ; --- the panel, then the river: lines 159 up to 0, then 255
        CALL panel
        LD A, 159
river:  PUSH AF
        CALL newrow
        POP AF
        DEC A
        CP 254
        JR NZ, river
        CALL centre     ; the ship in mid-river

        ; --- both interrupts (S15)
        CALL irqon
        CALL lineon
        LD A, 159
        LD B, 19
        CALL setreg     ; the split: line 159 + R#23
        LD A, 16h
        LD B, 0
        CALL setreg     ; R#0: Graphics 4, line interrupt on
        LD A, 0E2h
        LD B, 1
        CALL setreg     ; R#1: display, frame interrupt, 16x16 sprites
        EI

; --- the game loop: once a frame
main:   CALL waitf
        RST 8
        DEFB 181        ; MOS: a key, or 0 (never from onframe)
        CP 'Q'
        JR Z, quit
        CP 'q'
        JR Z, quit
        LD A, (over)
        OR A
        JR NZ, ending
        CALL steer      ; the joystick
        CALL shoot      ; fire, and the bullet's flight
        CALL rocks      ; the rocks float down
        CALL hits       ; what touched what
        CALL banks      ; over the bank?
        JR main

ending: LD HL, overt    ; game over: wait, then leave
        DEC (HL)
        JR NZ, main

; --- back to MOS: line interrupt off first (S15), then MOS's screen
quit:   DI
        LD A, 06h
        LD B, 0
        CALL setreg     ; R#0 without IE1
        LD A, 08h
        LD B, 8
        CALL setreg     ; R#8: sprites on, as MOS left it
        LD A, 03h
        LD B, 6
        CALL setreg     ; R#6: MOS's sprite patterns, 1800h
        LD A, 0C0h
        CALL irqoff
        CALL tomos      ; (BE puts the palette back too)
        RET

; --- steer: joystick A, two dots a frame (bits 2 left, 3 right; 0 = held)
steer:  IN A, (30h)
        LD C, A
        LD HL, ship+1   ; the ship's X
        BIT 2, C
        JR NZ, sr1
        LD A, (HL)
        CP 2
        JR C, sr1       ; at the left edge
        SUB 2
        LD (HL), A
sr1:    BIT 3, C
        RET NZ
        LD A, (HL)
        CP 239
        RET NC          ; at the right edge
        ADD A, 2
        LD (HL), A
        RET

; --- shoot: a bullet in flight climbs 4 lines a frame; fire starts one
shoot:  LD HL, bullet   ; its Y
        LD A, (HL)
        CP PARK
        JR Z, sh2       ; none in flight
        SUB 4
        JR NC, sh1
        LD A, PARK      ; off the top: park it
sh1:    LD (HL), A
        RET
sh2:    IN A, (20h)
        BIT 1, A        ; joystick A's fire: 0 while pressed
        RET NZ
        LD A, (ship+1)
        LD (bullet+1), A
        LD (HL), SHIPY-12
        RET

; --- rocks: each floats down a line a frame, with the river. Off the
;     bottom it waits out of sight for a while, then comes in at the top.
rocks:  LD HL, rock
        LD B, 4
rk1:    LD A, (HL)
        INC A
        CP WAIT         ; just gone off the bottom?
        JR NZ, rk2
        PUSH HL
        LD HL, seed
        CALL rnd
        POP HL
        AND 63
        ADD A, WAIT+1   ; wait 16 to 79 frames
rk2:    LD (HL), A
        CP 240          ; 16 lines above the top: coming in
        CALL Z, spawn
        INC HL
        INC HL
        INC HL
        DJNZ rk1
        RET

; --- spawn: give the rock at HL an X in the water at the top line
spawn:  PUSH HL
        PUSH BC
        LD HL, seed
        CALL rnd
        AND 63
        ADD A, 8        ; 8-71 dots into the river
        LD C, A
        LD A, (scroll)  ; the line at the top of the picture
        LD L, A
        LD H, BANKS/256
        LD A, (HL)
        ADD A, A        ; its left bank, in dots
        ADD A, C
        POP BC
        POP HL
        INC HL
        LD (HL), A
        DEC HL
        RET

; --- hits: STATUS bit 5 says two sprites touched in the last picture.
;     The coordinates say which. Only the rocks in sight are checked.
hits:   LD A, (STATUS)
        BIT 5, A
        RET Z           ; nothing touched
        LD HL, rock
        LD B, 4
ht1:    LD A, (HL)
        CP WAIT
        JR NC, ht3      ; out of sight
        LD D, A         ; D = the rock's Y
        INC HL
        LD E, (HL)      ; E = its X
        DEC HL
        PUSH BC
        LD A, (bullet)  ; the bullet?
        CP PARK
        JR Z, ht2
        SUB D
        LD C, 12
        CALL near
        JR NC, ht2
        LD A, (bullet+1)
        SUB E
        LD C, 10
        CALL near
        JR NC, ht2
        LD (HL), WAIT   ; shot: the rock goes, the bullet too
        LD A, PARK
        LD (bullet), A
        LD A, (score+1) ; and 10 points: one more in BCD
        ADD A, 1
        DAA
        LD (score+1), A
        LD A, (score)
        ADC A, 0
        DAA
        LD (score), A
        LD A, 1
        LD (dirty), A
        POP BC
        JR ht3
ht2:    LD A, SHIPY     ; the ship?
        SUB D
        LD C, 16
        CALL near
        JR NC, ht4
        LD A, (ship+1)
        SUB E
        LD C, 16
        CALL near
        JR NC, ht4
        POP BC
        JP crash
ht4:    POP BC
ht3:    INC HL
        INC HL
        INC HL
        DJNZ ht1
        RET

; --- near: carry if the difference A is less than C, either way
near:   OR A
        JP P, nr1
        NEG
nr1:    CP C
        RET

; --- banks: is the ship's middle line over water?
banks:  LD A, (scroll)
        ADD A, SHIPY+8  ; the VRAM line under the ship's middle
        LD L, A
        LD H, BANKS/256
        LD A, (HL)
        ADD A, A        ; the left bank, in dots
        LD B, A
        LD A, (ship+1)
        SUB B           ; the ship's X from the left bank
        JR C, crash     ; left of it
        CP 2*WATER-16+1
        RET C           ; 0-80: in the river
        ; fall through

; --- crash: a life lost. Flash the water, clear the river, start again.
crash:  LD A, 14
        LD (flash), A   ; 14 frames of red, fading (onframe)
        LD HL, lives
        DEC (HL)
        LD A, 1
        LD (dirty), A
        LD HL, rock     ; the rocks back out of sight
        LD B, 4
        LD A, WAIT
cr1:    LD (HL), A
        INC HL
        INC HL
        INC HL
        ADD A, 20
        DJNZ cr1
        LD A, PARK
        LD (bullet), A
        CALL centre
        LD A, (lives)
        OR A
        RET NZ
        INC A
        LD (over), A    ; no lives: game over
        LD A, PARK
        LD (ship), A    ; and no ship
        RET

; --- centre: the ship in the middle of the river
centre: LD A, (scroll)
        ADD A, SHIPY+8
        LD L, A
        LD H, BANKS/256
        LD A, (HL)
        ADD A, A
        ADD A, WATER-8  ; bank + half the river - half the ship
        LD (ship+1), A
        RET

; --- onframe: the frame interrupt's work (frame.asm calls it)
onframe:
        LD A, 5Fh
        LD B, 2
        CALL setreg     ; the playfield's page for the top
        LD HL, scroll
        DEC (HL)        ; down a line: R#23 one less
        LD A, (HL)
        LD B, 23
        CALL setreg
        LD A, (scroll)
        ADD A, 159
        LD B, 19
        CALL setreg     ; the split stays at line 160
        LD A, 08h
        LD B, 8
        CALL setreg     ; R#8: sprites on for the playfield
        CALL fade       ; the water's colour
        CALL sprites    ; the sprites, placed for this scroll
        CALL clip       ; no sprite row on line 160
        LD A, (scroll)
        DEC A           ; the line above the top, out of sight
        CALL newrow
        LD A, (dirty)
        OR A
        RET Z
        XOR A
        LD (dirty), A
        JP numbers      ; the panel's numbers, when they change

; --- online: line 159 - the panel from the next line; no sprites there
online: LD A, 7Fh
        OUT (9), A
        LD A, 128+2
        OUT (9), A      ; R#2: the panel's page
        LD A, 96
        OUT (9), A
        LD A, 128+23
        OUT (9), A      ; R#23: screen line 160 = panel line 0
        LD A, 0Ah
        OUT (9), A
        LD A, 128+8
        OUT (9), A      ; R#8 bit 1 (SPD): sprites off
        RET

; --- fade: after a crash, palette entry 4 goes from red back to blue
fade:   LD A, (flash)
        OR A
        RET Z
        DEC A
        LD (flash), A
        SRL A           ; f = 6 down to 0
        LD C, A
        INC A           ; red 7 down to 1
        RLCA
        RLCA
        RLCA
        RLCA
        LD B, A
        LD A, 7
        SUB C           ; blue 1 up to 7
        OR B
        LD B, A
        LD A, 4
        OUT (9), A
        LD A, 128+16
        OUT (9), A      ; R#16 = 4: palette entry 4
        LD A, B
        OUT (0Ah), A    ; 0RRR0BBB
        LD A, 1
        OUT (0Ah), A    ; 00000GGG: green 1
        RET

; --- sprites: the attribute table from the objects below. Their Y is a
;     screen line; the chip wants it with R#23 added (and less one).
sprites:
        LD HL, (ship)
        LD (overlay), HL ; the overlay goes where the ship is
        LD A, 7
        LD B, 14
        CALL setreg
        LD HL, 3A00h
        CALL vwrite     ; 1FA00h
        LD HL, ship
        LD DE, ys
        LD B, 7
sp1:    LD A, (scroll)
        DEC A
        ADD A, (HL)     ; Y = line - 1 + scroll
        CP 216
        JR NZ, sp2
        INC A           ; 216 would end the list: a line lower instead
sp2:    OUT (8), A
        LD (DE), A      ; kept for clip
        INC DE
        INC HL
        LD A, (HL)
        OUT (8), A      ; X
        INC HL
        LD A, (HL)
        OUT (8), A      ; pattern
        INC HL
        OUT (8), A      ; the fourth byte, unused
        DJNZ sp1
        LD A, 216
        OUT (8), A      ; the end of the list
        RET

; --- clip: the line handler's last write, the one that turns the
;     sprites off, lands a line late. On line 160 the chip has already
;     changed R#23 to the panel's 96, so it draws there whichever sprite
;     row sits on line 96 + 160 = 256 (that is, 0) of the scrolled picture.
;     Rewrite the colour table each frame with that one row of each
;     sprite in colour 0, which is not drawn.
clip:   LD A, 7
        LD B, 14
        CALL setreg
        LD HL, 3800h
        CALL vwrite     ; 1F800h: the colour table
        LD HL, cols
        LD DE, ys       ; the Y each sprite was sent with
        LD B, 7
cl1:    LD A, (DE)
        INC A
        NEG
        LD C, A         ; C = the row on scrolled line 0, if 0-15
        PUSH DE
        LD E, 0         ; E = the row
cl2:    LD A, E
        CP C
        LD A, (HL)
        JR NZ, cl3
        XOR A           ; that row: colour 0
cl3:    OUT (8), A
        INC HL
        INC E
        BIT 4, E
        JR Z, cl2       ; 16 rows a sprite
        POP DE
        INC DE
        DJNZ cl1
        RET

; --- newrow: the next row of the river onto VRAM line A
newrow: LD (line), A
        LD HL, rowc     ; every 16 rows, a new direction
        INC (HL)
        LD A, (HL)
        AND 15
        JR NZ, nw2
        LD HL, rseed
        CALL rnd
        AND 3           ; 0 left, 1 straight, 2 right, 3 straight
        DEC A
        CP 2
        JR NZ, nw1
        XOR A
nw1:    LD (dir), A
nw2:    LD A, (rowc)    ; the bank moves every other row
        RRCA
        JR NC, nw4
        LD HL, dir
        LD A, (bank)
        ADD A, (HL)
        CP 4
        JR NC, nw3
        LD A, 4         ; the left limit: turn right
        LD (HL), 1
nw3:    CP 128-WATER-4+1
        JR C, nw3a
        LD A, 128-WATER-4 ; the right limit: turn left
        LD (HL), 0FFh
nw3a:   LD (bank), A
nw4:    LD A, (line)    ; remember this line's bank
        LD L, A
        LD H, BANKS/256
        LD A, (bank)
        LD (HL), A
        ; build the row: bank, water, bank
        LD HL, ROW
        LD B, A
nw5:    LD (HL), 0CCh   ; two dots of dark green
        INC HL
        DJNZ nw5
        LD B, WATER
nw6:    LD (HL), 44h    ; two dots of water
        INC HL
        DJNZ nw6
        LD A, (bank)
        LD C, A
        LD A, 128-WATER
        SUB C
        LD B, A
nw7:    LD (HL), 0CCh
        INC HL
        DJNZ nw7
        LD HL, ROW-1    ; sand at both edges of the water
        LD D, 0
        LD E, C
        ADD HL, DE
        LD (HL), 0AAh
        LD DE, WATER+1
        ADD HL, DE
        LD (HL), 0AAh
        LD HL, rseed    ; and one dot somewhere, so you see it move
        CALL rnd
        AND 127
        LD E, A
        LD D, 0
        LD HL, ROW
        ADD HL, DE
        LD A, (HL)
        CP 44h
        JR NZ, nw8
        LD (HL), 4Fh    ; in the water, a white glint
        JR nw9
nw8:    CP 0CCh
        JR NZ, nw9
        LD (HL), 0C3h   ; on the bank, a light green dot
nw9:    LD A, (line)    ; send it: R#14 = 4 or 5, offset (line AND 127) x 128
        RLCA
        AND 1
        ADD A, 4
        LD B, 14
        CALL setreg
        LD A, (line)
        RRCA
        LD C, A
        AND 80h
        OUT (9), A
        LD A, C
        AND 3Fh
        OR 40h
        OUT (9), A
        LD HL, ROW
        LD BC, 128*256+8
        OTIR
        RET

; --- rnd: A = a random byte, from the 16-bit seed at HL
rnd:    PUSH DE
        LD E, (HL)
        INC HL
        LD D, (HL)
        SRL D
        RR E
        JR NC, rn1
        LD A, D
        XOR 0B4h
        LD D, A
rn1:    LD (HL), D
        DEC HL
        LD (HL), E
        LD A, E
        POP DE
        RET

; --- vwrite: VRAM write address HL in the page R#14 selects
vwrite: LD A, L
        OUT (9), A
        LD A, H
        OR 40h
        OUT (9), A
        RET

; --- panel: lines 0-31 of the page at 18000h (S15): a white line, then
;     black, with the words that never change
panel:  LD A, 6
        LD B, 14
        CALL setreg
        LD HL, 0
        CALL vwrite
        LD A, 0FFh
        LD B, 128
pn1:    OUT (8), A
        DJNZ pn1
        LD DE, 31*128
pn2:    LD A, 11h
        OUT (8), A
        DEC DE
        LD A, D
        OR E
        JR NZ, pn2
        LD HL, words1
        LD E, 1
        CALL ptext
        LD HL, words2
        LD E, 14
        JP ptext

; --- numbers: the score, the lives and (at the end) GAME OVER
numbers:
        LD HL, digits
        LD A, (score)
        CALL two
        LD A, (score+1)
        CALL two
        LD (HL), '0'    ; points go up in tens
        INC HL
        LD (HL), 0
        LD HL, digits
        LD E, 7
        CALL ptext
        LD A, (lives)
        ADD A, '0'
        LD (digits), A
        XOR A
        LD (digits+1), A
        LD HL, digits
        LD E, 20
        CALL ptext
        LD A, (over)
        OR A
        RET Z
        LD HL, words3
        LD E, 23
        JP ptext

; --- two: the BCD byte A as two characters at HL
two:    PUSH AF
        RRCA
        RRCA
        RRCA
        RRCA
        CALL one
        POP AF
one:    AND 0Fh
        ADD A, '0'
        LD (HL), A
        INC HL
        RET

; --- ptext: the text at HL (ending 0) at character column E
ptext:  LD A, (HL)
        OR A
        RET Z
        CALL glyph
        INC HL
        INC E
        JR ptext

; --- glyph: character A at column E, panel lines 12-19 (S15)
glyph:  PUSH HL
        PUSH DE
        LD L, A
        LD H, 0
        ADD HL, HL
        ADD HL, HL
        ADD HL, HL
        LD BC, 1800h
        ADD HL, BC      ; MOS's font: 1800h + code x 8
        LD D, 12
gl1:    XOR A
        LD B, 14
        CALL setreg
        LD A, L
        OUT (9), A
        LD A, H
        OUT (9), A      ; read address
        IN A, (8)
        LD C, A         ; the glyph's row
        LD A, 6
        LD B, 14
        CALL setreg
        LD A, D
        RRCA
        AND 80h
        LD B, A
        LD A, E
        ADD A, A
        ADD A, A
        OR B
        OUT (9), A
        LD A, D
        SRL A
        OR 40h
        OUT (9), A
        LD B, 4
gl2:    LD A, 11h
        SLA C
        JR NC, gl3
        OR 0E0h
gl3:    SLA C
        JR NC, gl4
        OR 0Eh
gl4:    OUT (8), A
        DJNZ gl2
        INC HL
        INC D
        LD A, D
        CP 20
        JR NZ, gl1
        POP DE
        POP HL
        RET

; --- the game's state
scroll: DEFB 0          ; R#23
bank:   DEFB 40         ; the left bank of the newest row, in bytes
dir:    DEFB 0          ; its direction: FFh left, 0, 1 right
rowc:   DEFB 0          ; rows made
line:   DEFB 0          ; newrow's line
seed:   DEFW 0ACE1h     ; random numbers for the rocks ...
rseed:  DEFW 1D2Bh      ; ... and for the river (onframe's own)
score:  DEFB 0, 0       ; BCD, in tens
lives:  DEFB 3
over:   DEFB 0          ; 1 = game over
overt:  DEFB 150        ; frames from GAME OVER to MOS
flash:  DEFB 0          ; frames of red left
dirty:  DEFB 1          ; 1 = redraw the numbers
digits: DEFB 0, 0, 0, 0, 0, 0
ys:     DEFB 0, 0, 0, 0, 0, 0, 0 ; each sprite's Y as last sent
words1: DEFB "SCORE", 0
words2: DEFB "LIVES", 0
words3: DEFB "GAME OVER", 0

; the sprites: Y (a screen line), X, pattern - in sprite order
ship:   DEFB SHIPY, 0, 0        ; 0: the ship
overlay: DEFB SHIPY, 0, 4       ; 1: its overlay, CC (S9)
bullet: DEFB PARK, 0, 8         ; 2: the bullet
rock:   DEFB WAIT, 0, 12        ; 3-6: the rocks
        DEFB WAIT+20, 0, 12
        DEFB WAIT+40, 0, 12
        DEFB WAIT+60, 0, 12

; colour bytes, 16 lines a sprite: bit 6 CC, bit 5 IC, bits 3-0 colour
CC:     EQU 40h
IC:     EQU 20h
cols:   DEFB 7, 7, 7, 7, 5, 5, 5, 5             ; 0, the ship (S9)
        DEFB 2, 2, 2, 2
        DEFB IC+11, IC+10, IC+11, IC+10         ;   exhaust: no collisions
        DEFB CC+8, CC+8, CC+8, CC+8, CC+8, CC+8, CC+8, CC+8   ; 1
        DEFB CC+8, CC+8, CC+8, CC+8, CC+8, CC+8, CC+8, CC+8
        DEFB 15, 15, 11, 11, 10, 10, 0, 0       ; 2, the bullet
        DEFB 0, 0, 0, 0, 0, 0, 0, 0
        DEFB 15, 15, 14, 14, 14, 14, 14, 14     ; 3-6, the rocks:
        DEFB 14, 6, 6, 6, 6, 6, 1, 1            ;   lit from above
        DEFB 15, 15, 14, 14, 14, 14, 14, 14
        DEFB 14, 6, 6, 6, 6, 6, 1, 1
        DEFB 15, 15, 14, 14, 14, 14, 14, 14
        DEFB 14, 6, 6, 6, 6, 6, 1, 1
        DEFB 15, 15, 14, 14, 14, 14, 14, 14
        DEFB 14, 6, 6, 6, 6, 6, 1, 1

; 16x16 patterns, a quarter at a time in the order the chip reads them:
; top-left, bottom-left, top-right, bottom-right. 1 = a dot.
pats:
; the ship (pattern 0): top-left
        DEFB 00000001b
        DEFB 00000011b
        DEFB 00000011b
        DEFB 00000111b
        DEFB 00000111b
        DEFB 00001111b
        DEFB 00011111b
        DEFB 00111111b
; the ship (pattern 1): bottom-left
        DEFB 01111111b
        DEFB 11111111b
        DEFB 11110111b
        DEFB 11100011b
        DEFB 00000011b
        DEFB 00000001b
        DEFB 00000011b
        DEFB 00000001b
; the ship (pattern 2): top-right
        DEFB 10000000b
        DEFB 11000000b
        DEFB 11000000b
        DEFB 11100000b
        DEFB 11100000b
        DEFB 11110000b
        DEFB 11111000b
        DEFB 11111100b
; the ship (pattern 3): bottom-right
        DEFB 11111110b
        DEFB 11111111b
        DEFB 11101111b
        DEFB 11000111b
        DEFB 11000000b
        DEFB 10000000b
        DEFB 11000000b
        DEFB 10000000b
; the overlay (pattern 4): top-left
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000001b
        DEFB 00000001b
        DEFB 00000001b
        DEFB 00000000b
        DEFB 01100000b
        DEFB 00000000b
; the overlay (pattern 5): bottom-left
        DEFB 10000001b
        DEFB 11000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
; the overlay (pattern 6): top-right
        DEFB 00000000b
        DEFB 00000000b
        DEFB 10000000b
        DEFB 10000000b
        DEFB 10000000b
        DEFB 00000000b
        DEFB 00000110b
        DEFB 00000000b
; the overlay (pattern 7): bottom-right
        DEFB 10000001b
        DEFB 00000011b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
; the bullet (pattern 8): top-left
        DEFB 00000001b
        DEFB 00000001b
        DEFB 00000001b
        DEFB 00000001b
        DEFB 00000001b
        DEFB 00000001b
        DEFB 00000000b
        DEFB 00000000b
; the bullet (pattern 9): bottom-left
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
; the bullet (pattern 10): top-right
        DEFB 10000000b
        DEFB 10000000b
        DEFB 10000000b
        DEFB 10000000b
        DEFB 10000000b
        DEFB 10000000b
        DEFB 00000000b
        DEFB 00000000b
; the bullet (pattern 11): bottom-right
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b
; a rock (pattern 12): top-left
        DEFB 00000111b
        DEFB 00011111b
        DEFB 00111111b
        DEFB 01111011b
        DEFB 01111111b
        DEFB 11111111b
        DEFB 11011111b
        DEFB 11111111b
; a rock (pattern 13): bottom-left
        DEFB 11111111b
        DEFB 11111111b
        DEFB 11111110b
        DEFB 01111111b
        DEFB 01111111b
        DEFB 00111111b
        DEFB 00011111b
        DEFB 00000111b
; a rock (pattern 14): top-right
        DEFB 11100000b
        DEFB 11111000b
        DEFB 11111100b
        DEFB 11111110b
        DEFB 11101110b
        DEFB 11111111b
        DEFB 11111111b
        DEFB 11111011b
; a rock (pattern 15): bottom-right
        DEFB 11111111b
        DEFB 10111111b
        DEFB 11111111b
        DEFB 11111110b
        DEFB 11111110b
        DEFB 11111100b
        DEFB 11111000b
        DEFB 11100000b

        INCLUDE "frame.asm"
        INCLUDE "s15-lineint.asm"

Build and run

Save it as s19-river-run.asm next to frame.asm and s15-lineint.asm. Build and run it as in S2. Joystick A steers and fires; Q quits.

What you should see

A blue river between dark green banks, edged with yellow sand, with white glints in the water. The whole picture moves down one line every frame, smoothly, and the banks wander left and right as new river comes in at the top. Your ship sits near the bottom of the river. Below the river, after one white line, a black panel reads SCORE 00000 LIVES 3. It never moves.

These two frames are half a second apart. The river has moved down 25 lines; the panel is exactly the same; the ship has moved right with the joystick.

The river at the start, the ship near the bottom, the score panel underneath

Half a second later: the banks have moved down, the ship is further right, the panel unchanged

Rocks, white on top and dark underneath, float down the river. Hold fire and the bullets climb from the ship's nose, four lines a frame, one at a time. When one reaches a rock, the rock goes and the score goes up by 10:

A bullet meeting a rock above the ship

A tenth of a second later: the rock is gone and the score reads 00010

Touch a rock, or let the ship cross onto a bank, and the water and the border flash red, then fade back to blue over about a quarter of a second. The rocks are gone, the ship is back in mid-river, and the panel shows one life fewer:

The water and the border red after a crash, LIVES 2

When the last life goes, the panel says GAME OVER, the ship is gone and the empty river scrolls on:

The empty river scrolling above a panel reading SCORE 00020 LIVES 0 GAME OVER

Three seconds later MOS's screen comes back with the register display and the prompt. Press Q during the game and you get the same at once.

MOS's screen after the game: the register display and the prompt

Change One Thing

  • In onframe, change DEC (HL) to INC (HL). Which way does the river scroll now? Is the line onframe feeds, R#23 - 1, still out of sight? And how do the rocks, which still move down a line a frame, look against the water?
  • Take CALL clip out of onframe. Watch the white line at the top of the panel as rocks and bullets go about the river. What appears on it, and why doesn't it line up with any rock you can see?
  • In sprites, take out the three lines that turn 216 into 217. Play for a while, and watch the rocks. What should you expect to see now and then, and which sprites does it affect?
  • Change the ship's colour bytes from CC+8 to plain 8. What happens to the ship's colours, and what does hits now see every frame?

When It Goes Wrong

SymptomCause
Short coloured dashes on the panel's white line, at places with no rock or bullet near themThe SPD write lands on line 160, after R#23 has already changed to the panel's 96: the chip draws the sprite rows on scrolled line 0. Blank that row of each sprite (clip)
With the R#8 write moved first in online, the panel's top line is wrongThe R#23 write now lands a line late. Keep R#2 and R#23 first
The assembler stops with duplicate definition of labelOne of your labels is also used in frame.asm or s15-lineint.asm (st1, ls1, wf1 ...). Rename yours

Summary

You can build a whole game on the 256's video chip: an endless river that scrolls a line a frame for 128 bytes, a panel that never moves, sprites with a colour a line, all of it drawn by the frame interrupt while the main loop plays the game. You know that R#23 moves the sprites too, so you keep screen positions and add the scroll as you send them, stepping round Y = 216; that SPD hides the sprites under a split; and that a colour byte of 00h is how you take one line of a sprite away.

Next

S20, Where You Go From Here: what the primer leaves out, and what to try next - starting with this game.

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.