Skip to content

Instantly share code, notes, and snippets.

@arseniiv
Last active March 20, 2024 21:42
Show Gist options
  • Select an option

  • Save arseniiv/9d7826d82d1b2ee4f52ad407c9ff367a to your computer and use it in GitHub Desktop.

Select an option

Save arseniiv/9d7826d82d1b2ee4f52ad407c9ff367a to your computer and use it in GitHub Desktop.
Comparing simple strategies of playing one-suit black jack with individual decks for each player
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
from fractions import Fraction
from typing import Callable, Final, Iterable, Iterator
@dataclass(frozen=True, slots=True)
class Prob[TDom, TProb: (float, Fraction)]:
"""Probability monad."""
dist: tuple[tuple[TDom, TProb | int], ...]
def __init__(self, dist: Iterable[tuple[TDom, TProb | int]]) -> None:
dist = tuple((val, prob) for val, prob in dist if prob)
if not dist:
raise ValueError('distribution has zero probability')
object.__setattr__(self, 'dist', dist)
def __iter__(self) -> Iterator[tuple[TDom, TProb | int]]:
return iter(self.dist)
def values(self) -> Iterator[TDom]:
for items in self.dist:
yield items[0]
def total(self) -> TProb | int:
return sum(items[1] for items in self.dist)
def __truediv__(self, other: TProb) -> Prob[TDom, TProb]:
return Prob((val, prob / other) for val, prob in self.dist)
def map[TDom2](self, fun: Callable[[TDom], TDom2]) -> Prob[TDom2, TProb]:
res: defaultdict[TDom2, TProb | int]
res = defaultdict(lambda: 0)
for val, prob in self.dist:
res[fun(val)] += prob
return Prob(res.items())
@staticmethod
def pure(val: TDom, type_: type[TProb]) -> Prob[TDom, TProb]:
return Prob(((val, type_(1)),))
@staticmethod
def uniform(values: Iterable[TDom], type_: type[TProb]) -> Prob[TDom, TProb]:
values = tuple(values)
prob = 1 / type_(len(values))
res: defaultdict[TDom, TProb | int]
res = defaultdict(lambda: 0)
for val in values:
res[val] += prob
return Prob(res.items())
def bind[TDom2](self, monadic_fun: Callable[[TDom], Iterable[tuple[TDom2, TProb | int]]]) -> Prob[TDom2, TProb]:
res: defaultdict[TDom2, TProb | int]
res = defaultdict(lambda: 0)
for val, prob in self.dist:
for val2, prob2 in monadic_fun(val):
res[val2] += prob * prob2
return Prob(res.items())
def sorted(self) -> Prob[TDom, TProb]:
return Prob(tuple(sorted(self.dist)))
def probability(self, predicate: Callable[[TDom], bool]) -> TProb | int:
return sum(prob for val, prob in self.dist if predicate(val))
type Hand = tuple[int, frozenset[int]] # points total and cards
DECK: Final = frozenset(range(1, 13 + 1))
EMPTY_HAND: Final[Hand] = (int(), frozenset())
def linear_points(points: int) -> int:
"""Allows determining who won by just comparing (larger result wins)."""
# … < 23 < 22 < 0 < 1 < … < 20 < 21
return points if points <= 21 else -points
def stands_at(points: int) -> Prob[int, Fraction]:
"""The distribution of scores for a player who doesn’t grab cards after getting this many points."""
def enough(dist: Prob[Hand, Fraction]) -> bool:
return all(val[0] >= points for val in dist.values())
def add_to_hand(hand: Hand, card: int) -> Hand:
total = hand[0] + (card if card != 1 else 11)
if card == 1 and total > 21:
total -= 10 # ace taken to be 1 point instead of 11
return total, hand[1] | {card}
def hit(hand: Hand) -> Prob[Hand, Fraction]:
"""Add another card, if appropriate, probabilistically."""
if hand[0] >= points:
return Prob.pure(hand, Fraction)
draw = DECK - hand[1]
return Prob.uniform((add_to_hand(hand, card) for card in draw), Fraction)
dist = Prob.pure(EMPTY_HAND, Fraction)
while not enough(dist):
dist = dist.bind(hit)
return dist.map(lambda hand: hand[0])
def main() -> None:
PLAYERS: Final = tuple(stands_at(n).sorted() for n in range(21 + 1))
for n1, player1 in enumerate(PLAYERS):
print(f'Player who stands at {n1}:')
dist = player1.dist
print(', '.join(f'{val}: {prob:.0%}' for val, prob in dist))
for n2, player2 in enumerate(PLAYERS[n1:], start=n1):
score = player1.bind(
lambda pts1: player2.map(
lambda pts2: linear_points(pts1) - linear_points(pts2)))
wins = score.probability(lambda x: x > 0)
draws = score.probability(lambda x: x == 0)
loses = 1 - wins - draws
if wins > loses:
print(f'Wins against {n2} in {wins}{wins:.3%}')
elif wins < loses:
print(f'Loses to {n2} in {loses}{loses:.3%}')
else:
print(f'Wins/loses equally with {n2} in {wins}{wins:.3%}')
if draws:
print(f'Draws with {n2} in {draws}{draws:.3%}')
print()
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment