Archive

Posts Tagged ‘game’

How To Write ZX Spectrum Games – Chapter 11

October 2, 2013 Leave a comment

Enemy Movement

Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.

So we have our playfield, and can allow the player to manipulate a sprite around it, but what we now need are some enemy sprites for the player to avoid.  A new programmer could struggle here, but it really is far simpler than it first appears.

Patrolling Enemies

The easiest type of enemy to program is that with a fixed algorithm to follow, or a predetermined patrol route.  We covered one such technique in the Centipede game earlier.  Another very simple example is that found in games such as JetSet Willy, where a sprite travels in a single direction until it reaches the end of its patrol, then switches direction and heads back to its starting point, before changing direction again and starting the cycle again.  As you might imagine, these routines are incredibly easy to write.

Firstly we set up our alien structure table with minimum and maximum coordinate positions and the present direction.  It’s generally a good idea to comment these tables so we’ll do that too.

; Alien data table, 6 bytes per alien.
; ix     = graphic type, ie chicken/Amoebatron etc.
; ix + 1 = direction, 0=up, 1=right, 2=down, 3=left.
; ix + 2 = current x coordinate.
; ix + 3 = current y coordinate.
; ix + 4 = minimum x or y coord, depends on direction.
; ix + 5 = maximum x or y coord, depends on direction.

altab  defb 0,0,0,0,0,0
       defb 0,0,0,0,0,0
       defb 0,0,0,0,0,0

Then to manipulate our sprite we might write something like this:

       ld a,(ix+1)         ; alien movement direction.
       rra                 ; rotate low bit into carry.
       jr nc,movav         ; no carry = 0 or 2, must be vertical.

; direction is 1 or 3 so it's horizontal.

       rra                 ; rotate next bit into carry for test.
       jr nc,movar         ; direction 1 = move alien right.

; Move alien left.

moval  ld a,(ix+3)         ; get y coordinate.
       sub 2               ; move left.
       ld (ix+3),a
       cp (ix+4)           ; reached mimimum yet?
       jr z,movax          ; yes - change direction.
       jr c,movax          ; oops, gone past it.
       ret

; Move alien right.

movar  ld a,(ix+3)         ; get y coordinate.
       add a,2             ; move right.
       ld (ix+3),a
       cp (ix+5)           ; reached maximum yet?
       jr nc,movax         ; yes - change direction.
       ret

; Move alien vertically.

movav  rra                 ; test direction.
       jr c,movad          ; direction 2 is down.

; Move alien up.

movau  ld a,(ix+2)         ; get x coordinate.
       sub 2               ; move up.
       ld (ix+2),a
       cp (ix+4)           ; reached mimimum yet?
       jr z,movax          ; yes - change direction.
       ret

; Move alien down.

movad  ld a,(ix+2)         ; get x coordinate.
       add a,2             ; move down.
       ld (ix+2),a         ; new coordinate.
       cp (ix+5)           ; reached maximum yet?
       jr nc,movax         ; yes - change direction.
       ret

; Change alien direction.

movax  ld a,(ix+1)         ; direction flag.
       xor 2               ; switch direction, either
                           ; horizontally or vertically.
       ld (ix+1),a         ; set new direction.
       ret

If we wanted to go further we might introduce an extra flag to our table, ix+6, to control the speed of the sprite, and only move it, say, every other frame if the flag is set. While simple to write and easy on memory usage, this sort of movement is rather basic and predictable and of limited use. For more complicated patrolling enemies, for example the alien attack waves in a shoot-em-up, we need tables of coordinates and while the code is again easy to write, coordinate tables quickly chew up memory especially if both x and y coordinates are stored. To access such a table we need two bytes per sprite which act as a pointer to the coordinate table.

A typical section of code could look like this:

       ld l,(ix+2)         ; pointer low byte, little endian.
       ld h,(ix+3)         ; pointer high byte.
       ld c,(hl)           ; put x coordinate in c.
       inc hl              ; point to y coord.
       ld b,(hl)           ; put y coordinate in b.
       inc hl              ; point to next position.
       ld (ix+2),l         ; next pointer low byte.
       ld (ix+3),h         ; next pointer high byte.

The slightly more complicated example below demonstrates an 8-ship attack wave using a table of vertical coordinates. The horizontal position of each sprite moves left at a constant rate of 2 pixels per frame so there’s no need to bother storing it. It uses the shifter sprite routine from chapter 8 so the sprites are a little flickery, but that’s not important here.

mloop  halt                ; wait for TV beam.
       ld ix,entab         ; point to odd spaceships.
       call mship          ; move spaceships.
       halt
       ld ix,entab+4       ; point to even spaceships.
       call mship          ; move even spaceships.
       call gwave          ; generate fresh waves.
       jp mloop            ; back to start of loop.

; Move enemy spaceships.

mship  ld b,4              ; number to process.
mship0 push bc             ; store count.
       ld a,(ix)           ; get pointer low.
       ld l,a              ; put into l.
       ld h,(ix+1)         ; get high byte.
       or h                ; check pointer is set up.
       and a               ; is it?
       call nz,mship1      ; yes, process it then.
       ld de,8             ; skip to next-but-one entry.
       add ix,de           ; point to next enemy.
       pop bc              ; restore count.
       djnz mship0         ; repeat for all enemies.
       ret

mship1 push hl             ; store pointer to coordinate.
       call dship          ; delete this ship.
       pop hl              ; restore coordinate.
       ld a,(hl)           ; fetch next coordinate.
       inc hl              ; move pointer on.
       ld (ix),l           ; new pointer low byte.
       ld (ix+1),h         ; pointer high byte.
       ld (ix+2),a         ; set x coordinate.
       ld a,(ix+3)         ; fetch horizontal position.
       sub 2               ; move left 2 pixels.
       ld (ix+3),a         ; set new position.
       cp 240              ; reached the edge of the screen yet?
       jp c,dship          ; not at the moment, display at new position.
       xor a               ; zeroise accumulator.
       ld (ix),a           ; clear low byte of pointer.
       ld (ix+1),a         ; clear high byte of pointer.
       ld hl,numenm        ; number of enemies on screen.
       dec (hl)            ; one less with which to cope.
       ret

gwave  ld hl,shipc         ; ship counter.
       dec (hl)            ; one less.
       ld a,(hl)           ; check new value.
       cp 128              ; waiting for next attack?
       jr z,gwave2         ; attack is imminent so set it up.
       ret nc              ; yes.
       and 7               ; time to generate a new ship?
       ret nz              ; not yet it isn't.
       ld ix,entab         ; enemy table.
       ld de,4             ; size of each entry.
       ld b,8              ; number to check.
gwave0 ld a,(ix)           ; low byte of pointer.
       ld h,(ix+1)         ; high byte.
       or h                ; are they zero?
       jr z,gwave1         ; yes, this entry is empty.
       add ix,de           ; point to next ship slot.
       djnz gwave0         ; repeat until we find one.
       ret
gwave2 ld hl,wavnum        ; present wave number.
       ld a,(hl)           ; fetch current setting.
       inc a               ; next one along.
       and 3               ; start again after 4th wave.
       ld (hl),a           ; write new setting.
       ret
gwave1 ld hl,numenm        ; number of enemies on screen.
       inc (hl)            ; one more to deal with.
       ld a,(wavnum)       ; wave number.
       ld hl,wavlst        ; wave data pointers.
       rlca                ; multiple of 2.
       rlca                ; multiple of 4.
       ld e,a              ; displacement in e.
       ld d,0              ; no high byte.
       add hl,de           ; find wave address.
       ld a,(shipc)        ; ship counter.
       and 8               ; odd or even attack?
       rrca                ; make multiple of 2 accordingly.
       rrca
       ld e,a              ; displacement in e.
       ld d,0              ; no high byte.
       add hl,de           ; point to first or second half of attack.
       ld e,(hl)           ; low byte of attack pointer.
       inc hl              ; second byte.
       ld d,(hl)           ; high byte of attack pointer.
       ld (ix),e           ; low byte of pointer.
       ld (ix+1),d         ; high byte.
       ld a,(de)           ; fetch first coordinate.
       ld (ix+2),a         ; set x.
       ld (ix+3),240       ; start at right edge of screen.

; Display enemy ships.

dship  ld hl,shipg         ; sprite address.
       ld b,(ix+3)         ; y coordinate.
       ld c,(ix+2)         ; x coordinate.
       ld (xcoord),bc      ; set up sprite routine coords.
       jp sprite           ; call sprite routine.
shipc  defb 128            ; plane counter.
numenm defb 0              ; number of enemies.

; Attack wave coordinates.
; Only the vertical coordinate is stored as the ships all move left
; 2 pixels every frame.

coord0 defb 40,40,40,40,40,40,40,40
       defb 40,40,40,40,40,40,40,40
       defb 42,44,46,48,50,52,54,56
       defb 58,60,62,64,66,68,70,72
       defb 72,72,72,72,72,72,72,72
       defb 72,72,72,72,72,72,72,72
       defb 70,68,66,64,62,60,58,56
       defb 54,52,50,48,46,44,42,40
       defb 40,40,40,40,40,40,40,40
       defb 40,40,40,40,40,40,40,40
       defb 38,36,34,32,30,28,26,24
       defb 22,20,18,16,14,12,10,8
       defb 6,4,2,0,2,4,6,8
       defb 10,12,14,16,18,20,22,24
       defb 26,28,30,32,34,36,38,40
coord1 defb 136,136,136,136,136,136,136,136
       defb 136,136,136,136,136,136,136,136
       defb 134,132,130,128,126,124,122,120
       defb 118,116,114,112,110,108,106,104
       defb 104,104,104,104,104,104,104,104
       defb 104,104,104,104,104,104,104,104
       defb 106,108,110,112,114,116,118,120
       defb 122,124,126,128,130,132,134,136
       defb 136,136,136,136,136,136,136,136
       defb 136,136,136,136,136,136,136,136
       defb 138,140,142,144,146,148,150,152
       defb 154,156,158,160,162,164,166,168
       defb 170,172,174,176,174,172,170,168
       defb 166,164,162,160,158,156,154,152
       defb 150,148,146,144,142,140,138,136

; List of attack waves.

wavlst defw coord0,coord0,coord1,coord1
       defw coord1,coord0,coord0,coord1

wavnum defb 0              ; current wave pointer.

; Spaceship sprite.

shipg  defb 248,252,48,24,24,48,12,96,24,48,31,243,127,247,255,247
       defb 255,247,127,247,31,243,24,48,12,96,24,48,48,24,248,252

sprit7 xor 7               ; complement last 3 bits.
       inc a               ; add one for luck!
sprit3 rl d                ; rotate left...
       rl c                ; ...into middle byte...
       rl e                ; ...and finally into left character cell.
       dec a               ; count shifts we've done.
       jr nz,sprit3        ; return until all shifts complete.

; Line of sprite image is now in e + c + d, we need it in form c + d + e.

       ld a,e              ; left edge of image is currently in e.
       ld e,d              ; put right edge there instead.
       ld d,c              ; middle bit goes in d.
       ld c,a              ; and the left edge back into c.
       jr sprit0           ; we've done the switch so transfer to screen.

sprite ld a,(xcoord)       ; draws sprite (hl).
       ld (tmp1),a         ; store vertical.
       call scadd          ; calculate screen address.
       ld a,16             ; height of sprite in pixels.
sprit1 ex af,af'           ; store loop counter.
       push de             ; store screen address.
       ld c,(hl)           ; first sprite graphic.
       inc hl              ; increment poiinter to sprite data.
       ld d,(hl)           ; next bit of sprite image.
       inc hl              ; point to next row of sprite data.
       ld (tmp0),hl        ; store in tmp0 for later.
       ld e,0              ; blank right byte for now.
       ld a,b              ; b holds y position.
       and 7               ; how are we straddling character cells?
       jr z,sprit0         ; we're not straddling them, don't bother shifting.
       cp 5                ; 5 or more right shifts needed?
       jr nc,sprit7        ; yes, shift from left as it's quicker.
       and a               ; oops, carry flag is set so clear it.
sprit2 rr c                ; rotate left byte right...
       rr d                ; ...through middle byte...
       rr e                ; ...into right byte.
       dec a               ; one less shift to do.
       jr nz,sprit2        ; return until all shifts complete.
sprit0 pop hl              ; pop screen address from stack.
       ld a,(hl)           ; what's there already.
       xor c               ; merge in image data.
       ld (hl),a           ; place onto screen.
       inc l               ; next character cell to right please.
       ld a,(hl)           ; what's there already.
       xor d               ; merge with middle bit of image.
       ld (hl),a           ; put back onto screen.
       inc hl              ; next bit of screen area.
       ld a,(hl)           ; what's already there.
       xor e               ; right edge of sprite image data.
       ld (hl),a           ; plonk it on screen.
       ld a,(tmp1)         ; temporary vertical coordinate.
       inc a               ; next line down.
       ld (tmp1),a         ; store new position.
       and 63              ; are we moving to next third of screen?
       jr z,sprit4         ; yes so find next segment.
       and 7               ; moving into character cell below?
       jr z,sprit5         ; yes, find next row.
       dec hl              ; left 2 bytes.
       dec l               ; not straddling 256-byte boundary here.
       inc h               ; next row of this character cell.
sprit6 ex de,hl            ; screen address in de.
       ld hl,(tmp0)        ; restore graphic address.
       ex af,af'           ; restore loop counter.
       dec a               ; decrement it.
       jp nz,sprit1        ; not reached bottom of sprite yet to repeat.
       ret                 ; job done.
sprit4 ld de,30            ; next segment is 30 bytes on.
       add hl,de           ; add to screen address.
       jp sprit6           ; repeat.
sprit5 ld de,63774         ; minus 1762.
       add hl,de           ; subtract 1762 from physical screen address.
       jp sprit6           ; rejoin loop.
scadd  ld a,(xcoord)       ; fetch vertical coordinate.
       ld e,a              ; store that in e.

; Find line within cell.

       and 7               ; line 0-7 within character square.
       add a,64            ; 64 * 256 = 16384 = start of screen display.
       ld d,a              ; line * 256.

; Find which third of the screen we're in.

       ld a,e              ; restore the vertical.
       and 192             ; segment 0, 1 or 2 multiplied by 64.
       rrca                ; divide this by 8.
       rrca
       rrca                ; segment 0-2 multiplied by 8.
       add a,d             ; add to d give segment start address.
       ld d,a

; Find character cell within segment.

       ld a,e              ; 8 character squares per segment.
       rlca                ; divide x by 8 and multiply by 32,
       rlca                ; net calculation: multiply by 4.
       and 224             ; mask off bits we don't want.
       ld e,a              ; vertical coordinate calculation done.

; Add the horizontal element.

       ld a,(ycoord)       ; y coordinate.
       rrca                ; only need to divide by 8.
       rrca
       rrca
       and 31              ; squares 0 - 31 across screen.
       add a,e             ; add to total so far.
       ld e,a              ; de = address of screen.
       ret

xcoord defb 0              ; display coord.
ycoord defb 0              ; display coord.
tmp0   defw 0              ; workspace.
tmp1   defb 0              ; temporary vertical position.

; Enemy spaceship table, 8 entries x 4 bytes each.

entab  defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0

Intelligent Aliens

So far we have dealt with predictable drones, but what if we want to give the player the illusion that enemy sprites are thinking for themselves?  One way we could start to do this would be to give them an entirely random decision making process.

Here is the source code for Turbomania, a game originally written for the 1K coding competition in 2005.  It’s very simple, but incorporates purely random movement.  Enemy cars travel in a direction until they can no longer move, then select another direction at random.  Additionally, a car may change direction at random even if it can continue in its present direction.  It’s very primitive of course, just take a look at the mcar routine and you’ll see exactly what I mean.

       org 24576

; Constants.

YELLOW equ 49              ; dull yellow attribute.
YELLOB equ YELLOW + 64     ; bright yellow attribute.

; Main game code.

; Clear the screen to give green around the edges.

       ld hl,23693         ; system variable for attributes.
       ld (hl),36          ; want green background.

waitk  ld a,(23560)        ; read keyboard.
       cp 32               ; is SPACE pressed?
       jr nz,waitk         ; no, wait.
       call nexlev         ; play the game.
       jr waitk            ; SPACE to restart game.

; Clear down level data.

nexlev call 3503           ; clear the screen.
       ld hl,rmdat         ; room data.
       ld de,rmdat+1
       ld (hl),1           ; set up a shadow block.
       ld bc,16            ; length of room minus first byte.
       ldir                ; copy to rest of first row.
       ld bc,160           ; length of room minus first row.
       ld (hl),b           ; clear first byte.
       ldir                ; clear room data.

; Set up the default blocks.

       ld c,15             ; last block position.
popbl0 ld b,9              ; last row.
popbl1 call filblk         ; fill the block.
       dec b               ; one column up.
       jr z,popbl2         ; done column, move on.
       dec b               ; and again.
       jr popbl1
popbl2 dec c               ; move on row.
       jr z,popbl3         ; done column, move on.
       dec c               ; next row.
       jr popbl0

; Now draw the bits unique to this level.

popbl3 ld b,7              ; number of blocks to insert.
popbl5 push bc             ; store counter.
       call random         ; get a random number.
       and 6               ; even numbers in range 0-6 please.
       add a,2             ; make it 2-8.
       ld b,a              ; that's the column.
popbl4 call random         ; another number.
       and 14              ; even numbers 0-12 wanted.
       cp 14               ; higher than we want?
       jr nc,popbl4        ; yes, try again.
       inc a               ; place it in range 1-13.
       ld c,a              ; that's the row.
       call filblk         ; fill block.
popbl6 call random         ; another random number.
       and 14              ; only want 0-8.
       cp 9                ; above number we want?
       jr nc,popbl6        ; try again.
       inc a               ; make it 1-9.
       ld b,a              ; vertical coordinate.
       call random         ; get horizontal block.
       and 14              ; even, 0-14.
       ld c,a              ; y position.
       call filblk         ; fill in that square.
       pop bc              ; restore count.
       djnz popbl5         ; one less to do.

       xor a               ; zero.
       ld hl,playi         ; player's intended direction.
       ld (hl),a           ; default direction.
       inc hl              ; point to display direction.
       ld (hl),a           ; default direction.
       inc hl              ; player's next direction.
       ld (hl),a           ; default direction.
       out (254),a         ; set border colour while we're at it.
       call atroom         ; show current level layout.

       ld hl,168+8*256     ; coordinates.
       ld (encar2+1),hl    ; set up second car position.
       ld h,l              ; y coord at right.
       ld (encar1+1),hl    ; set up first car position.
       ld l,40             ; x at top of screen.
       ld (playx),hl       ; start player off here.
       ld hl,encar1        ; first car.
       call scar           ; show it.
       ld hl,encar2        ; second car.
       call scar           ; show it.
       call dplayr         ; show player sprite.
       call blkcar         ; make player's car black.

; Two-second delay before we start.

       ld b,100            ; delay length.
waitt  halt                ; wait for interrupt.
       djnz waitt          ; repeat.

mloop  halt                ; electron beam in top left.
       call dplayr         ; delete player.

; Make attributes blue ink again.

       call gpatts         ; get player attributes.
       defb 17,239,41      ; remove green paper, add blue paper + ink.
       call attblk         ; set road colours.

; Move player's car.

       ld a,(playd)        ; player direction.
       ld bc,(playx)       ; player coordinates.
       call movc           ; move coords.
       ld hl,(dispx)       ; new coords.
       ld (playx),hl       ; set new player position.

; Can we change direction?

       ld a,(playi)        ; player's intended direction.
       ld bc,(playx)       ; player coordinates.
       call movc           ; move coords.
       call z,setpn        ; set player's new direction.

; Change direction.

       ld a,(nplayd)       ; new player direction.
       ld (playd),a        ; set current direction.

       call dplayr         ; redisplay at new position.

; Set attributes of car.

       call blkcar         ; make player car black.

; Controls.

       ld a,239            ; keyboard row 6-0 = 61438.
       ld e,1              ; direction right.
       in a,(254)          ; read keys.
       rra                 ; player moved right?
       call nc,setpd       ; yes, set player direction.
       ld e,3              ; direction left.
       rra                 ; player moved left?
       call nc,setpd       ; yes, set player direction.
       ld a,247            ; 63486 is port for row 1-5.
       ld e,0              ; direction up.
       in a,(254)          ; read keys.
       and 2               ; check 2nd key in (2).
       call z,setpd        ; set direction.
       ld a,251            ; 64510 is port for row Q-T.
       ld e,2              ; direction down.
       in a,(254)          ; read keys.
       and e               ; check 2nd key from edge (W)..
       call z,setpd        ; set direction.

; Enemy cars.

       ld hl,encar1        ; enemy car 1.
       push hl             ; store pointer.
       call procar         ; process the car.
       pop hl              ; restore car pointer.
       call coldet         ; check for collisions.
       ld hl,encar2        ; enemy car 2.
       push hl             ; store pointer.
       halt                ; synchronise with display.
       call procar         ; process the car.
       pop hl              ; restore car pointer.
       call coldet         ; check for collisions.

; Count remaining yellow spaces.

       ld hl,22560         ; address.
       ld bc,704           ; attributes to count.
       ld a,YELLOB         ; attribute we're seeking.
       cpir                ; count characters.
       ld a,b              ; high byte of result.
       or c                ; combine with low byte.
       jp z,nexlev         ; none left, go to next level.

; End of main loop.

       jp mloop

; Black car on cyan paper.

blkcar call gpatts         ; get player attributes.
       defb 17,232,40      ; remove red paper/blue ink, add blue paper.

; Set 16x16 pixel attribute block.

attblk call attlin         ; paint horizontal line.
       call attlin         ; paint another line.
       ld a,c              ; vertical position.
       and 7               ; is it straddling cells?
       ret z               ; no, so no third line.

attlin call setatt         ; paint the road.
       call setatt         ; and again.
       ld a,b              ; horizontal position.
       and 7               ; straddling blocks?
       jr z,attln0         ; no, leave third cell as it is.
       call setatt         ; set attribute.
       dec l               ; back one cell again.
attln0 push de             ; preserve colours.
       ld de,30            ; distance to next one.
       add hl,de           ; point to next row down.
       pop de              ; restore colour masks.
       ret

; Set single cell attribute.

setatt ld a,(hl)           ; fetch attribute cell contents.
       and e               ; remove colour elements in c register.
       or d                ; add those in b to form new colour.
       ld (hl),a           ; set colour.
       inc l               ; next cell.
       ret

; Collision detection, based on coordinates.

coldet call getabc         ; get coords.
       ld a,(playx)        ; horizontal position.
       sub c               ; compare against car x.
       jr nc,coldt0        ; result was positive.
       neg                 ; it was negative, reverse sign.
coldt0 cp 16               ; within 15 pixels?
       ret nc              ; no collision.
       ld a,(playy)        ; player y.
       sub b               ; compare against car y.
       jr nc,coldt1        ; result was positive.
       neg                 ; it was negative, reverse sign.
coldt1 cp 16               ; within 15 pixels?
       ret nc              ; no collision.
       pop de              ; remove return address from stack.
       ret

setpd  ex af,af'
       ld a,e              ; direction.
       ld (playi),a        ; set intended direction.
       ex af,af'
       ret

setpn  ld a,(playi)        ; new intended direction.
       ld (nplayd),a       ; set next direction.
       ret

; Move coordinates of sprite in relevant direction.

movc   ld (dispx),bc       ; default position.
       and a               ; direction zero.
       jr z,movcu          ; move up.
       dec a               ; direction 1.
       jr z,movcr          ; move up.
       dec a               ; direction 2.
       jr z,movcd          ; move up.
movcl  dec b               ; left one pixel.
       dec b               ; left again.
movc0  call chkpix         ; check pixel attributes.
       ld (dispx),bc       ; new coords.
       ret
movcu  dec c               ; up a pixel.
       dec c               ; and again.
       jr movc0
movcr  inc b               ; right one pixel.
       inc b               ; right again.
       jr movc0
movcd  inc c               ; down a pixel.
       inc c               ; once more.
       jr movc0

; Check pixel attributes for collision.
; Any cells with green ink are solid.

chkpix call ataddp         ; get pixel attribute address.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       inc hl              ; next square to the right.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       inc hl              ; next square to the right.
       ld a,b              ; horizontal position.
       and 7               ; straddling cells?
       jr z,chkpx1         ; no, look down next.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
chkpx1 ld de,30            ; distance to next cell down.
       add hl,de           ; point there.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       inc hl              ; next square to the right.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       inc hl              ; next square to the right.
       ld a,b              ; horizontal position.
       and 7               ; straddling cells?
       jr z,chkpx2         ; no, look down next.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
chkpx2 ld a,c              ; distance from top of screen.
       and 7               ; are we straddling cells vertically?
       ret z               ; no, move is therefore okay.
       add hl,de           ; point there.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       inc hl              ; next square to the right.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       inc hl              ; next square to the right.
       ld a,b              ; horizontal position.
       and 7               ; straddling cells?
       ret z               ; no, move is fine.
       ld a,(hl)           ; get attributes.
       and 4               ; check ink colours.
       jr nz,chkpx0        ; invalid, block the move.
       ret                 ; go ahead.

chkpx0 pop de              ; remove return address from stack.
       ret

; Fill a block in the map.

; Get block address.

filblk ld a,b              ; row number.
       rlca                ; multiply by 16.
       rlca
       rlca
       rlca
       add a,c             ; add to displacement total.
       ld e,a              ; that's displacement low.
       ld d,0              ; no high byte required.
       ld hl,rmdat         ; room data address.
       add hl,de           ; add to block address.

; Block address is in hl, let's fill it.

       ld (hl),2           ; set block on.
       ld de,16            ; distance to next block down.
       add hl,de           ; point there.
       ld a,(hl)           ; check it.
       and a               ; is it set yet?
       ret nz              ; yes, don't overwrite.
       ld (hl),1           ; set the shadow.
       ret

; Draw a screen consisting entirely of attribute blocks.

atroom ld hl,rmdat         ; room data.
       ld a,1              ; start at row 1.
       ld (dispx),a        ; set up coordinate.
       ld b,11             ; row count.
atrm0  push bc             ; store counter.
       ld b,15             ; column count.
       ld a,1              ; column number.
       ld (dispy),a        ; set to left of screen.
atrm1  push bc             ; store counter.
       ld a,(hl)           ; get next block type.
       push hl             ; store address of data.
       rlca                ; double block number.
       rlca                ; and again for multiple of 4.
       ld e,a              ; displacement to block address.
       ld d,0              ; no high byte required.
       ld hl,blkatt        ; block attributes.
       add hl,de           ; point to block we want.
       call atadd          ; get address for screen position.
       ldi                 ; transfer first block.
       ldi                 ; and the second.
       ld bc,30            ; distance to next row.
       ex de,hl            ; switch cell and screen address.
       add hl,bc           ; point to next row down.
       ex de,hl            ; switch them back.
       ldi                 ; do third cell.
       ldi                 ; fourth attribute cell.
       ld hl,dispy         ; column number.
       inc (hl)            ; move across one cell.
       inc (hl)            ; and another.
       pop hl              ; restore room address.
       pop bc              ; restore column counter.
       inc hl              ; point to next block.
       djnz atrm1          ; do rest of row.
       inc hl              ; skip one char so lines are round 16.
       ld a,(dispx)        ; vertical position.
       add a,2             ; look 2 cells down.
       ld (dispx),a        ; new row.
       pop bc              ; restore column counter.
       djnz atrm0          ; do remaining rows.
       ret

; Background block attributes.

blkatt defb YELLOB,YELLOB  ; space.
       defb YELLOB,YELLOB
       defb YELLOW,YELLOW  ; shadow space.
       defb YELLOB,YELLOB
       defb 124,68         ; black/white chequered flag pattern.
       defb 68,124

; Calculate address of attribute for character at (dispx, dispy).

atadd  push hl             ; need to preserve hl pair.
       ld hl,(dispx)       ; coords to check, in char coords.
       add hl,hl           ; multiply x and y by 8.
       add hl,hl
       add hl,hl
       ld b,h              ; copy y coord to b.
       ld c,l              ; put x coord in c.
       call ataddp         ; get pixel address.
       ex de,hl            ; put address in de.
       pop hl              ; restore hl.
       ret

; Get player attributes.

gpatts ld bc,(playx)       ; player coordinates.

; Calculate address of attribute for pixel at (c, b).

ataddp ld a,c              ; Look at the vertical first.
       rlca                ; divide by 64.
       rlca                ; quicker than 6 rrca operations.
       ld l,a              ; store in l register for now.
       and 3               ; mask to find segment.
       add a,88            ; attributes start at 88*256=22528.
       ld h,a              ; that's our high byte sorted.
       ld a,l              ; vertical/64 - same as vertical*4.
       and 224             ; want a multiple of 32.
       ld l,a              ; vertical element calculated.
       ld a,b              ; get horizontal position.
       rra                 ; divide by 8.
       rra
       rra
       and 31              ; want result in range 0-31.
       add a,l             ; add to existing low byte.
       ld l,a              ; that's the low byte done.
       ld a,(hl)           ; get cell contents.
       ret                 ; attribute address now in hl.

; Move car - change of direction required.

mcarcd ld a,(hl)           ; current direction.
       inc a               ; turn clockwise.
       and 3               ; only 4 directions.
       ld (hl),a           ; new direction.

; Move an enemy car.

mcar   push hl             ; preserve pointer to car.
       call getabc         ; fetch coordinates and direction.
       call movc           ; move the car.
       pop hl              ; refresh car pointer.
       jr nz,mcarcd        ; can't move there, turn around.
       inc hl              ; point to x.
       ld a,c              ; store x pos in c.
       ld (hl),a           ; x position.
       inc hl              ; point to y.
       ld (hl),b           ; new placing.
       or b                ; combine the two.
       and 31              ; find position straddling cells.
       cp 8                ; are we at a valid turning point?
       ret nz              ; no, can't change direction.
       ld a,r              ; crap random number.
       cp 23               ; check it's below this value.
       ret nc              ; it isn't, don't change.
       push hl             ; store car pointer.
       call random         ; get random number.
       pop hl              ; restore car.
       dec hl              ; back to x coordinate.
       dec hl              ; back again to direction.
       and 3               ; direction is in range 0-3.
       ld (hl),a           ; new direction.
       ret

; Fetch car coordinates and direction.

getabc ld a,(hl)           ; get direction.
       inc hl              ; point to x position.
       ld c,(hl)           ; x coordinate.
       inc hl              ; point to y.
       ld b,(hl)           ; y position.
       ret

; Process car to which hl points.

procar push hl             ; store pointer.
       push hl             ; store pointer.
       call scar           ; delete car.
       pop hl              ; restore pointer to car.
       call mcar           ; move car.
       pop hl              ; restore pointer to car.

; Show enemy car.

scar   call getabc         ; get coords and direction.
       jr dplay0

; Display player sprite.

dplayr ld bc,(playx)       ; player coords.
       ld a,(playd)        ; player direction.
dplay0 rrca                ; multiply by 32.
       rrca
       rrca
       ld e,a              ; sprite * 32 in low byte.
       ld d,0              ; no high byte.
       ld hl,cargfx        ; car graphics.
       add hl,de           ; add displacement to sprite.
       jr sprite           ; show the sprite.

; This is the sprite routine and expects coordinates in (c ,b) form,
; where c is the vertical coord from the top of the screen (0-176), and
; b is the horizontal coord from the left of the screen (0 to 240).
; Sprite data is stored as you'd expect in its unshifted form as this
; routine takes care of all the shifting itself.  This means that sprite
; handling isn't particularly fast but the graphics only take 1/8th of the
; space they would require in pre-shifted form.

; On entry HL must point to the unshifted sprite data.

sprit7 xor 7               ; complement last 3 bits.
       inc a               ; add one for luck!
sprit3 rl d                ; rotate left...
       rl c                ; ...into middle byte...
       rl e                ; ...and finally into left character cell.
       dec a               ; count shifts we've done.
       jr nz,sprit3        ; return until all shifts complete.

; Line of sprite image is now in e + c + d, we need it in form c + d + e.

       ld a,e              ; left edge of image is currently in e.
       ld e,d              ; put right edge there instead.
       ld d,c              ; middle bit goes in d.
       ld c,a              ; and the left edge back into c.
       jr sprit0           ; we've done the switch so transfer to screen.

sprite ld (dispx),bc       ; store coords in dispx for now.
       call scadd          ; calculate screen address.
       ld a,16             ; height of sprite in pixels.
sprit1 ex af,af'           ; store loop counter.
       push de             ; store screen address.
       ld c,(hl)           ; first sprite graphic.
       inc hl              ; increment poiinter to sprite data.
       ld d,(hl)           ; next bit of sprite image.
       inc hl              ; point to next row of sprite data.
       ld (sprtmp),hl      ; store it for later.
       ld e,0              ; blank right byte for now.
       ld a,b              ; b holds y position.
       and 7               ; how are we straddling character cells?
       jr z,sprit0         ; we're not straddling them, don't bother shifting.
       cp 5                ; 5 or more right shifts needed?
       jr nc,sprit7        ; yes, shift from left as it's quicker.
       and a               ; oops, carry flag is set so clear it.
sprit2 rr c                ; rotate left byte right...
       rr d                ; ...through middle byte...
       rr e                ; ...into right byte.
       dec a               ; one less shift to do.
       jr nz,sprit2        ; return until all shifts complete.
sprit0 pop hl              ; pop screen address from stack.
       ld a,(hl)           ; what's there already.
       xor c               ; merge in image data.
       ld (hl),a           ; place onto screen.
       inc l               ; next character cell to right please.
       ld a,(hl)           ; what's there already.
       xor d               ; merge with middle bit of image.
       ld (hl),a           ; put back onto screen.
       inc l               ; next bit of screen area.
       ld a,(hl)           ; what's already there.
       xor e               ; right edge of sprite image data.
       ld (hl),a           ; plonk it on screen.
       ld a,(dispx)        ; vertical coordinate.
       inc a               ; next line down.
       ld (dispx),a        ; store new position.
       and 63              ; are we moving to next third of screen?
       jr z,sprit4         ; yes so find next segment.
       and 7               ; moving into character cell below?
       jr z,sprit5         ; yes, find next row.
       dec l               ; left 2 bytes.
       dec l               ; not straddling 256-byte boundary here.
       inc h               ; next row of this character cell.
sprit6 ex de,hl            ; screen address in de.
       ld hl,(sprtmp)      ; restore graphic address.
       ex af,af'           ; restore loop counter.
       dec a               ; decrement it.
       jp nz,sprit1        ; not reached bottom of sprite yet to repeat.
       ret                 ; job done.
sprit4 ld de,30            ; next segment is 30 bytes on.
       add hl,de           ; add to screen address.
       jp sprit6           ; repeat.
sprit5 ld de,63774         ; minus 1762.
       add hl,de           ; subtract 1762 from physical screen address.
       jp sprit6           ; rejoin loop.

; This routine returns a screen address for (c, b) in de.

scadd  ld a,c              ; get vertical position.
       and 7               ; line 0-7 within character square.
       add a,64            ; 64 * 256 = 16384 (Start of screen display)
       ld d,a              ; line * 256.
       ld a,c              ; get vertical again.
       rrca                ; multiply by 32.
       rrca
       rrca
       and 24              ; high byte of segment displacement.
       add a,d             ; add to existing screen high byte.
       ld d,a              ; that's the high byte sorted.
       ld a,c              ; 8 character squares per segment.
       rlca                ; 8 pixels per cell, mulplied by 4 = 32.
       rlca                ; cell x 32 gives position within segment.
       and 224             ; make sure it's a multiple of 32.
       ld e,a              ; vertical coordinate calculation done.
       ld a,b              ; y coordinate.
       rrca                ; only need to divide by 8.
       rrca
       rrca
       and 31              ; squares 0 - 31 across screen.
       add a,e             ; add to total so far.
       ld e,a              ; hl = address of screen.
       ret

; Pseudo-random number generator.
; Steps a pointer through the ROM (held in seed), returning the contents
; of the byte at that location.

random ld hl,(seed)        ; pointer to ROM.
       res 5,h             ; stay within first 8K of ROM.
       ld a,(hl)           ; get "random" number from location.
       xor l               ; more randomness.
       inc hl              ; increment pointer.
       ld (seed),hl        ; new position.
       ret

; Sprite graphic data.
; Going up first.

cargfx defb 49,140,123,222,123,222,127,254,55,236,15,240,31,248,30,120
       defb 29,184,108,54,246,111,255,255,247,239,246,111,103,230,3,192

; Second image looks right.

       defb 60,0,126,14,126,31,61,223,11,238,127,248,252,254,217,127
       defb 217,127,252,254,127,248,11,238,61,223,126,31,126,14,60,0

; Third is pointing down.

       defb 3,192,103,230,246,111,247,239,255,255,246,111,108,54,29,184
       defb 30,120,31,248,15,240,55,236,127,254,123,222,123,222,49,140

; Last car looks left.

       defb 0,60,112,126,248,126,251,188,119,208,31,254,127,63,254,155
       defb 254,155,127,63,31,254,119,208,251,188,248,126,112,126,0,60

; Variables used by the game.

       org 32768

playi  equ $               ; intended direction when turn is possible.
playd  equ playi+1         ; player's current direction.
nplayd equ playd+1         ; next player direction.
playx  equ nplayd+1        ; player x.
playy  equ playx+1         ; player's y coordinate.

encar1 equ playy+1         ; enemy car 1.
encar2 equ encar1+3        ; enemy car 2.

dispx  equ encar2+3        ; general-use coordinates.
dispy  equ dispx+1
seed   equ dispy+1         ; random number seed.
sprtmp equ seed+2          ; sprite temporary address.
termin equ sprtmp+2        ; end of variables.

rmdat  equ 49152

If you have assembled this game and tried it out you will realise that it quickly becomes boring. It is very easy to stay out of the reach of enemy cars to cover one side of the track, then wait until the cars move and cover the other side. There is no hunter-killer aspect in this algorithm so the player is never chased down. What’s more, this routine is so simple cars will reverse direction without warning. In most games this is only acceptable if a sprite reaches a dead end and cannot move in any other direction.

Perhaps we should instead be writing routines where aliens interact with the player, and home in on him. Well, the most basic algorithm would be something along the lines of a basic x/y coordinate check, moving an alien sprite towards the player. The routine below shows how this might be achieved, the homing routine almov is the one which moves the chasing sprite around. Try guiding the number 1 block around the screen with keys ASD and F, and the number 2 block will follow you around the screen. However, in doing this we soon discover the basic flaw with this type of chase – it is very easy to trap the enemy sprite in a corner because the routine isn’t intelligent enough to move backwards in order to get around obstacles.

; Randomly cover screen with yellow blocks.

       ld de,1000          ; address in ROM.
       ld b,64             ; number of cells to colour.
yello0 push bc             ; store register.
       ld a,(de)           ; get first random coord.
       and 127             ; half height of screen.
       add a,32            ; at least 32 pixels down.
       ld c,a              ; x coord.
       inc de              ; next byte of ROM.
       ld a,(de)           ; fetch value.
       inc de              ; next byte of ROM.
       ld b,a              ; y coord.
       call ataddp         ; find attribute address.
       ld (hl),48          ; set attributes.
       pop bc              ; restore loop counter.
       djnz yello0         ; repeat a few times.

       ld ix,aldat         ; alien data.
       call dal            ; display alien.
       call dpl            ; display player.

mloop  halt                ; wait for electron beam.
       call dal            ; delete alien.
       call almov          ; alien movement.
       call dal            ; display alien.
       halt                ; wait for electron beam.
       call dpl            ; delete player.
       call plcon          ; player controls.
       call dpl            ; display the player.
       jp mloop            ; back to start of main loop.

aldat  defb 0,0,0          ; alien data.

; Display/delete alien.

dal    ld c,(ix)           ; vertical position.
       ld b,(ix+1)         ; horizontal position.
       ld (xcoord),bc      ; set sprite coordinates.
       ld hl,algfx         ; alien graphic.
       jp sprite           ; xor sprite onto screen.

; Display/delete player sprite.

dpl    ld bc,(playx)       ; coordinates.
       ld (xcoord),bc      ; set the display coordinates.
       ld hl,plgfx         ; player graphic.
       jp sprite           ; xor sprite on or off the screen.

; Player control.

plcon  ld bc,65022         ; port for keyboard row.
       in a,(c)            ; read keyboard.
       ld b,a              ; store result in b register.
       rr b                ; check outermost key.
       call nc,mpl         ; player left.
       rr b                ; check next key.
       call nc,mpr         ; player right.
       rr b                ; check next key.
       call nc,mpd         ; player down.
       rr b                ; check next key.
       call nc,mpu         ; player up.
       ret
mpl    ld hl,playy         ; coordinate.
       ld a,(hl)           ; check value.
       and a               ; at edge of screen?
       ret z               ; yes, can't move that way.
       sub 2               ; move 2 pixels.
       ld (hl),a           ; new setting.
       ret
mpr    ld hl,playy         ; coordinate.
       ld a,(hl)           ; check value.
       cp 240              ; at edge of screen?
       ret z               ; yes, can't move that way.
       add a,2             ; move 2 pixels.
       ld (hl),a           ; new setting.
       ret
mpu    ld hl,playx         ; coordinate.
       ld a,(hl)           ; check value.
       and a               ; at edge of screen?
       ret z               ; yes, can't move that way.
       sub 2               ; move 2 pixels.
       ld (hl),a           ; new setting.
       ret
mpd    ld hl,playx         ; coordinate.
       ld a,(hl)           ; check value.
       cp 176              ; at edge of screen?
       ret z               ; yes, can't move that way.
       add a,2             ; move 2 pixels.
       ld (hl),a           ; new setting.
       ret

; Alien movement routine.

almov  ld a,(playx)        ; player x coordinate.
       ld c,(ix)           ; alien x.
       ld b,(ix+1)         ; alien y.
       cp c                ; check alien x.
       jr z,alv0           ; they're equal, do horizontal.
       jr c,alu            ; alien is below, move up.
ald    inc c               ; alien is above, move down.
       jr alv0             ; now check position for walls.
alu    dec c               ; move down.
alv0   call alchk          ; check attributes.
       cp 56               ; are they okay?
       jr z,alv1           ; yes, set x coord.
       ld c,(ix)           ; restore old x coordinate.
       jr alh              ; now do horizontal.
alv1   ld (ix),c           ; new x coordinate.
alh    ld a,(playy)        ; player horizontal.
       cp b                ; check alien horizontal.
       jr z,alok           ; they're equal, check for collision.
       jr c,all            ; alien is to right, move left.
alr    inc b               ; alien is to left, move right.
       jr alok             ; check for walls.
all    dec b               ; move right.
alok   call alchk          ; check attributes.
       cp 56               ; are they okay?
       ret nz              ; no, don't set new y coord.
       ld (ix+1),b         ; set new y.
       ret

; Check attributes at alien position (c, b).

alchk  call ataddp         ; get attribute address.
       ld a,3              ; cells high.
alchk0 ex af,af'           ; store loop counter.
       ld a,(hl)           ; check cell colour.
       cp 56               ; is it black on white?
       ret nz              ; no, can't move here.
       inc hl              ; cell right.
       ld a,(hl)           ; check cell colour.
       cp 56               ; is it black on white?
       ret nz              ; no, can't move here.
       inc hl              ; cell right.
       ld a,(hl)           ; check cell colour.
       cp 56               ; is it black on white?
       ret nz              ; no, can't move here.
       ld de,30            ; distance to next cell down.
       add hl,de           ; look there.
       ex af,af'           ; height counter.
       dec a               ; one less to go.
       jr nz,alchk0        ; repeat for all rows.
       ld (ix),c           ; set new x.
       ld (ix+1),b         ; set new y.
       ret

; Calculate address of attribute for pixel at (c, b).

ataddp ld a,c              ; Look at the vertical first.
       rlca                ; divide by 64.
       rlca                ; quicker than 6 rrca operations.
       ld l,a              ; store in l register for now.
       and 3               ; mask to find segment.
       add a,88            ; attributes start at 88*256=22528.
       ld h,a              ; that's our high byte sorted.
       ld a,l              ; vertical/64 - same as vertical*4.
       and 224             ; want a multiple of 32.
       ld l,a              ; vertical element calculated.
       ld a,b              ; get horizontal position.
       rra                 ; divide by 8.
       rra
       rra
       and 31              ; want result in range 0-31.
       add a,l             ; add to existing low byte.
       ld l,a              ; that's the low byte done.
       ret                 ; attribute address now in hl.

playx  defb 80             ; player coordinates.
playy  defb 120
xcoord defb 0              ; general-purpose coordinates.
ycoord defb 0

; Shifter sprite routine.

sprit7 xor 7               ; complement last 3 bits.
       inc a               ; add one for luck!
sprit3 rl d                ; rotate left...
       rl c                ; ...into middle byte...
       rl e                ; ...and finally into left character cell.
       dec a               ; count shifts we've done.
       jr nz,sprit3        ; return until all shifts complete.

; Line of sprite image is now in e + c + d, we need it in form c + d + e.

       ld a,e              ; left edge of image is currently in e.
       ld e,d              ; put right edge there instead.
       ld d,c              ; middle bit goes in d.
       ld c,a              ; and the left edge back into c.
       jr sprit0           ; we've done the switch so transfer to screen.

sprite ld a,(xcoord)       ; draws sprite (hl).
       ld (tmp1),a         ; store vertical.
       call scadd          ; calculate screen address.
       ld a,16             ; height of sprite in pixels.
sprit1 ex af,af'           ; store loop counter.
       push de             ; store screen address.
       ld c,(hl)           ; first sprite graphic.
       inc hl              ; increment poiinter to sprite data.
       ld d,(hl)           ; next bit of sprite image.
       inc hl              ; point to next row of sprite data.
       ld (tmp0),hl        ; store in tmp0 for later.
       ld e,0              ; blank right byte for now.
       ld a,b              ; b holds y position.
       and 7               ; how are we straddling character cells?
       jr z,sprit0         ; we're not straddling them, don't bother shifting.
       cp 5                ; 5 or more right shifts needed?
       jr nc,sprit7        ; yes, shift from left as it's quicker.
       and a               ; oops, carry flag is set so clear it.
sprit2 rr c                ; rotate left byte right...
       rr d                ; ...through middle byte...
       rr e                ; ...into right byte.
       dec a               ; one less shift to do.
       jr nz,sprit2        ; return until all shifts complete.
sprit0 pop hl              ; pop screen address from stack.
       ld a,(hl)           ; what's there already.
       xor c               ; merge in image data.
       ld (hl),a           ; place onto screen.
       inc l               ; next character cell to right please.
       ld a,(hl)           ; what's there already.
       xor d               ; merge with middle bit of image.
       ld (hl),a           ; put back onto screen.
       inc hl              ; next bit of screen area.
       ld a,(hl)           ; what's already there.
       xor e               ; right edge of sprite image data.
       ld (hl),a           ; plonk it on screen.
       ld a,(tmp1)         ; temporary vertical coordinate.
       inc a               ; next line down.
       ld (tmp1),a         ; store new position.
       and 63              ; are we moving to next third of screen?
       jr z,sprit4         ; yes so find next segment.
       and 7               ; moving into character cell below?
       jr z,sprit5         ; yes, find next row.
       dec hl              ; left 2 bytes.
       dec l               ; not straddling 256-byte boundary here.
       inc h               ; next row of this character cell.
sprit6 ex de,hl            ; screen address in de.
       ld hl,(tmp0)        ; restore graphic address.
       ex af,af'           ; restore loop counter.
       dec a               ; decrement it.
       jp nz,sprit1        ; not reached bottom of sprite yet to repeat.
       ret                 ; job done.
sprit4 ld de,30            ; next segment is 30 bytes on.
       add hl,de           ; add to screen address.
       jp sprit6           ; repeat.
sprit5 ld de,63774         ; minus 1762.
       add hl,de           ; subtract 1762 from physical screen address.
       jp sprit6           ; rejoin loop.
scadd  ld a,(xcoord)       ; fetch vertical coordinate.
       ld e,a              ; store that in e.

; Find line within cell.

       and 7               ; line 0-7 within character square.
       add a,64            ; 64 * 256 = 16384 = start of screen display.
       ld d,a              ; line * 256.

; Find which third of the screen we're in.

       ld a,e              ; restore the vertical.
       and 192             ; segment 0, 1 or 2 multiplied by 64.
       rrca                ; divide this by 8.
       rrca
       rrca                ; segment 0-2 multiplied by 8.
       add a,d             ; add to d give segment start address.
       ld d,a

; Find character cell within segment.

       ld a,e              ; 8 character squares per segment.
       rlca                ; divide x by 8 and multiply by 32,
       rlca                ; net calculation: multiply by 4.
       and 224             ; mask off bits we don't want.
       ld e,a              ; vertical coordinate calculation done.

; Add the horizontal element.

       ld a,(ycoord)       ; y coordinate.
       rrca                ; only need to divide by 8.
       rrca
       rrca
       and 31              ; squares 0 - 31 across screen.
       add a,e             ; add to total so far.
       ld e,a              ; de = address of screen.
       ret

tmp0   defw 0
tmp1   defb 0

plgfx  defb 127,254,255,255,254,127,252,127,248,127,248,127,254,127,254,127
       defb 254,127,254,127,254,127,254,127,248,31,248,31,255,255,127,254
algfx  defb 127,254,254,63,248,15,240,135,227,231,231,231,255,199,255,15
       defb 252,31,248,127,241,255,227,255,224,7,224,7,255,255,127,254

The best alien movement routines use a combination of random elements and hunter-killer algorithms. In order to overcome the problem in the listing above we need an extra flag to indicate the enemy’s present state or in this case its direction. We can move the sprite along in a certain direction until it becomes possible to switch course vertically or horizontally, whereupon a new direction is selected depending upon the player’s position. However, should it not be possible to move in the desired direction we go in the opposite direction instead. Using this method a sprite can find its own way around most mazes without getting stuck too often. In fact, to absolutely guarantee that the sprite will not get stuck we can add a random element so that every so often the new direction is chosen on a random basis rather than the difference in x and y coordinates.

 

Cranking up the Difficulty Levels

The weighting applied to the direction-changing decision will determine the sprite’s intelligence levels.  If the new direction has a 90% chance of being chosen on a random basis and a 10% chance based on coordinates the alien will wander around aimlessly for a while and only home in on the player slowly.  That said, a random decision can sometimes be the right one when chasing the player.  An alien on a more difficult screen might have a 60% chance of choosing a new direction randomly, and a 40% chance of choosing the direction based on the player’s relative position.  This alien will track the player a little more closely.  By tweaking these percentage levels it is possible to determine difficulty levels throughout a game and ensure a smooth transition from the simplest of starting screens to fiendishly difficult final levels.

Tutorial: ZX Spectrum Machine Code Game in 30 Minutes!

January 12, 2010 38 comments

This tutorial by Jon Kingsman (bigjon) originally appeared in a thread on WoSF. Reproduced with permission.

Roadrace game by bigjon, incorporating suggestions from Dr Beep, skoolkid and Matt B

Hi folks, I’m a machine code novice who coded a very small roadrace game to help me learn.

I reckon you can learn the basics of machine code in half an hour by coding this game step by step.

This tutorial assumes you have a working knowledge of ZX Spectrum basic, and the ZX Spin emulator.

Make yourself a large cup of tea – by the time you’ve drunk it, you be able to program in machine code!

CHAPTER 1 – Create a machine code function that returns the score to BASIC
Machine code programs are a series of bytes in the Spectrum’s memory.
In this chapter we will

  • – Use the Spin assembler to write a few bytes into the memory generating a score for our game.
  • – Write a BASIC program to run the machine code and print the score we have obtained from it.

Open ZX Spin.  Select Tools -> Z80 Assembler.
To run our roadrace game, we need to execute the following steps:

MAIN 	;label for main section of program as opposed to graphics data etc	

	;arrange to put our machine code at free, fast-running (over 32768) and memorable address in RAM
	org 33000  

	;initialise score
	;initialise road, car

PRINCIPALLOOP ;label for the loop in the game that will execute over and over
	;read keyboard
	;set new carposition
	;crash? if so, go to GAMEOVER.
	;print car
	;scroll road
	;random road left or right
	;jump back to PRINCIPALLOOP

GAMEOVER ;label for the cleaning up that needs to be done before returning to BASIC
	;return score to BASIC

Copy and paste the paragraph above into the Spin Assembler.It will appear as 15 lines of mainly grey text.

The text is grey because text after a ; is a comment. The assembler ignores it but it’s there for our benefit.

You can TAB comments over towards the right-hand side of the assembler page to make your code more readable.

The labels are in pink.The assembler won’t put anything in RAM for them but will use them as entry points to jump to.

In the assembler, do File -> Save as and type something like mc30mintut.asm into the save box.
We’ll do the first and last of these steps in this chapter, starting with the last one.

The assembly language instruction for ‘return to calling program’ (in our case a BASIC routine) is ‘ret’.

Click on the end of line 15, press enter to create line 16 and type ret
The word ret‘ appears in blue. This is Spin’s colour code for an instruction.

When the Spin assembler gets to the instruction ret it writes the byte 201 into the memory at an address we choose.

The computer knows that the first byte it meets will be an instruction byte.

It does something different for each byte from 0 to 255. There’s a list in Appendix A of the Spectrum Manual.

Some instruction bytes, like 201 for ret, are complete as is – no further info is needed to complete the action.

Some instruction bytes need one or two bytes of data afterwards for the computer to know what to do.

Some other instruction bytes need a further instruction byte to clarify the action required.

Now we’ll initialise the score, ready for the mc program to report it back to BASIC at the end of the game.

The computer does most of its work using 7 temporary byte-sized addresses called registers.

The first register deals with single bytes only (numbers 0 to 255), the other six are in pairs to deal with 0 to 65535.

The first (single-byte) register is called the A register, sometimes also referred to as the accumulator.

The other three register pairs are called BC, DE, and HL (H is for High byte, L is for Low byte)

Any machine code function called from basic will return the value from 0 to 65535 in the BC register.

We will write the value 0 into the BC register, ready to increase it by 1 each time the game goes round its principal loop.

At the beginning of line 4, type ld bc,0. ld is the instruction for load a value into a register or register pair.

The instruction byte for ld is different for each register or register pair that is loaded.

The instruction byte for ld bc is 1. The computer then expects 2 data bytes to give it a number from 0 to 65535.

In our case the two data bytes will be 0,0.  So the assembler will write 1,0,0,201 at address 33000 in RAM.

We’ll assemble this code now. Do File -> Save, then File -> Assemble. Type 33000 into the Start Address box and click OK.

At the bottom window of the assembler you should see a report that says “No errors in 16 lines. 4 bytes generated”.

You can see the four bytes are now at memory address 33000 by clicking in the main Spin display window on Tools -> Debugger.

To run these four bytes of machine code, enter this one-line program in the main Spin display window:
10 PRINT AT 0,0; “Your score was “; USR 33000

Now RUN the program. Did you get “Your score was 0”? Congratulations – you have coded your first machine code program!

Do File -> Save in the main Spin display window and save as something like mc30mintut.sna. Here ends Chapter 1!

CHAPTER 2 – Display material on the screen.
There are two areas of the Spectrum’s memory which have a direct effect on the screen display.
The complicated way is the display file, from addresses 16384 to 22527, which stores a dash of 8 pixels per byte.

Try POKE-ing 255 into bytes within this range to see the funny order in which this memory area is mapped onto the screen.

The simple way is the attribute file, from 22528 to 23296, which affects an 8×8 pixel block per byte, in logical order.

In this chapter we will

  • Draw our ‘car’ by changing the paper colour of one character square to blue.
  • Draw our ‘road’ by using a loop to create two vertical stripes of black paper colour down the screen.

In the spin assembler line 5, delete the word ‘road’ in the comments.

At the beginning of line 5, type ld hl,23278. This points HL to the middle of the bottom row in the display file.

Insert line 6, ld a,8. This puts a blue PAPER colour into the A register. Why 8? See the BASIC manual chapter 16.

Insert line 7, ld (hl),a. The brackets round hl mean the load will be to the address in RAM that hl is pointing to.

Insert line 8, ld (32900),hl ;save car posn. We’ll store the attribute file address of the ‘car’ in some free bytes in RAM.

Now for the road. Insert line 4, ld hl,22537 ;initialise road. This points to a third of the way along the top line.

To save the road position, which we’ll need frequently, we’ll let the computer choose where to store it, on its ‘stack’.

Chapter 24 of the manual has a diagram showing where the machine stack is in the RAM.
To write to the stack we use push. To write from the stack we use pop. What goes on the stack first will come off last.

Insert line 5, push hl ;save road posn. Insert line 21, pop hl ;empty stack

To print a black road we need 0 in the accumulator (ch16 of the BASIC manual).

We could do ld a, 0 but this takes 2 bytes whereas xor a takes only one. Insert line 6, xor a.
xor compares the chosen register to the A register and puts a 1 in the A register for each bit that is different.

We’ll print the top line of the road. Two double squares of black with a 9-square gap between them.

Insert line 7, then copy and paste the following code:

	ld (hl),a
	inc hl ;inc increases the register by one, dec decreases it by one.
	ld (hl),a
	ld de,9 ;for a 9-space gap in the road.
	add hl,de ;add adds the registers together, so hl points to the right hand side of the road.
	ld (hl),a
	inc hl
	ld (hl),a

To get hl to point to the left hand verge on the next line, we need to move 21 bytes further in the attribute file.

Insert line 15, ld de, 21 ;point to left verge on next line
Insert line 16, add hl,de

To fill the screen with the road we will use machine code’s equivalent of a FOR-NEXT loop, djnz.
djnz stands for Decrement then Jump if Not Zero. We load the b register with the number of times we want to loop.

Insert line 7, ld b,24 ;print road verge on 24 lines.
Insert line 8, fillscreen – this is the label for our loop to jump back to.
Insert line 19, djnz fillscreen.

Because our routine will continue from the loop when b=0, we no longer need to initialise b as well as c to 0 in the next line.

Change line 20 ld bc, 0 to ld c,b.  This is one byte shorter.

Assemble and save. If you want to see the blue ‘car’, you’ll need to add something like 20 PAUSE 0 to your basic program.

CHAPTER 3 – move the car, test for collision.
Time to start playing the game! First we need to erase the car ready to move it if the player wants to.

Insert line 26, then copy and paste the following code:

	ld hl,(32900) ;retrieve car posn
	ld a,56 ;erase car
	ld (hl),a

Before we read the keyboard we will lock the keyboard for most of the game, and unlock it only when we want to read the keys.

The instruction to lock the keyboard is di = ‘disable interrupts. Its opposite is ei = ‘enable interrupts’.

Replace line 3 with di. Insert line 29, ei. Insert line 31, di. Insert line 41, ei

To read the keys we use the IN ports – see ch23 of the BASIC manual – to read the left and right half of the bottom row.

We load bc with the port number and use the instruction cp (compare) to see if the number has dropped to show a keypress.

Delete line30 and replace with the following code:

	ld bc,65278 ;read keyboard caps to v
	in a,(c)
	cp 191
	jr nz, moveright
	inc l
moveright
	ld bc,32766 ;read keyboard space to b
	in a,(c)
	cp 191
	jr nz, dontmove
	dec l
dontmove

jr nz stands for jump relative if not zero. It skips over the instruction to increment / decrement the car position.

Replace line 43 with the following to see if we bump into the oncoming road 32 bytes (1 screen) down the attribute file:

	ld (32900),hl ;store car posn
	ld de, 32 ;new carposn
	xor a  ;set carry flag to 0
	sbc hl,de
	ld a,(hl) ;crash?
	or a
	jr z,gameover
	ld a,8  ;print car
	ld (hl),a

We’d like to sub hl,de but there’s no such instruction so we use sbc, subtract with carry, and set the carry flag to zero.

or compares the register to the a register bit by bit and leaves a 1 in the a register for each bit that is 1 in either.

If all the digits are zero, then the zero flag will be set, so we can use or a to test for a black paper colour.

Delete line 53. Delete line 53 again!

To clean up the score at GAMEOVER insert line21, push bc; save score. Replace line 57 with pop bc;retrieve score

To cycle round the game before GAMEOVER change line 55 to jp PRINCIPALLOOP.

Assemble, save, and run. You’ll need to deliberately crash to get out!

CHAPTER 4 – scroll and move the road, keep score, adjust speed.
To scroll the road down the screen we copy the screen attribute bytes to the line beneath 736 times.

We use the instruction lddr, which stand for LoaD ((hl) to (de)),Decrement (hl and de) and Repeat (until bc is zero).

Replace line 53 with the following:

	ld hl,23263 ;scroll road
	ld de,23295
	ld bc,736
	lddr
	pop bc  ;retrieve score

To add 1 to the score and save it ready for GAMEOVER, insert the following into line 59:

inc bc ;add 1 to score
push bc ;save score

To move the road randomly left or right on the top line we use the following algorithm –
Choose a location in ROM where the are 256 random looking bytes and add the low byte of the score in bc to it.

If it is odd, lower the road position in hl by one. If it is even, increase by one.

(To test the last bit for odd and even we use ‘and 1’ which “masks” the last bit and sets the zero flag if it is 0).

Check to see if the road has reached the edge of the screen and bump it away if it has.
Print the new road top line like we did in chapter 2.

Replace line 58 with the following hefty chunk of code:

	pop hl  ;retrieve road posn
	push hl  ;save road posn
	ld a,56  ;delete old road
	ld (hl),a
	inc hl
	ld (hl),a
	ld de,9
	add hl,de
	ld (hl),a
	inc hl
	ld (hl),a
	;random road left or right
	ld hl,14000 ;source of random bytes in ROM
	ld d,0
	ld e,c
	add hl, de
	ld a,(hl)
	pop hl  ;retrieve road posn
	dec hl  ;move road posn 1 left
	and 1
	jr z, roadleft
	inc hl
	inc hl
roadleft
	ld a,l  ;check left
	cp 255
	jr nz, checkright
	inc hl
	inc hl
checkright
	ld a,l
	cp 21
	jr nz, newroadposn
	dec hl
	dec hl
newroadposn
	push hl  ;save road posn
	xor a  ;print new road
	ld (hl),a
	inc hl
	ld (hl),a
	ld de,9
	add hl,de
	ld (hl),a
	inc hl
	ld (hl),a

The last thing we need to do to have a playable game is slow down our blindingly fast machine code.

Insert the following into line 106 (as extension material, you could adjust the figure in bc with a keypress to ‘brake’):

;wait routine
	ld bc,$1fff ;max waiting time
wait
	dec bc
	ld a,b
	or c
	jr nz, wait

Save, assemble, and run – and that’s it! Has your tea gone cold yet?

A full listing follows, with my email address at the end for your comments and suggestions:

main
	org 33000
	di
	ld hl, 22537 ;initialise road
	push hl  ;save road posn
	xor a
	ld b,24
fillscreen
	ld (hl),a
	inc hl
	ld (hl),a
	ld de,9
	add hl,de
	ld (hl),a
	inc hl
	ld (hl),a
	ld de,21
	add hl,de
	djnz fillscreen
	ld c,b  ;initialise score
	push bc  ;save score
	ld hl,23278 ;initialise car
	ld a,8
	ld (hl),a
	ld (32900),hl ;save car posn
principalloop
	ld hl,(32900) ;retrieve car posn
	ld a,56  ;erase car
	ld (hl),a
	ei
	ld bc,65278 ;read keyboard caps to v
	in a,(c)
	cp 191
	jr nz, moveright
	inc l
moveright
	ld bc,32766 ;read keyboard space to b
	in a,(c)
	cp 191
	jr nz, dontmove
	dec l
dontmove
	di
	ld (32900),hl ;store car posn
	ld de, 32 ;new carposn
	xor a  ;set carry flag to 0
	sbc hl,de
	ld a,(hl) ;crash?
	or a
	jr z,gameover
	ld a,8  ;print car
	ld (hl),a
	ld hl,23263 ;scroll road
	ld de,23295
	ld bc,736
	lddr
	pop bc  ;retrieve score
	pop hl  ;retrieve road posn
	push hl  ;save road posn
	ld a,56  ;delete old road
	ld (hl),a
	inc hl
	ld (hl),a
	ld de,9
	add hl,de
	ld (hl),a
	inc hl
	ld (hl),a
	;random road left or right
	ld hl,14000 ;source of random bytes in ROM
	ld d,0
	ld e,c
	add hl, de
	ld a,(hl)
	pop hl  ;retrieve road posn
	dec hl  ;move road posn 1 left
	and 1
	jr z, roadleft
	inc hl
	inc hl
roadleft
	ld a,l  ;check left
	cp 255
	jr nz, checkright
	inc hl
	inc hl
checkright
	ld a,l
	cp 21
	jr nz, newroadposn
	dec hl
	dec hl
newroadposn
	push hl  ;save road posn
	xor a  ;print new road
	ld (hl),a
	inc hl
	ld (hl),a
	ld de,9
	add hl,de
	ld (hl),a
	inc hl
	ld (hl),a
	inc bc  ;add 1 to score
	push bc  ;save score
	;wait routine
	ld bc,$1fff ;max waiting time
wait
	dec bc
	ld a,b
	or c
	jr nz, wait
	jp principalloop
gameover
	pop bc  ;retrieve score
	pop hl  ;empty stack
	ei
	ret; game and tutorial written by Jon Kingsman ('bigjon', 'bj'). electronic mail gmail.com - atsign - jon.kingsman (reversed)
Categories: Z80 Assembly Tags: , ,

Beginners guide to BASIC – 3

January 12, 2010 Leave a comment

Welcome to the third and final edition of this guide to BASIC!

Now that we’ve created the graphics for our game and have the key handling routines in place, we can now finally move on to the fun part of animating stuff and watch the screen come alive with millions of tiny coloured pixels (okay, maybe not millions but what the heck, who’s counting?)!

Firstly, if you remember the plot of our game, our intrepid hero of the game Krapz has to survive as long as he can by collecting little quanta particles until he gains enough power to jump out the warp into… yet another more difficult screen. He also has to avoid touching the “ST fluctuation trails” he leaves behind in his wake plus he must stay clear of the deadly boundaries of the warp. In order to pull this fancy (oh sure! – Ed) stuff off, we’ll break it down to a set of tasks.

Task #1: Set up the playing area. This involves drawing the boundaries of the warp and populating the playing field with randomly distributed set of quanta particles. Since every new screen that Krapz jumps into involves the above steps, it makes sense to package it as a sub-routine. Which is what we’ll do:

115 LET quanta=5*level: LET time=seedtime+(level*30): CLS

120 PRINT AT 1,0; INK 3;"\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b";AT 20,0; INK 3;"\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"

130 FOR f=1 TO 19

140 PRINT AT f,0; INK 3;"\b";AT f,31; INK 3;"\b"

150 NEXT f

160 LET x=10: LET y=10

250 FOR f=1 TO 5*level

260 LET p=INT (RND*17)+2

270 LET q=INT (RND*28)+2

280 IF ((p=y AND q=x) OR (ATTR (p,q)=5)) THEN GO TO 260: REM don't overwrite player or another quanta!

290 PRINT AT p,q; INK 5;"\g"

300 NEXT f

999 RETURN

The first line looks a bit complex but is actually a rather simple way of increasing the difficulty level as Krapz progresses through the game. The first LET statement simply increases the number of quanta particle in a screen by a factor of 5 depending on which level (screen) Krapz is playing on. For the first screen (level = 1), there will be only 5 quanta particles to collect. On the 2nd screen (level = 2) there will be 10, and so on and so forth. The second LET statement controls how much time Krapz has to collect all the quanta particles. This is again a factor of the level Krapz is on – 30 times the level as a matter of fact, which compensates for the fact that there are more quantas on each screen as we progress. You’ll notice that a seedtime has been tacked on in the equation to ensure that the player has a basic minimum time to start with on each level.

After having cleared the screen, it’s time to draw the boundaries of the warp – in our case it’s simply a rectangular arena to delineate the playing field. First we draw the top and bottom edges, which is simply a matter of printing 32 characters of the UDG “b” (incorrectly reproduced as \b  in the listing above) that we created earlier on. Line 120 prints the top edge and the bottom edge in lurid purple ink.

To draw the vertical left and right edges we employ a FOR-NEXT loop from line 130 to 150 that print UDG “b” 19 times vertically.

It’s now time to randomly populate the playing area with quanta particles. Line 160 first sets the initial co-ordinates of Krapz on each screen – he always starts on row 10, column 10 on every screen.

We then employ a FOR-NEXT loop to print the actual quanta. The number of particles is simply a factor of the level the player is on, and is in fact exactly the same as the variable quanta we calculated on line 115. In fact we can substitute the 5*level with quanta and the loop will work just as well. Line 260 and 270 calculate the row and column (p,q) to print the quanta on. INT (RND* 17) gives us a random number from 0 to 16. We add a factor of 2 to ensure we don’t print anything on row 0 and 1 – the top edge of the playing arena. Note that if got 16 and added 2 we get 18, which still within the bottom edge (column 20) of the arena. Similarl, we keep the quanta within the left and right edges of the playing arena in line 270.

Line 280 introduces a necessary check that ensures that we aren’t printing a quanta on top of another already existing quanta on screen (we do want all our quantas visible individually on the screen!). Plus we don’t want to print at the position Krapz is already in (10,10). So, what we are checking for is “whether the quanta co-ordinates are the same as Krapz’s co-ordinates OR whether the quanta particle is being printed on top of another quanta” state. This is achieved by the IF statement, where the two expressions are separated by an OR (as in the above statement) condition which returns true if any one of the expression is true (either printing on Krapz or printing on a quanta).

To check for whether the quanta co-ordinates are same as Krapz’s co-ordinates, we simply have to check if the column and row co-ordinates for both match up. If they are the same, then we are at the same location on screen. This is calculated using the expression in the IF statement p=y AND q=x. The AND condition returns true if and only if both the sub-expressions are true.

The next thing we have to check for is the case of a quanta overwrite condition. This is done by using the ATTR command, which given a x & y co-ordinate returns the colour attribute at that co-ordinate. The BASIC manual explains how to interpret this value but suffice to say that for our purpose if ATTR returns a value of 5 it means that a cyan coloured character (INK 5) is present at that position. As you will see on line 290, where we print our quanta particles, they are printed with INK 5. In essence, what we are saying is that if ATTR returns 5, we assume that we have a quanta particle there regardless of what actually might be there. In order to not trip up our assumptions we ensure that we do not print any other stuff in INK 5 anywhere the screen. In our program only the quanta particles can have the cyan colour when playing the game.Period.

If we are overwriting Krapz or another quanta, we simply go back to line 160 and re-calculate a new quanta position until we are satisfied we aren’t overwriting anything we shouldn’t be overwriting! If all is well, we proceed to actually print a quanta particle on the screen at the position we calculated (line 290). Once we’ve printed all the quanta particles required for a level we RETURN from the sub-routine via line 999.

There! We’re all set to actually do some gameplay and stuff! Lets move on to the task of moving Krapz around on the playing area. Remember we set up our control keys in Code Shed Guide 2, so it’s time to use those keys to move Krapz around.The control scheme we’ll follow is a simple one. Pressing one of the four direction keys changes Krapz’s direction instantly (inertia? What’s that?).  The key needn’t be held down if one wishes Krapz to continue in that direction though- his inertia (that word again!) will keep him going in that direction until a different direction key is pressed.

Have a look at this code which begins our main game loop (so called because we’ll be executing it repeatedly till something happens to break proceedings) proper:

1010 PRINT AT 21,0;"Lives: ";lives;TAB 20;"Score: ";score

1020 PRINT AT 0,0;"Time: ";time;"      "

1030 PRINT AT 0,20;"Level: ";level

2000 LET a$=INKEY$

2010 IF a$=k$(1) THEN LET dir=1: REM right

2020 IF a$=k$(2) THEN LET dir=2: REM left

2030 IF a$=k$(3) THEN LET dir=3: REM up

2040 IF a$=k$(4) THEN LET dir=4: REM down

2045 IF dir<>0 THEN PRINT AT y,x; INK 1;CHR$ (143)

2050 IF dir=1 THEN LET x=x+1

2060 IF dir=2 THEN LET x=x-1

2070 IF dir=3 THEN LET y=y-1

2080 IF dir=4 THEN LET y=y+1

Lines 1010 to 1030 give information regarding the status of the number of lives left, the current score, the time left and the current level being played. It’s our HUD if you will. Since it’s within the main game loop, it will be updated continously.

Lines 2000-2080 are responsible for our inertial control mechanism. The logic is quite simple. We sample the keyboard for any keypress. If it’s a direction key we change Krapz’s direction of movement. If no directional key is pressed, Krapz continues to move along in the same direction. Here’s how. Line 2000 reads in a key from the keyboard (null if no key is pressed). Lines 20101 to 2040 compare the value in a$ with the value in k$ (our desired control keys). Depending on which key is pressed, a variable called dir is set to a particular value that signifies a direction.

Line 2045 prints the trail behind Krapz. Basically, all it does is see if Krapz is moving (Krapz doesn’t start moving until you press a key initially), we don’t bother printing a trail. If he is moving (dir will have some non-zero value then), we will print a solid block of blue at the current Krapz co-ordinates. CHR$ (143) will print the solid graphic block you see on the numeric 8 key on the Speccy keyboard (Graphic mode + shift + 8). What’s this CHR$ you ask? Well it’s one way of printing a character on the screen. For example,if you do PRINT CHR$(65) it will print the letter “A” on the screen. This is because CHR$ converts a number to its string equivalent from the character set. If you take a look at the character set table in the Speccy manual you’ll see that numbers from 32 to 127 represent characters from the ASCII standard. Which is why 65 corresponds to letter “A”. From 128 to 255, the character set is unique to the Spectrum with some special characters like the Graphic Blocks taking up positions 129 to 143. You can print any of these characters by passing the code number to CHR$. Some unprintable characters (no, not the sort you’re thinking of) can do some fancy print formatting tricks – refer to the manual for more on that.

Coming back to the code, so we print the blue block at the current Krapz co-ordinates to signify a trail. “Hang on!” you say. “Won’t we be overwriting Krapz in the process?”. But of course! But since we’ll be repositioning Krapz at new co-ordinates anyway, it doesn’t matter. In fact, the code to calculate Krapz’s new co-ordinates follows:

2050 IF dir=1 THEN LET x=x+1

2060 IF dir=2 THEN LET x=x-1

2070 IF dir=3 THEN LET y=y-1

2080 IF dir=4 THEN LET y=y+1

2090 IF (ATTR (y,x)=3 OR ATTR (y,x)=1) THEN FOR f=0 TO 4: PRINT AT y,x; PAPER 2; INK 6; FLASH 1;CHR$ (145+f); FLASH 0: BEEP 0.4,RND*f: NEXT f: PAUSE 10: GO TO 6000

Lines 2050 to 2080 recalculate the new x or y position depending on the direction of travel. Simple, eh?

In line 2090 we check whether Krapz has crashed into the boundary walls or walked into his own trail (ST fluctuations are bad remember?). This is handled by a single IF statement that checks whether the attribute colour at the newly calculated Krapz co-ordinate matches INK 3 (the magenta colour with which the walls are drawn) or INK 1 (blue colour of Krapz’s trail). If it is, it’s time to play a sad ditty expressing our condolences and create a nifty looking explosion. Since I’m not musically inclined I’m just using a simple BEEP statement that randomly plays some stuff in a low pitch. Creating the explosion is simplicity in itself – I’ve created 4 explosion UDG’s using BASin’s UDG creator tool. To simulate the explosion effect, I just print the four characters (with full FLASH and stuff for added effect) one atop the other with a small time gap inbetween to slow things down a bit. It’s not the greatest explosion effect you’ll ever see but it’s good enough for our purpose. After the dust has settled down, we send the program off to line 6000 where the last recitals are performed. More on that later.

2095 IF ATTR (y,x)=5 THEN LET score=score+(level*10): LET quanta=quanta-1: BEEP 0.01,0.1: IF quanta=0 THEN PRINT AT 10,3; PAPER 1; INK 5; FLASH 1;"Space-Time Jump! Get Ready!"; FLASH 0: FOR f=1 TO 10: BEEP 0.03,RND*f: NEXT f: PAUSE 50: LET level=level+1: GO TO 6020

3000 PRINT AT y,x; INK 4;"\a"

Line 2095 determines what happens when Krapz picks up a quanta particle (we simply perform an attribute check for that). What happens is this: first the score is increased by 10 times the level we are on (as a measure of difficulty).The number of active quantas on the screen is decreased by one. If there are no more quantas left, it’s time to trigger a Space-Time jump and warp to the next level which is easily done with a simple message, a few random beeps and a jump to Line 6020 that sets a few variables back to initial values and then re-draws the level screen by jumping to line 1010.

If a space-time warp hasn’t been triggered (because there are quantas still left on the screen), we go on to print Krapz at the new locatio in line 3000.

We come to a final bit of code that deals with the player running out of time.

3005 LET time=time-1

3010 IF time=0 THEN PRINT AT 10,5; PAPER 2; INK 6; FLASH 1;"S-T Field Collapsing!"; FLASH 0: FOR f=10 TO 1 STEP -1: BEEP 0.1,f/2: NEXT f: PAUSE 50: FOR f=2 TO 19: PRINT AT f,1; PAPER 1; INK 2;"\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::\::": BEEP 0.05,RND*f/2: NEXT f: GO TO 6000

4000 GO TO 1010

6000 LET lives=lives-1

6010 IF lives=0 THEN PRINT AT 10,10; PAPER 4; INK 1;"  GAME OVER!  ": PAUSE 100

6012 IF lives=0 THEN IF score>hiscore THEN LET hiscore=score: PRINT AT 12,10; PAPER 5; INK 1; FLASH 1;"New High Score!"; FLASH 0: PAUSE 100

6015 IF lives=0 THEN RETURN

Line 3005 simply decreases the available time by 1 unit.

Line 3010 determines what happens next. If we’ve run out of time it’s time for another dramatic message with annoying random beeps and appropriately dangerous looking flashing text. We up the ante by printing a line of solid block (the odd ::\ is the same GRAPHIC MODE + SHIFT + 8 friend we met a while back) that slowly fills up the entire screen to signify a field collapse. We then jump to line 6000 to finish off the formalities.

If we haven’t run out of time we simply loop back to line 1010 via Line 4000 to continue playing.

Line 6000 is where the case of death is handled. First the number of available lives is decreased by 1. Line 6010 checks if we’ve used up all our lives, in which case it’s really Game Over! Line 6012 next checks if we created a new high score by simply comparing the current score with the high score. If our current score is higher, we save it as the high score and congratulate the player on achieving the same.

Line 6015 proceeds to return from the main game loop routine and dumps us back at the main menu screen from where we’d first come from thus bringing us back full circle to square one.

And that my friends brings us to an end to this guide! Hopefully, you’ll go on to write better and bigger stuff than the Dash-it game described here. If not, don’t forget to send your entry to the annual Crap Games Competition! Cheers!

Categories: Sinclair BASIC Tags: , , ,

Review: Exolon DX

January 5, 2010 Leave a comment

Game: Exolon DX

Publisher: Retrospec

Authors: Graham Goring, John Blythe & Infamous


When I first played Exolon by Raffaele Cecco on the speccy I was blown away by the colourful graphics and the super smooth animation of the main character – Vitorc. To date it remains as a reminder as to what can be done on the speccy when a competent coder and graphic artist design a game with the limitations and abilities of the speccy in mind.

So when retrospec announced a Exolon remake in the offing way back in 2005, I was hugely interested in knowing how the talented folks at retrospec would remake this classic. The answer is ExolonDX, the second ever remake done by Graham Goring for retrospec.

The starting intro sets the mood for the remake nicely. Colourful, well drawn graphics with all sorts of embellishments greet you before whisking you away to the main menu. The first thing you should do is go to the options screen and switch the display to 16 or 32 bit full screen mode (unless you don’t want to of course!) to get the maximum pleasure out of the game. You will need to quit and restart the game for the setting to take effect though, which is a bit annoying but easily done.

The first screen in the game shows off the graphics nicely. Cool looking spaceship, nifty status panel, smooth parallax-scrolling starfield and ,whey-hey, the Cybernoid ship zooming off screen in the background! Okay enough sight-seeing. Time to lay into that gun emplacement with a quick rocket from the rocket launcher. Woohoo! Watch that sucker explode! Well then, not a bad start eh? Wonder what lies beyond that crazy looking alien artifact…

And with that you’re on your way. The first thing to note in the remake is that the number of lives has reduced from the gracious (but much needed) nine lives to a mere two. Yep, that’s right – two. That’s probably because the remake doesn’t require you to be pixel perfect as in the original speccy version so things are indeed a bit easier. Plus the remake is pretty much faithful to the original version so there aren’t any nasty surprises in store to upset your march through the game.

Amidst the graphical extravaganza the only thing I found a bit amiss was the graphic for Vitorc himself. He looks like a spaceman in a spacesuit – as he should – but lacks the character that Vitorc had in the original version. Plus the upgrade suit doesn’t alter Vitorc’s appearance as dramatically. I’m probably nit-picking but it’s these two points that caught my eye early on in the game.

Apart from that Exolon is a visual feast with extremely colourful well defined graphics and really cool visual effects that bring the game to life. In the sound department, Exolon boasts of a thumpingly good  soundtrack that plays in the background and competent but varied sound effects that accompany most of the actions in the game.

I must say I thoroughly enjoyed playing the remake for the simple reason that it’s a well crafted remake that stays to the original but significantly improves on all the aspects of the original game. Highly recommended!

Interestingly, ExolonDX credits Hugh Binns as the creator of the original graphics. However, I can’t find any reference to Hugh Binn as the author of the graphics for the speccy version (on which ExolonDX is based). Indeed, he isn’t mentioned anywhere in the credits for the speccy version of Exolon. Hugh Binns, however, did do the graphics for the Amstrad CPC version, so there’s probably been a mix-up somewhere. Either that or Raffaele cheekily took all the credit!

Categories: Games Tags: , ,
%d bloggers like this: