05/23 Phase 15
This commit is contained in:
+105
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
core/odds.py
|
||||
------------
|
||||
Mathematical prize-odds calculator using combinatorics.
|
||||
Supports games with 0 or 1 bonus ball; bonus_count >= 2 returns jackpot only.
|
||||
"""
|
||||
|
||||
import math
|
||||
from db.models import get_game_by_id
|
||||
|
||||
|
||||
def _comb(n: int, r: int) -> int:
|
||||
if r < 0 or r > n:
|
||||
return 0
|
||||
return math.comb(n, r)
|
||||
|
||||
|
||||
def total_combinations(main_count: int, main_max: int,
|
||||
bonus_count: int, bonus_max: int) -> int:
|
||||
"""Total distinct tickets for the given game parameters."""
|
||||
main = _comb(main_max, main_count)
|
||||
if bonus_count == 0:
|
||||
return main
|
||||
if bonus_count == 1:
|
||||
return main * bonus_max
|
||||
return main * _comb(bonus_max, bonus_count)
|
||||
|
||||
|
||||
def _tier_ways(main_count: int, main_max: int,
|
||||
bonus_count: int, bonus_max: int,
|
||||
req_main: int, req_bonus: bool) -> int:
|
||||
"""Tickets matching exactly req_main main balls and the given bonus state."""
|
||||
main_ways = _comb(main_count, req_main) * _comb(main_max - main_count, main_count - req_main)
|
||||
if bonus_count == 0:
|
||||
return main_ways if not req_bonus else 0
|
||||
return main_ways * (1 if req_bonus else bonus_max - 1)
|
||||
|
||||
|
||||
def _fmt_odds(odds: float) -> str:
|
||||
rounded = round(odds)
|
||||
if abs(odds - rounded) < 0.01:
|
||||
return f"1 in {rounded:,}"
|
||||
return f"1 in {odds:,.2f}"
|
||||
|
||||
|
||||
def prize_odds(main_count: int, main_max: int,
|
||||
bonus_count: int, bonus_max: int) -> list[dict]:
|
||||
"""
|
||||
Returns a list of dicts for each prize tier:
|
||||
{tier, ways, total, odds, odds_str, probability}
|
||||
Tiers are generated dynamically; bonus_count > 1 returns jackpot only.
|
||||
"""
|
||||
total = total_combinations(main_count, main_max, bonus_count, bonus_max)
|
||||
|
||||
if bonus_count > 1:
|
||||
return [{"tier": "Jackpot", "ways": 1, "total": total,
|
||||
"odds": float(total), "odds_str": _fmt_odds(float(total)),
|
||||
"probability": 1.0 / total}]
|
||||
|
||||
rows = []
|
||||
for k in range(main_count, -1, -1):
|
||||
tier_defs = []
|
||||
|
||||
if bonus_count == 1:
|
||||
if k == main_count:
|
||||
label = "Jackpot"
|
||||
elif k > 0:
|
||||
label = f"Match {k} + Bonus"
|
||||
else:
|
||||
label = "Bonus Only"
|
||||
tier_defs.append((k, True, label))
|
||||
|
||||
if bonus_count == 0 and k == main_count:
|
||||
tier_defs.append((k, False, "Jackpot"))
|
||||
elif k > 0:
|
||||
tier_defs.append((k, False, f"Match {k}"))
|
||||
# k == 0 no-bonus → No Prize, skip
|
||||
|
||||
for req_main, req_bonus, label in tier_defs:
|
||||
ways = _tier_ways(main_count, main_max, bonus_count, bonus_max, req_main, req_bonus)
|
||||
if ways <= 0:
|
||||
continue
|
||||
odds = total / ways
|
||||
rows.append({
|
||||
"tier": label,
|
||||
"ways": ways,
|
||||
"total": total,
|
||||
"odds": odds,
|
||||
"odds_str": _fmt_odds(odds),
|
||||
"probability": ways / total,
|
||||
})
|
||||
|
||||
rows.sort(key=lambda r: r["odds"], reverse=True)
|
||||
return rows
|
||||
|
||||
|
||||
def game_odds(game_id: int) -> list[dict]:
|
||||
"""Compute prize odds for a game looked up by ID."""
|
||||
game = get_game_by_id(game_id)
|
||||
if game is None:
|
||||
return []
|
||||
return prize_odds(
|
||||
game["main_count"], game["main_max"],
|
||||
game["bonus_count"], game["bonus_max"],
|
||||
)
|
||||
Reference in New Issue
Block a user