(Oops, I know your name is "Steve". I don't know why I typed "Steven".)
On Tue, Feb 13, 2024 at 12:53 AM B 9 <[email protected]> wrote: > Thanks for the walkthrough, Steven! While adding in the fast counters > wouldn't hurt (probably), I don't think Dice Box needs algorithmic > improvement now that I understand how it works. > > First, it's pretty clever to get more randomness from the users at each > dice roll (`ZX`), not kludgy at all. > > Second, you used the extra randomness in the right way. My first instinct > would have been to reseed the Random Number Generator (RNG) as Anderson > suggests — using `POKE` or with `RND(-`_x_``)` — but that would have > actually made the dice rolls *less* uniformly distributed because it > depends upon the distribution of your time measurements. (Seeding an RNG > should only be done once in a program.) > > My second instinct would have also been wrong: modify the result from > `RND(1)` algebraically or using modulo arithmetic. Again, that would skew > the distribution unless extremely carefully programmed. You simply used the > time interval to skip ahead in the RNG by a few steps, which is perfectly > valid and leverages Bill Gates' hard work of building a mathematically > correct RNG. > > The only algorithmic improvement I can think of would be to use Lloyd's > method of calling RND(1) in a loop with `INKEY$()` on the splash screen. > That would (perhaps) help as it increments the RNG by about 80 steps per > second, giving more possible starting states for the first die roll than > measurements in seconds. > > In fact, you could skip measuring seconds all together if you replaced > `INPUT$(1)` with a loop like, > > Q=RND(1): IF INKEY$="" GOTO 10 > > —B > > P.S. If you do start fixing up Dice Box, one feature which I'd personally > use would be the option for a "simple mode" instead of picking a "system". > I imagine a prompt where one could type "d20" to roll a twenty-sided die, > "6d6" to roll 6 six-sided dice, or "d1" to flip a coin. (Hitting enter > would roll the same dice again until something different is typed. Q to > quit.) Or maybe it would be menu driven so one could pass the Model T to a > player and ask them to press a key to roll the dice. The exact time they > hit the key determines the result. > > > On Mon, Feb 12, 2024 at 1:07 PM Steve Baker <[email protected]> > wrote: > >> 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> >> >> >>
