Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.
Generating random numbers in machine code can be a tricky problem for a novice programmer.
First of all, let’s get one thing straight. There is no such thing as a random number generator. The CPU merely follows instructions and has no mind of its own, it cannot simply pluck a number out of thin air based on a whim. Instead, it needs to follow a formula which will produce an unpredictable sequence of numbers which do not appear to follow any sort of pattern, and therefore give the impression of randomness. All we can do is return a false – or pseudo – random number.
One method of obtaining a pseudo-random number would be to use the Fibonacci sequence, however the easiest and quickest method of generating a pseudo-random 8-bit number on the Spectrum is by stepping a pointer through the ROM, and examining the contents of the byte at each location in turn. There is one small drawback to this method – the Sinclair ROM contains a very uniform and non-random area towards the end which is best avoided. By limiting the pointer to, say, the first 8K of ROM we still have a sequence of 8192 “random” numbers, more than enough for most games. In fact, every game I have ever written with a random number generator uses this method, or a very similar one:
; Simple 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 ld a,h and 31 ; keep it within first 8k of ROM. ld h,a ld a,(hl) ; Get "random" number from location. inc hl ; Increment pointer. ld (seed),hl ret seed defw 0
Let’s put our new random number generator to use in our Centipede game. Every Centipede game needs mushrooms – lots of them – scattered randomly across the play area, and we can now call the random routine to supply coordinates for each mushroom as we display them. The bits underlined are those we need to add.
; We want a black screen. ld a,71 ; white ink (7) on black paper (0), ; bright (64). ld (23693),a ; set our screen colours. xor a ; quick way to load accumulator with zero. call 8859 ; set permanent border colours. ; Set up the graphics. ld hl,blocks ; address of user-defined graphics data. ld (23675),hl ; make UDGs point to it. ; Okay, let's start the game. call 3503 ; ROM routine - clears screen, opens chan 2. ; Initialise coordinates. ld hl,21+15*256 ; load hl pair with starting coords. ld (plx),hl ; set player coords. call basexy ; set the x and y positions of the player. call splayr ; show player base symbol. ; Now we want to fill the play area with mushrooms. ld a,68 ; green ink (4) on black paper (0), ; bright (64). ld (23695),a ; set our temporary colours. ld b,50 ; start with a few. mushlp ld a,22 ; control code for AT character. rst 16 call random ; get a 'random' number. and 15 ; want vertical in range 0 to 15. rst 16 call random ; want another pseudo-random number. and 31 ; want horizontal in range 0 to 31. rst 16 ld a,145 ; UDG 'B' is the mushroom graphic. rst 16 ; put mushroom on screen. djnz mushlp ; loop back until all mushrooms displayed. ; This is the main loop. mloop equ $ ; Delete the player. call basexy ; set the x and y positions of the player. call wspace ; display space over player. ; Now we've deleted the player we can move him before redisplaying him ; at his new coordinates. ld bc,63486 ; keyboard row 1-5/joystick port 2. in a,(c) ; see what keys are pressed. rra ; outermost bit = key 1. push af ; remember the value. call nc,mpl ; it's being pressed, move left. pop af ; restore accumulator. rra ; next bit along (value 2) = key 2. push af ; remember the value. call nc,mpr ; being pressed, so move right. pop af ; restore accumulator. rra ; next bit (value 4) = key 3. push af ; remember the value. call nc,mpd ; being pressed, so move down. pop af ; restore accumulator. rra ; next bit (value 8) reads key 4. call nc,mpu ; it's being pressed, move up. ; Now he's moved we can redisplay the player. call basexy ; set the x and y positions of the player. call splayr ; show player. halt ; delay. ; Jump back to beginning of main loop. jp mloop ; Move player left. mpl ld hl,ply ; remember, y is the horizontal coord! ld a,(hl) ; what's the current value? and a ; is it zero? ret z ; yes - we can't go any further left. dec (hl) ; subtract 1 from y coordinate. ret ; Move player right. mpr ld hl,ply ; remember, y is the horizontal coord! ld a,(hl) ; what's the current value? cp 31 ; is it at the right edge (31)? ret z ; yes - we can't go any further left. inc (hl) ; add 1 to y coordinate. ret ; Move player up. mpu ld hl,plx ; remember, x is the vertical coord! ld a,(hl) ; what's the current value? cp 4 ; is it at upper limit (4)? ret z ; yes - we can go no further then. dec (hl) ; subtract 1 from x coordinate. ret ; Move player down. mpd ld hl,plx ; remember, x is the vertical coord! ld a,(hl) ; what's the current value? cp 21 ; is it already at the bottom (21)? ret z ; yes - we can't go down any more. inc (hl) ; add 1 to x coordinate. ret ; Set up the x and y coordinates for the player's gunbase position, ; this routine is called prior to display and deletion of gunbase. basexy ld a,22 ; AT code. rst 16 ld a,(plx) ; player vertical coord. rst 16 ; set vertical position of player. ld a,(ply) ; player's horizontal position. rst 16 ; set the horizontal coord. ret ; Show player at current print position. splayr ld a,69 ; cyan ink (5) on black paper (0), ; bright (64). ld (23695),a ; set our temporary screen colours. ld a,144 ; ASCII code for User Defined Graphic 'A'. rst 16 ; draw player. ret wspace ld a,71 ; white ink (7) on black paper (0), ; bright (64). ld (23695),a ; set our temporary screen colours. ld a,32 ; SPACE character. rst 16 ; display space. ret ; Simple 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 ld a,h and 31 ; keep it within first 8k of ROM. ld h,a ld a,(hl) ; Get "random" number from location. inc hl ; Increment pointer. ld (seed),hl ret seed defw 0 plx defb 0 ; player's x coordinate. ply defb 0 ; player's y coordinate. ; UDG graphics. blocks defb 16,16,56,56,124,124,254,254 ; player base. defb 24,126,255,255,60,60,60,60 ; mushroom.
Once run this listing looks more like a Centipede game than it did before, but there’s a major problem. The mushrooms are distributed in a random fashion around the screen, but the player can move straight through them. Some form of collision detection is required to prevent this happening, and we shall cover this in the next chapter.
The random number generators given here originally appeared as posts by Patrik Rak on WoSF and are used here with permission.
The default built-in pseudo random number generator in the ZX Spectrum cycles through 65536 numbers (2^16 sequence of numbers). Patrik proposes two alternatives for better random number generation starting with the XOR shift method (based on George Marsaglia’s derivation), in assembly for the Z80:
Yesterday I had a bit of spare time so I put together a quick test for finding the a,b,c parameters for n=8 and four seeds (I first verified the Marsaglia’s 81 parameters to make sure I got it all right). There are six sets of parameters which provide the desired order of 2^32-1: (1,1,3), (3,6,1), (3,3,2), (5,3,2), (1,7,2), (6,7,1). Of these, the first four seem to do reasonably well on the Diehard test. Here is the Z80 code for the (1,1,3) variant, the result being the 8 bit in the A register:
rnd ld hl,0xA280 ; xz -> yw ld de,0xC0DE ; yw -> zt ld (rnd+1),de ; x = y, z = w ld a,e ; w = w ^ ( w << 3 ) add a,a add a,a add a,a xor e ld e,a ld a,h ; t = x ^ (x << 1) add a,a xor h ld d,a rra ; t = t ^ (t >> 1) ^ w xor d xor e ld h,l ; y = z ld l,a ; w = t ld (rnd+4),hl ret
In order to return the 8 bit random number to BASIC, do a LD B,0 and LD C, A just before the RET in the end.
Seeing the interest regarding the Xor-Shift random number generator for Z80, I became curious how difficult it would be to implement CMWC (Complimentary-Multiply-With-Carry) RNG for Z80. These generators are based on a prime p of the form a*b^r + 1, carry c < a and a sequence of r x’s < b such that t = a * x(n-r) + c(n-1), c(n) = t div b, x(n) = (b-1) – t mod b.
To make it suitable for Z80, I have chosen base b=2^8. I was also interested how well it could do with fairly small state, so I have chosen r=8 rather than the tempting r=256. From the possible primes, I have chosen the one with a=253, for which the order of the generated sequence is (p-1)/2^5 = 253*2^59 (assuming I got all the math right ), which is almost 2^67, and more than 2^66 or 10^20.
I have tested the C version of the generator with diehard, and it seems to pass all the tests. Here is the corresponding Z80 variant, which returns the 8 bit result in A:
org 40000 call rnd ; BASIC driver ld c,a ld b,0 ret rnd ld de,0 ; c,i ld b,0 ld c,e ld hl,table add hl,bc ld c,(hl) ; y = q[i] push hl ld a,e ; i = ( i + 1 ) & 7 inc a and 7 ld e,a ld h,c ; t = 256 * y ld l,b sbc hl,bc ; t = 255 * y sbc hl,bc ; t = 254 * y sbc hl,bc ; t = 253 * y ld c,d add hl,bc ; t = 253 * y + c ld d,h ; c = t / 256 ld (rnd+1),de ld a,l ; x = t % 256 cpl ; x = (b-1) - x = -x - 1 = ~x + 1 - 1 = ~x pop hl ld (hl),a ; q[i] = x ret table db 82,97,120,111,102,116,20,12
As you can see, the table access is quite expensive compared to the computation itself. Having the table aligned to 256 byte boundary could be used to optimize this. Also, when generating more than 8 bits at once, most of this could be easily factored out and reused. But I leave these optimizations for anyone interested…
The original posts contain some more tidbits that may be of use including BASIC and ZX Boriel Compiler versions: