LLX > Neil Parker > Apple II > New RND

MSWSRAND: A Modern Random Number Generator for Applesoft

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).

Usage

BRUN MSWSRAND

This 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.

USR

 USR (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:

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.

Internal Variables

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):

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

Downloads

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.

Source Code

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