Greg:

Spent the night trying to reverse engineer Microsoft's libraries to figure
out how their random number algorithm ticks. After much struggling with crappy
shareware (and crappy pirated commercial) disassemblers, nothing beat the
simplicity of CodeView for DOS. :)

These are my notes. Thought you'd get a kick out of it.

First, recall I had discovered that VBDOS will output assembly code. So, I
gave it this do-nothing program:

RANDOMIZE
PRINT RND


...which produced this assembly listing...

Offset  Data    Source Line      Microsoft (R) Visual Basic (TM) for MS-DOS (R)

 0030   0006    
 0030   0006    RANDOMIZE
 0030   0006    PRINT RND
 0030   0006    
 0030   0006    
 0030    **        I00002:   call    B$RNZ0
 0035    **                  call    B$RND0
 003A    **                  mov     bx,ax
 003C    **                  push    02h[bx]
 003F    **                  push    [bx]
 0041    **                  call    B$PER4
 0046    **                  call    B$CENP
 004B   0006    

42347 Bytes Available
42197 Bytes Free

    0 Warning Error(s)
    0 Severe  Error(s)

To break that down...

The BASIC API appears to follow this name-mangling scheme: "B$" + a four-digit
string. I guess that's for streamlining the compiler or something. The names
therefore don't always make sense, or at least aren't very descriptive, as
you'll see.

Presumably "B$RNZ0" is the function name for RANDOMIZE without arguments, and
"B$RND0" is RND without arguments (both have integer-accepting equivalents).
Functions return data in register AX, which is moved to BX. BX serves as a
pointer to the SINGLE value that RND returned. This value is pushed onto the
stack (two bytes at a time--we are in 16-bit real mode--two PUSHes, total),
for a call to "B$PER4", which is presumably an incarnation of PRINT. (I wonder
if there are separate PRINT functions for each data type...) "B$CENP" is
probably the low-level equivalent of exit() for BASIC. "ENP" is probably "ENd
Program."

Using LIB.EXE on VBDCL10E.LIB (VBDOS support library), showed that all the
random functions were placed in one object file, "random", so hopefully this
means all the code would be clumped together in the final EXE. (Turns out I
was right about that.)

(From LIB.EXE outputted listings...)

Library name
  |
  |
  V
random            Offset: 00073be0H  Code and data size: a6H
  B$RND0            B$RND1            B$RNZ0            B$RNZP
    ^                 ^                 ^                 ^
    |                 |                 |                 |
    |                 |                 |                 |
  function name     function name     function name     function name

...So I compiled my dinky little program with CodeView support (which VBDOS
has an option for) loaded it into CodeView, set a breakpoint on B$RND0(),
which I had identified earlier as the RND function without arguments, (thanks
to the assembly output from BC.EXE...) and found everything I wanted clumped
together. CodeView even lets you dump the disassembly to a file.

Some notes on VBDOS calling conventions:
According to the VBDOS Professional Features manual, "Basic always uses a FAR
call," (page 51) which means that any given function pointer is FOUR bytes
(segment + offset). They also pass an extra parameter on the stack to
functions that plan on returning anything other than 2 or 4 byte integers
(INTEGER and LONG). This extra parameter is an offset in memory where the
return value is copied by the called function. This offset is also returned
in AX, for convenience. Yikes. Apparently only BASIC code needs to follow
this, and internal routines like RND do not. I guess.

If a BASIC function's argument is supposed to be an INTEGER, and you pass it
a SINGLE or DOUBLE, the floating point values may round (2.49999 gives you 2,
but 2.5 gives you 3)...be careful. This is different than C, which truncates
the fractional segment of the number to make it whole. Further research
shows that B$RND1 takes a SINGLE argument (four bytes). B$RNZ0 takes a
DOUBLE argument (eight bytes). B$RNZP and B$RND0 take no arguments.

RANDOMIZE x does NOT set the same seed everytime; the randomize function
does NOT completely reset everything, but a specific seed will alter the last
one. You are only guaranteed the same sequence of numbers with the same
sequence of RANDOMIZE and RND calls from the start of execution.

The exeception seems to be RND(x), where (x < 0). Apparently, this overwrites
the random seed. This has been confirmed in Qbasic tests to make sure I'm not
hallucinating. The number returned by a negative argument will be identical
every time, and resets the random seed. RND(-5) will always return
the same thing, which will be different from RND(-4), etc...furthermore,
after you call RND(-5), RND(1) : RND(2) : RND(n) will return a definite
duplicatable pattern with every execution, regardless of what you did with
RANDOMIZE. I would guess this is a holdover from very primitive BASICs, where
RND() would be used for what modern BASICs separates into RANDOMIZE and RND.

Some other identifiers of importance: B$TIMR == TIMER(), as you'll see in
B$RNZ0 (RANDOMIZE without arguments), since it gets it's seed from the
timer...quite literally from the TIMER function. b$RndVar is where VBDOS
stores it's random number seed, and every call to the RNG (random number
generator), and RANDOMIZE modifies it. RND() with a negative argument
downright changes it. b$RndVar seems to initialize at program startup to
0x50000. The program also makes use of an identifier called b$EightSpaces,
which I suspect is a scratch buffer, but I'm not sure yet.

Here it is. CodeView adds a few comments (and somewhat human-readable
identifiers, but I've even changed some of those), but most of them are
my own. In fact, this looks very little like the original CodeView output
at this point.

b$EightSpaces is an identifier referenced in the following code. It's a
pointer to just that: eight ASCII 32 chars (' '). What lies before it is
beyond me, but this code accesses it via [b$EightSpaces-000C], etc...
I suspect it might be for the sake of entropy; we're reading whatever might
happen to be stored in those bits of memory, regardless of whatever else
might have written to it during execution of other BASIC functions. It might
even be a temporary scratch area used by the code generated by the BASIC
compiler, so it gets overwritten everytime a VARIANT is converted, for
example. In short, it changes a lot, and at unpredictable (within the scope
of the random number generator) times. This, however will not result in a
psuedorandom number. At least, it does not guarantee a reproducable sequence
unless the entire program and data set is identical between each run.
And while that could be considered good for something like an encryption
program, in general BASIC, we want a reproducable sequence given the same
random seed and same series of calls to RND(). So we will take it for granted
right now that we do not know what is in [b$EightSpaces-0009], but it will be
the same between runs, and will never change during a run. I hope.

Apparently, the B$RND1 function is expecting some extra krapola on the stack,
since it has to expect a CALL into the middle of itself from B$RND0, so it
will even CALL into itself for generating the next or last number in the
sequence. Hence, the entry points: rndNextEntry, and rndLastEntry.

If I haven't mentioned it yet, it's worth noting that all this was clearly
hand-coded in assembly in the first place. B$RNZ0 doesn't set up the
frame pointer, so it probably wasn't written in a higher-level language
like C, which would have done so by default. Plus all the CALLing into
oneself (another big no-no in C: inner functions) in RND() without a paranoid
regard for the stack is also demonstrative of this fact. This is fine; I
bring this up to point out how much easier our effort was made by this.
Assembly written by humans must have some trace of human-readable logic,
which, being human ourselves, makes it easier to follow. If this was written
in C originally, we could still disassemble and trace the code, but there'd be
a lot more, "What the hell does THIS mean?" as we try to grok compiler-
generated code optimizations. Just wanted to point that out.

Anyhow, the code:

        ;
        ; RND with no arguments is identical to RND(1) (or any value > 0).
        ;  In fact, it calls right into the middle of the RND-with-argument
        ;  version of the function.
        ;

B$RND0:   ; RND with NO arguments.
3563:0000 55             PUSH      BP                   ; Save base pointer.
3563:0001 8BEC           MOV       BP,SP                ; setup new base.
3563:0003 E82D00         CALL      rndNextEntry         ; call into B$RND1
3563:0006 5D             POP       BP                   ; restore base pointer.
3563:0007 CB             RETF                           ; return far.


        ;
        ; Qbasic help file on RND tells this about param's value:
        ;        Less than 0                    The same number for any n#
        ;        Greater than 0 (or omitted)    The next random number
        ;        0                              The last number generated
        ;
        ; The stack after frame setup:      bp+8   1/2 of parameter (SINGLE).
        ;                                   bp+6   1/2 of parameter (SINGLE).
        ;                                   bp+4   return address segment.
        ;                                   bp+2   return address offset.
        ;                                   bp+0   prev base pointer

B$RND1:   ; RND with integer argument (the other RND just calls into here...)
3563:0008 55          PUSH      BP                   ; Save base pointer.
3563:0009 8BEC        MOV       BP,SP                ; setup new base.
3563:000B 8B4608      MOV       AX,WORD PTR [BP+08]  ; put param into AX.
3563:000E 0BC0        OR        AX,AX                ; is it 0?
3563:0010 741A        JZ        rndLastGen           ; yes? goto rndlastGen.
3563:0012 7913        JNS       rndNextGen           ; if !neg, get next num.

                                                     ; if here, param is < 0.
3563:0014 8B5606      MOV       DX,WORD PTR [BP+06]  ; other half of arg->dx.
3563:0017 02D4        ADD       DL,AH                ; add high byte of AH->DL
3563:0019 80D600      ADC       DH,00                ; add w/ carry 0 -> DH.
3563:001C 1400        ADC       AL,00                ; add w/ carry 0 -> AL.
3563:001E 32E4        XOR       AH,AH                ; zero AH.
3563:0020 89168D03    MOV       WORD PTR [b$RndVar],DX       ;  fill in...
3563:0024 A38F03      MOV       WORD PTR [b$RndVar+0002],AX  ; ... new seed.

        ; positive and negative params to RND(x), and RND(void) fall here.

rndNextGen:     ; Get next random number.
3563:0027 E80900      CALL      rndNextEntry         ; Call entry point.    
3563:002A EB03        JMP       rndGetOut            ; jump to exit point.


rndLastGen:     ; Get last random number.
3563:002C E83600      CALL      rndLastEntry         ; Call entry point.    
                                                     ; Falls into exit point.

rndGetOut:      ; exit point for function.
3563:002F 5D          POP       BP                   ; cleanup...
3563:0030 CA0400      RETF      0004                 ;  ...and return far.


        ;
        ; Here's the dirt on rndNextEntry: We PUSH DI onto the stack, since
        ;  we're gonna need to use the register. Then, we move the bottom
        ;  word of the random seed into AX, and whatever is in the word
        ;  at [b$EightSpaces-000C] into CX, and multiply the two. The bottom
        ;  half of the 4-byte result is moved to DI, and the top half to BX.
        ;  The other word of the random seed is then placed in AX, and
        ;  multiplied against CX (which still has the word from
        ;  [b$EightSpaces-000C] in it. The bottom half of this product is
        ;  then added to BX (the top half of the previous product).
        ;
        ; Still with me? We're about to execute the code at 3563:0047.
        ;
        ; That pesky word at [b$EightSpaces-000C] is then placed back in AX.
        ;  It is multiplied against the bottom word of the random seed.
        ;  Bottom word of that product is added to BX (which we added the
        ;  last multiplication to, as well.)
        ;
        ; The word at [b$EightSpaces-0008] is then added to DI (which was
        ;  previously holding the bottom half of the first multiplication.
        ;
        ; The byte located at [b$EightSpaces-0008] is then added (with carry)
        ;  to the low byte of BX (or BL for short). BH is set to zero.
        ;  This is probably to make sure the division by 0x1000000 later on
        ;  always yields a number between 0.0 and 1.0 ...
        ;
        ; DI is moved to DX for dumping to a memory address. From here, that
        ;  value becomes the bottom word of the random seed. The top word is
        ;  overwritten with the value of BX.
        ;
        ; DI is POPped back off the stack, restoring its original value.
        ;
        ; No concern is given to overflow; these are just collections of
        ;  bits, not actual numbers, right now.
        ;
        ; We drop into the rndLastEntry code from rndNextEntry. This is
        ;  fine; we just set up the new seed, now its time to generate
        ;  the next number in the sequence with it. Obviously if we keep
        ;  calling rndLastEntry by itself, it will never update the seed,
        ;  so we'll keep getting the same number.
        ;

rndNextEntry:   ; Entry to next number in sequence generation algorithm...
3563:0033 57          PUSH      DI                ; Save DI; we need the reg.
3563:0034 A18D03      MOV       AX,WORD PTR [b$RndVar]
3563:0037 8B0E4606    MOV       CX,WORD PTR [b$EightSpaces-000C]
3563:003B F7E1        MUL       CX  
3563:003D 97          XCHG      AX,DI  
3563:003E 8BDA        MOV       BX,DX  
3563:0040 A18F03      MOV       AX,WORD PTR [b$RndVar+0002]
3563:0043 F7E1        MUL       CX  
3563:0045 03D8        ADD       BX,AX  
3563:0047 A14606      MOV       AX,WORD PTR [b$EightSpaces-000C]
3563:004A F7268D03    MUL       WORD PTR [b$RndVar]
3563:004E 03D8        ADD       BX,AX  
3563:0050 033E4A06    ADD       DI,WORD PTR [b$EightSpaces-0008]
3563:0054 121E4A06    ADC       BL,BYTE PTR [b$EightSpaces-0008]
3563:0058 32FF        XOR       BH,BH  
3563:005A 8BD7        MOV       DX,DI  
3563:005C 89168D03    MOV       WORD PTR [b$RndVar],DX
3563:0060 891E8F03    MOV       WORD PTR [b$RndVar+0002],BX
3563:0064 5F          POP       DI  


        ;
        ; rndLastEntry isn't as hard as it looks. All these interrupt calls
        ;  were actually a clever hack. The runtime library installs
        ;  handlers on a few interrupts, and then the programmer need not
        ;  concern himself with whether a math coprocessor is installed
        ;  or not; if there isn't, the interrupt handler will do
        ;  coprocessor emulation for whatever the programmer intended. The
        ;  extra bytes after the opcode are the desired operation. If a
        ;  coprocessor is installed, it is used, but the interrupt handler
        ;  will also PATCH THE CALLING CODE with the correct coprocessor
        ;  opcodes, so the interrupt only gets called the first time through
        ;  each operation. Brilliant.
        ;
        ; CodeView was nice enough to auto add a comment as to what the
        ;  interrupt will patch the code to be. Of course, just putting a
        ;  breakpoint on B$RND1 would have shown us the patched code the
        ;  second time around on a coprocessor-enabled machine.
        ;
        ; Anyhow, the short of it is this: Put the random seed on the
        ;  floating point stack, divide it by the DWORD located at
        ;  b$EightSpaces-0004 and store the DWORD (BASIC type SINGLE)
        ;  result in 0x144.
        ;
        ; The data in b$EightSpaces-0004 seems to always be 0x4B800000,
        ;  which is a number in floating-point format. Since floating
        ;  point format interprets a collection of bits differently than
        ;  integer format, once this is on the coprocessor stack, it
        ;  actually is 16777216.0 (or 0x1000000; it might as well be
        ;  an integer, but the coprocessor is probably faster and easier
        ;  to code. This calculation is probably to make sure the next number
        ;  in the sequence is always between 0.0 and 1.0.
        ;
        ; Remember that functions returning a floating point number return the
        ;  address in memory where you can find the real retval. 0x144
        ;  is where the retVal is stored. Returns from functions (in this
        ;  case, the address of the real retval), must be in AX, so that
        ;  explains the XCHG AX,BX ... then an FWAIT, to let the coprocessor
        ;  catch up (if needed). In vbSlacker, we just return the SINGLE
        ;  directly.
        ;

rndLastEntry:   ; Entry to last number in sequence generation algorithm...
3563:0065 CD37068D03  INT       37 ;FILD     DWORD PTR [b$RndVar]
3563:006A CD34364E06  INT       34 ;FDIV     DWORD PTR [b$EightSpaces-0004]
3563:006F BB4401      MOV       BX,0144  
3563:0072 CD351F      INT       35 ;FSTP     DWORD PTR [BX] 
3563:0075 93          XCHG      AX,BX  
3563:0076 CD3D        INT       3D ;fwait 
3563:0078 C3          RET         


        ;
        ; Qbasic help file on RANDOMIZE tells this about param's value:
        ;
        ;     Any type of numeric expression; used to initialize the
        ;     random-number generator; if omitted, the value of the
        ;      TIMER function is used.
        ;
        ; Where the comment is "stick in seed." below, the random seed is only
        ;  partially overwritten. We stick 16 new bits into the middle of a
        ;  block of 32.
        ;
        ; What happens is this: The argument is 8 bytes long, and we don't
        ;  care that it's a DOUBLE so much as it is a collection of 64 bits.
        ;  The bottom half of the parameter is thrown away, and the third word
        ;  (16 bits) is XOR'd with the fourth. The new word produced by this
        ;  XOR operation is then placed into the seed variable, at a one byte
        ;  offset, which gives us a completely new set of bits, and is more or
        ;  less suitable for reseeding the random number generator. The new
        ;  seed then can be viewed as a SINGLE number again, instead of as a
        ;  collection of 32 bits.
        ;
        ; Note that since we leave the first and last bytes of the seed
        ;  "as-is", this function is not suitable for a complete resetting of
        ;  the random number sequence. You need to use RND(x) where x < 0, or
        ;  restart your program to get a consistent sequence from a value
        ;  passed to this function. Yuck.
        ;
        ; The stack after frame setup:      bp+0C  1/4 of parameter (DOUBLE).
        ;                                   bp+0A  1/4 of parameter (DOUBLE).
        ;                                   bp+08  1/4 of parameter (DOUBLE).
        ;                                   bp+06  1/4 of parameter (DOUBLE).
        ;                                   bp+04  return address segment.
        ;                                   bp+02  return address offset.
        ;                                   bp+00  prev base pointer
        ;
    
B$RNZP:   ; RANDOMIZE with specific seed.
3563:0079 55          PUSH      BP                          ; Save base ptr.
3563:007A 8BEC        MOV       BP,SP                       ; Set new base.
3563:007C 8D5E0A      LEA       BX,WORD PTR [BP+0A]         ; bytes 5&6 of arg.
3563:007F 8B07        MOV       AX,WORD PTR [BX]            ; put them in AX.
3563:0081 334702      XOR       AX,WORD PTR [BX+02]         ; XOR with 7&8.
3563:0084 A38E03      MOV       WORD PTR [b$RndVar+0001],AX ; stick in seed.
3563:0087 5D          POP       BP                          ; Restore base ptr.
3563:0088 CA0800      RETF      0008                        ; Return far.


        ;
        ; RANDOMIZE without an argument is more or less just shorthand for
        ;  RANDOMIZE TIMER, and makes your code a few opcodes smaller, since
        ;  it does the work inside the runtime library. So the code below
        ;  does just that; CALLs B$TIMR (BASIC's TIMER function, not the
        ;  statement version), takes the retval, and gives it the same XOR
        ;  treatment as above. Note that TIMER returns a SINGLE, even though
        ;  RANDOMIZE-with-an-argument needs a DOUBLE. This would explain why
        ;  four bytes of the argument you pass to RANDOMIZE are thrown away;
        ;  for compatibility with this (and probably legacy BASIC) code.
        ;
        ; The stack after frame setup:      bp+02  return address segment.
        ; (there is no setup, really.)      bp+00  return address offset.
        ;

B$RNZ0:   ; RANDOMIZE without specific seed.
3563:008B 9AD0066335  CALL      B$TIMR                      ; Call TIMER().
3563:0090 93          XCHG      AX,BX                       ; retval offset->BX
3563:0091 8B07        MOV       AX,WORD PTR [BX]            ; Get 1st word...
3563:0093 334702      XOR       AX,WORD PTR [BX+02]         ;  ...XOR with 2nd.
3563:0096 A38E03      MOV       WORD PTR [b$RndVar+0001],AX ; stick in seed.
3563:0099 CB          RETF                                  ; Return far.

; That's all the ASM we should need...



So at this point, we have the following:

/*
 * This probably won't compile. This is just a cheap,
 *  hand-hacked 80x86-Assembly-to-C writeup.
 */

#include "StdBasic.h"

typedef struct
{
    BYTE h;
    BYTE l;
} ByteRegisters;

typedef union
{
    WORD X;
    ByteRegisters H;
} RegUnion;

DWORD rndVar = 0x50000;

RegUnion AX;
RegUnion BX;
RegUnion CX;
RegUnion DX;
RegUnion DI;

WORD eightSpaces000C = ?;
WORD eightSpaces0008 = ?;

void *rnd0(void)
{
        /*
         * I don't know if this is even valid syntax. The
         *  below code is supposed to cast the address of the
         *  rndNextEntry line label to a void-returning function
         *  that takes void arguments. This will be modified in our
         *  next revision to be more C-like, so just take my word
         *  that this is what the following line means.  :)
         */
    (void (*)(void)) &&rndNextEntry();
} /* rnd0 */


__single *rnd1(__single operation)
{
    union
    {
        DWORD dword;
        WORD  words[2];
    } seedUnion;
    seedUnion.dword = __rndVar;

    union
    {
        DWORD dword;
        WORD  words[2];
    } AXDX;

    BYTE tmp;

    AX.X = operation;

    if (AX.X == 0)
        goto rndLastGen;
    else if (AX.X > 0)
        goto rndNextGen;

        /* AX.X < 0 ... completely overwrite the seed. */

    DX.X = seedUnion.words[0];

        /* Add with carry's a bitch in C... */
    if ( (WORD) ((WORD) DX.H.l + (WORD) AX.H.h) > (WORD) 255 )
    {
        if ( (WORD) ((WORD) (DX.H.h) + 1) > (WORD) 255 )
            AX.H.l += 1;
        DX.H.h += 1;
    } /* if */

    DX.H.l += AX.H.h;
    AX.H.h = 0;

    seedUnion.words[0] = DX.X;
    seedUnion.words[1] = AX.X;
    __rndVar = seedUnion.dword;

rndNextGen:
    return((void (*)(void)) &&rndNextEntry());

rndLastGen:
    return((void (*)(void)) &&rndLastEntry());

rndNextEntry:
    AX.X = seedUnion.words[0];
    CX.X = eightSpaces000C;
    AXDX.dword = AX.X * CX.X;
    DI = AXDX.words[0];
    BX.X = AXDX.words[1];
    AX.X = seedUnion.words[1];
    AXDX.dword = AX.X * CX.X;
    BX.X += AXDX.words[0];
    AX.X = eightSpaces000C;
    AXDX.dword = AX.X * seedUnion.words[0];
    BX.X += AXDX.words[0];
    DI += eightSpaces0008;
    tmp = *((char *) &eightSpaces0008);    // convenience tmp.
    __addWithCarry(BX.h.l, tmp);
    BX.h.h = 0;
    seedUnion.words[0] = DI;
    seedUnion.words[1] = BX.X;
    // fall into rndLastEntry...

rndLastEntry:
    union
    {
        DWORD dword;
        __single single;
    } rndvarunion;

    rndvarunion.dword = rndVar;
    return(rndvarunion.single / (__single) eightSpaces0004);
} /* rnd1 */


void randomizeP(__double seed)
{
        /*
         * This points to the 2nd byte in the random seed variable.
         *  Writing an __integer (16-bit word) here leaves one byte
         *  on each side of the random seed unmolested.
         */
    __integer *globalSeed = (__integer *) ( ((char *) &rndVar) + 1 );

    union
    {
        __double double;
        __integer integers[4];
    } unionedArg;

    unionedArg.double = seed;                          /* fill in value. */
    unionedArg.integers[2] ^= unionedArg.integers[3];  /* XOR words.     */

    *globalSeed = unionedArg.integers[2];    /* write to center of seed. */
} /* randomizeP */


    /*
     * We cheat here: We won't do the XOR'ing again, but just cast to
     *  a double and pass it to the other RANDOMIZE function. We can spare
     *  the extra execution time for a cast and function call overhead that
     *  VBDOS thought was so costly.  :)
     */
void randomize0(__void)
{
    randomizeP((__double) TIMER());
} /* randomize0 */



-----------

So, when all is said and done, and we've turned the rough translation of
ASM to C into decently structured C, we end up with something like this:

#include "BasicLib.h"

/*
 * in the final prime-time version, we need to thread-proof this,
 *  so threads don't overwrite the seed while another one is doing
 *  so as well. Here, it's not worth the effort.
 */

__single _vbf_timer(void);   /* need this for _vbp_randomize() .. */


#ifdef BIGENDIAN
#   define WORD0     0x0000FFFF
#   define WORD1     0xFFFF0000
#   define DWORD1    0x00000000FFFFFFFF
#   define LOWBYTE   0xFF00
#   define SHIFTHIGH >>
#else
#   define WORD0     0xFFFF0000
#   define WORD1     0x0000FFFF
#   define DWORD1    0xFFFFFFFF00000000
#   define LOWBYTE   0x00FF
#   define SHIFTHIGH <<
#endif


    /* These are the (hopefully) constants in b$EightSpaces... */
#define ENTROPY1 0x43FD                     // -000C
#define ENTROPY2 0x9EC3                     // -0008
#define ENTROPY3 0xC3                       // just first char of -0008.
#define ENTROPY4 (__single) 0x1000000       // -0004  (dword)

    /* The psuedorandom seed. */
static DWORD randomSeed = 0x50000;

void _vbpf_randomize(__double seedAlterer)
/*
 * Alter the psuedorandom number generator's seed. Note that the
 *  seed is merely ALTERED, and not overwritten. While a consistent
 *  series of calls to RND() and RANDOMIZE with consistent arguments
 *  will always produce the same series of psuedorandom numbers, the
 *  series may not be reproduced midprogram with the RANDOMIZE function.
 *
 * Randomize does NOT restart the seed, EVER.  You can restart the seed
 *  (although not to the beginning seed, at least not easily) with RND(x),
 *  where (x < 0). If you REALLY need a reproducible series of numbers,
 *  Call RND(-1) before your series of RANDOMIZE and RND() calls. Then
 *  repeat the process to get the same series. Then consider why you are
 *  using random numbers in the first place, if you must do this.
 *
 *    params : seedAlterer == double param to alter seed.
 *   returns : void.
 */
{
        /*
         * (middleSeed) points to the 2nd byte in the random seed variable.
         *  Writing a 16-bit word here leaves one byte on each side
         *  of the random seed unmolested. This is why RANDOMIZE only
         *  alters, and doesn't overwrite the seed.
         */
    WORD *middleSeed = (WORD *) ( ((BYTE *) &randomSeed) + 1 );
    DWORD trimmedBits;
    WORD result;
    union  /* preserve bit pattern. */
    {
        __double d;
        QWORD q;
    } alterUnion;

    alterUnion.d = seedAlterer;
    trimmedBits = (alterUnion.q & DWORD1);
    result = (trimmedBits & WORD0);
    result ^= (trimmedBits & WORD1);
    *middleSeed = result;              /* write to center of seed. */
} /* _vbpf_randomize */


void _vbp_randomize(void)
/*
 * Alter the psuedorandom number generator's seed.
 *  This is equivalent to the BASIC command "RANDOMIZE TIMER".
 *
 *    params : void.
 *   returns : void.
 */
{
    _vbpd_randomize((__double) _vbf_timer());
} /* _vbp_randomize */


static __single getCurrentGeneration(void)
/*
 * Get the current generation in the sequence of psuedorandom numbers.
 *  The seed is unchanged by this function.
 *
 *    params : void.
 *   returns : next generation in sequence.
 */
{
    union  /* save bit pattern. */
    {
        DWORD dword;
        __single single;
    } seedUnion;

    seedUnion.dword = randomSeed;
    return(seedUnion.single / ENTROPY4);
} /* getCurrentGeneration */


static __single getNextGeneration(void)
/*
 * Get the next generation in the sequence of psuedorandom numbers.
 *  This updates the seed and then figures the current number based
 *  on the update.
 *
 *    params : void.
 *   returns : next generation in sequence.
 */
{
    DWORD tmp;
    WORD lowWord;
    WORD highWord;

    tmp = (randomSeed & WORD0) * ENTROPY1;
    lowWord = (tmp & WORD0);
    highWord = (tmp & WORD1);

    tmp = (randomSeed & WORD1) * ENTROPY1;
    highWord += (tmp & WORD0);

    tmp = (randomSeed & WORD0) * ENTROPY1;
    lowWord += ENTROPY2;
    highWord += (tmp & WORD0);

    __addWithCarry(highWord.h.l, ENTROPY3);
    highWord &= LOWBYTE;  /* clear high byte. */

    randomSeed = (lowWord | (highWord SHIFTHIGH (sizeof (WORD) * 8)));

    return(getCurrentGeneration());
} /* getNextGeneration */


static __single completelyReseed(__single arg)
/*
 * This is the called when rnd(x) is called where (x < 0). The random
 *  number seed is completely overwritten with a new number based on
 *  (arg). Therefore, psuedorandom sequences generated after this
 *  function are guaranteed to have identical sequences for to every
 *  other call where (arg) is identical. That is, -5.0 will always then
 *  seed to the exact same sequence as the next time this function is
 *  called with -5.0, for example.
 *
 *     params : arg == argument to base new seed upon.
 *    returns : first number in new psuedorandom number sequence.
 */
{
} /* completelyReseed */


__single _vbff_rnd(__single arg)
{
    __single retVal = 0.0;

    if (arg > 0.0)
        retVal = getNextGeneration();
    else if (arg == 0.0)
        retVal = getCurrentGeneration();
    else
        retVal = completelyReseed(arg);

    return(retVal);
} /* _vbff_rnd */


__single _vbf_rnd(void)
{
    return(_vbf_rnd(1.0));
} /* _vbf_rnd */


/* end of RandomFunctions.c ... */

---------------------

That's all!

(*yawn*)

--ryan.


