Spaces:
Sleeping
Sleeping
Commit
•
b578b56
1
Parent(s):
b6952bb
initial commit
Browse files- app.py +137 -0
- holdem.py +100 -0
- poker_functions.py +367 -0
- requirements.txt +4 -0
- simulation.py +267 -0
app.py
ADDED
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import logging
|
3 |
+
from typing import List
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
from landingai.common import Prediction
|
7 |
+
from landingai.predict import Predictor
|
8 |
+
from landingai.st_utils import render_svg, setup_page
|
9 |
+
from landingai.visualize import overlay_predictions
|
10 |
+
from PIL import Image
|
11 |
+
|
12 |
+
import simulation as s
|
13 |
+
from holdem import run_simulation
|
14 |
+
|
15 |
+
setup_page(page_title="LandingLens Holdem Odds Calculator")
|
16 |
+
|
17 |
+
Image.MAX_IMAGE_PIXELS = None
|
18 |
+
|
19 |
+
|
20 |
+
_HAND = "hand"
|
21 |
+
_FLOP = "flop"
|
22 |
+
|
23 |
+
|
24 |
+
def main():
|
25 |
+
st.markdown("""
|
26 |
+
<h3 align="center">Holdem Odds Calculator</h3>
|
27 |
+
<p align="center">
|
28 |
+
<img width="100" height="100" src="https://github.com/landing-ai/landingai-python/raw/main/assets/avi-logo.png">
|
29 |
+
</p>
|
30 |
+
""", unsafe_allow_html=True)
|
31 |
+
tab1, tab2 = st.tabs(["Your hand", "Flop"])
|
32 |
+
with tab1:
|
33 |
+
image_file_hand = st.file_uploader("Your hand")
|
34 |
+
# image_file_hand = image_file = st.camera_input("Your hand")
|
35 |
+
if image_file_hand is not None:
|
36 |
+
preds = inference(image_file_hand)
|
37 |
+
if len(preds) != 2:
|
38 |
+
_show_error_message(2, preds, image_file_hand, "hand")
|
39 |
+
image_file_hand = None
|
40 |
+
return
|
41 |
+
st.session_state[_HAND] = preds, image_file_hand
|
42 |
+
with tab2:
|
43 |
+
image_file_flop = st.file_uploader("Flop")
|
44 |
+
# image_file_flop = image_file = st.camera_input(label="Flop")
|
45 |
+
if image_file_flop is not None:
|
46 |
+
preds = inference(image_file_flop)
|
47 |
+
if len(preds) != 3:
|
48 |
+
_show_error_message(3, preds, image_file_flop, "flop")
|
49 |
+
image_file_flop = None
|
50 |
+
return
|
51 |
+
st.session_state[_FLOP] = preds, image_file_flop
|
52 |
+
|
53 |
+
if _HAND not in st.session_state:
|
54 |
+
st.info("Please take a photo of your hand.")
|
55 |
+
return
|
56 |
+
if _FLOP not in st.session_state:
|
57 |
+
_show_predictions(*st.session_state[_HAND], "Your hand")
|
58 |
+
hand = [_convert_name(det.label_name) for det in st.session_state[_HAND][0]]
|
59 |
+
run_simulation(hand=hand)
|
60 |
+
st.info("Please take a photo of the flop to continue.")
|
61 |
+
return
|
62 |
+
col1, col2 = st.columns(2)
|
63 |
+
with col1:
|
64 |
+
_show_predictions(*st.session_state[_HAND], "Your hand")
|
65 |
+
with col2:
|
66 |
+
_show_predictions(*st.session_state[_FLOP], "Flop")
|
67 |
+
|
68 |
+
hand = [_convert_name(det.label_name) for det in st.session_state[_HAND][0]]
|
69 |
+
flop = [_convert_name(det.label_name) for det in st.session_state[_FLOP][0]]
|
70 |
+
if not _validate_cards(hand, flop):
|
71 |
+
return
|
72 |
+
run_simulation(hand=hand, flop=flop)
|
73 |
+
st.write("Interested in building more Computer Vision applications? Check out our [LandingLens](https://landing.ai/) platform and our [open source Python SDK](https://github.com/landing-ai/landingai-python)!")
|
74 |
+
|
75 |
+
|
76 |
+
def _validate_cards(hand, flop) -> bool:
|
77 |
+
check = hand + flop
|
78 |
+
if s.dedup(check):
|
79 |
+
st.error(
|
80 |
+
"There is a duplicate card. Please check the board and your hand and try again.",
|
81 |
+
icon="🚨",
|
82 |
+
)
|
83 |
+
return False
|
84 |
+
if not s.validate_card(check):
|
85 |
+
st.error(
|
86 |
+
"At least one of your cards is not valid. Please try again.", icon="🚨"
|
87 |
+
)
|
88 |
+
return False
|
89 |
+
return True
|
90 |
+
|
91 |
+
|
92 |
+
def _convert_name(name: str) -> str:
|
93 |
+
if name.startswith("10"):
|
94 |
+
return f"T{name[2:].lower()}"
|
95 |
+
else:
|
96 |
+
return f"{name[0].upper()}{name[1:].lower()}"
|
97 |
+
|
98 |
+
|
99 |
+
# TODO Rename this here and in `main`
|
100 |
+
def _show_error_message(expected_len, preds, img_file, pred_name):
|
101 |
+
msg = f"Detected {len(preds)}, expects {expected_len} cards in your {pred_name}. Please try again with a new photo."
|
102 |
+
st.error(msg)
|
103 |
+
_show_predictions(preds, img_file, pred_name)
|
104 |
+
|
105 |
+
|
106 |
+
@st.cache_data
|
107 |
+
def inference(image_file) -> List[Prediction]:
|
108 |
+
image = Image.open(image_file).convert("RGB")
|
109 |
+
predictor = Predictor(endpoint_id="2549edc1-35ad-45aa-b27e-f49e79f5922e", api_key=os.environ.get("LANDINGAI_API_KEY"))
|
110 |
+
logging.info("Running Poker prediction")
|
111 |
+
preds = predictor.predict(image)
|
112 |
+
preds = _dedup_preds(preds)
|
113 |
+
logging.info(
|
114 |
+
f"Poker prediction done successfully against {image_file} with {len(preds)} predictions."
|
115 |
+
)
|
116 |
+
return preds
|
117 |
+
|
118 |
+
|
119 |
+
def _show_predictions(preds, image_file, caption: str) -> None:
|
120 |
+
image = Image.open(image_file).convert("RGB")
|
121 |
+
display_image = overlay_predictions(preds, image)
|
122 |
+
st.image(
|
123 |
+
display_image,
|
124 |
+
channels="RGB",
|
125 |
+
caption=caption,
|
126 |
+
)
|
127 |
+
|
128 |
+
|
129 |
+
def _dedup_preds(preds: List[Prediction]) -> List[Prediction]:
|
130 |
+
"""Deduplicate predictions by the prediction value."""
|
131 |
+
result = {p.label_name: p for p in preds}
|
132 |
+
return list(result.values())
|
133 |
+
|
134 |
+
|
135 |
+
# Run the app
|
136 |
+
if __name__ == "__main__":
|
137 |
+
main()
|
holdem.py
ADDED
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
from prettytable import PrettyTable
|
3 |
+
|
4 |
+
import simulation as s
|
5 |
+
from poker_functions import Card
|
6 |
+
|
7 |
+
|
8 |
+
def run_simulation(
|
9 |
+
hand: list[str],
|
10 |
+
flop: list[str] = None,
|
11 |
+
turn: list[str] = None,
|
12 |
+
river: list[str] = None,
|
13 |
+
sims=10000,
|
14 |
+
) -> None:
|
15 |
+
sim = s.simulation_one_player(hand, flop, turn, river, sims=sims)
|
16 |
+
hc_pct = s.percent(sim[1], sim[0])
|
17 |
+
hc_ratio = s.ratio(sim[1], sim[0])
|
18 |
+
pair_pct = s.percent(sim[2], sim[0])
|
19 |
+
pair_ratio = s.ratio(sim[2], sim[0])
|
20 |
+
two_pair_pct = s.percent(sim[3], sim[0])
|
21 |
+
two_pair_ratio = s.ratio(sim[3], sim[0])
|
22 |
+
three_ok_pct = s.percent(sim[4], sim[0])
|
23 |
+
three_ok_ratio = s.ratio(sim[4], sim[0])
|
24 |
+
straight_pct = s.percent(sim[5], sim[0])
|
25 |
+
straight_ratio = s.ratio(sim[5], sim[0])
|
26 |
+
flush_pct = s.percent(sim[6], sim[0])
|
27 |
+
flush_ratio = s.ratio(sim[6], sim[0])
|
28 |
+
boat_pct = s.percent(sim[7], sim[0])
|
29 |
+
boat_ratio = s.ratio(sim[7], sim[0])
|
30 |
+
quads_pct = s.percent(sim[8], sim[0])
|
31 |
+
quads_ratio = s.ratio(sim[8], sim[0])
|
32 |
+
strt_flush_pct = s.percent(sim[9], sim[0])
|
33 |
+
strt_flush_ratio = s.ratio(sim[9], sim[0])
|
34 |
+
|
35 |
+
hole_card_str = f"{Card(hand[0]).pretty_name} {Card(hand[1]).pretty_name}"
|
36 |
+
|
37 |
+
table = PrettyTable()
|
38 |
+
table.field_names = ["Your Hand", "Board"]
|
39 |
+
if flop is None:
|
40 |
+
flop = []
|
41 |
+
if turn is None:
|
42 |
+
turn = []
|
43 |
+
if river is None:
|
44 |
+
river = []
|
45 |
+
board_str = "".join(f"{Card(card).pretty_name} " for card in (flop + turn + river))
|
46 |
+
table.add_row([hole_card_str, board_str])
|
47 |
+
|
48 |
+
odds = PrettyTable()
|
49 |
+
odds.add_column(
|
50 |
+
"Best Final Hand",
|
51 |
+
[
|
52 |
+
"High Card",
|
53 |
+
"Pair",
|
54 |
+
"Two Pair",
|
55 |
+
"Three of a Kind",
|
56 |
+
"Straight",
|
57 |
+
"Flush",
|
58 |
+
"Full House",
|
59 |
+
"Four of a Kind",
|
60 |
+
"Straight Flush",
|
61 |
+
],
|
62 |
+
)
|
63 |
+
odds.add_column(
|
64 |
+
"% Prob",
|
65 |
+
[
|
66 |
+
hc_pct,
|
67 |
+
pair_pct,
|
68 |
+
two_pair_pct,
|
69 |
+
three_ok_pct,
|
70 |
+
straight_pct,
|
71 |
+
flush_pct,
|
72 |
+
boat_pct,
|
73 |
+
quads_pct,
|
74 |
+
strt_flush_pct,
|
75 |
+
],
|
76 |
+
)
|
77 |
+
odds.add_column(
|
78 |
+
"Odds",
|
79 |
+
[
|
80 |
+
hc_ratio,
|
81 |
+
pair_ratio,
|
82 |
+
two_pair_ratio,
|
83 |
+
three_ok_ratio,
|
84 |
+
straight_ratio,
|
85 |
+
flush_ratio,
|
86 |
+
boat_ratio,
|
87 |
+
quads_ratio,
|
88 |
+
strt_flush_ratio,
|
89 |
+
],
|
90 |
+
)
|
91 |
+
st.divider()
|
92 |
+
st.subheader("Odds")
|
93 |
+
st.write(table, "\n")
|
94 |
+
st.write(f"We ran your hand {sims} times. Here's the odds:\n")
|
95 |
+
st.write(odds, "\n")
|
96 |
+
st.divider()
|
97 |
+
|
98 |
+
|
99 |
+
if __name__ == "__main__":
|
100 |
+
run_simulation(["As", "Ah"], ["2s", "3s", "4s"], sims=4000)
|
poker_functions.py
ADDED
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import random
|
2 |
+
from collections import Counter
|
3 |
+
from dataclasses import dataclass
|
4 |
+
|
5 |
+
card_values = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
|
6 |
+
ranks = ['2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A']
|
7 |
+
rank_value = dict(zip(ranks, card_values))
|
8 |
+
value_rank = dict(zip(card_values, ranks))
|
9 |
+
suits = ['c', 'd', 'h', 's']
|
10 |
+
hand_values = {'hc': 1,
|
11 |
+
'pair': 2,
|
12 |
+
'2pair': 3,
|
13 |
+
'3ok': 4,
|
14 |
+
'straight': 5,
|
15 |
+
'flush': 6,
|
16 |
+
'boat': 7,
|
17 |
+
'4ok': 8,
|
18 |
+
'straight_flush': 9
|
19 |
+
}
|
20 |
+
|
21 |
+
HAND_REGISTRY = []
|
22 |
+
|
23 |
+
##### CLASSES #####
|
24 |
+
|
25 |
+
@dataclass
|
26 |
+
class Card:
|
27 |
+
def __init__(self, card_str):
|
28 |
+
self.rank = str(card_str[0])
|
29 |
+
self.suit = card_str[1]
|
30 |
+
self.name = self.rank + self.suit
|
31 |
+
self.value = rank_value[self.rank]
|
32 |
+
|
33 |
+
def __str__(self):
|
34 |
+
return self.name
|
35 |
+
|
36 |
+
@property
|
37 |
+
def pretty_name(self):
|
38 |
+
rank = '10' if self.rank == 'T' else self.rank
|
39 |
+
suit_map = {
|
40 |
+
"c": "♣",
|
41 |
+
"d": "♦",
|
42 |
+
"h": "♥",
|
43 |
+
"s": "♠",
|
44 |
+
}
|
45 |
+
return f'{rank}{suit_map[self.suit]}'
|
46 |
+
|
47 |
+
|
48 |
+
def __getitem__(self, item):
|
49 |
+
if item == 'rank':
|
50 |
+
return self.rank
|
51 |
+
elif item == 'suit':
|
52 |
+
return self.suit
|
53 |
+
elif item == 'name':
|
54 |
+
return self.name
|
55 |
+
elif item == 'value':
|
56 |
+
return self.value
|
57 |
+
|
58 |
+
|
59 |
+
@dataclass()
|
60 |
+
class Hand:
|
61 |
+
def __init__(self, type, high_value, low_value = 0, kicker=0):
|
62 |
+
"""Type = name of hand (e.g. Pair)
|
63 |
+
value = value of the hand (i.e. Straight Flush is the most valuable)
|
64 |
+
high_value = value. either the high card in straight or flush, the set in full house, the top pair in 2pair, etc
|
65 |
+
low_value = the lower pair in 2 pair or the pair in a full house
|
66 |
+
kicker = value of the kicker in the hand. Can be null
|
67 |
+
"""
|
68 |
+
kicker_rank = value_rank[kicker] if kicker in card_values else 0
|
69 |
+
low_rank = value_rank[low_value] if low_value in card_values else 0
|
70 |
+
self.type = type
|
71 |
+
self.hand_value = hand_values[type]
|
72 |
+
self.kicker = kicker
|
73 |
+
self.kicker_rank = kicker_rank
|
74 |
+
self.high_value = high_value
|
75 |
+
self.high_rank = value_rank[self.high_value]
|
76 |
+
self.low_value = low_value
|
77 |
+
self.low_rank = low_rank
|
78 |
+
|
79 |
+
def __str__(self):
|
80 |
+
return f'{self.type}-{self.high_rank}'
|
81 |
+
|
82 |
+
def __getitem__(self, item):
|
83 |
+
if item in ['hand_value', 'high_value']:
|
84 |
+
return self.high_value
|
85 |
+
elif item == 'high_rank':
|
86 |
+
return self.high_rank
|
87 |
+
elif item == 'kicker':
|
88 |
+
return self.kicker
|
89 |
+
elif item == 'kicker_rank':
|
90 |
+
return self.kicker_rank
|
91 |
+
elif item == 'low_rank':
|
92 |
+
return self.low_rank
|
93 |
+
elif item == 'low_value':
|
94 |
+
return self.low_value
|
95 |
+
elif item == 'type':
|
96 |
+
return self.type
|
97 |
+
|
98 |
+
|
99 |
+
class Deck(list):
|
100 |
+
def __init__(self, deck):
|
101 |
+
self.deck = deck
|
102 |
+
|
103 |
+
def __getitem__(self, item):
|
104 |
+
return self.deck[item]
|
105 |
+
|
106 |
+
def __iter__(self):
|
107 |
+
yield from self.deck
|
108 |
+
|
109 |
+
def __len__(self):
|
110 |
+
return len(self.deck)
|
111 |
+
|
112 |
+
def deal_card(self):
|
113 |
+
"""Select a random card from the deck. Return the card and the deck with the card removed"""
|
114 |
+
i = random.randint(0, len(self)-1)
|
115 |
+
card = self[i]
|
116 |
+
self.deck.pop(i)
|
117 |
+
return card, self
|
118 |
+
|
119 |
+
def update_deck(self, card):
|
120 |
+
"""Remove card from deck"""
|
121 |
+
deck_names = [card.name for card in self.deck]
|
122 |
+
card_name = card.name if isinstance(card, Card) else card
|
123 |
+
deck_idx = deck_names.index(card_name)
|
124 |
+
self.deck.pop(deck_idx)
|
125 |
+
|
126 |
+
|
127 |
+
##### USEFUL FUNCTIONS #####
|
128 |
+
|
129 |
+
def register(func):
|
130 |
+
"""Add a function to the hand register"""
|
131 |
+
HAND_REGISTRY.append(func)
|
132 |
+
return func
|
133 |
+
|
134 |
+
def make_card(input_list):
|
135 |
+
"""Input_list is either a list of Card objects or string Objects. If Cards, return the cards.
|
136 |
+
If string, convert to Card and return"""
|
137 |
+
if len(input_list) == 0:
|
138 |
+
return input_list
|
139 |
+
elif isinstance(input_list[0], Card):
|
140 |
+
return input_list
|
141 |
+
else:
|
142 |
+
card_list = [Card(card) for card in input_list]
|
143 |
+
return card_list
|
144 |
+
|
145 |
+
|
146 |
+
def generate_deck():
|
147 |
+
deck = []
|
148 |
+
for rank in ranks:
|
149 |
+
for suit in suits:
|
150 |
+
card_str = rank + suit
|
151 |
+
_card = Card(card_str)
|
152 |
+
deck.append(_card)
|
153 |
+
deck = Deck(deck)
|
154 |
+
return deck
|
155 |
+
|
156 |
+
|
157 |
+
##### POKER #####
|
158 |
+
def find_multiple(hand, board, n=2):
|
159 |
+
"""Is there a pair, three of a kind, four of a kind/?"""
|
160 |
+
hand = make_card(hand)
|
161 |
+
board = make_card(board)
|
162 |
+
multiple = False
|
163 |
+
multiple_hand = None
|
164 |
+
total_hand = hand + board
|
165 |
+
values = [card.value for card in total_hand]
|
166 |
+
c = Counter(values)
|
167 |
+
for value in set(values):
|
168 |
+
if c[value] == 2 and n == 2:
|
169 |
+
multiple = True
|
170 |
+
hand_type = 'pair'
|
171 |
+
high_value = value
|
172 |
+
low_value = max([value for value in values if value != high_value])
|
173 |
+
kicker = max([value for value in values if value not in [high_value, low_value]])
|
174 |
+
multiple_hand = Hand(hand_type, high_value, low_value=low_value, kicker=kicker)
|
175 |
+
return multiple_hand
|
176 |
+
elif c[value] == 3 and n == 3:
|
177 |
+
multiple = True
|
178 |
+
hand_type = '3ok'
|
179 |
+
high_value = value
|
180 |
+
low_value = max([foo for foo in values if foo != high_value])
|
181 |
+
kicker = max([bar for bar in values if bar not in [high_value, low_value]])
|
182 |
+
multiple_hand = Hand(hand_type, high_value, low_value=low_value, kicker=kicker)
|
183 |
+
return multiple_hand
|
184 |
+
elif c[value] == 4 and n == 4:
|
185 |
+
multiple = True
|
186 |
+
hand_type = '4ok'
|
187 |
+
high_value = value
|
188 |
+
low_value = max([value for value in values if value != high_value])
|
189 |
+
multiple_hand = Hand(hand_type, high_value, low_value=low_value)
|
190 |
+
return multiple_hand
|
191 |
+
return multiple
|
192 |
+
|
193 |
+
|
194 |
+
def evaluate_straight(values):
|
195 |
+
"""Evaluates a list of card values to determine whether there are 5 consecutive values"""
|
196 |
+
straight = False
|
197 |
+
count = 0
|
198 |
+
straight_hand_values = []
|
199 |
+
sranks = [bit for bit in reversed(range(2, 15))]
|
200 |
+
sranks.append(14)
|
201 |
+
for rank in sranks:
|
202 |
+
if rank in values:
|
203 |
+
count += 1
|
204 |
+
straight_hand_values.append(rank)
|
205 |
+
if count == 5:
|
206 |
+
straight = True
|
207 |
+
return straight, straight_hand_values
|
208 |
+
else:
|
209 |
+
count = 0
|
210 |
+
straight_hand_values = []
|
211 |
+
return straight, straight_hand_values
|
212 |
+
|
213 |
+
|
214 |
+
@register
|
215 |
+
def find_straight_flush(hand, board):
|
216 |
+
"""Find a straight flush in a given hand/board combination"""
|
217 |
+
hand = make_card(hand)
|
218 |
+
board = make_card(board)
|
219 |
+
straight_flush = False
|
220 |
+
flush = find_flush(hand, board)
|
221 |
+
if flush:
|
222 |
+
total_hand = hand + board
|
223 |
+
total_hand = [card for card in total_hand]
|
224 |
+
hand_suits = [card.suit for card in total_hand]
|
225 |
+
c = Counter(hand_suits)
|
226 |
+
flush_suit = c.most_common(1)[0][0]
|
227 |
+
flush_hand = [card.value for card in total_hand if card.suit == flush_suit]
|
228 |
+
straight_flush, straight_hand = evaluate_straight(flush_hand)
|
229 |
+
if straight_flush:
|
230 |
+
high_value = max(straight_hand)
|
231 |
+
hand_type = 'straight_flush'
|
232 |
+
straight_flush_hand = Hand(hand_type,high_value)
|
233 |
+
return straight_flush_hand
|
234 |
+
else:
|
235 |
+
return straight_flush
|
236 |
+
else:
|
237 |
+
return straight_flush
|
238 |
+
|
239 |
+
|
240 |
+
@register
|
241 |
+
def find_quads(hand, board):
|
242 |
+
quads = find_multiple(hand, board, n=4)
|
243 |
+
return quads
|
244 |
+
|
245 |
+
|
246 |
+
@register
|
247 |
+
def find_full_house(hand, board):
|
248 |
+
"""Is there a full house?"""
|
249 |
+
hand = make_card(hand)
|
250 |
+
board = make_card(board)
|
251 |
+
boat = False
|
252 |
+
boat_hand = None
|
253 |
+
total_hand = hand + board
|
254 |
+
values = [card.value for card in total_hand]
|
255 |
+
c = Counter(values)
|
256 |
+
for value in set(values):
|
257 |
+
if c[value] == 3:
|
258 |
+
high_value = value
|
259 |
+
c.pop(value)
|
260 |
+
for value in set(values):
|
261 |
+
if c[value] > 1:
|
262 |
+
low_value = value
|
263 |
+
kicker = max([value for value in values if value != high_value and value != low_value])
|
264 |
+
boat_hand = Hand('boat', high_value, low_value=low_value, kicker=kicker)
|
265 |
+
boat = True
|
266 |
+
return boat_hand
|
267 |
+
return boat
|
268 |
+
|
269 |
+
|
270 |
+
@register
|
271 |
+
def find_flush(hand, board):
|
272 |
+
"""Does any combination of 5 cards in hand or on board amount to 5 of the same suit"""
|
273 |
+
hand = make_card(hand)
|
274 |
+
board = make_card(board)
|
275 |
+
total_hand = hand + board
|
276 |
+
total_hand_suits = [card.suit for card in total_hand]
|
277 |
+
flush = False
|
278 |
+
c = Counter(total_hand_suits)
|
279 |
+
for suit in total_hand_suits:
|
280 |
+
if c[suit] >= 5:
|
281 |
+
flush = True
|
282 |
+
if flush:
|
283 |
+
flush_cards = [card for card in total_hand if card.suit == c.most_common(1)[0][0]]
|
284 |
+
high_value = max([card.value for card in flush_cards])
|
285 |
+
flush_hand = Hand('flush', high_value)
|
286 |
+
return flush_hand
|
287 |
+
else:
|
288 |
+
return flush
|
289 |
+
|
290 |
+
|
291 |
+
@register
|
292 |
+
def find_straight(hand, board):
|
293 |
+
"""Find a straight in a given hand/board combination"""
|
294 |
+
hand = make_card(hand)
|
295 |
+
board = make_card(board)
|
296 |
+
straight = False
|
297 |
+
straight_hand = None
|
298 |
+
high_value = 2
|
299 |
+
reqd_hand_size = 5 # required hand size gives us some flexibility at the cost of more lines. could be more efficient if we say 'if len(values)<5'
|
300 |
+
total_hand = hand + board
|
301 |
+
values = [*set(card.value for card in total_hand)]
|
302 |
+
slices = len(values) - reqd_hand_size
|
303 |
+
if slices < 0:
|
304 |
+
return straight
|
305 |
+
else:
|
306 |
+
straight, straight_hand_values = evaluate_straight(values)
|
307 |
+
if straight:
|
308 |
+
hand_type = 'straight'
|
309 |
+
if 14 in straight_hand_values: # all([5,14]) does not work here so using nested ifs.
|
310 |
+
if 5 in straight_hand_values:
|
311 |
+
high_value = 5
|
312 |
+
else:
|
313 |
+
high_value = max(straight_hand_values)
|
314 |
+
straight_hand = Hand(hand_type, high_value)
|
315 |
+
return straight_hand
|
316 |
+
else:
|
317 |
+
return straight
|
318 |
+
|
319 |
+
|
320 |
+
@register
|
321 |
+
def find_trips(hand, board):
|
322 |
+
trips = find_multiple(hand, board, n=3)
|
323 |
+
return trips
|
324 |
+
|
325 |
+
|
326 |
+
@register
|
327 |
+
def find_two_pair(hand, board):
|
328 |
+
"""Is there two-pair?"""
|
329 |
+
hand = make_card(hand)
|
330 |
+
board = make_card(board)
|
331 |
+
two_pair = False
|
332 |
+
# two_pair_hand = None
|
333 |
+
total_hand = hand + board
|
334 |
+
values = [card.value for card in total_hand]
|
335 |
+
c = Counter(values)
|
336 |
+
for value in values:
|
337 |
+
if c[value] > 1:
|
338 |
+
pair1 = Hand('pair', value)
|
339 |
+
c.pop(value)
|
340 |
+
for value in values:
|
341 |
+
if c[value] > 1:
|
342 |
+
pair2 = Hand('pair', value)
|
343 |
+
kicker = max([value for value in values if value != pair1.high_value and value != pair2.high_value])
|
344 |
+
two_pair_hand = Hand('2pair', max(pair1.high_value, pair2.high_value), low_value=min(pair1.high_value, pair2.high_value), kicker=kicker)
|
345 |
+
two_pair = True
|
346 |
+
return two_pair_hand
|
347 |
+
return two_pair
|
348 |
+
|
349 |
+
|
350 |
+
@register
|
351 |
+
def find_pair(hand, board):
|
352 |
+
pair = find_multiple(hand, board, n=2)
|
353 |
+
return pair
|
354 |
+
|
355 |
+
|
356 |
+
@register
|
357 |
+
def find_high_card(hand, board):
|
358 |
+
hand = make_card(hand)
|
359 |
+
board = make_card(board)
|
360 |
+
total_hand = hand + board
|
361 |
+
total_hand_values = [card.value for card in total_hand]
|
362 |
+
total_hand_values.sort()
|
363 |
+
high_value = total_hand_values[-1]
|
364 |
+
low_value = total_hand_values[-2]
|
365 |
+
kicker = total_hand_values[-3]
|
366 |
+
high_card_hand = Hand('hc', high_value,low_value=low_value, kicker=kicker)
|
367 |
+
return high_card_hand
|
requirements.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
streamlit
|
2 |
+
extra-streamlit-components
|
3 |
+
landingai
|
4 |
+
prettytable
|
simulation.py
ADDED
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import poker_functions as p
|
2 |
+
from fractions import Fraction
|
3 |
+
from collections import Counter
|
4 |
+
|
5 |
+
|
6 |
+
class Player:
|
7 |
+
def __init__(self, number, cards=[]):
|
8 |
+
if len(cards) > 0:
|
9 |
+
cards = p.make_card(cards)
|
10 |
+
else:
|
11 |
+
cards = []
|
12 |
+
self.number = number
|
13 |
+
self.cards = cards
|
14 |
+
self.hand = None
|
15 |
+
self.starting_cards = None
|
16 |
+
self.wins = 0
|
17 |
+
|
18 |
+
def __str__(self):
|
19 |
+
return "player_" + str(self.number)
|
20 |
+
|
21 |
+
def dedup(board):
|
22 |
+
duplicate = False
|
23 |
+
c = Counter(board)
|
24 |
+
for card in board:
|
25 |
+
if c[card] > 1:
|
26 |
+
duplicate = True
|
27 |
+
return duplicate
|
28 |
+
return duplicate
|
29 |
+
|
30 |
+
|
31 |
+
def validate_card(check):
|
32 |
+
"""Detect invalid cards in a passed collection"""
|
33 |
+
valid = True
|
34 |
+
deck = p.generate_deck()
|
35 |
+
valid_cards = [card.name for card in deck]
|
36 |
+
for card in check:
|
37 |
+
if card not in valid_cards:
|
38 |
+
valid = False
|
39 |
+
return valid
|
40 |
+
return valid
|
41 |
+
|
42 |
+
|
43 |
+
def convert_and_update(deck, cards):
|
44 |
+
if len(cards) == 0:
|
45 |
+
return deck, cards
|
46 |
+
else:
|
47 |
+
cards = p.make_card(cards)
|
48 |
+
for card in cards:
|
49 |
+
deck.update_deck(card)
|
50 |
+
return deck, cards
|
51 |
+
|
52 |
+
##### SIMULATIONS #####
|
53 |
+
def evaluate_hand(hole_cards, flop=[], turn=[], river=[]):
|
54 |
+
board = flop + turn + river
|
55 |
+
hand = None
|
56 |
+
if len(hole_cards + board) < 5:
|
57 |
+
return hand
|
58 |
+
else:
|
59 |
+
for func in p.HAND_REGISTRY:
|
60 |
+
func = func(hole_cards, board)
|
61 |
+
if not func:
|
62 |
+
continue
|
63 |
+
else:
|
64 |
+
return func
|
65 |
+
|
66 |
+
|
67 |
+
def score_game(contestants):
|
68 |
+
# TODO make this more elegant by functionizing repeated code.
|
69 |
+
"""Application will determine the highest hand, including low and kicker for each player in player_list"""
|
70 |
+
high = ['flush', 'straight', 'straight_flush']
|
71 |
+
kick = ['4ok']
|
72 |
+
hi_lo = ['boat']
|
73 |
+
hi_lo_kick = ['2pair', 'hc', '3ok', 'pair']
|
74 |
+
high_hand = max(contestants, key=lambda x: x.hand.hand_value) # contestant with highest hand
|
75 |
+
same_high_hand = [player for player in contestants if player.hand.hand_value == high_hand.hand.hand_value]
|
76 |
+
if len(same_high_hand) == 1:
|
77 |
+
same_high_hand[0].wins += 1
|
78 |
+
return contestants
|
79 |
+
elif high_hand.hand.type in high:
|
80 |
+
high_card = max(same_high_hand, key=lambda x: x.hand.high_value)
|
81 |
+
same_high_card = [player for player in same_high_hand if player.hand.high_value == high_card.hand.high_value]
|
82 |
+
if len(same_high_card) == 1:
|
83 |
+
high_card.wins += 1
|
84 |
+
return contestants
|
85 |
+
else:
|
86 |
+
return contestants
|
87 |
+
elif high_hand.hand.type in hi_lo:
|
88 |
+
over = max(same_high_hand, key=lambda x: x.hand.high_value) # Highest pair in hand
|
89 |
+
same_over = [player for player in same_high_hand if player.hand.high_value == over.hand.high_value]
|
90 |
+
if len(same_over) == 1:
|
91 |
+
over.wins += 1
|
92 |
+
return contestants
|
93 |
+
else:
|
94 |
+
under = max(same_over, key=lambda x: x.hand.low_value) # lowest pair in hand
|
95 |
+
same_under = [player for player in same_over if player.hand.low_value == under.hand.low_value]
|
96 |
+
if len(same_under) == 1:
|
97 |
+
under.wins += 1
|
98 |
+
return contestants
|
99 |
+
else:
|
100 |
+
return contestants
|
101 |
+
elif high_hand.hand.type in hi_lo_kick:
|
102 |
+
over = max(same_high_hand, key=lambda x: x.hand.high_value) # Highest pair in hand
|
103 |
+
same_over = [player for player in same_high_hand if player.hand.high_value == over.hand.high_value]
|
104 |
+
if len(same_over) == 1:
|
105 |
+
over.wins += 1
|
106 |
+
return contestants
|
107 |
+
else:
|
108 |
+
under = max(same_over, key=lambda x: x.hand.low_value) # lowest pair in hand
|
109 |
+
same_under = [player for player in same_over if player.hand.low_value == under.hand.low_value]
|
110 |
+
if len(same_under) == 1:
|
111 |
+
under.wins += 1
|
112 |
+
return contestants
|
113 |
+
else:
|
114 |
+
kicker = max(same_under, key=lambda x: x.hand.kicker)
|
115 |
+
same_kicker = [player for player in same_under if player.hand.kicker == kicker.hand.kicker]
|
116 |
+
if len(same_kicker) == 1:
|
117 |
+
kicker.wins += 1
|
118 |
+
return contestants
|
119 |
+
else:
|
120 |
+
return contestants
|
121 |
+
elif high_hand.hand.type in kick:
|
122 |
+
low_val = max(same_high_hand, key=lambda x: x.hand.low_value)
|
123 |
+
same_low_val = [player for player in same_high_hand if player.hand.low_value == low_val.hand.low_value]
|
124 |
+
if len(same_low_val) == 1:
|
125 |
+
low_val.wins += 1
|
126 |
+
return contestants
|
127 |
+
else:
|
128 |
+
return contestants
|
129 |
+
|
130 |
+
|
131 |
+
def simulation_one_player(hole, flop=None, turn=None, river=None, sims=100000):
|
132 |
+
if flop is None:
|
133 |
+
flop = []
|
134 |
+
if turn is None:
|
135 |
+
turn = []
|
136 |
+
if river is None:
|
137 |
+
river = []
|
138 |
+
full_board = 7 # number of cards required to run sim
|
139 |
+
passed_cards = len(hole) + len(flop) + len(turn) + len(river)
|
140 |
+
passed_flop = list(flop)
|
141 |
+
high_cards = 0
|
142 |
+
pairs = 0
|
143 |
+
two_pairs = 0
|
144 |
+
trips = 0
|
145 |
+
straights = 0
|
146 |
+
flushes = 0
|
147 |
+
boats = 0
|
148 |
+
quads = 0
|
149 |
+
straight_flushes = 0
|
150 |
+
invalid = 0
|
151 |
+
for i in range(sims):
|
152 |
+
deck = p.generate_deck()
|
153 |
+
deck, hole = convert_and_update(deck, hole)
|
154 |
+
deck, flop = convert_and_update(deck, flop)
|
155 |
+
deck, turn = convert_and_update(deck, turn)
|
156 |
+
deck, river = convert_and_update(deck, river)
|
157 |
+
j = full_board - passed_cards
|
158 |
+
for _ in range(j):
|
159 |
+
deal, deck = deck.deal_card()
|
160 |
+
flop.append(deal) # Adding to flop because it shouldn't matter, will revert flop back at end of loop
|
161 |
+
hand = evaluate_hand(hole, flop, turn, river)
|
162 |
+
if hand.type == '2pair':
|
163 |
+
two_pairs += 1
|
164 |
+
elif hand.type == '3ok':
|
165 |
+
trips += 1
|
166 |
+
elif hand.type == '4ok':
|
167 |
+
quads += 1
|
168 |
+
elif hand.type == 'boat':
|
169 |
+
boats += 1
|
170 |
+
elif hand.type == 'flush':
|
171 |
+
flushes += 1
|
172 |
+
elif hand.type == 'hc':
|
173 |
+
high_cards += 1
|
174 |
+
elif hand.type == 'pair':
|
175 |
+
pairs += 1
|
176 |
+
elif hand.type == 'straight':
|
177 |
+
straights += 1
|
178 |
+
elif hand.type == 'straight_flush':
|
179 |
+
straight_flushes += 1
|
180 |
+
else:
|
181 |
+
invalid += 1
|
182 |
+
i += 1
|
183 |
+
flop = list(passed_flop)
|
184 |
+
return sims, high_cards, pairs, two_pairs, trips, straights, flushes, boats, quads, straight_flushes
|
185 |
+
|
186 |
+
|
187 |
+
def simulation_multiplayer(hole_one, hole_two=[], hole_three=[], hole_four=[], hole_five=[], hole_six=[],
|
188 |
+
flop = [], turn = [], river = [], opponents=2, sims=10000):
|
189 |
+
contestant_hands = [hole_one, hole_two, hole_three, hole_four, hole_five, hole_six]
|
190 |
+
contestants = []
|
191 |
+
flop = p.make_card(flop)
|
192 |
+
turn = p.make_card(turn)
|
193 |
+
river = p.make_card(river)
|
194 |
+
passed_flop_stable = [card for card in flop]
|
195 |
+
for n in range(opponents):
|
196 |
+
player_name = 'opponent' + str(n+1)
|
197 |
+
player_name = Player(n, contestant_hands[n])
|
198 |
+
contestants.append(player_name)
|
199 |
+
i = 0
|
200 |
+
passed_board = len(flop) + len(turn) + len(river)
|
201 |
+
full_board = 5
|
202 |
+
k = full_board - passed_board
|
203 |
+
for i in range(sims):
|
204 |
+
deck = p.generate_deck()
|
205 |
+
for contestant in contestants: # TODO move assigning Player.starting_cards to init
|
206 |
+
if len(contestant.cards) == 2:
|
207 |
+
contestant.starting_cards = True
|
208 |
+
for card in contestant.cards:
|
209 |
+
deck.update_deck(card) # remove known hole cards from deck
|
210 |
+
else:
|
211 |
+
contestant.starting_cards = False
|
212 |
+
hole_cards = []
|
213 |
+
for j in range(2):
|
214 |
+
deal, deck = deck.deal_card()
|
215 |
+
hole_cards.append(deal)
|
216 |
+
contestant.cards = hole_cards # assign new hole cards if not passed
|
217 |
+
for l in range(k): # complete the board as needed
|
218 |
+
deal, deck = deck.deal_card()
|
219 |
+
flop.append(deal)
|
220 |
+
for contestant in contestants:
|
221 |
+
hand = evaluate_hand(contestant.cards, flop, turn, river)
|
222 |
+
contestant.hand = hand
|
223 |
+
# Compare hand values in contestants
|
224 |
+
contestants = score_game(contestants)
|
225 |
+
i += 1
|
226 |
+
# Revert to starting state
|
227 |
+
flop = [card for card in passed_flop_stable]
|
228 |
+
for contestant in contestants:
|
229 |
+
if contestant.starting_cards is False:
|
230 |
+
contestant.cards = []
|
231 |
+
hole_cards = []
|
232 |
+
return contestants
|
233 |
+
|
234 |
+
|
235 |
+
|
236 |
+
# TODO for single and mult: find and return most likely hand. Return number of outs and odds.
|
237 |
+
|
238 |
+
##### MATH #####
|
239 |
+
def percent(hits, sims):
|
240 |
+
percent = round((hits / sims) * 100,0)
|
241 |
+
return percent
|
242 |
+
|
243 |
+
def ratio(hits, sims):
|
244 |
+
"""Return a ratio (e.g. 3:5) for two input numbers"""
|
245 |
+
percent = round((hits / sims),2)
|
246 |
+
fraction = str(Fraction(percent).limit_denominator())
|
247 |
+
fraction = fraction.replace('/', ':')
|
248 |
+
return fraction
|
249 |
+
|
250 |
+
|
251 |
+
##### REFERENCE #####
|
252 |
+
outs = {'1':('46:1','45:1',"22:1"),
|
253 |
+
'2':('22:1','22:1','11:1'),
|
254 |
+
'3':('15:1', '14:1', '7:1'),
|
255 |
+
'4':('11:1','10:1','5:1'),
|
256 |
+
'5':('8.5:1', '8:1','4:1'),
|
257 |
+
'6':('7:1','7:1','3:1'),
|
258 |
+
'7':('6:1','6:1','2.5:1'),
|
259 |
+
'8':('5:1','5:1','2.5:1'),
|
260 |
+
'9':('4:1','4:1','2:1'),
|
261 |
+
'10':('3.5:1','3.5:1','1.5:1'),
|
262 |
+
'11':('3.3:1','3.2:1','1.5:1'),
|
263 |
+
'12':('3:1','3:1','1.2:1'),
|
264 |
+
}
|
265 |
+
|
266 |
+
|
267 |
+
rank_value = p.rank_value
|