|
EXPLORING SMARTBASIC
RANDOM NUMBERS
I have written on several occasions about random numbers.
I will probably continue to do so until such time as we have
exhausted all the questions, gripes, and hacks. This expose will
explain how and why RANDOM works, and help you get the most out
of it.
The RND function returns a random number between 0 and 1.
Although the value
is never 1, it is possible for it to equal exactly 0. There are
3 ways to ask for a random number:
RND(1) or RND(2), or any positive number, will extract the
NEXT random number from the generator. The argument value is
unimportant.
RND(0) will restore the previously generated random number.
This might be useful if your program FORGETS what the last random
number was and you want to double check its value without affecting
anything else
RND(-x) will reset the random seed to a particular value based
on the supplied number. This can be useful in GAME debug situations
where you may want to recreate an exact condition.
Now what to do with random numbers? What use is a number between
0 and 1. Well quite simply, you multiply it by the number of
choices you have to make. If you want to randomly decide whether
to go North South East or West, use something like:
move = INT(RND(1)*4+1)
Note that we multiply by 4 (the number of choices) and add
1 before taking the INT. This will give us a random INTEGER which
is 1, 2, 3, or 4. This result can be used with an ON GOTO statement
to branch to the correct routine.
Random number generation on the ADAM is accomplished by a
complex, routine at 4696(1258). It either loads the floating
point accumulator with the 4-byte CURRENT random seed, or with
4 bytes representing the user-supplied value if RND(-x) was used.
It then points to 4 constants at address 4552(11C8). These values
are 45, 230, 64, and 187. You can change those if you wish but
they will likely cause your random number generator to fail.
Each of these numbers is multiplied in turn to the current value
in the FPA. After each recursion, one of the 4 CURRENT RANDOM
bytes is updated with the OVERFLOW of the calculation.
The resulting value in the floating point accumulator is then
reduced to a number between 0 and 1 to return to the caller.
If all that sounds confusing, it is! But there is more...
NEW and RUN insist on re-initializing the random seed by copying
the 4 static bytes FB 40 D2 92 into the random generator at address
16190 (3F3E) and 16192 (3F40). The routine that accomplishes
this task is found from 11907 (2E83) to 11918 (2E8E). This is
supposed to prevent the random number generator from breaking
down but it has the effect of generating the same random numbers
ever time a program is run. I have run some quite severe tests
on the random generator. I have left it crunching away for a
full day and periodically reporting the distribution of numbers.
I have seen no evidence of the generator breaking down.
You may have seen some random routines which try to overcome
this problem. While there are many approaches, I will outline
some of the more common ones, illustrating their concept and
their shortcomings.
1) The random keypress
In the beginning, we had little knowledge of ADAMs workings,
but we knew something about RND(-x). So what we needed was a
variable (-x). Some of the first efforts just asked you to press
any key; its value would be the random seed. Unfortunately, most
people press the same key when ANY KEY is asked for. Thus a program
using this approach will likely be random between different players
rather than different each time the same player plays it:
10 PRINT Press any key to start
20 GET keys
30 x=ASC(key$)
40 r=RND(-x)
2) The PDL approach
If a game uses the joystick, then the PDL function is useful
to set a random seed. Here, the basic principle is HOW LONG before
a button on the keypad is pressed:
10 PRINT Press any key on the keypad
20 x=x-1:REM start to set a random seed
30 if not PDL(11) goto 20: REM carry on until key is pressed
40 r=RND(x):REM set the seed
This routine can be defeated quite easily by holding down
a button while typing RUN. You can make this more complex by
asking for multiple buttons; it is unlikely that buttons will
be pressed at the same speed every time:
10 PRINT Press 1-2-3 on the keypad
20 GOSUB 100: REM wait for a button
30 if x<>1 goto 20:REM did not press 1
40 GOSUB 100: REM wait for a button
50 if x<>2 goto 40
60 GOSUB 100: REM wait for a button
70 if x<>3 goto 60
80 goto 120
100 x=PDL(13):if x<15 then RETURN:rem a button was pressed
110 y=y-1:GOTO 100:REM set seed and wait
Using this kind of routine was for a long time our only source
of randomness. It became annoying when a program used only the
keyboard for input but required joystick input to start it.
3) Reading the keyboard
Trying to use the above approach with the keyboard was unsuccessful
at first. The GET function would wait forever until a key was
pressed. This was partly the reason for something like a random
key as outlined in number 1. Finally, we discovered that the
last keypress was recorded in memory address 64885. We had also
figured out that we could POKE that high by resetting the POKE
limit. The following (with a few editorial remarks) was submitted
years ago by Bob Tarnowski (unknown origin):
10 POKE 16149,255:POKE 16150,255: REM reset POKE limit
15 POKE 64885,0:REM reset the last keypress
20 PRINT Press Any Key to Continue
30 a=a+1:if a>2000 then a=1
40 if PEEK(64885) then r=RND(-a):GOTO 60
50 GOTO 30
60 REM continue with your program
This routine was quite advanced at its time. Bob had figured
out that large RND(-x) numbers did not effectively create a random
seed. You can illustrate this quite easily by trying the following
program:
10 INPUT Large number please ;x
20 r=RND(-x)
30 for x=1 to 10:PRINT INT(RND(1)*10); ;:NEXT
40 PRINT: x=x+l: GOTO 20
This program will show the difference in random seeds from
something like 100000, 100001, 100002, etc. With numbers of that
size, everything appears normal. Now try 100,000,000 or any number
in that vicinity. You will see that the random numbers dont
appear to be as random any more. Loosely, that can be equated
to the INFINITY+1=INFINITY paradox; numbers of that size cant
distin-guish themselves from another that is just one bigger.
Rather than use the RND(-x) approach, why not just DEAL random
numbers off the top of the deck until the contestant yells STOP!
This is the way magicians perform random card tricks is it not?
Since we already suspect that the random number generator does
not deteriorate, there is no problem if hundreds (or thousands)
of numbers are dealt.
So now you can go back over the previous examples and substitute
the INCREMENT value with something like R=RND(1). Thus each time
a PASS is made, another random number is dealt from the top of
the deck. Dont forget to also remove the RND(-X) line in
the routines.
4) The Random Word
This one is similar to the PDL function one using multiple
keypresses. In this case, we ask the player to type his/her name.
The random seed is set, not by the letters themselves, but by
the time taken to type them in. I would suggest the use of this
type of routine only if the players name will actually
be used somewhere in the program:
100 POKE 16149,255:POKE 16150,255: REM POKE limit
110 PRINT Enter your Name:
120 POKE 64985,0
130 p=PEEK(64885):if p=0 then r=RND(1):goto 130: REM wait
140 if p=13 goto 200: REM must be end of name
150 n$=n$+CHR$(p):GOTO 120: REM build name one letter at a time
200 REM program starts here
Particularly if a players name is very long, this routine
could yield thousands of different starting positions.
5) High tech approach
Machine language programmers know that the Z-80 has a refresh
register. This register will contain a random value between 0
and 127. It is the trigger that is used to REFRESH memory. Enter
this submission, also from an unknown source:
100 for i=0 to 5:READ d:POKE 1056+i,d:NEXT
110 DATA 237,95,50,38,4,201
120 CALL 1056:x=RND(-PEEK(1062))
The first two lines POKE in a routine which gets the refresh
value and POKE it into address 1062. We then extract that value
and use it to create a -x type seed. Unfortunately, this method
only gives us 128 unique starting positions. Still, it is better
than nothing and it cannot be cheated since there is no way the
PLAYER can know what state the refresh register is in. We will
come back to this one in a moment.
But why should BASIC reset the seed and force me to do all
this extra work? NO REASON. As a matter of fact, you can POKE
zeroes into addresses 11907 to 11911 to disable the re-setting
of the random seed. I have experienced no ill effects from this
approach.
Furthermore, for those using my zero page clock, you can make
your random generator use the Hours, Minutes, Seconds, and Jiffies
as a random seed with the following:
10 DATA 42,84,0,34,62,63,42,86,0,34,64,63
20 FOR x=11907 to 11918: READ y: POKE x,y: NEXT
Add this routine to your HELLO file and you wont have
to worry about randoms any more. Remember, however, that the
clock is turned off when in GRAPHICS modes. A game that terminates
in graphics mode will restart with the same random seed if RUN
is typed from the graphics mode. Accordingly, those games should
end with a TEXT command to restart the clock.
6) Higher tech
If we go back to example number 4, we can combine a few things
we have learned. Rather than having to CALL a routine that gets
the REFRESH register, why not incorporate it into the routine
that would normally have RESET the seed every time a program
is run..... follow losely:
110 DATA 237,95 :REM this gets the REFRESH into register A
120 DATA 111 :REM this puts it in register L
130 for i=0 to 2:READ d:POKE 11907+i,d:POKE 11913+i,d:NEXT
The last line replaces the two routines which load the HL
register with a static value with one that inserts a DYNAMIC
one. This true random fix can be inserted into your HELLO file
and run only once as it is a permanent fix. This routine will
work if you are in GR HGR or TEXT mode. Its only disadvantage
is that it creates only about 256 starting random seeds. As it
is always functioning for you automatically, you could supplement
it with a simple keypress routine as in the first three examples.
Although this approach cannot be MY personal favorite
since I did NOT invent it, it is the one that I recommend the
most because of its simplicity and apparently permanent nature.
7) Using the interrupts
The following routine, with a few more remarks thrown in,
is also from an unknown source. It makes use of the Z-80 non-maskable-interrupt
(or NMI). BASIC uses the NMI as an automatic/timed interrupt
to run the FLASH mode. It does not matter whether or not FLASH
has been activated, the routine at 66(hex) or 102(decimal) is
executed 60 times per second. The following routine must be executed
in the exact order shown below as timing is critical when POKEing
around in a routine that gets executed 60 times per second...
there is no room
for error:
100 DATA 229,42,64,63,35,34,64,63,225,201
120 for x=172 to 181:read ml:poke x,ml:next
130 poke 171,0: REM let the end of the routine fall through
140 poke 11907,201: REM disable the random RESET routine
Line 120 POKES in a routine which increments two of the 4-byte
random seed number. Such a small change, effected at 60 times
per second is enough to truly randomize not only the first number
you get, but all the others that follow. This means that even
if by chance, you start at a predetermined position in the DECK,
your chances of getting the same number on the second pass are
very remote. The routine itself is POKEd in AFTER the RETURN
from the NMI routine at 102 (it ends at 171). Thus while we are
POKEing data, nothing is happening. Once
all the data is POKEd in, we open the door at line 103. The final
line disables the routine that puts those nasty static numbers
every time a program is RUN. While it is an ingenious way of
creating truly random numbers, this routine places some extra
work on the CPU. The only way you might notice a step down in
speed is when you have a very intensive loop such as in a sort
routine. Note also that this routine will have no effect on the
seed while in GR or HGR mode.
WARNING!!!! if you are using my zero page clock, this routine
is incompatible as it uses some of the same memory areas.
8) Low tech but effective
The following is my favorite in simplicity and safety -- it
is a hard one to understand let alone CHEAT. You can prelude
this one with a preset from the refresh register or by disabling
the RUN reset altogether if you want a bit more flexibility.
Basically, it remembers what key was pressed at the time the
program started. It then stalls until a different key is pressed.
Thus if you
type RUN followed by N, the program will
not start until you press a key other than N and
then press N again. It is possible to cheat this
one as well but I wont tell you how. Many of my previous
games used something along those lines so I dont want to
give away all my secrets.
100 REM ask for instructions and set random seed
110 REM can be included at any program start
120 REM Instructions or help question is a convenient way
130 REM of getting a key press
150 PRINT Do You Want Instructions (Y/N)?
160 p=PEEK(64885): REM record current keypress value
165 REM stay here until a different key is pressed
170 if p=PEEK(64885) then r=RND(1):goto 170:REM deal out a number
180 p=PEEK(64885):REM get current key
190 REM now check for y Y n N
200 REM if none of the above branch back to 170 until different
While not fool proof, this method will give the hackers a
hard time. But look at it this way: If I play a game its
no fun if it is the same all the time. Those who want to play
THE SAME GAME will find a way so dont bother with them
9) The last word
Finally, with special thanks to Bob Currie EAUG (Edmonton)
and the ANN network which published his article on random numbers,
I have another addition to the randomness of random numbers.
The following is an extract from his article:
If we scan all of the accessible memory locations in
the ADAM, we find that there are three addresses at which one
does not always get the same number. At 17003(dec) we get a zero
seven times out of ten and a one three times out of ten. At 65220(dec)
we get a 4 seven times out of ten and a 140 three times out of
ten. At 17011(dec) we get the numbers from 1 to 12 with a pretty
much even chance of getting any one of them.
Bob goes on to say that you can add the 3 values at these
addresses to form the basis for a RND(-x) routine. I have come
upon a more ingenious method. Firstly, adding the numbers together
gives results ranging from 5 to 17 or 141 to 153 or 26 unique
combinations. If we instead use the value at 17003 as a multiplier,
we can increase the range to 48 unique combinations:
r=PEEK(65220)+PEEK(17011) + 12*PEEK(17003)
The first half of the equation yields 5 to 16 or 141 to 152.
Adding 12 times the value at 17003 expands this range to 5 to
28 or 141 to 164. But now what do we do with this number? Well
we could always use a RND(-x) function (as Bob suggests), but
that would limit us to ONLY 48 unique combinations. Just POKE
it into one of the RANDOM SEED values prior to scanning the keyboard.
This will effect some change in the seed number while not always
having the same effect:
100 REM ask for instructions and set random seed
110 REM can be included at any program start
120 REM Instructions or help question is a convenient way
130 REM of getting a key press
140 REM
145 POKE 16191,PEEK(65220)+PEEK(17011)+12*PEEK(17003)
150 PRINT Do You Want Instructions (Y/N)?
160 p=PEEK(64885): REM record current keypress value
165 REM stay here until a different key is pressed
170 if p=PEEK(64885) then r=RND(1):goto 170
180 p=PEEK(64885):REM get current key
190 REM now check for y Y n N
200 REM if none of the above branch back to 170 until different
Guy Cousineau
1059 Hindley Street
OTTAWA Canada
K2B 5L9
Back to Top
|