Author Topic: Steve's Simple Dice Roller  (Read 5470 times)

0 Members and 1 Guest are viewing this topic.

Offline SMcNeill

  • QB64 Developer
  • Forum Resident
  • Posts: 3972
    • View Profile
    • Steve’s QB64 Archive Forum
Steve's Simple Dice Roller
« on: January 17, 2019, 10:03:15 am »
My old dice rolling routine, shared for Staxtix, to help showcase the core idea behind my new and improved utility which I'm working up:

Code: QB64: [Select]
  1. _TITLE "Steve's Simple Dice Roller"
  2. SCREEN _NEWIMAGE(1280, 720, 256)
  3.  
  4. REDIM SHARED Results(0, 0) AS LONG
  5.  
  6.  
  7. QuickRoll 10, 2, 10, 0, 0, 0, 0
  8. PrintResults
  9. QuickRoll 2, 5, 6, 1, 0, 3, 0
  10. PrintResults
  11.  
  12.     INPUT "Dice to Roll =>"; d$
  13.     IF LTRIM$(d$) = "" THEN EXIT DO
  14.     IF UCASE$(d$) = "R" THEN d$ = old$ 'R for Reroll
  15.     old$ = d$
  16.     RollDice d$
  17.     PrintResults
  18.  
  19.  
  20.  
  21.  
  22. SUB QuickRoll (S, D, N, DM, TM, KH, KL)
  23.     REDIM Results(S, D) AS LONG
  24.     REDIM Rolls(D - 1) AS LONG, LowRolls(D - 1) AS LONG
  25.  
  26.     IF KL < 0 THEN KL = 0
  27.     IF KH < 0 THEN KH = 0
  28.     IF KL > D THEN KL = D
  29.     IF KH > D THEN KH = D
  30.  
  31.     Results(0, 0) = TM
  32.     FOR j = 1 TO S
  33.         FOR i = 1 TO D
  34.             Rolls(i - 1) = INT(RND * N) + 1 + DM
  35.             'PRINT Rolls(i - 1),                               'UNREMARK THIS LINE TO SEE UNSORTED NUMBERS
  36.         NEXT
  37.         'PRINT                                                 'AND THIS LINE TO SEE UNSORTED NUMBERS
  38.  
  39.         'The dice sorting routine, optimized to use _MEM and a comb sort algorithm.  It's more than fast enough for our needs here I think.  ;)
  40.         IF D > 1 THEN 'No need to try and sort only 1 dice.
  41.             DIM m AS _MEM
  42.             DIM o AS _OFFSET, o1 AS _OFFSET
  43.             DIM t AS LONG, t1 AS LONG
  44.             m = _MEM(Rolls())
  45.             $CHECKING:OFF
  46.             gap = UBOUND(Rolls)
  47.  
  48.             DO
  49.                 gap = 10 * gap \ 13
  50.                 IF gap < 1 THEN gap = 1
  51.                 i = 0
  52.                 swapped = 0
  53.                 DO
  54.                     o = m.OFFSET + i * 4
  55.                     o1 = m.OFFSET + (i + gap) * 4
  56.                     IF _MEMGET(m, o, LONG) > _MEMGET(m, o1, LONG) THEN
  57.                         _MEMGET m, o1, t1
  58.                         _MEMGET m, o, t
  59.                         _MEMPUT m, o1, t
  60.                         _MEMPUT m, o, t1
  61.                         swapped = -1
  62.                     END IF
  63.                     i = i + 1
  64.                 LOOP UNTIL i + gap > UBOUND(Rolls)
  65.             LOOP UNTIL swapped = 0 AND gap = 1
  66.             $CHECKING:ON
  67.             _MEMFREE m
  68.         END IF
  69.  
  70.         'FOR i = 1 TO d: PRINT Rolls(i - 1),: NEXT: PRINT      'UNREMARK THIS LINE TO SEE THE SORTED NUMBERS, BEFORE WE DISCARD LOW OR HIGH
  71.  
  72.         IF KH THEN 'drop the undesired low rolls
  73.             FOR i = 1 TO D - KH: Rolls(i - 1) = 0: NEXT
  74.         END IF
  75.         IF KL THEN 'drop the undesired high rolls
  76.             FOR i = KL + 1 TO D: Rolls(i - 1) = 0: NEXT
  77.         END IF
  78.         FOR i = 1 TO D
  79.             Results(j, i) = Rolls(i - 1)
  80.         NEXT
  81.     NEXT
  82.  
  83.  
  84.     FOR j = 1 TO S
  85.         total = 0
  86.         FOR i = 1 TO D
  87.             total = total + Results(j, i)
  88.         NEXT
  89.         total = total + Results(0, 0) 'Results 0,0 holds our total modifier
  90.         Results(j, 0) = total 'And store the total in the 0 element of the array
  91.     NEXT
  92.  
  93.  
  94.  
  95.  
  96.  
  97. SUB RollDice (text$)
  98.     DIM s AS LONG, d AS LONG, n AS LONG
  99.     DIM dm AS LONG, tm AS LONG
  100.  
  101.     'First strip spaces if any
  102.     FOR i = 1 TO LEN(text$)
  103.         IF MID$(text$, i, 1) <> " " THEN t$ = t$ + MID$(text$, i, 1)
  104.     NEXT
  105.     d$ = UCASE$(text$) 'Then make it UCASE for ease of parsing
  106.  
  107.     'Parse the text for the proper number and size dice to roll
  108.     l = INSTR(d$, "D") 'l is the location of the D which tells us how many dice to roll and what size
  109.     MID$(d$, l) = "@" 'Replace the D with something else so we don't accept it as a scientific notation value
  110.     l1 = INSTR(l + 1, d$, "+") 'l1 is the location of the dice modifier (+/-), which is optional
  111.     IF l1 = 0 THEN l1 = INSTR(l + 1, d$, "-")
  112.  
  113.     l2 = INSTR(d$, "S") 'l2 is the location of S, which is the number of sets of dice we want to roll
  114.  
  115.     l3 = INSTR(d$, "T") 'l3 is the location of the total modifier, if any
  116.     IF l1 > l3 AND l3 <> 0 THEN l1 = 0 'In case we have a T-100 and not a +/- to the dice rolls first
  117.  
  118.     l4 = INSTR(d$, "KH") 'l4 tells us how many of the high rolls to keep
  119.     IF l4 = 0 THEN l4 = -INSTR(d$, "KL") 'or how many of the low rolls to keep
  120.  
  121.     s = VAL(LEFT$(d$, l2)): IF s < 1 THEN s = 1
  122.     d = VAL(MID$(d$, l2 + 1)): IF d < 1 THEN d = 1
  123.     n = VAL(MID$(d$, l + 1)): IF n <= 0 THEN REDIM Results(0, 0) AS LONG: EXIT SUB
  124.  
  125.     IF l1 THEN dm = VAL(MID$(d$, l1))
  126.     IF l3 THEN tm = VAL(MID$(d$, l3 + 1))
  127.     IF l4 > 0 THEN kh = VAL(MID$(d$, l4 + 2))
  128.     IF l4 < 0 THEN kl = VAL(MID$(d$, -l4 + 2))
  129.     IF kl < 0 THEN kl = 0
  130.     IF kh < 0 THEN kh = 0
  131.     IF kl > d THEN kl = d
  132.     IF kh > d THEN kh = d
  133.  
  134.     REDIM Results(s, d) AS LONG
  135.     REDIM Rolls(d - 1) AS LONG, LowRolls(d - 1) AS LONG
  136.     Results(0, 0) = tm
  137.     FOR j = 1 TO s
  138.         FOR i = 1 TO d
  139.             Rolls(i - 1) = INT(RND * n) + 1 + dm
  140.             'PRINT Rolls(i - 1),                               'UNREMARK THIS LINE TO SEE UNSORTED NUMBERS
  141.         NEXT
  142.         'PRINT                                                 'AND THIS LINE TO SEE UNSORTED NUMBERS
  143.  
  144.         'The dice sorting routine, optimized to use _MEM and a comb sort algorithm.  It's more than fast enough for our needs here I think.  ;)
  145.         IF d > 1 THEN 'No need to try and sort just one dice
  146.             DIM m AS _MEM
  147.             DIM o AS _OFFSET, o1 AS _OFFSET
  148.             DIM t AS LONG, t1 AS LONG
  149.             m = _MEM(Rolls())
  150.             $CHECKING:OFF
  151.             gap = UBOUND(Rolls)
  152.  
  153.             DO
  154.                 gap = 10 * gap \ 13
  155.                 IF gap < 1 THEN gap = 1
  156.                 i = 0
  157.                 swapped = 0
  158.                 DO
  159.                     o = m.OFFSET + i * 4
  160.                     o1 = m.OFFSET + (i + gap) * 4
  161.                     IF _MEMGET(m, o, LONG) > _MEMGET(m, o1, LONG) THEN
  162.                         _MEMGET m, o1, t1
  163.                         _MEMGET m, o, t
  164.                         _MEMPUT m, o1, t
  165.                         _MEMPUT m, o, t1
  166.                         swapped = -1
  167.                     END IF
  168.                     i = i + 1
  169.                 LOOP UNTIL i + gap > UBOUND(Rolls)
  170.             LOOP UNTIL swapped = 0 AND gap = 1
  171.             $CHECKING:ON
  172.             _MEMFREE m
  173.         END IF
  174.  
  175.         'FOR i = 1 TO d: PRINT Rolls(i - 1),: NEXT: PRINT      'UNREMARK THIS LINE TO SEE THE SORTED NUMBERS, BEFORE WE DISCARD LOW OR HIGH
  176.  
  177.         IF kh THEN 'drop the undesired low rolls
  178.             FOR i = 1 TO d - kh: Rolls(i - 1) = 0: NEXT
  179.         END IF
  180.         IF kl THEN 'drop the undesired high rolls
  181.             FOR i = kl + 1 TO d: Rolls(i - 1) = 0: NEXT
  182.         END IF
  183.         FOR i = 1 TO d
  184.             Results(j, i) = Rolls(i - 1)
  185.         NEXT
  186.     NEXT
  187.  
  188.  
  189.     FOR j = 1 TO s
  190.         total = 0
  191.         FOR i = 1 TO d
  192.             total = total + Results(j, i)
  193.         NEXT
  194.         total = total + Results(0, 0) 'Results 0,0 holds our total modifier
  195.         Results(j, 0) = total 'And store the total in the 0 element of the array
  196.     NEXT
  197.  
  198.  
  199.  
  200.  
  201.  
  202. SUB PrintResults
  203.     FOR s = 1 TO UBOUND(results, 1)
  204.         FOR i = 1 TO UBOUND(results, 2)
  205.             PRINT Results(s, i);
  206.             IF i < UBOUND(results, 2) THEN PRINT " + ";
  207.         NEXT
  208.         PRINT " = "; Results(s, 0)
  209.     NEXT
  210.  

So there's a ton of dice rolling routines out there already you say...   You ask, "What makes this one different?"

Well, I'm glad you asked!  (Or that I imagined you'd ask...)

This routine lets us send it a STRING, and it parses that single string and sends us back the results that we wanted.
String format is: #S#D#+#T#

Sounds complicated at first, but it's not.
#S is the numbers of sets of dice we want rolled.
#D# is the number of dice in the set, and the size of the dice.
+# is the modifier for each dice
T# is the total modifier.

So some working examples -- run the program and type these in and see the way it works.
3d4
3d6
3d10
3d4+10
3d4-10
3d4+10T100
3d4-10T-100

Now those will get you single runs of the wanted rolls.   Also give these a fast try
5s3d10
10s2d4+1T10

To break down the last valid command there (10s2d4+1T10), what it's telling the program to do is:
Roll 10 SETS
Of 2
D4 (a four sided dice)
add 1 to each dice roll
and then add 10 to the total

so the numbers we generate will be 1-4+1, or 2-5.   For 2 dice, which gives us a range of 4-10.    And then we add 10, so our range is now 14-20.
And we do this for 10 sets which fall into that end range of 14-20...



Notice this has two main processing routines for us:

QuickDice, which is an easy way to plug into an existing QB64 program and pass simple parameters to it for, and then it generates our formula for processing (for folks who aren't interested in learning the rolling syntax).

RollDice, which takes the proper syntax as illustrated above, and then sends us the results so we can use them in our programs however we see fit. 

Results from the diceroll are returned via a 2 dimensional array called, fitting enough, Results(set, roll)

So if we send it "3S2d6" -- the command to roll 3 sets of 2D6, what we get back is a 2 dimensional array,   Results(3,2), which looks like:
Results(1,1) = 1
Results(1,2) = 3
Results(2,1) = 2
Results(2,2) = 5
Results(3,1) = 3
Results(3,2) = 1

The first 2 results are the first set (1,3), the next are the second set (2,5), and the last is the third set (3,1).

If all we care about is the TOTAL of the rolls, it's pre-added in the second index.
Results(1,0) = 4
Results(2,0) = 7
Results(3,0) = 4

Send it a string formula, get back an array of results...



It's actually a very powerful little routine, which can be used in all sorts of ways.  The main purpose of Steve's Ole Dice Roller (the work in progress) is to add more functionality and a simple UI so people who aren't used to these type dice rolls will have an easier way to interact with the roller.  ;)


https://github.com/SteveMcNeill/Steve64 — A github collection of all things Steve!

Offline STxAxTIC

  • Library Staff
  • Forum Resident
  • Posts: 1091
  • he lives
    • View Profile
Re: Steve's Simple Dice Roller
« Reply #1 on: January 17, 2019, 12:27:38 pm »
Thanks for the info-download... I've been having a look.

One question that I had fun pursing is the probability of getting certain numbers on a dice roll knowing that certain results are excluded. I may or may not have come up with an alternative derivation of geometric series along the way.

Beginning with the clear question, start with a dice that is inherently six sided, but modify the "2" face so that some other random face is attained in place of 2 during a random toss. (Our definition of a re-roll.) What is the probability of getting a "3" after one trial, or two, or N, or infinite?

Denote A as the avoided face "2", and denote B as *all* other valid numbers 1,3,4,5,6. After one trial, we find:

P(A) = 1/6     and     P(B) = 5/6

Denote the vertical slash ( | ) as the word "given", so we may handle trials with multiple tosses. It follows that the probability of getting two "2"-s in a row is written:

P(A|A) = (1/6)*(1/6)

... and meanwhile the probability of re-rolling to yield valid number after first landing on "2" is:

P(B|A) = (5/6)*(1/6)

Extending this out to N trials, we evidently have:

P(A|...|A) = (1/6)^N

P(B|...|A) = (5/6) * (1/6)^(N-1)

Those are the star results, but before writing a sermon on them, exploit the probabilistic axiom that the probability of all events must sum to 1. First suppose the first toss resulted in a valid number, so we list P(B). Then suppose the first toss landed on 2, which re-rolled to give a valid number, corresponding to P(B|A). Next, suppose that even the re-roll landed on a 2, but the second re-roll was valid, so we must list P(B|A|A) as that event... and so on... After expending this analysis, we write:

P(All valid events) = P(B) + P(B|A) + P(B|A|A) + P(B|A|A|A) + P(B|A|A|A|A) + ...

On the left side, we simply have 1. On the right, the series becomes:

1 = (5/6) + (5/6)*(1/6) + (5/6)*(1/6)^2 + (5/6)*(1/6)^3 + (5/6)*(1/6)^4 + ...

1 = (5/6) * (1 + (1/6) + (1/6)^2 + (1/6)^3 + (1/6)^4 + ...)

... which, for those who know about geometric series, converges to the proper fraction. For those who didn't, we just proved that:

1 + (1/6) + (1/6)^2 + (1/6)^3 + (1/6)^4 + ... = 6/5

... Alright that's enough for a minute. With all this I advance the question: what is the probability of getting two 3's in a row on a dice that refuses 2's IF (i) infinite re-rolls are allowed, or (ii) infinite re-rolls are forbidden?

« Last Edit: January 17, 2019, 12:48:01 pm by STxAxTIC »
You're not done when it works, you're done when it's right.

Offline SMcNeill

  • QB64 Developer
  • Forum Resident
  • Posts: 3972
    • View Profile
    • Steve’s QB64 Archive Forum
Re: Steve's Simple Dice Roller
« Reply #2 on: January 17, 2019, 12:53:36 pm »
Quote
... Alright that's enough for a minute. With all this I advance the question: what is the probability of getting two 3's in a row on a dice that refuses 2's?

Assuming you have a 6-sided dice, I’d say 1/25 as long as you don’t count the discards...

Roll a D6.  The chance of any number coming up should be an equal 1 in 6 probability...  IF a 2 is rolled, regardless of number of rerolls needed to get rid of it, the numbers that replace it all have the same equal chance of appearing, effectively reducing your probability of any number appearing to become 1 in 5.

With that base 1 in 5, the chance of the same number appearing twice would be (1/5) * (1/5) = (1/25), or 1 in 25...

Now IF you count the discard, you only have a “natural” 1 in 6 chance, so (1/6) * (1/6) = (1/36) chance on a normal dice.

Unless my logic is wrong somehow?
https://github.com/SteveMcNeill/Steve64 — A github collection of all things Steve!

Offline STxAxTIC

  • Library Staff
  • Forum Resident
  • Posts: 1091
  • he lives
    • View Profile
Re: Steve's Simple Dice Roller
« Reply #3 on: January 17, 2019, 01:20:57 pm »
Ah, that answers my original thing about your method - you are indeed allowing infinite re-rolls when its needed, so the dice is ultimately N-1 sided.

To prove this out in my notation, let C equal any valid number (not all of them like B was), and we write:

P(C) = p(c) + p(C|A) + p(c|A|A) + ...
P(C) = (1/6) + (1/6)^2 + (1/6)^3 + ...
P(C) = (1/6)*(5/6)
P(C) = 1/5

So what just happened? If we have a dice that allows for infinite, on-the-spot re-rolls when it lands on a given face, the rest of the faces have probability 1/5.

This means the answer is (1/5)^2 for infinite re-rolls allowed,
... and the answer is (1/6)^2 for zero re-rolls...

So NOW we may wonder about the in-between cases... oh boy...

EDIT: But of course if you rule out the in-between cases and go only with the infinite re-roll - well, for those following this thread - we just found it isn't actually needed. That's right, no re-roll needed, because you just use an N-1 face dice in that slot.
« Last Edit: January 17, 2019, 01:32:33 pm by STxAxTIC »
You're not done when it works, you're done when it's right.

Offline Pete

  • Forum Resident
  • Posts: 2361
  • Cuz I sez so, varmint!
    • View Profile
Re: Steve's Simple Dice Roller
« Reply #4 on: January 17, 2019, 01:38:43 pm »
I hope you two figure this out before QB-Quantum is released. Then this project will have to be renamed Steve's not so Simple Quantum Dice Roller.

My advice would be to establish a standard and base the code on that standard. We love your programs Steve, but no worries about any of us loving this one so much we make an infinite number of rolls when using it. So if you go with the infinity model, you should be OK.

Pete
Want to learn how to write code on cave walls? https://www.tapatalk.com/groups/qbasic/qbasic-f1/

Offline SMcNeill

  • QB64 Developer
  • Forum Resident
  • Posts: 3972
    • View Profile
    • Steve’s QB64 Archive Forum
Re: Steve's Simple Dice Roller
« Reply #5 on: January 17, 2019, 01:53:50 pm »
Quote
This means the answer is (1/5)^2 for infinite re-rolls allowed,
... and the answer is (1/6)^2 for zero re-rolls...

So NOW we may wonder about the in-between cases... oh boy...

In between cases would converge on the 1/5 probability with the more dice rolls allowed, with a chance for “Invalid Result” to appear based upon your limit.

Roll twice:
1 in 6 chance the first roll is invalid (2).
1 in 6 chance the second roll is invalid (2).   <— 1 in 36 chance we don’t even get a valid result...

To get a 3, with only one reroll, the chance is:
1/6 natural + (1/6 for a 2) * (1/6 for a 3 on the reroll)

The more rerolls, the more the + (1/6 for a 2) * (1/6 for a 3 on the reroll) factors, leading towards the convergence of 1/25 of infinite rerolls.
https://github.com/SteveMcNeill/Steve64 — A github collection of all things Steve!

Offline STxAxTIC

  • Library Staff
  • Forum Resident
  • Posts: 1091
  • he lives
    • View Profile
Re: Steve's Simple Dice Roller
« Reply #6 on: January 17, 2019, 02:05:19 pm »
All sounds good. So will there be an option in the program to set the number of re-rolls, or will this be infinite?
You're not done when it works, you're done when it's right.

Offline SMcNeill

  • QB64 Developer
  • Forum Resident
  • Posts: 3972
    • View Profile
    • Steve’s QB64 Archive Forum
Re: Steve's Simple Dice Roller
« Reply #7 on: January 17, 2019, 02:19:17 pm »
All sounds good. So will there be an option in the program to set the number of re-rolls, or will this be infinite?

I can add another option to set one easily enough, if somebody needs such a thing, but unless it’s requested, I’ll just go with infinite rerolls.  I can’t see much point in rolling a dice for gameplay and saying, “It’s nothing — invalid result.”  ;)
https://github.com/SteveMcNeill/Steve64 — A github collection of all things Steve!

Offline STxAxTIC

  • Library Staff
  • Forum Resident
  • Posts: 1091
  • he lives
    • View Profile
Re: Steve's Simple Dice Roller
« Reply #8 on: January 17, 2019, 02:31:48 pm »
Excellent - so if it's going to be stuck to the infinite re-roll case, which is indistinguishable from an N-1 dice, why still include it? Could save you dozens or hundreds of lines to realize this early.
You're not done when it works, you're done when it's right.

Offline SMcNeill

  • QB64 Developer
  • Forum Resident
  • Posts: 3972
    • View Profile
    • Steve’s QB64 Archive Forum
Re: Steve's Simple Dice Roller
« Reply #9 on: January 17, 2019, 02:39:12 pm »
Because most people don’t want to have to custom set dice faces....

Roll 2D6, reroll 2’s... 

You have equal chances of rolling (1, 3, 4, 5, 6), but PRINT INT(RND * 5) + 1 would only give you values of (1,2,3,4,5)...  You’d have to set custom faces to the 5-sided die to see those results, which most folks find much easier to just roll a D6 and then reroll the 2s.  ;)
https://github.com/SteveMcNeill/Steve64 — A github collection of all things Steve!

Offline STxAxTIC

  • Library Staff
  • Forum Resident
  • Posts: 1091
  • he lives
    • View Profile
Re: Steve's Simple Dice Roller
« Reply #10 on: January 17, 2019, 02:48:03 pm »
Okay, so it's a tradeoff, right?

(i) You either re-map the calculated result to the public-facing values... i.e. re-map all 2's to something else... which happens in one pass... Which is of course done internally so the user is none-the-wiser...

or your method,

(ii) Re-roll on the fly until the a desirable answer comes up. This is problematic for a six-sided dice, but consider something with fewer sides, like 3. This means you will spend about a third of total loop time discarding bad answers. These examples are trivial but are enough so show it doesn't scale. Just saying, remapping is where its at.
You're not done when it works, you're done when it's right.

Offline SMcNeill

  • QB64 Developer
  • Forum Resident
  • Posts: 3972
    • View Profile
    • Steve’s QB64 Archive Forum
Re: Steve's Simple Dice Roller
« Reply #11 on: January 17, 2019, 03:30:33 pm »
Only problem with re-mapping is it’s a lot more work than rerolling.

Say you want to roll a 100-sided dice and reroll all unlucky 13s...

A simple loop of the following fixes the reroll issue:

DO
    result = INT(RND * 100) + 1
LOOP UNTIL result <> 13


To remap, you’d need:
DIM sides (1 TO 99)
FOR I = 1 TO 99
   count = count + 1
   IF count <> 13 then sides(I) = count
NEXT

result = sides(INT(RND * 99) + 1)

************

Just seems simpler to me to do a reroll, and loop time should be inconsequential. Even if we go crazy and reroll all 1’s on a 2-sided dice, the chances are we’ll get a 2 is almost guaranteed after a dozen rolls or so, and I can’t think of any PC that can’t do those calculations in a few nanoseconds for us.  ;)
https://github.com/SteveMcNeill/Steve64 — A github collection of all things Steve!

Offline STxAxTIC

  • Library Staff
  • Forum Resident
  • Posts: 1091
  • he lives
    • View Profile
Re: Steve's Simple Dice Roller
« Reply #12 on: January 17, 2019, 03:52:59 pm »
Meh, if we're going to argue by knocking over straw men, at least use a similarly-trivial example of remapping:

DO
    result = INT(RND * 99) + 1
    IF result = 13 THEN result = 100
    PRINT result
LOOP

The idea is: to skip 13 on a 100 sided dice, roll a 99 sided dice instead, and boot all instances of 13 to the 100 case. This extends to far more complicated cases with zero wasted loop time.
You're not done when it works, you're done when it's right.