Just adding a completely worked example here, including an implementation quirk/feature I didnāt mention above.
Letās imagine weāre implementing an engine to play rock paper scissors in-game. And, for some reason, weāre concerned with the fairness with which NPCs will be throwing the various moves. Our basic NPC AI for playing rock paper scissors might look something like:
// A naive engine for playing rock paper scissors.
rockPaperScissors: object
randomNumber() { return(rand(3) + 1); }
// We can't call this method "throw", because that's a reserved word.
shoot() {
local r;
switch(randomNumber) {
case 1:
r = 'rock';
break;
case 2:
r = 'paper';
break;
case 3:
r = 'scissors';
break;
}
return(r);
}
;
This is a little silly and ornate for what it does, but itās vaguely in the form of something we might use for more serious stuff. Specifically, weāre designing the decision logic as a sort of black box that we access by a method thatās meant to be called externally: rockPaperScissors.shoot()
. That is, the behavior weāre concerned about being āfairā is at base dependent on something weāre probably not worried about (the native T3 rand()
function), but we want to make sure whatever weāre doing with it isnāt introducing any hidden errors.
Okay, so much for the RPS āengineā. To test this via the stuff in the statTest
module, we just wrap a call to the decision method in a test class:
// Test the rock paper scissors "logic" defined above.
class RPSTest: StatTestFencepost
// These are the outcomes
outcomes = static [ 'rock', 'paper', 'scissors' ]
// Invoke the rock paper scissors selection logic once.
pickOutcome() { return(rockPaperScissors.shoot()); }
;
The thing to note here is that we can have options that are non-numeric as inputs to our statistical methods. Under the hood the individual tests are doing something like idx = outcomes.indexOf(pickOutcome())
and then using the index values (instead of using the return values from pickOutcome()
directly). This means thereās no problem using strings or objects or enums. The only caveat being that elements of outcomes
need to be unique (that is, only appear in the array once).
All thatās left is to actually run the test, and that works exactly as the example I posted upthread:
local t;
t = new RPSTest();
t.runTest();
t.report();
Hereās the complete source as a compilable āgameā:
#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>
// A naive engine for playing rock paper scissors.
rockPaperScissors: object
// Our method for selecting a random number. This version
// works; to illustrate how the fenceposting logic can catch
// off-by-one errors, change this to be just "return(rand(3))".
randomNumber() { return(rand(3) + 1); }
// This is a plausible-looking off-by-one error.
//randomNumber() { return(rand(3)); }
// We can't call this method "throw", because that's a reserved word.
shoot() {
local r;
switch(randomNumber) {
case 1:
r = 'rock';
break;
case 2:
r = 'paper';
break;
case 3:
r = 'scissors';
break;
}
return(r);
}
;
// Test the rock paper scissors "logic" defined above.
class RPSTest: StatTestFencepost
// These are the outcomes
outcomes = static [ 'rock', 'paper', 'scissors' ]
// Invoke the rock paper scissors selection logic once.
pickOutcome() { return(rockPaperScissors.shoot()); }
;
versionInfo: GameID;
gameMain: GameMainDef
newGame() {
local t;
t = new RPSTest();
t.runTest();
t.report();
}
;
Compiling this with the -D __DEBUG_STAT_TEST
flag and then running in your favorite interpreter will output something like:
StatTestFencepost: passed
[Hit any key to exit.]
If you compile without the -D __DEBUG_STAT_TEST
itāll just exit without output, indicating that it detected no errors (this is intended to make scripted regression testing a little easier).
In the complete example, thereās an alternate randomNumber()
method that contains an off-by-one error:
randomNumber() { return(rand(3)); }
This will return values from 0 to 2, not from 1 to 3. This means itāll generate a bunch of values that are out of range low and it will never pick the third defined option (scissors). Commenting out the āgoodā method, uncommenting the ābadā method, re-compiling, and then running the new story file, it will report the problems just mentioned:
StatTestFencepost: ERROR: 3346 outcomes under value
StatTestFencepost: ERROR: bucket 3 (scissors) empty
StatTestFencepost: FAILED
[Hit any key to exit.]
This is probably the test in the module thatās likely to be most useful to most developers, because this kind of bug can be pretty sneaky.