Unit tests, and API implemented.
This commit is contained in:
parent
f37debf73f
commit
32d1d2b507
2 changed files with 321 additions and 55 deletions
337
README.org
337
README.org
|
@ -6,6 +6,8 @@
|
|||
#+PROPERTY: header-args :tangle pydiceprob.py
|
||||
* Introduction
|
||||
: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-17 Wed 13:40]--[2024-04-17 Wed 14:14] => 0:34
|
||||
: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
|
||||
#+end_quote
|
||||
|
||||
* TODOS:
|
||||
- Unit tests
|
||||
- REST API
|
||||
I appear to have completed the task, at a comfortable pace, in about 4 hours.
|
||||
I have omitted the unit tests for the API portion of this, as it would have been
|
||||
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
|
||||
The following is python code blocks, with the documentation attached. Using
|
||||
=org-babel=, these are tangled into =pydiceprob.py=,
|
||||
|
@ -54,6 +73,8 @@ which is the final python script.
|
|||
import sys
|
||||
import json
|
||||
import pprint
|
||||
from fastapi import FastAPI, Header
|
||||
from pydantic import BaseModel
|
||||
#+end_src
|
||||
|
||||
#+RESULTS:
|
||||
|
@ -67,7 +88,7 @@ And a usage printe
|
|||
#+begin_src python
|
||||
def usagequit():
|
||||
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
|
||||
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.
|
||||
|
@ -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.""")
|
||||
quit()
|
||||
#+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 function for managing CLI interactions, and dispatch
|
||||
#+begin_src python
|
||||
|
@ -102,37 +146,16 @@ def dispatch(mode, k):
|
|||
if mode not in modes:
|
||||
usagequit()
|
||||
if mode == "single":
|
||||
print(first_turn_probability(k))
|
||||
print(one_turn_single(k))
|
||||
elif mode == "multi" :
|
||||
print(multi_turn_single(k))
|
||||
elif mode == "single-table":
|
||||
print(first_turn_protatbilities(k))
|
||||
print(one_turn_table(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=.
|
||||
|
@ -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]~.
|
||||
#+begin_src python
|
||||
def first_turn_probabilities(k):
|
||||
def one_turn_table(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]
|
||||
def one_turn_single(k):
|
||||
return one_turn_table(k)[k]
|
||||
|
||||
#+end_src
|
||||
|
||||
|
@ -197,3 +220,235 @@ To run as a script.
|
|||
if __name__ == "__main__":
|
||||
main()
|
||||
#+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
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import sys
|
||||
import json
|
||||
import pprint
|
||||
from fastapi import FastAPI, Header
|
||||
from pydantic import BaseModel
|
||||
|
||||
pp = pp.Printer = pprint.PrettyPrinter(indent=2, compact=True)
|
||||
|
||||
def usagequit():
|
||||
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
|
||||
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.
|
||||
|
@ -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.""")
|
||||
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():
|
||||
global k
|
||||
try:
|
||||
|
@ -35,30 +54,22 @@ def dispatch(mode, k):
|
|||
if mode not in modes:
|
||||
usagequit()
|
||||
if mode == "single":
|
||||
print(first_turn_probability(k))
|
||||
print(one_turn_single(k))
|
||||
elif mode == "multi" :
|
||||
print(multi_turn_single(k))
|
||||
elif mode == "single-table":
|
||||
print(first_turn_protatbilities(k))
|
||||
print(one_turn_table(k))
|
||||
elif mode == "multi-table":
|
||||
print(multi_turn_table(k))
|
||||
|
||||
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))
|
||||
|
||||
def first_turn_probabilities(k):
|
||||
def one_turn_table(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]
|
||||
def one_turn_single(k):
|
||||
return one_turn_table(k)[k]
|
||||
|
||||
def multi_turn_single(k):
|
||||
p_win = 1 / k # Winning probability on any given throw
|
||||
|
|
Loading…
Reference in a new issue