This article looks at the random number generator code used by the Commodore Amiga game K240, written in 68k assembly language, and how it is used in practice to generate random chances.
K240’s random number generator code
_SeedRNG: ; called during game startup
TST.L D0
BEQ.S _GenerateSeed
MOVE.L D0,intRNG2
RTS
_GenerateSeed:
MOVEQ #0,D1
MOVE.W VHPOSR,D1 ; read CRT beam position, Amiga-specific
DIVU #$0064,D1
SWAP D1
loop00C02:
MOVEQ #100,D0
BSR.S _RandInt ; Generate a random number from 0 to 99
DBF D1,loop00C02
RTS
_RandInt:
MOVEM.L D1-D4,-(A7)
MOVE.B intRNG5,D1
MOVE.B intRNG4,D2
MOVE.B intRNG3,D3
MOVE.B D2,D4
LSL.B #1,D4
EOR.B D3,D4
ROXL.B #2,D4 ; shuffling bytes around
ROXR.B #1,D1
ROXR.B #1,D2
ROXR.B #1,D3
MOVE.B D1,intRNG3
MOVE.B D2,intRNG5
MOVE.B D3,intRNG4
MOVEQ #0,D1
MOVE.W intRNG4,D1
DIVS D0,D1
SWAP D1
MOVE.W D1,D0
MOVEM.L (A7)+,D1-D4
TST.W D0 ; set condition codes
RTS
intRNG0:
DS.L 1 ; 00 00 00 00
intRNG2:
DS.B 1 ; 00
intRNG3:
DC.B $13 ; 19
intRNG4:
DC.B $a5 ; 165
intRNG5:
DC.B $1d ; 29
Function
Since the most popular models of Amiga did not include a real-time clock, this
game seeds the initial random value from VHPOSR, the register which contains
the current horizontal position of the CRT beam on screen. The exact position
isn’t important, only that it provides unpredictable number.
This number is used to determine the number of initial times to call _RandInt,
the random number generator subroutine that is often called during the game.
The results of these initial rolls are discarded, but each time _RandInt is
called it advances the RNG internally.
The subroutine _RandInt takes the number in register d0 and returns a
randomly selected value between 0 and d0 minus one. For example, if d0 is
100 when _RandInt is called, d0 will then contain a random number from 0
to 99.
Each time _RandInt is called, it shuffles the numbers held in three bytes in a
certain manner, using LSL.B (logical shift left, here effectively divide by
two), EOR.B (exclusive OR) and a series of ROXL and ROXR (rotate left and
right), storing the results in the three bytes in a different order.
In other words, the RNG is purely deterministic, and the only question is how many iterations into the RNG the player will achieve. Since K240 calls on the RNG a lot, and it shuffles each time it is called, it’s difficult to predict or bias the RNG (“RNG manipulation”) to the player’s benefit.
After the shuffle, it takes the last two of the shuffled bytes as a word, which
may be between $0000 and $FFFF, i.e. between 0 and 65536. It then performs a
DIVS to divide that by the number provided in d0, taking the modulo of that
and returning it in d0.
Note that it only produces 16-bit numbers, i.e. 0 to 65536. This is plenty for the game’s purposes. The biggest random number range I see K240 generate is 1,000, used to calculate the alien population in a false intel report (although the maximum result of 999 will be rounded to 992).
How it is called
The subroutine _RandInt is called at a number of places in K240, often to
decide a chance for an event to take place or not. Sometimes, it instead
determines a number, such as a random value or location.
An example of both is generating the amount of ore is in an asteroid:
_InitOre:
LEA tblInitOre,A1 ; table with chance and amount of each ore
LEA (60,A0),A2 ; ore
MOVEQ #9,D1 ; set up a loop from 0-9
loop12CFC:
CLR.W (A2) ; reset ore to 0
MOVEQ #100,D0
JSR _RandInt ; 0-99
CMP.W (A1),D0 ; Draws chance from table
BPL.S .next_12D20
MOVE.W (4,A1),D0 ; maximum ore amount from table
SUB.W (2,A1),D0 ; minimum ore amount from table
ADDQ.W #1,D0 ; +1
JSR _RandInt ; return 0 to (min-max)
ADD.W (2,A1),D0 ; +min
MOVE.W D0,(A2) ; store that amount
.next_12D20:
ADDQ.L #6,A1 ; Next entry in table
ADDQ.L #2,A2 ; Next ore
DBF D1,loop12CFC
RTS
Chance calculations are usually performed by generating a random number, then
comparing it to a given value (using CMP, CMPI or TST). It then either
branches or continues based on some condition code (e.g. BPL, BMI, etc).
In the ore generation, for example, CMP and BPL (compare and branch if
positive) are used to continue on a percentage chance:
MOVEQ #100,D0
JSR _RandInt
CMP.W (A1),D0 ; Draws chance from table
BPL.S .next_12D20 ; The code after this generates the ore
With CMP and BPL, the code continues with a chance of the value given to
CMP divided by the value in d0. Another example of this is the 20% chance to
shoot (technically CMPI here since it’s comparing an immediate value, but the
effect is the same):
MOVEQ #100,D0
JSR _RandInt
CMPI.B #20,D0 ; 20% chance
BPL.S ret10D84 ; The code after this fires the Vortex
LaunchVortex: ; This code will run 20% of the time
Here’s another example using a four in ten chance for the Shipyard to continue working during a manpower shortage:
MOVEQ #10,D0
JSR _RandInt
CMPI.B #$04,D0
BPL.S ret143C4 ; The next lines process shipbuilding
The reverse, branching on a percentage chance, can be performed using CMPI
and BMI. From the code which fires Vortex Mine from alien ships:
_AlienVortex04:
MOVEQ #100,D0
JSR _RandInt
CMPI.B #10,D0 ; 10%
BMI.S LaunchVortex ; LaunchVortex will trigger 10% of the time
RTS ; It will reach here the other 90% of the time
Care must be taken to avoid errors. In the following example, the programmer has
written a continue-on-chance, usually CMP and BPL. However, they’ve instead
used CMP and BMI, and reversed the operands in the CMP. The result is an
off-by-one error that makes the chance to continue 1% higher:
MOVE.B (0,A1,D1.W),D1 ; d1 = radiation chance
MOVEQ #100,D0
JSR _RandInt
CMP.B D0,D1 ; note: D0,D1 instead of D1,D0
BMI.S _14988 ; chance is 1% higher than table suggests
SUBQ.W #1,D6
_14988:
TST is essentially just CMPI with zero, and easy to use with BEQ or BNE
to give a one in d0 chance of something.
For example:
MOVEQ #2,D0
JSR _RandInt ; will return 0 or 1
TST.B D0 ; essentially the same as CMPI.B #0,D0
BEQ.S _0F292 ; branch on 0, continue on 1
Another way to do continue-on-chance with a one in d0 chance, for example, 1 in 10:
MOVEQ #10,D0
JSR _RandInt
BNE.S ret109A4 ; will branch on non-zero
« Back to index page