Greetings again!

Thanks for your note (below, last week) about my RPG dice roller program and 
for the insightful follow-up about the Model T randomizer capabilities! I am 
not a particularly well-versed programmer and undertook the dice project to (a) 
goof around with the Model T, (b) familiarize myself with how to code it, and 
(c) to have something cool to take with me on game nights! Through that lens, I 
had a lot of fun and was able to accomplish what I set out to do.

As you suspected, I did indeed find my inspiration for the randomizer function 
from the excellent "Programming Tips, Peeks, and Pokes” book by Tony Anderson; 
the specific section that is most relevant here is Chapter 13, document pages 
29-31 (in the PDF, it is pages 31-33):

https://archive.org/details/ProgrammingTipsPeeksAndPokesForTheTandyPortableComputers/page/n31/mode/2up

In particular, this paragraph was helpful for me (from around the middle of 
page 30):

You can "reseed" the random number generator yourself at the beginning of the 
program by asking for the user to choose a number between 1 and 100, or by 
using the seconds digits from the clock, or by reading one of the two running 
counters in high memory, then poking the value into the memory seed location at 
64634 (100 and 102) or 63277 (200).

My approach only extended this by reseeding things based on the clock’s seconds 
digits when the user interacted with the app. (In hindsight, I could’ve 
explored those two running counters in high memory instead of, or in addition 
to, the seconds digits). My loose logic was that they’d likely be unable to 
pull off two dice rolls within the same second, as it takes them a bit of time 
to choose what to roll, watch the results, do whatever they’re going to do with 
the results (e.g., playing RPGs), etc.

Here’s the current version of my DICE.DO source code:

http://www.club100.org/memfiles/index.php?action=downloadfile&filename=DICE.DO&directory=Steve%20Baker&;

Let’s start with the important bits from the above source code that show how I 
chose to leverage the tips from Tony’s book. Basically it’s a three-part play:

1. First, a one-time step to setup a variable ZZ with the current seconds 
digits when the code first runs

2. As the user interacts with the program, it resets a second variable ZY to 
the current seconds digits — my assumption was that ZY will be different from 
ZZ at least most of the time, as there’s of course a chance it could 
coincidentally be the same seconds digits as ZZ on occasion.

3.  Finally, a third variable ZX fetches the current seconds digits again when 
we’re actually rolling a dice — it resets every time a dice is rolled, which 
could be several times (for instance, a 3d20 request triggers this routine 
three separate times to roll the d20 dice once each time, etc.)

So, my use of the randomizer is three-deep, in that we have ZZ that is set 
once, ZY that is set when they use the menu, and ZX that is set with each dice 
roll. I put the code the does the heavy lifting toward the front of the program 
so it’s faster to access during runtime, etc. The following elaborates on those 
three steps with some actual code snippets.

* First step — one-time initialization of variable ZZ (via the “GOSUB8900" call)

There’s a big chunk of variable initialization stuff at the end of the code (I 
read that having the “only use once” initialization stuff at the back of the 
program is more efficient). That chunk begins on line 8900. First, ZZ gets the 
seconds digits from the clock when the app initializes:

8900ZZ=VAL(RIGHT$(TIME$,2)): … the other initialization stuff

* Second step — setting variable ZY with each menu interaction (via the 
“GOSUB45” call)

When we search for “GOSUB45” in the code, we see that it is executed when the 
user interacts with the menu, e.g. makes a selection from the set of choices.

45ZY=VAL(RIGHT$(TIME$,2)):RETURN

* Third step — the loop that sets variable ZX and uses the three variable in a 
loop to actually pluck out a number (via the “GOSUB42” call)

When we search for “GOSUB42” in the code, we see that it is executed for each 
roll of the dice. In the subroutine, LZ is the loop index and Q is used to pop 
the randomizer function to burn off some quantity of random numbers (we don’t 
keep Q). The global variable DI is set before the roll with the number of sides 
for the dice (e.g., DI=10 for a d10 dice, 12 for a d12, etc.). The global 
variable RL is short for “roll” as in dice roll result, and this subroutine 
modifies that variable with the resulting roll.

42ZX=VAL(RIGHT$(TIME$,2)):FORLZ=1TO(ZX+ZZ+(RND(1)*ZY)):Q=RND(1):NEXTLZ:IFDI>1THENRL=INT(RND(1)*DI)+1:RETURN
43IFRND(1)>=0.5THENRL=1:RETURNELSERL=0:RETURN

… and line 43 is used for dice rolls that are either 1 or 0 as in true or false 
or hit/miss (those absolute times when it is either yes or no). We get to line 
43 when the final if statement in line 42, where it looks to see if DI is 
greater than one, fails. Put another way, when DI is set to 1 then we’re 
looking for a d1 roll. In this situation line 43 looks at the random number and 
if it’s greater than or equal to 0.5 then it’s yes/true/success/etc., and if 
it’s less than 0.5 then it’s no/false/failure.

Line 42 is not particularly efficient and I figured that’s alright because my 
use of the randomizer is heavily dependent on the seconds digits as well on as 
the user themselves. My intent was not to intentionally stall for time, so to 
speak, but “good" numbers over lighting-fast responses.

One interesting thought is that by depending on the user’s interaction to 
finalize the random number, it replicates the act of them shaking the dice. At 
least to me, the vision of them influencing the selection of their random 
number is like they are basically shaping their destiny, LOL.

Looking back on all this with a few years’ perspective, perhaps hitting those 
two running counters (maybe with grabbing the seconds digits a time or two) 
might be simple and efficient. One of these days (years?) I may revisit the 
code to streamline it a bit now that I know a lot more.

Given all this, I’m sure there’s probably a way to instrument something to test 
the RNG uniformity via chi squared or equivalent tests. Just not sure where or 
how to put that into the code, given this kludgy three-part algorithm is quite 
user-dependent (their actions trigger most of it).

My apologies for the delay in this response (I’d forgotten about the football 
stuff this past weekend and I ended up having plans both days). Hopefully this 
helps a little bit at least? It was fun to revisit the code after so long 
(well, four years at least) and the memories of writing it.

Cheers and thanks,
Steve

PS, I am still (very slowly) photographing and inventorying my T102s, T200s, 
T600s, peripherals, and the like. to offer for sale to this group. My hope is 
to sell fewer, larger lots of gear rather than one-offs, but will be open to 
special situations and whatnot. As they say, stay tuned!



--
Greetings from Steve Baker (he/him/his)
“Gravity brings me down…”




> On Feb 8, 2024, at 8:08 PM, B 9 <[email protected]> wrote:
> 
> On Tue, Dec 26, 2023 at 4:36 PM Steve Baker <[email protected]> wrote:
> 
> That was one of the hardest parts about writing that app — I did a lot of 
> research about tips and tricks, and have a few nested randomizer routines in 
> there to compensate. While I have not done a statistical study with a 
> significant sample set, it kicks out dice rolls that are very random, and the 
> folks that I play with feel comfortable with its results. The source code is 
> available and I’d be happy to highlight the key bits (some upfront prep early 
> in the code and most of the randomizer functions are towards the back, etc).
> 
> Steve,
> 
> I was curious how nesting the randomizer had affected the quality of the 
> randomization, so I poked around at the source code for DICEBOX. While I had 
> a lot of fun trying to weave my way through the tightly crunched code, in the 
> end I wasn't able to extract just the randomizer. Could you help me grok it? 
> Once the routine is extracted, I think your RNG might be useful for anyone 
> else who wants to reuse your work.
> 
> I've already written code that can do a check of RNG uniformity via the χ² 
> (chi squared) test on a Model T. (See attachment). It rolls a D sided die N 
> times and returns a probability p that the RNG is selecting every number 
> equally. I just need your routine at line 10000 which takes variable D 
> (number of sides) as input and outputs variable R (random integer from 1 to 
> D, inclusive). 
> 
> —b9
> 
> P.S. On the Tandy 200, DICEBOX needs a line 15 GOTO 100 or it gives an error.
> 
> 
> --- CUT HERE ---
> 
> 0 GOTO 1000 REM RNG Tester by hackerb9
> 1 DATA 0.000, 0.000, 0.001, 0.004, 0.016, 2.706, 3.841, 5.024, 6.635, 7.879
> 2 DATA 0.010, 0.020, 0.051, 0.103, 0.211, 4.605, 5.991, 7.378, 9.210, 10.597
> 3 DATA 0.072, 0.115, 0.216, 0.352, 0.584, 6.251, 7.815, 9.348, 11.345, 12.838
> 4 DATA 0.207, 0.297, 0.484, 0.711, 1.064, 7.779, 9.488, 11.143, 13.277, 14.860
> 5 DATA 0.412, 0.554, 0.831, 1.145, 1.610, 9.236, 11.070, 12.833, 15.086, 
> 16.750
> 6 DATA 0.676, 0.872, 1.237, 1.635, 2.204, 10.645, 12.592, 14.449, 16.812, 
> 18.548
> 7 DATA 0.989, 1.239, 1.690, 2.167, 2.833, 12.017, 14.067, 16.013, 18.475, 
> 20.278
> 8 DATA 1.344, 1.646, 2.180, 2.733, 3.490, 13.362, 15.507, 17.535, 20.090, 
> 21.955
> 9 DATA 1.735, 2.088, 2.700, 3.325, 4.168, 14.684, 16.919, 19.023, 21.666, 
> 23.589
> 10 DATA 2.156, 2.558, 3.247, 3.940, 4.865, 15.987, 18.307, 20.483, 23.209, 
> 25.188
> 11 DATA 2.603, 3.053, 3.816, 4.575, 5.578, 17.275, 19.675, 21.920, 24.725, 
> 26.757
> 12 DATA 3.074, 3.571, 4.404, 5.226, 6.304, 18.549, 21.026, 23.337, 26.217, 
> 28.300
> 13 DATA 3.565, 4.107, 5.009, 5.892, 7.042, 19.812, 22.362, 24.736, 27.688, 
> 29.819
> 14 DATA 4.075, 4.660, 5.629, 6.571, 7.790, 21.064, 23.685, 26.119, 29.141, 
> 31.319
> 15 DATA 4.601, 5.229, 6.262, 7.261, 8.547, 22.307, 24.996, 27.488, 30.578, 
> 32.801
> 16 DATA 5.142, 5.812, 6.908, 7.962, 9.312, 23.542, 26.296, 28.845, 32.000, 
> 34.267
> 17 DATA 5.697, 6.408, 7.564, 8.672, 10.085, 24.769, 27.587, 30.191, 33.409, 
> 35.718
> 18 DATA 6.265, 7.015, 8.231, 9.390, 10.865, 25.989, 28.869, 31.526, 34.805, 
> 37.156
> 19 DATA 6.844, 7.633, 8.907, 10.117, 11.651, 27.204, 30.144, 32.852, 36.191, 
> 38.582
> 20 DATA 7.434, 8.260, 9.591, 10.851, 12.443, 28.412, 31.410, 34.170, 37.566, 
> 39.997
> 21 DATA 8.034, 8.897, 10.283, 11.591, 13.240, 29.615, 32.671, 35.479, 38.932, 
> 41.401
> 22 DATA 8.643, 9.542, 10.982, 12.338, 14.041, 30.813, 33.924, 36.781, 40.289, 
> 42.796
> 23 DATA 9.260, 10.196, 11.689, 13.091, 14.848, 32.007, 35.172, 38.076, 
> 41.638, 44.181
> 24 DATA 9.886, 10.856, 12.401, 13.848, 15.659, 33.196, 36.415, 39.364, 
> 42.980, 45.559
> 25 DATA 10.520, 11.524, 13.120, 14.611, 16.473, 34.382, 37.652, 40.646, 
> 44.314, 46.928
> 26 DATA 11.160, 12.198, 13.844, 15.379, 17.292, 35.563, 38.885, 41.923, 
> 45.642, 48.290
> 27 DATA 11.808, 12.879, 14.573, 16.151, 18.114, 36.741, 40.113, 43.195, 
> 46.963, 49.645
> 28 DATA 12.461, 13.565, 15.308, 16.928, 18.939, 37.916, 41.337, 44.461, 
> 48.278, 50.993
> 29 DATA 13.121, 14.256, 16.047, 17.708, 19.768, 39.087, 42.557, 45.722, 
> 49.588, 52.336
> 30 DATA 13.787, 14.953, 16.791, 18.493, 20.599, 40.256, 43.773, 46.979, 
> 50.892, 53.672
> 40 DATA 20.707, 22.164, 24.433, 26.509, 29.051, 51.805, 55.758, 59.342, 
> 63.691, 66.766
> 50 DATA 27.991, 29.707, 32.357, 34.764, 37.689, 63.167, 67.505, 71.420, 
> 76.154, 79.490
> 60 DATA 35.534, 37.485, 40.482, 43.188, 46.459, 74.397, 79.082, 83.298, 
> 88.379, 91.952
> 70 DATA 43.275, 45.442, 48.758, 51.739, 55.329, 85.527, 90.531, 95.023, 
> 100.425, 104.215
> 80 DATA 51.172, 53.540, 57.153, 60.391, 64.278, 96.578, 101.879, 106.629, 
> 112.329, 116.321
> 90 DATA 59.196, 61.754, 65.647, 69.126, 73.291, 107.565, 113.145, 118.136, 
> 124.116, 128.299
> 100 DATA 67.328, 70.065, 74.222, 77.929, 82.358, 118.498, 124.342, 129.561, 
> 135.807, 140.169
> 110 REM Calculate p from df and X2
> 120 IF DF<1 THEN DF=1: PRINT "Using df =" DF
> 130 IF DF>100 THEN DF=100: PRINT "Using df =" DF
> 140 IF DF>30 AND (DF MOD 10) > 0 THEN DF=DF\10: PRINT "Using df =" DF
> 150 ' Select a DATA line to read from
> 151 IF DF=1 THEN RESTORE 1
> 152 IF DF=2 THEN RESTORE 2
> 153 IF DF=3 THEN RESTORE 3
> 154 IF DF=4 THEN RESTORE 4
> 155 IF DF=5 THEN RESTORE 5
> 156 IF DF=6 THEN RESTORE 6
> 157 IF DF=7 THEN RESTORE 7
> 158 IF DF=8 THEN RESTORE 8
> 159 IF DF=9 THEN RESTORE 9
> 160 IF DF=10 THEN RESTORE 10
> 161 IF DF=11 THEN RESTORE 11
> 162 IF DF=12 THEN RESTORE 12
> 163 IF DF=13 THEN RESTORE 13
> 164 IF DF=14 THEN RESTORE 14
> 165 IF DF=15 THEN RESTORE 15
> 166 IF DF=16 THEN RESTORE 16
> 167 IF DF=17 THEN RESTORE 17
> 168 IF DF=18 THEN RESTORE 18
> 169 IF DF=19 THEN RESTORE 19
> 170 IF DF=20 THEN RESTORE 20
> 171 IF DF=21 THEN RESTORE 21
> 172 IF DF=22 THEN RESTORE 22
> 173 IF DF=23 THEN RESTORE 23
> 174 IF DF=24 THEN RESTORE 24
> 175 IF DF=25 THEN RESTORE 25
> 176 IF DF=26 THEN RESTORE 26
> 177 IF DF=27 THEN RESTORE 27
> 178 IF DF=28 THEN RESTORE 28
> 179 IF DF=29 THEN RESTORE 29
> 180 IF DF=30 THEN RESTORE 30
> 181 IF DF=40 THEN RESTORE 40
> 182 IF DF=50 THEN RESTORE 50
> 183 IF DF=60 THEN RESTORE 60
> 184 IF DF=70 THEN RESTORE 70
> 185 IF DF=80 THEN RESTORE 80
> 186 IF DF=90 THEN RESTORE 90
> 187 IF DF=100 THEN RESTORE 100
> 210 REM p value for columns in data
> 220 A(0)=1
> 230 A(1)=.995: A(2)=.990: A(3)=.975
> 240 A(4)=.950: A(5)=.900: A(6)=.100
> 250 A(7)=.050: A(8)=.025: A(9)=.010
> 260 A(10)=.005
> 300 REM X2 is chi square, M is alpha
> 310 FOR T=1 TO 10
> 320 READ M
> 330 IF X2 > M THEN NEXT
> 350 P=A(T-1)
> 390 RETURN
> 990 REM By hackerb9 February 2024.
> 999 '
> 1000 REM Chi Squared statistic to 
> 1001 REM check for uniformity of RNG.
> 1002 ' Note: This does NOT check for
> 1003 ' independence. For that, use
> 1004 ' autocorrelation, gap, poker
> 1005 ' or other "diehard" tests.
> 1010 ES$=CHR$(27) 'Screen Esc sequences
> 1020 PRINT ES$"U"; ' Turn off labels
> 1030 CLS
> 1038 ' d is number of histogram bins
> 1039 ' or, equivalently, sides of a die.
> 1040 D=6
> 1050 DIM X(D)
> 1059 ' n is number of times to roll.
> 1060 N=100
> 1069 ' is the count expected in a bin
> 1070 E=N/D
> 1099 '
> 1100 REM Roll dice and show histogram bins.
> 1120 PRINT"Rolling a";D;"sided die";N;"times";
> 1130 FOR I=1 TO N
> 1140 GOSUB 10000 ' R = random(D)
> 1150 X(R)=X(R)+1
> 1160 TT=(R-1)*4: GOSUB 1550
> 1170 PRINT X(R);
> 1180 NEXT I
> 1190 TT=D*4:GOSUB 1550: PRINT
> 1199 '
> 1200 REM Calculate chi square statistic
> 1201 ' Chi-Square = sum{(x(i)-E)^2 / E}
> 1210 FOR I=1 TO D: IF X(I)<5 THEN PRINT "Results may be inaccurate due to 
> insufficient trials (n ="N", d ="D")": ELSE NEXT
> 1230 X2=0: DF=-1
> 1240 FOR I=1 TO D
> 1250 X2 = X2 + ( (X(I) - E)^2 / E)
> 1260 IF X(I) > 0 THEN DF=DF+1
> 1270 NEXT I
> 1280 PRINT "Chi square is";X2
> 1290 PRINT "Degrees of Freedom is";df
> 1299 '
> 1300 REM Set p (percentage points) by 
> 1301 REM looking up x2 (chi square)
> 1302 REM at df (degree of freedom) in
> 1303 REM the chisq distribution table.
> 1310 GOSUB 110
> 1399 '
> 1400 REM Interpret result 
> 1410 PRINT "p =" P*100 "%"
> 1420 PRINT"This Random Number Generator"
> 1430 IF P>0.95 THEN PRINT "is too regular. (BAD)": END
> 1440 IF P<0.05 THEN PRINT "is not uniformly distributed. (BAD)": END
> 1450 PRINT "seems uniformly distributed."
> 1460 END
> 1499 '
> 1500 'Move cursor to Row AA and Col BB
> 1510 PRINT ES$"Y"CHR$(32+AA)CHR$(32+BB);
> 1520 RETURN
> 1550 REM Move cursor to position TT
> 1560 AA=TT\40+1:BB=TT MOD 40:GOSUB 1500
> 1570 RETURN
> 9999 '
> 10000 REM RANDOM NUMBER GENERATOR 
> 10001 ' Input D, integer
> 10002 ' Output R, integer
> 10003 ' 1..D, inclusive
> 10010 R=INT(D*RND(1))+1
> 10090 RETURN 
> 
> 
> 
> <x2.ba>

Reply via email to