Mr. Cluey
: Classes : theDiceGod does not play dice with the universe
I'm not a big fan of random numbers in a test bed. Trusting to luck to find bugs hardly strikes me as being a rigorous procedure.
Furthermore - what if you do find a bug? If you can't reproduce it reliably, you haven't done yourself a lot of good.
How do we ensure that we can reproduce a series of random events? By controlling the seed used to generate those events.
Consider this fragment
main ()
{
integer i ;
for i = 1 to 100
{
print ( RandInt( 1,10) ) ; //prints a random number between 1 and 10 (inclusive)
}
}
Perfectly straight forward - we get a seemingly random sequence of numbers. Each time we run the script, it prints a different series. However, with a one line change, the effect is drastically different...
main ()
{
integer i ;
for i = 1 to 100
{
RandSeed( 7 ) ;
print ( RandInt( 1,10) ) ; //prints a random number between 1 and 10 (inclusive)
}
}
This fragment hardly seems random at all! What's going on?
Essentially, partner has a fixed list of random numbers that it doles out. RandSeed allows you to specify where in the list to start. Each random call access the next item in the list. By specifying the same point in the list each time through, you are guaranteed to get the same results.
(Experiment will show that is a gross oversimplification. For a more detailed explanation of reality, pick up a copy of Knuth).
The random value operations in 4Test aren't quite adequate. Here are some of the things that we need to be able to do...
The CDice class encapsulates the random value functions, adding the desired functionality.
winclass CDice
{
//Wrapper Methods
integer RandInt( integer iMin, integer iMax )
{
CheckSeed () ;
return ::RandInt( iMin, iMax ) ;
}
anytype RandPick( list lList )
{
CheckSeed () ;
return ::RandPick( lList ) ;
}
real RandReal ()
{
CheckSeed () ;
return ::RandReal () ;
}
string RandStr( string sPicture )
{
CheckSeed () ;
return ::RandStr( sPicture ) ;
}
// Implementation details
boolean bSeedRequired = true ;
integer SEED_MIN = 0 ;
integer SEED_MAX = 100000 ;
string pMyDefaultSeed = "RAND_SEED" ;
Reset () { Load( GetSeed() ) ; }
Load ( integer Seed optional )
{
if IsNull(Seed) Seed = GetSeed() ;
Print( "{this}.Load( {Seed} )" ) ;
RandSeed( Seed ) ;
bSeedRequired = false ;
}
integer GetSeed()
{
integer tmpSeed ;
do
{
tmpSeed = @(pMyDefaultSeed) ;
print ( "{this}.GetSeed() fixed seed {pMyDefaultSeed} in use...." ) ;
}
except
tmpSeed = ::RandInt(SEED_MIN,SEED_MAX) ;
return (tmpSeed) ;
}
CheckSeed ()
{
if ( bSeedRequired ) Load () ;
}
}
Note that the wrapper methods have the same names and methods as the functions that are wrapped. The key detail is the call to CheckSeed(), which ensures that a seed value has been loaded.
Load() allows you to "load the dice" - specify which seed you want in use. If no seed is specified, the object generates a seed for itself. Load() also reports which seed is going to be used. If your test generates a bug, you will be able to look in the script and determine which value will reproduce the problem. Mighty handy that.
GetSeed() is slightly convoluted. pMyDefaultSeed would normally be the name of a compiler constant loaded with a seed value. If that variable hasn't been set, it generates a seed for itself (remember, the key is that we always want to know which seed is in effect).
I added the print statement to the GetSeed() method because without it it would be very easy to forget that the seed has been added to the list of compiler constants.
SEED_MIN and SEED_MAX were chosen arbitrarily (too lazy to look up the actual ranges that could be used).
By setting pMyDefaultSeed as a member variable (rather than hardwiring a constant) it gives the coder a way to set two different CDice going using different seed values. For instance, you might see
window CDice MyDice { string pMyDefaultSeed = "MY_SEED" ; }
There is one problem with the above - the various instances of CDice are not independent of each other. They are using the same random value operations, including RandSeed(). Resetting one automatically resets the other.
use "cdice.inc" ;
window CDice A { string pMyDefaultSeed = "A_SEED" ; }
window CDice B { string pMyDefaultSeed = "B_SEED" ; }
//These would normally be compiler constants
const A_SEED = 7 ;
const B_SEED = 8 ;
main ()
{
RandSeed( A_SEED ) ;
real A_Expected = RandReal() ;
RandSeed( B_SEED ) ;
real B_Expected = RandReal() ;
A.Load () ;
B.Load () ;
real A_Actual = A.RandReal () ;
if ( A_Actual != A_Expected )
if ( A_Actual == B_Expected )
LogError ( "oops {A_Actual} {A_Expected}" ) ;
}
When you read oops in the results file, you can see what happened. B.Load replaced A's seed value with B's - rather defeating the purpose.
There are several options at this point. You can decide to live with this (bad choice). You can remove the dependence on the random value functions by rolling your own (this is muy macho all by itself - don't try it without Knuth handy). You can build some reference counting into the CDice class so that it resets itself each time, then skips ahead to the correct value before proceding.
For my money, the best bet is to rewrite the code so that there can be only one CDice object. It's not terribly difficult - here is what my CDice class file looks like.
private winclass CDice
{
//implementation as above
}
window CDice theDice {;}
Voila! theDice is the only instance of CDice that can exist. Scripts can call on theDice however they would like, but they cannot create their own. Therefore, there are no conflicts - scripts cannot trip over each other.
Of course, unless you do decide to roll your own random methods, the class cannot protect itself against calls to RandSeed().
| Mr. Cluey : Classes : theDice |