Oh, now I feel silly. I hadn't known what "Standard Expression" meant. It
sounds like you've already implemented exactly what I was imagining and I
just didn't recognize it.

With that wish fulfilled in advance, I think Dice Box is well-nigh perfect.
The only possible other things one could add to Dice Box would be frills:
fancy graphics, a help system, or F-key labels for common rolls. None of
which Dice Box actually needs, but might be fun.

Ken Petit: Is your AsciiPixels graphics library fast enough to animate a
small sprite, like a die tumbling across the screen?

--b9

On Tuesday, February 13, 2024, Steve Baker <[email protected]> wrote:
> Hello again!  :-)
> No worries at all, and thanks for your thoughtful feedback and
suggestions! The “simple mode” idea is a good one… I will fire up a
background process to think about that a bit more.
> Currently, to do a simple roll the user selects menu option 1 (standard
expression) and then types “d20” or “6d6” and gets the result. Hitting
enter after that first roll will perform another roll of the same kind
(e.g., d20 or 6d6). In effect, if one were to pass the Model T to the next
player, all they’d need to do is press enter to perform the same roll, etc.
> Cheers,
> Steve
>
> --
> Greetings from Steve Baker (he/him/his)
> “Gravity brings me down…”
>
>
>
> On Feb 13, 2024, at 3:55 AM, B 9 <[email protected]> wrote:
> (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>
>
>

Reply via email to