Unit tests, and API implemented.

This commit is contained in:
Phil Bajsicki 2024-04-20 09:21:06 +02:00
parent f37debf73f
commit 32d1d2b507
2 changed files with 321 additions and 55 deletions

View file

@ -6,6 +6,8 @@
#+PROPERTY: header-args :tangle pydiceprob.py #+PROPERTY: header-args :tangle pydiceprob.py
* Introduction * Introduction
:LOGBOOK: :LOGBOOK:
CLOCK: [2024-04-20 Sat 09:05]
CLOCK: [2024-04-20 Sat 07:57]--[2024-04-20 Sat 09:04] => 1:07
CLOCK: [2024-04-19 Fri 10:33]--[2024-04-19 Fri 12:50] => 2:17 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 CLOCK: [2024-04-17 Wed 13:40]--[2024-04-17 Wed 14:14] => 0:34
:END: :END:
@ -28,23 +30,40 @@ op8onal Header for “k”, and return:
- A single probability in JSON format if a “k” is provided in the Header - A single probability in JSON format if a “k” is provided in the Header
#+end_quote #+end_quote
* TODOS: I appear to have completed the task, at a comfortable pace, in about 4 hours.
- Unit tests I have omitted the unit tests for the API portion of this, as it would have been
- REST API overkill.
I did choose to include 100 in the calculations, as I find that just a little
bit more elegant. There is something special about ending a list on a nice round number.
* Usage
This repository contains multiple files.
1. The pdf with the problem I was given.
2. README.org - an Emacs org-mode file, which you're reading right now.
3. pydiceprob.py - the main python file, which works in CLI as such:
- ~python pydiceprob.py [dice_size] [mode]~
- ~dice_size~ can be anything in the CLI, but note that excessively large
numbers are pointless, as the probability of a single throw will always be
1/dice_size, while the probability of winning in the game will always be
approaching 50%.
- The modes are:
- ~single~ (for single throw)
- ~single-table~ (for a table of probabilities to win on a single throw
across a variety of dice sizes)
- ~multi~ (winning probability in the game)
- ~multi-table~ (winning probabilities in the game across a variety of dice sizes)
- *Alternatively*, you can start a REST API (implemented using FastAPI) using
uvicorn. Run ~uvicorn pydiceprob:app~ from the same directory as the
=pydiceprob.py= file.
- You can then see the outputs using =curl=, such as:
- ~curl 127.0.0.1:8000~ or
- ~curl 127.0.0.1:8000 -H "k: [dice_size]"~
- Please note that I have chosen to limit the dice size to be between 6
and 100 inclusive on the API.
4. tests.py - simple tests for the probability calculations.
* 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 * Code
The following is python code blocks, with the documentation attached. Using The following is python code blocks, with the documentation attached. Using
=org-babel=, these are tangled into =pydiceprob.py=, =org-babel=, these are tangled into =pydiceprob.py=,
@ -54,6 +73,8 @@ which is the final python script.
import sys import sys
import json import json
import pprint import pprint
from fastapi import FastAPI, Header
from pydantic import BaseModel
#+end_src #+end_src
#+RESULTS: #+RESULTS:
@ -67,7 +88,7 @@ And a usage printe
#+begin_src python #+begin_src python
def usagequit(): def usagequit():
print("""Usage: python pydiceprob.py [k] [mode] print("""Usage: python pydiceprob.py [k] [mode]
k between 6 and 99 k between 6 and 99 (higher numbers are supported, up to however much your memory can handle; given that win probabilities approach 50%, this isn't very useful past about 100.)
modes: single, single-table, multi, multi-table modes: single, single-table, multi, multi-table
single prints out the probability of a single throw with a k-sided dice yielding k. 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. single-table prints out a table between 6 and k with the probabilities of a single throw yielding k.
@ -75,6 +96,29 @@ def usagequit():
multi-table prints out the probability of winning for a range between 6 and k if you have the first throw.""") multi-table prints out the probability of winning for a range between 6 and k if you have the first throw.""")
quit() quit()
#+end_src #+end_src
** FastAPI
:LOGBOOK:
CLOCK: [2024-04-20 Sat 09:04]--[2024-04-20 Sat 09:05] => 0:01
:END:
#+begin_src python
app = FastAPI()
class APIget(BaseModel):
result: dict
@app.get("/")
async def api_get(k: int = Header(default=None)):
if not isinstance(k, int) and k is not None:
return {"message": "Header k must be an integer between 6 and 100 inclusive or not present."}
if k is None:
result = one_turn_table(100)
return {"Probabilities of throwing the highest number on dice of size": result}
elif k >= 6 and k <= 100:
result = one_turn_single(k)
return {f"Probability of throwing the highest number of dice of size {k}": result}
else:
return {"message": "Usage: no request body, use header k to specify what probability you want, if no k, then a table is given between dice sizes 6 and 100 inclusive."}
#+end_src
** Main ** Main
Main function for managing CLI interactions, and dispatch Main function for managing CLI interactions, and dispatch
#+begin_src python #+begin_src python
@ -102,37 +146,16 @@ def dispatch(mode, k):
if mode not in modes: if mode not in modes:
usagequit() usagequit()
if mode == "single": if mode == "single":
print(first_turn_probability(k)) print(one_turn_single(k))
elif mode == "multi" : elif mode == "multi" :
print(multi_turn_single(k)) print(multi_turn_single(k))
elif mode == "single-table": elif mode == "single-table":
print(first_turn_protatbilities(k)) print(one_turn_table(k))
elif mode == "multi-table": elif mode == "multi-table":
print(multi_turn_table(k)) print(multi_turn_table(k))
#+end_src #+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 ** Single turn win probability
Then, we have to calculate the probability of a win on each throw given a dice Then, we have to calculate the probability of a win on each throw given a dice
of size =k=. of size =k=.
@ -147,14 +170,14 @@ for a = 6 and b = 10, it'll give an array of 4 items (10-6 = 4) as such:
Using =k+1= we can get the correct, inclusive array that we desire: ~[6, 7, 8, 9, 10]~. Using =k+1= we can get the correct, inclusive array that we desire: ~[6, 7, 8, 9, 10]~.
#+begin_src python #+begin_src python
def first_turn_probabilities(k): def one_turn_table(k):
result = {} result = {}
for k in range(6, int(k)+1): for k in range(6, int(k)+1):
result[k] = 1/k result[k] = 1/k
return result return result
def first_turn_probability(k): def one_turn_single(k):
return first_turn_probabilities(k)[k] return one_turn_table(k)[k]
#+end_src #+end_src
@ -197,3 +220,235 @@ To run as a script.
if __name__ == "__main__": if __name__ == "__main__":
main() main()
#+end_src #+end_src
* Unit testing
** Imports
#+begin_src python :tangle test.py
import unittest
import pydiceprob
#+end_src
** Single-turn win probabilities
Then we test the range from 6 to 100. The script supports more, but this is the
part that /must/ be correct.
Because the function =one_turn_single= pulls directly from
=one_turn_table=, we can simply iterate over =one_turn_single=,
ensuring that both functions work as desired at the same time.
This, however, is only because we're dealing with exceedingly trivial code,
where the singular function merely narrows down the data from the plural.
#+begin_src python :tangle test.py
class TestSingle(unittest.TestCase):
def test_single(self):
data = {6: 0.16666666666666666,
7: 0.14285714285714285,
8: 0.125,
9: 0.1111111111111111,
10: 0.1,
11: 0.09090909090909091,
12: 0.08333333333333333,
13: 0.07692307692307693,
14: 0.07142857142857142,
15: 0.06666666666666667,
16: 0.0625,
17: 0.058823529411764705,
18: 0.05555555555555555,
19: 0.05263157894736842,
20: 0.05,
21: 0.047619047619047616,
22: 0.045454545454545456,
23: 0.043478260869565216,
24: 0.041666666666666664,
25: 0.04,
26: 0.038461538461538464,
27: 0.037037037037037035,
28: 0.03571428571428571,
29: 0.034482758620689655,
30: 0.03333333333333333,
31: 0.03225806451612903,
32: 0.03125,
33: 0.030303030303030304,
34: 0.029411764705882353,
35: 0.02857142857142857,
36: 0.027777777777777776,
37: 0.02702702702702703,
38: 0.02631578947368421,
39: 0.02564102564102564,
40: 0.025,
41: 0.024390243902439025,
42: 0.023809523809523808,
43: 0.023255813953488372,
44: 0.022727272727272728,
45: 0.022222222222222223,
46: 0.021739130434782608,
47: 0.02127659574468085,
48: 0.020833333333333332,
49: 0.02040816326530612,
50: 0.02,
51: 0.0196078431372549,
52: 0.019230769230769232,
53: 0.018867924528301886,
54: 0.018518518518518517,
55: 0.01818181818181818,
56: 0.017857142857142856,
57: 0.017543859649122806,
58: 0.017241379310344827,
59: 0.01694915254237288,
60: 0.016666666666666666,
61: 0.01639344262295082,
62: 0.016129032258064516,
63: 0.015873015873015872,
64: 0.015625,
65: 0.015384615384615385,
66: 0.015151515151515152,
67: 0.014925373134328358,
68: 0.014705882352941176,
69: 0.014492753623188406,
70: 0.014285714285714285,
71: 0.014084507042253521,
72: 0.013888888888888888,
73: 0.0136986301369863,
74: 0.013513513513513514,
75: 0.013333333333333334,
76: 0.013157894736842105,
77: 0.012987012987012988,
78: 0.01282051282051282,
79: 0.012658227848101266,
80: 0.0125,
81: 0.012345679012345678,
82: 0.012195121951219513,
83: 0.012048192771084338,
84: 0.011904761904761904,
85: 0.011764705882352941,
86: 0.011627906976744186,
87: 0.011494252873563218,
88: 0.011363636363636364,
89: 0.011235955056179775,
90: 0.011111111111111112,
91: 0.01098901098901099,
92: 0.010869565217391304,
93: 0.010752688172043012,
94: 0.010638297872340425,
95: 0.010526315789473684,
96: 0.010416666666666666,
97: 0.010309278350515464,
98: 0.01020408163265306,
99: 0.010101010101010102,
100: 0.01}
for t in range(6, 100):
result = pydiceprob.one_turn_single(t)
self.assertEqual(result, data[t], f"Should be {data[t]} for dice size {t}.")
#+end_src
** Multi-turn test
As with the other test, we're feeding the correct values, iterating over the
possible outputs of the =multi_turn_single= function.
#+begin_src python :tangle test.py
def test_multi(self):
data = { 6: 0.5454545454545455,
7: 0.5384615384615383,
8: 0.5333333333333333,
9: 0.5294117647058822,
10: 0.5263157894736844,
11: 0.5238095238095235,
12: 0.5217391304347823,
13: 0.5200000000000005,
14: 0.5185185185185185,
15: 0.5172413793103451,
16: 0.5161290322580645,
17: 0.5151515151515152,
18: 0.5142857142857141,
19: 0.5135135135135128,
20: 0.5128205128205127,
21: 0.5121951219512191,
22: 0.511627906976745,
23: 0.5111111111111115,
24: 0.5106382978723412,
25: 0.510204081632653,
26: 0.5098039215686275,
27: 0.5094339622641498,
28: 0.5090909090909096,
29: 0.5087719298245623,
30: 0.5084745762711862,
31: 0.5081967213114762,
32: 0.5079365079365079,
33: 0.5076923076923086,
34: 0.5074626865671636,
35: 0.5072463768115941,
36: 0.507042253521126,
37: 0.506849315068494,
38: 0.5066666666666677,
39: 0.5064935064935064,
40: 0.5063291139240501,
41: 0.506172839506172,
42: 0.5060240963855414,
43: 0.5058823529411758,
44: 0.5057471264367819,
45: 0.5056179775280893,
46: 0.5054945054945053,
47: 0.505376344086021,
48: 0.5052631578947361,
49: 0.5051546391752573,
50: 0.5050505050505041,
51: 0.5049504950495041,
52: 0.504854368932038,
53: 0.5047619047619043,
54: 0.5046728971962623,
55: 0.5045871559633027,
56: 0.5045045045045037,
57: 0.5044247787610603,
58: 0.5043478260869556,
59: 0.5042735042735057,
60: 0.504201680672268,
61: 0.5041322314049588,
62: 0.5040650406504076,
63: 0.5039999999999981,
64: 0.5039370078740157,
65: 0.5038759689922497,
66: 0.5038167938931295,
67: 0.5037593984962382,
68: 0.5037037037037063,
69: 0.5036496350364972,
70: 0.5035971223021601,
71: 0.5035460992907805,
72: 0.5034965034965061,
73: 0.5034482758620673,
74: 0.5034013605442197,
75: 0.5033557046979886,
76: 0.5033112582781448,
77: 0.5032679738562092,
78: 0.5032258064516143,
79: 0.5031847133757983,
80: 0.5031446540880515,
81: 0.5031055900621104,
82: 0.503067484662576,
83: 0.5030303030303014,
84: 0.502994011976049,
85: 0.5029585798816592,
86: 0.5029239766081863,
87: 0.5028901734104042,
88: 0.5028571428571439,
89: 0.5028248587570601,
90: 0.502793296089388,
91: 0.5027624309392276,
92: 0.5027322404371553,
93: 0.5027027027027039,
94: 0.5026737967914459,
95: 0.5026455026455015,
96: 0.5026178010471216,
97: 0.5025906735751302,
98: 0.502564102564102,
99: 0.5025380710659929,
100: 0.5025125628140696}
for t in range(6, 100):
result = pydiceprob.multi_turn_single(t)
self.assertEqual(result, data[t], f"Should be {data[t]} for dice size {t}.")
#+end_src
** Test main entry
#+begin_src python :tangle test.py
if __name__ == '__main__':
unittest.main()
#+end_src

View file

@ -1,12 +1,14 @@
import sys import sys
import json import json
import pprint import pprint
from fastapi import FastAPI, Header
from pydantic import BaseModel
pp = pp.Printer = pprint.PrettyPrinter(indent=2, compact=True) pp = pp.Printer = pprint.PrettyPrinter(indent=2, compact=True)
def usagequit(): def usagequit():
print("""Usage: python pydiceprob.py [k] [mode] print("""Usage: python pydiceprob.py [k] [mode]
k between 6 and 99 k between 6 and 99 (higher numbers are supported, up to however much your memory can handle; given that win probabilities approach 50%, this isn't very useful past about 100.)
modes: single, single-table, multi, multi-table modes: single, single-table, multi, multi-table
single prints out the probability of a single throw with a k-sided dice yielding k. 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. single-table prints out a table between 6 and k with the probabilities of a single throw yielding k.
@ -14,6 +16,23 @@ def usagequit():
multi-table prints out the probability of winning for a range between 6 and k if you have the first throw.""") multi-table prints out the probability of winning for a range between 6 and k if you have the first throw.""")
quit() quit()
app = FastAPI()
class APIget(BaseModel):
result: dict
@app.get("/")
async def api_get(k: int = Header(default=None)):
if not isinstance(k, int) and k is not None:
return {"message": "Header k must be an integer between 6 and 100 inclusive or not present."}
if k is None:
result = one_turn_table(100)
return {"Probabilities of throwing the highest number on dice of size": result}
elif k >= 6 and k <= 100:
result = one_turn_single(k)
return {f"Probability of throwing the highest number of dice of size {k}": result}
else:
return {"message": "Usage: no request body, use header k to specify what probability you want, if no k, then a table is given between dice sizes 6 and 100 inclusive."}
def main(): def main():
global k global k
try: try:
@ -35,30 +54,22 @@ def dispatch(mode, k):
if mode not in modes: if mode not in modes:
usagequit() usagequit()
if mode == "single": if mode == "single":
print(first_turn_probability(k)) print(one_turn_single(k))
elif mode == "multi" : elif mode == "multi" :
print(multi_turn_single(k)) print(multi_turn_single(k))
elif mode == "single-table": elif mode == "single-table":
print(first_turn_protatbilities(k)) print(one_turn_table(k))
elif mode == "multi-table": elif mode == "multi-table":
print(multi_turn_table(k)) print(multi_turn_table(k))
def dice(k): def one_turn_table(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))
def first_turn_probabilities(k):
result = {} result = {}
for k in range(6, int(k)+1): for k in range(6, int(k)+1):
result[k] = 1/k result[k] = 1/k
return result return result
def first_turn_probability(k): def one_turn_single(k):
return first_turn_probabilities(k)[k] return one_turn_table(k)[k]
def multi_turn_single(k): def multi_turn_single(k):
p_win = 1 / k # Winning probability on any given throw p_win = 1 / k # Winning probability on any given throw