LLX > Neil Parker > Apple II > New RND
The inadequacies of Applesoft's random number generator, RND, have long been noted. Several replacements with better properties have
been published—these are mostly linear
congruential generators, which, though they have well-known flaws, are
at least a lot better than RND, and at the time (the mid
1980's) few reasonable alternatives were available.
The state of the art in random number generators has advanced a great deal since then, and there are now a number of high-quality random number generators known which pass even the strongest statistical tests. But alas, most of these are designed to run efficiently on modern 32- and 64-bit processors, and implementing them on an 8-bit CPU often requires an uncomfortably large amount of code.
While browsing Wikipedia's list of random number generators, my eye was caught by Bernard Widynski's Middle-Square Weyl Sequence random number generator, which gets good-quality random numbers out an attractively simple fragment of C code:
uint64_t x = 0, w = 0, s = 0xb5ad4eceda1ce2a9;
inline static uint32_t msws32() {
x *= x; x += (w += s); return x = (x>>32) | (x<<32);
}
On a 64-bit x86 CPU, this can be compiled to just four assembly-language
instructions. On the 6502, of course, it takes quite a bit more code than
that, but the resulting routine isn't a whole lot bigger than the linear
congruential RND-replacements from the 1980's.
So just for the fun of it, I implemented it as an Applesoft
USR function. While I doubt that anybody today is still
doing serious Monte Carlo simulations on an Apple II, nevertheless, with the
software downloadable from this page, modern high-quality random numbers are
available to Applesoft. The price for this (beyond the memory consumed by
its code) is speed—it's a bit slower than RND.
Despite what the author claims in the paper, this generator is not suitable for cryptographic use.
MSWSRAND and its source code are freeware, and may be redistributed in accordance with the terms in the included LICENSE file (a 3-clause BSD license).
BRUN MSWSRANDThis command installs the random number generator. It consumes 512 bytes
above the file buffers under ProDOS, or 465 bytes above HIMEM under DOS 3.3.
BRUNning it again consumes more memory, so provisions have been
made for determining whether or not MSWSRAND has already been installed.
MSWSRAND initially loads at memory location 8192 ($2000), and then moves itself to high memory. It has been designed to coexist peacefully with other high-memory modules, and unlike some such modules, it need not be the first one loaded.
& "MSWSRAND"& "MSWSRAND"
This command does nothing. Its purpose is to help your program figure out whether MSWSRAND is already installed. It's intended primarily for ProDOS (though it's available in DOS 3.3 too), where it might be used like this:
10 ON PEEK (116) > 148 OR PEEK (1015) = 190 GOTO 40: ONERR GOTO 30 20 & "MSWSRAND": POKE 216,0: GOTO 50 30 CALL 62248: POKE 216,0 40 PRINT CHR$ (4);"BRUN MSWSRAND" 50 REM The rest of your program starts here
Line 10 tests whether HIMEM is too high to accommodate MSWSRAND, or whether
the & hook hasn't been used yet. Line 20 uses &
"MSWSRAND" to test whether MSWSRAND is already installed.
MSWSRAND's above-HIMEM hiding place under DOS 3.3 isn't safe from
FP, INT, or MAXFILES, so it's probably
best for a DOS 3.3 progam to just throw away any previously-loaded copy
and load it anew. For example,
10 POKE 1013,76: POKE 1014,88: POKE 1015,255: REM Reset & vector 20 PRINT CHR$ (4);"MAXFILES 3": REM Throw away above-HIMEM buffer 30 PRINT CHR$ (4);"BRUN MSWSRAND" 40 REM The rest of your program starts here
But that causes the generator to start over again using its initial seed. If you need the random number sequence the second time you run your program to continue from where the first run left off, then you need avoid reloading MSWSRAND, similar to the ProDOS code:
10 ON PEEK (1015) = 255 GOTO 40: ONERR GOTO 30 20 & "MSWSRAND": POKE 216,0: GOTO 50 30 CALL 62248: POKE 216,0 40 PRINT CHR$ (4);"BRUN MSWSRAND" 50 REM The rest of your program starts here
Beware, though—if FP or MAXFILES has been
used between the two runs, that could end up calling into memory that will
soon be overwritten, or may already have been overwritten, by
string variables.
Because BRUN MSWSRAND moves HIMEM, a DOS 3.3 program should
avoid using any string variables until after MSWSRAND is installed.
USRUSR (numeric-expression)
Return a new random number. The numeric-expression is
similar to Applesoft's RND, but not quite the same. Let
X be the numeric-expression—then:
X is 1 (or any other number greater than 0
but less than 2), a random number greater or equal to zero and less than
1 is returned.X is 2 or more, USR (X) is the same as
INT ( USR (1) * X), returning a number between 0 and
X-1 inclusive.USR (0) returns the previous random number again, as a
number greater than or equal to 0 and less than 1.X is negative, a new random number is returned, but
as an integer between 0 and 232-1 inclusive.
(Beware—many of these numbers are too big for Applesoft to print in
integer form.)Like all software random number generators, USR has a
finite period, after which it starts returning the same numbers in the
same order that it already returned before. USR's period is
not known, but is guaranteed to be at least 264 ≈ 1.8
× 1019.
The numbers returned from USR (1) and USR (-1)
are uniformly distributed and equally spaced. But numbers returned from
USR (X) when X >= 2 are not quite uniformely
distributed unless X is a power of 2—some numbers are
returned slightly more often than others. The effect is probably
negligible for most uses, but some uses cannot tolerate even that slight
non-uniformity—if you need perfect uniformity, pass a power of 2
larger than your X, and repeat until you get a number in the
right range. For example, here's a one-liner that rolls a 6-sided die with
perfect uniformity:
FOR ZZ = 0 TO 1:R = USR (8):ZZ = R < 6: NEXT
Or a larger power of 2 can be used to reduce the number of repeats:
FOR ZZ = 0 TO 1:R = USR (32768):ZZ = R < 32766: NEXT :R = R - INT (R / 6) * 6
Or if you really want to minimize the number of repeats, use a negative argument, like this:
FOR ZZ = 0 TO 1:R = USR ( - 1) - 4:ZZ = R > = 0: NEXT :R = R - INT (R / 6) * 6
which works because 232 MOD 6 = 4.
Unlike RND, this generator cannot be reseeded by
passing a negative number to USR. Reseeding the generator
requires more bits that can be passed in USR's argument,
so a separate command has been provided for reseeding.
& RND& RND numeric-expression[,numeric-expression]
This statement reseeds the random number generator. The two
numeric-expressions should be numbers between 0 and
232-1 inclusive. They are truncated to 32-bit integers, and
concatenated to give a 64-bit number which becomes the new seed for the
generator. If the second expression is omitted, it's treated as if it were
the same as the first expression.
In terms of the C routine shown at the top of the page, &
RND A,B sets both the x and w variables
to A + 232 * B.
The constant s is unchanged,
not because it shouldn't be changed, but because choosing good values for
it isn't quite trivial—it must be an odd number, and should have
equal or nearly-equal numbers of 0 and 1 bits, well-scattered through the
number. See below under Internal Variables to
learn how to find s in memory if you want to
POKE a new value into it.
If you want a truly random seed, finding 64 truly random bits for it will be a challenge. Asking the user to type something at the keyboard, and then looking at the keyboard input counter, can get you 16 random bits...if you need more than that, and can contrive an excuse to ask the user for input more than once, you can do something like this:
100 INPUT "What is your name? ";N$:A = PEEK (78) + 256 * PEEK (79) 110 INPUT "What is your quest? ";Q$:B = PEEK (78) + 256 * PEEK (79) 120 & RND A,B
A point about reseeding to be aware of: Here are the first five values
produced after & RND 0, and the first five values
produced after & RND 1:
& RND 0 |
& RND 1 |
|---|---|
| .709675718 | .709675718 |
| .872297785 | .823020196 |
| .0958417279 | .356044445 |
| .776805687 | .759970132 |
| .0665112 | .770461344 |
Note the resemblence in the first couple of rows. This can be expected
to happen whenever two seeds differ by only a few bits—it takes a
few iterations of USR to thoroughly mix the difference
between the seeds. If this might be a problem for your program, try
throwing away the first few values from USR after reseeding,
for example,
& RND A,B: FOR ZZ = 1 to 5:R = USR (1): NEXT
& RND 0,0 (or just & RND 0) will
cause USR to duplicate the output of the C routine. When
MSWSRAND is first installed, it behaves as if it were seeded by
& RND 0,0.
& USR& USR
This command points the USR vector at MSWSRAND's
USR function handler. This is normally done by BRUN
MSWSRAND, but since the USR vector can be used by only
one handler at a time, this provides a way to reconnect MSWSRAND's
USR handler after something else has usurped it.
& &If other & handlers were installed before MSWSRAND, they
remain available, and can be invoked as if MSWSRAND were not present, unless
the prior command happens to have the same name as a MSWSRAND command. In
that case, putting a second & in front of the command
passes control to the prior handler.
The same convenience isn't available for USR, because it's
not clear how to determine whether a USR call should be passed
to another handler. At least with the & USR command, it's
possible to reconnect MSWSRAND's handler if something else replaces it.
For users who want to PEEK and POKE MSWSRAND's
internal variables, a pointer to them is available. After using
USR or any of the & statements (except
& &), memory locations 6 and 7 point to the start of
the buffer where MSWSRAND keeps the internal state of the generator.
After saying (for example), X = USR (0):P = PEEK (6) + 256 *
PEEK (7):
P
through P+7.P+8 through P+15.P+16 through
P+23.Each number is in big-endian order, with the highest byte in the lowest address and the lowest byte in the highest address.
You only need to compute the address P once in your
program—after MSWSRAND is loaded, the buffer won't move while your
program is running.
Here's how you might set a new value for s:
100 R = USR (0):P = PEEK (6) + 256 * PEEK (7) 110 FOR A = P + 16 TO P + 23: READ B: POKE A,B: NEXT 120 DATA 39,140,90,77,132,25,254,107: REM $278C5A4D8419FE6B
In addition to MSWSRAND and its source code (Merlin Pro assembler), these
contain the famous test that plots random points on the hi-res
screen...if you let it run long enough, it eventually fills the screen.
Applesoft's RND fails this test miserably, getting stuck in a
repeating sequence that plots no more new points long before the screen is
full.
Because this routine might be of interest to programmers not using Applesoft, or on 6502 platforms other than the Apple II, the core of the generator, without the Applesoft interface, is shown below.
Obviously this code has been economized for space rather than speed. Several opportunities for unrolling loops will be apparent. Also, the multiplication routine computes, and then throws away, the upper 8 bytes of the product—an earlier version of this code tried to avoid doing most of that unnecessary work, but that made the code bigger and harder to debug (I kept the routine shown below because it actually worked). No 65C02 instructions are used, because this was written for a platform where a 65C02 might not be available.
The labels for the state variables are spelled XX,
WW, and SS, because some assemblers don't accept
X or S as labels. All three variables are in
big-endian order, because that makes many of the loops slightly
more efficient that they would be the other way around.
Seed the generator by putting your seed values at XX,
WW, and SS. After calling the routine, the
result is found at XX+4 through XX+7.
Only two assembler pseudo-ops are used: =, which assigns
a value to a label, and HEX, which stores a sequence of bytes,
written as hexadecimal digits, in memory.
* Widynski's Middle-Square Weyl Sequence
* random number generator
*
* Translated to 6502 by Neil Parker
* February 17, 2025
* License for this translation: BSD
*
* These two temporary vars could go anywhere in memory:
COUNT = 6
MBYTE = 7
* TEMP needs to be 0 before starting the multiply
MSWS32 LDX #7
LDA #0
C1 STA TEMP,X
DEX
BPL C1
* Get the 8 lo bytes of XX*XX into TEMP. The multiply starts with
* the hi bit because that way only the 8 lo bytes of the result
* need to be stored.
LDY #0
MUL1 LDA XX,Y
BEQ MZERO ;If 0, fast shift
EOR #$FF ;(This saves a CLC in a nested loop)
STA MBYTE
LDA #1
STA COUNT
ML1 LDX #7 ;TEMP *= 2
CLC
MROT1 ROL TEMP,X
DEX
BPL MROT1
ASL MBYTE ;Get inverted hi bit
BCS MNEXT ;If 1, skip
LDX #7 ;If 0, TEMP += XX
MADD1 LDA XX,X
ADC TEMP,X
STA TEMP,X
DEX
BPL MADD1
MNEXT ASL COUNT
BCC ML1
BCS MNXB ;(always taken)
MZERO LDX #0
MZERO1 LDA TEMP+1,X
STA TEMP,X
INX
CPX #8
BNE MZERO1
MNXB INY
CPY #8
BNE MUL1
* The Weyl sequence: Add SS to WW.
LDX #7
CLC
WEYL1 LDA WW,X
ADC SS,X
STA WW,X
DEX
BPL WEYL1
* TEMP+WW becomes the new XX.
LDX #7
CLC
WEYL2 LDA TEMP,X
ADC WW,X
STA XX,X
DEX
BPL WEYL2
* Swap halves of XX, leaving the result in XX+4..XX+7.
LDX #3
SWAP1 LDY XX,X
LDA XX+4,X
STA XX,X
TYA
STA XX+4,X
DEX
BPL SWAP1
RTS
* Vars in BIG ENDIAN order
* Could be put in page 0, if you have enough free space there
XX HEX 0000000000000000
WW HEX 0000000000000000
SS HEX B5AD4ECEDA1CE2A9
TEMP HEX 0000000000000000
HEX 00 ;Extra 0 for MZERO
LLX > Neil Parker > Apple II > New RND
Original: June 25, 2025