197 lines
5.9 KiB
Org Mode
197 lines
5.9 KiB
Org Mode
|
#+title: Python Programming Dice Probabilities
|
||
|
#+auto_tangle: t
|
||
|
#+PROPERTY: session *python*
|
||
|
#+PROPERTY: cache yes
|
||
|
#+PROPERTY: exports both
|
||
|
#+PROPERTY: header-args :tangle pydiceprob.py
|
||
|
* Introduction
|
||
|
:LOGBOOK:
|
||
|
CLOCK: [2024-04-19 Fri 10:33]--[2024-04-19 Fri 12:50] => 2:17
|
||
|
CLOCK: [2024-04-17 Wed 13:40]--[2024-04-17 Wed 14:14] => 0:34
|
||
|
:END:
|
||
|
|
||
|
I was given a problem as such:
|
||
|
|
||
|
#+begin_quote
|
||
|
You have two players, Bob and Alice, that take turns in rolling a fair k-sided die. Whoever rolls a
|
||
|
k first wins the game. The Python program should output the probability that Bob wins the
|
||
|
game for k = 6 thru 99. That is, the output will be an array of probabilities where index 0 is the
|
||
|
probability when k = 6; index 1 when k = 7; etc.
|
||
|
#+end_quote
|
||
|
|
||
|
Bonus points:
|
||
|
#+begin_quote
|
||
|
If you have 8me and interest, create a REST server rather than a console program. Flask or
|
||
|
FastAPI can be used. The REST endpoint should be a GET, accept no Request Body, accept an
|
||
|
op8onal Header for “k”, and return:
|
||
|
- The array of probabili8es in JSON format if no “k” is provided in the Header
|
||
|
- A single probability in JSON format if a “k” is provided in the Header
|
||
|
#+end_quote
|
||
|
|
||
|
* Thinking
|
||
|
Given the problem's nature, we need the following functionality:
|
||
|
|
||
|
1. Output the probability for Bob to win.
|
||
|
2. Create an array of probabilities that contains the data for the output.
|
||
|
3. Calculate the probabilities based on the value of =k=.
|
||
|
4. Define the value of =k=, between 6 and 99 inclusive.
|
||
|
|
||
|
For the bonus secion:
|
||
|
1. Create a FastAPI endpoint that takes an optional Header for =k=.
|
||
|
2. Return either the full array of probabilities (no =k= provided), or return the
|
||
|
specific probability if =k= is provided.
|
||
|
|
||
|
|
||
|
* Code
|
||
|
The following is python code blocks, with the documentation attached. Using
|
||
|
=org-babel=, these are tangled into =pydiceprob.py=,
|
||
|
which is the final python script.
|
||
|
** Imports
|
||
|
#+begin_src python
|
||
|
import sys
|
||
|
import json
|
||
|
import pprint
|
||
|
#+end_src
|
||
|
|
||
|
#+RESULTS:
|
||
|
: None
|
||
|
** Pretty Print
|
||
|
#+begin_src python
|
||
|
pp = pp.Printer = pprint.PrettyPrinter(indent=2, compact=True)
|
||
|
#+end_src
|
||
|
And a usage printe
|
||
|
|
||
|
#+begin_src python
|
||
|
def usagequit():
|
||
|
print("""Usage: python pydiceprob.py [k] [mode]
|
||
|
k between 6 and 99
|
||
|
modes: single, single-table, multi, multi-table
|
||
|
single prints out the probability of a single throw with a k-sided dice yielding k.
|
||
|
single-table prints out a table between 6 and k with the probabilities of a single throw yielding k.
|
||
|
multi prints out the probability of winning (assuming we're going first) in a game where two players take turns and the person who throws k first wins.
|
||
|
multi-table prints out the probability of winning for a range between 6 and k if you have the first throw.""")
|
||
|
quit()
|
||
|
#+end_src
|
||
|
** Main
|
||
|
Main function for managing CLI interactions, and dispatch
|
||
|
#+begin_src python
|
||
|
def main():
|
||
|
global k
|
||
|
try:
|
||
|
if int(sys.argv[1]) >= 6:
|
||
|
k = int(sys.argv[1])
|
||
|
else:
|
||
|
usagequit()
|
||
|
except IndexError:
|
||
|
usagequit()
|
||
|
global mode
|
||
|
try:
|
||
|
mode = sys.argv[2]
|
||
|
except IndexError:
|
||
|
usagequit()
|
||
|
dispatch(mode, k)
|
||
|
|
||
|
#+end_src
|
||
|
|
||
|
#+begin_src python
|
||
|
def dispatch(mode, k):
|
||
|
modes = ["single", "multi", "single-table", "multi-table"]
|
||
|
if mode not in modes:
|
||
|
usagequit()
|
||
|
if mode == "single":
|
||
|
print(first_turn_probability(k))
|
||
|
elif mode == "multi" :
|
||
|
print(multi_turn_single(k))
|
||
|
elif mode == "single-table":
|
||
|
print(first_turn_protatbilities(k))
|
||
|
elif mode == "multi-table":
|
||
|
print(multi_turn_table(k))
|
||
|
|
||
|
|
||
|
#+end_src
|
||
|
** Array of dice and players
|
||
|
Second, we need to grab the value of =k=, the size of the dice we're calculating
|
||
|
the probability for. Depending on the implementation we choose to use, we'll be either
|
||
|
grabbing it from the CLI, or the REST API. For now, this will remain in the CLI.
|
||
|
|
||
|
Then for the bonus code, we need several steps. First, we need to generate a list
|
||
|
such that we have numbers between 6 and 99 in it.
|
||
|
#+begin_src python
|
||
|
def dice(k):
|
||
|
if k < 6:
|
||
|
print("Dice must be at least 6-sided")
|
||
|
elif k == 6:
|
||
|
dice_array = [6]
|
||
|
else:
|
||
|
dice_array = list(range(6, k))
|
||
|
#+end_src
|
||
|
|
||
|
#+RESULTS:
|
||
|
: None
|
||
|
|
||
|
|
||
|
** Single turn win probability
|
||
|
Then, we have to calculate the probability of a win on each throw given a dice
|
||
|
of size =k=.
|
||
|
|
||
|
Both players (Alice and Bob) throw dice, alternating, and the person who throws the top number first (=k=) wins. Bob always goes first for simplicity.
|
||
|
|
||
|
A dice of size =k= has a 1/k probability of winning each throw.
|
||
|
|
||
|
=k+1= here, because python calculates range(a, b) indices in the way of b-a, so
|
||
|
for a = 6 and b = 10, it'll give an array of 4 items (10-6 = 4) as such:
|
||
|
~[6, 7, 8, 9]~, as it counts the first item as 1.
|
||
|
|
||
|
Using =k+1= we can get the correct, inclusive array that we desire: ~[6, 7, 8, 9, 10]~.
|
||
|
#+begin_src python
|
||
|
def first_turn_probabilities(k):
|
||
|
result = {}
|
||
|
for k in range(6, int(k)+1):
|
||
|
result[k] = 1/k
|
||
|
return result
|
||
|
|
||
|
def first_turn_probability(k):
|
||
|
return first_turn_probabilities(k)[k]
|
||
|
|
||
|
#+end_src
|
||
|
|
||
|
#+RESULTS:
|
||
|
** Multi-turn win probability
|
||
|
Over many throws, the advantage of winning will decrease, approaching 50%.
|
||
|
|
||
|
We're assuming Bob goes first, and then is followed by Alice.
|
||
|
For each throw, the probability of winning is the same.
|
||
|
|
||
|
#+begin_src python
|
||
|
def multi_turn_single(k):
|
||
|
p_win = 1 / k # Winning probability on any given throw
|
||
|
p_lose = (k-1) / k # Losing probability on any given throw
|
||
|
bob_wins_prob_sum = 0
|
||
|
r = p_lose**2
|
||
|
probability_win = p_win / (1 - r)
|
||
|
return probability_win
|
||
|
#+end_src
|
||
|
|
||
|
And then we want to generate a table of all the probabilities up to =k=.
|
||
|
|
||
|
#+begin_src python
|
||
|
def multi_turn_table(k):
|
||
|
result = {}
|
||
|
for i in range(6, int(k+1)):
|
||
|
result[i] = multi_turn_single(i)
|
||
|
return result
|
||
|
#+end_src
|
||
|
** Print
|
||
|
#+begin_src python
|
||
|
def print(stuff):
|
||
|
pp.pprint(stuff)
|
||
|
#+end_src
|
||
|
|
||
|
** Script
|
||
|
To run as a script.
|
||
|
|
||
|
#+begin_src python
|
||
|
if __name__ == "__main__":
|
||
|
main()
|
||
|
#+end_src
|