# core


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

TODO:

- The goal is to create a minimum viable product. Focus on finishing
  basic common rules.

- Try class for `Card` later? `namedtuple` works for now.

- Explore ways to design `Player` and `Round` classes.

- Try `IntEnum` for Poker hand rankings. Currently, we use integer
  values from 14 to 1 to indicate the poker hand rankings. Using
  `IntEnum` may make it more readable. Read more about enum:
  https://docs.python.org/3/library/enum.html

- Try passing `Round` to `Player` when taking action to update
  `stage_bet`.

- Player should not be able to check if `stage_bet != player.bet`.

- **Odd chips vanish** — `self.pot // len(winners)` drops the remainder

- Implement the rule: If everyone folds except for one player, that
  player wins

- Try rewriting `evaluate_hand` lazily.

- Faster `equity`

- Implement all-in

- Rather than printing, maybe add note messages?

- Add GUI with fasthtml

- Add possible actions for players. (maybe with GUI?)

- Deploy on server

In this dialog, I want to go through the process of writing a poker
program myself but following the approach I learned in the cs212 class.
My goal is not to copy everything from the lesson exactly, but follow
the steps: understanding the problem, specifying, concept inventory,
etc. that I can apply to solving any problem. To challenge myself, I
will write texas hold’em poker.

Here’s the process Norvig taught:

**1. Understand** — Start with a vague understanding and refine it. Make
an inventory of all the concepts you’ll need to deal with (data types,
operations, etc.).

**2. Specify** — Define *what* needs to happen: inputs, outputs, what
each function should accomplish. Think of it as a contract — not the
implementation details, just the “what.”

**3. Design** — Figure out *how* to make it happen — the actual
algorithms, code structure, and implementation.

Along the way, key principles: - **Write tests early** (even before
implementation), including **extreme values** - **Reuse existing tools**
when possible - **Refactor** for elegance (DRY, clarity, simplicity,
generality) - Be aware of the **four dimensions**: correctness,
efficiency, features, elegance — and make conscious tradeoffs

## Card

`namedtuple` with `patch`

Let’s use emoji for suits and string of alphanumerics for ranks.

Rather than using a string to represent cards and using separate
functions to access suits and ranks, I thought it would be better to use
the `.` (dot) notation. One way to accomplish this is through `class`,
but I wanted to try using `namedtuple`, which allows me to use attribute
access and immutability. The ranks and suits from each card should not
change. It is more lightweight than `class`.

``` python
c1
```

    Card(suit='♠', rank='10')

I wanted the card representation to be more concise, so I used
fastcore’s `patch` for `__repr__`.

------------------------------------------------------------------------

### Card.\_\_repr\_\_

``` python
def __repr__():
```

*Return repr(self).*

``` python
c2 = Card('♠', 'K')
c2
```

    ♠K

Currently, we cannot compare the ranks directly as we are using string
comparisons lexicographically.

``` python
c1.rank > c2.rank, '10' > 'K'
```

    (False, False)

It is convenient to sort hands by rank values when evaluating hands.

Using `rank_values` mapping, we can convert the string ranks into
integers. From 2 to 10, their values are 2 to 10 respectively. Then,
from ‘J’ to ‘A’, 11 to 14.

Although I had `sort_cards` function that sorts cards by their
`rank_values` from the highest, it was more convenient to patch `__lt__`
and `__eq__` and just use default `sorted` function. When comparing
cards, we only care about their ranks, not suits.

------------------------------------------------------------------------

### Card.\_\_lt\_\_

``` python
def __lt__(
    other:Card
):
```

*Return self\<value.*

------------------------------------------------------------------------

### Card.\_\_eq\_\_

``` python
def __eq__(
    other:Card
):
```

*Return self==value.*

``` python
deck = [Card(s,r) for s in suits for r in ranks]
sorted(deck,reverse=True)[:5]
```

    [♠A, ♥A, ♦A, ♣A, ♠K]

## Hand

- `evaluate_hand(cards) -> HandRank` — given 7 cards, returns the best
  5-card `HandRank`
- `compare_ranks(ranks) -> tuple` — returns the winner rank

A hand has two cards. Community cards have five cards.

Poker hands: - royal flush (10) - straight flush (9, highest_value) -
four of a kind (8, highest_value, kicker) - full house (7, triple,
pair) - flush (6, five kickers) - straight (5, highest_value) - three of
a kind (4, triple_value, two kickers) - two pair (3, high_pair,
low_pair, kicker) - one pair (2, pair_value, three kickers) - high card
(1, highest, second highest, third, fourth, fifth)

### Sample hands

These are some example hands used for examples and tests.

``` python
cards_highcard = [Card(suits[0], ranks[0]), Card(suits[1], ranks[2]), Card(suits[2], ranks[4]),
    Card(suits[3], ranks[6]), Card(suits[0], ranks[8]), Card(suits[1], ranks[10]), Card(suits[2], ranks[12])]
cards_highcard
```

    [♠2, ♥4, ♦6, ♣8, ♠10, ♥Q, ♦A]

``` python
cards_2kind = [Card(suits[0], ranks[0]), Card(suits[1], ranks[0]), Card(suits[0], ranks[3]), Card(suits[1], ranks[5]), 
    Card(suits[2], ranks[7]), Card(suits[3], ranks[9]), Card(suits[0], ranks[11])]
cards_2kind
```

    [♠2, ♥2, ♠5, ♥7, ♦9, ♣J, ♠K]

``` python
cards_3kind = [Card(suits[0], ranks[0]), Card(suits[1], ranks[0]), Card(suits[2], ranks[0]), Card(suits[0], ranks[3]), 
    Card(suits[1], ranks[5]), Card(suits[2], ranks[7]), Card(suits[3], ranks[9])]
cards_3kind
```

    [♠2, ♥2, ♦2, ♠5, ♥7, ♦9, ♣J]

``` python
cards_4kind = [Card(suits[0], ranks[0]), Card(suits[1], ranks[0]), Card(suits[2], ranks[0]), Card(suits[3], ranks[0]),
    Card(suits[0], ranks[4]), Card(suits[1], ranks[7]), Card(suits[2], ranks[10])]
cards_4kind
```

    [♠2, ♥2, ♦2, ♣2, ♠6, ♥9, ♦Q]

``` python
cards_2pair = [Card(suits[0], ranks[0]), Card(suits[1], ranks[0]), Card(suits[2], ranks[1]), Card(suits[3], ranks[1]),
    Card(suits[0], ranks[4]), Card(suits[1], ranks[6]), Card(suits[2], ranks[8])]
cards_2pair
```

    [♠2, ♥2, ♦3, ♣3, ♠6, ♥8, ♦10]

``` python
cards_fullhouse = [Card(suits[0], ranks[0]), Card(suits[1], ranks[0]), Card(suits[0], ranks[1]), Card(suits[2], ranks[1]), 
    Card(suits[3], ranks[1]), Card(suits[1], ranks[6]), Card(suits[2], ranks[8])]
cards_fullhouse
```

    [♠2, ♥2, ♠3, ♦3, ♣3, ♥8, ♦10]

``` python
cards_straight = [Card(suits[0], ranks[1]), Card(suits[1], ranks[2]), Card(suits[2], ranks[3]),
    Card(suits[3], ranks[4]), Card(suits[0], ranks[5]), Card(suits[1], ranks[8]), Card(suits[2], ranks[10])]
cards_straight
```

    [♠3, ♥4, ♦5, ♣6, ♠7, ♥10, ♦Q]

``` python
cards_flush = [Card(suits[0], ranks[0]), Card(suits[0], ranks[2]), Card(suits[0], ranks[4]),
    Card(suits[0], ranks[6]), Card(suits[0], ranks[8]), Card(suits[1], ranks[3]), Card(suits[2], ranks[7])]
cards_flush
```

    [♠2, ♠4, ♠6, ♠8, ♠10, ♥5, ♦9]

``` python
cards_straightflush = [Card(suits[0], ranks[1]), Card(suits[0], ranks[2]), Card(suits[0], ranks[3]),
    Card(suits[0], ranks[4]), Card(suits[0], ranks[5]), Card(suits[1], ranks[8]), Card(suits[2], ranks[10])]
cards_straightflush
```

    [♠3, ♠4, ♠5, ♠6, ♠7, ♥10, ♦Q]

``` python
cards_royalflush = [Card(suits[0], ranks[12]), Card(suits[0], ranks[11]), Card(suits[0], ranks[10]), 
    Card(suits[0], ranks[9]), Card(suits[0], ranks[8]), Card(suits[1], ranks[2]), Card(suits[2], ranks[4])]
cards_royalflush
```

    [♠A, ♠K, ♠Q, ♠J, ♠10, ♥4, ♦6]

### Ranks and counts

By getting the counts from the cards, we can figure out
`four of a kind`, `full house`, `three of a kind`, `two pair`, and
`one pair`. One thing we have to be careful of is because we have seven
cards to evaluate, `full house` might have two `three of kind`s, rather
than a `three of kind` and a `pair`.

Here is `get_ranks_counts`, which takes a list of cards and returns a
tuple of ranks and counts. The output is sorted by the rank counts and
`rank_values` for tie breakers. Rank counts are useful for finding what
kind of poker ranking hand it is, and ranks are used for the main hand
components and kickers.

------------------------------------------------------------------------

### L.\_\_class_getitem\_\_

``` python
def __class_getitem__(
    cls:L, *item
):
```

*Call self as a function.*

------------------------------------------------------------------------

### get_ranks_counts

``` python
def get_ranks_counts(
    cards:list
)->(tuple[int, ...], tuple[int, ...]):
```

*Return ((ranks…), (counts…)) sorted by count then by rank value from
cards*

``` python
get_ranks_counts(cards_fullhouse), cards_fullhouse
```

    ([(3, 2, 10, 8), (3, 2, 1, 1)], [♠2, ♥2, ♠3, ♦3, ♣3, ♥8, ♦10])

``` python
def test_get_ranks_counts():
    test_eq(get_ranks_counts(cards_2kind), ((2, 13, 11, 9, 7, 5), (2, 1, 1, 1, 1, 1)))
    test_eq(get_ranks_counts(cards_3kind), ((2, 11, 9, 7, 5), (3, 1, 1, 1, 1)))
    test_eq(get_ranks_counts(cards_4kind), ((2, 12, 9, 6), (4, 1, 1, 1)))
    test_eq(get_ranks_counts(cards_2pair), ((3, 2, 10, 8, 6), (2, 2, 1, 1, 1)))
    test_eq(get_ranks_counts(cards_fullhouse), ((3, 2, 10, 8), (3, 2, 1, 1)))
    test_eq(get_ranks_counts(cards_highcard), ((14, 12, 10, 8, 6, 4, 2), (1, 1, 1, 1, 1, 1, 1)))
    test_eq(get_ranks_counts(cards_straight), ((12, 10, 7, 6, 5, 4, 3), (1, 1, 1, 1, 1, 1, 1)))

    # Edge case: two three-of-a-kinds (e.g. 222 333 K) — full house should pick higher triple
    cards_2trips = [Card(suits[0], ranks[0]), Card(suits[1], ranks[0]), Card(suits[2], ranks[0]),
                    Card(suits[0], ranks[1]), Card(suits[1], ranks[1]), Card(suits[2], ranks[1]),
                    Card(suits[3], ranks[11])]
    test_eq(get_ranks_counts(cards_2trips), ((3, 2, 13), (3, 3, 1)))

test_get_ranks_counts()
```

### flush

As long as the suit count is 5 or more, it is flush because we have
seven cards total.

------------------------------------------------------------------------

### is_flush

``` python
def is_flush(
    cards:list
)->list:
```

*If cards have flush, return the `rank_values` from the cards in sorted
order. If not, return \[\]*

``` python
is_flush(cards_flush)
```

    [♠10, ♠8, ♠6, ♠4, ♠2]

``` python
def test_is_flush():
    # Flush: 5 spades, two side cards
    test_eq([str(c) for c in is_flush(cards_flush)], ['♠10', '♠8', '♠6', '♠4', '♠2'])

    # Straight flush
    test_eq([str(c) for c in is_flush(cards_straightflush)], ['♠7', '♠6', '♠5', '♠4', '♠3'])

    # Royal flush
    test_eq([str(c) for c in is_flush(cards_royalflush)], ['♠A', '♠K', '♠Q', '♠J', '♠10'])

    # No flush: suits too spread out
    test_eq(is_flush(cards_fullhouse), [])
    test_eq(is_flush(cards_highcard), [])
    test_eq(is_flush(cards_4kind), [])
    
    # Edge: all 7 cards same suit — returns all 7
    all_spades = [Card('♠', r) for r in 'A,K,Q,J,10,9,8'.split(',')]
    test_eq([str(c) for c in is_flush(all_spades)], ['♠A', '♠K', '♠Q', '♠J', '♠10', '♠9', '♠8'])
    five_hearts = [Card('♥', r) for r in '2,3,4,5,6'.split(',')] + [Card('♠', 'A'), Card('♦', 'K')]
    test_eq([str(c) for c in is_flush(five_hearts)], ['♥6', '♥5', '♥4', '♥3', '♥2'])

test_is_flush()
```

### straight

Finding straight is trickier because Ace can serve as 1 or 14. Also,
there are seven cards, but only five are used.

------------------------------------------------------------------------

### is_straight

``` python
def is_straight(
    cards:list
)->int:
```

*Return the highest value of the straight if cards contain 5 consecutive
ranks, else 0*

``` python
def test_is_straight():
    # Regular straight: 3-4-5-6-7
    test_eq(is_straight(cards_straight), 7)

    # Ace-high straight (10-J-Q-K-A)
    ace_high = [Card('♠','10'), Card('♥','J'), Card('♦','Q'), Card('♣','K'), Card('♠','A'), Card('♥','3'), Card('♦','6')]
    test_eq(is_straight(ace_high), 14)

    # Wheel: A-2-3-4-5 (ace low)
    wheel = [Card('♠','A'), Card('♥','2'), Card('♦','3'), Card('♣','4'), Card('♠','5'), Card('♥','9'), Card('♦','J')]
    test_eq(is_straight(wheel), 5)

    # No straight — gapped
    test_eq(is_straight(cards_2pair), 0)

    # No straight — bunch of duplicates that run together in rank
    dupes = [Card('♠','5'), Card('♥','5'), Card('♦','6'), Card('♣','6'), Card('♠','7'), Card('♥','7'), Card('♦','8')]
    test_eq(is_straight(dupes), 0)

test_is_straight()
```

### evaluate_hand

Now that we can detect flush and straight, we can update
`evaluate_hand`.

------------------------------------------------------------------------

### evaluate_hand

``` python
def evaluate_hand(
    cards:list
)->tuple:
```

*Return hand rank tuple for the best 5-card hand from `cards`.* Hands
ranked from 1 (high card) to 10 (royal flush). Each rank tuple starts
with the hand category, followed by tiebreaker values (kickers or
relevant ranks).

``` python
def test_evaluate_hand():
    # Royal flush: A♠ K♠ Q♠ J♠ 10♠ → (10,)
    test_eq(evaluate_hand(cards_royalflush), (10,))

    # Straight flush: 3♠ 4♠ 5♠ 6♠ 7♠ → (9, 7)
    test_eq(evaluate_hand(cards_straightflush), (9, 7))

    # Four of a kind: 2222 + Q 9 6 → (8, 2, 12)
    test_eq(evaluate_hand(cards_4kind), (8, 2, 12))

    # Full house: 333 22 + 10 8 → (7, 3, 2)
    test_eq(evaluate_hand(cards_fullhouse), (7, 3, 2))

    # Flush: 5 spades → (6, 10, 8, 6, 4, 2)
    test_eq(evaluate_hand(cards_flush), (6, 10, 8, 6, 4, 2))

    # Straight: 3-4-5-6-7 → (5, 7)
    test_eq(evaluate_hand(cards_straight), (5, 7))

    # Three of a kind: 222 + J 9 7 5 → (4, 2, 11, 9)
    test_eq(evaluate_hand(cards_3kind), (4, 2, 11, 9))
    
    # Two pair: 33 22 + 10 8 6 → (3, 3, 2, 10)
    test_eq(evaluate_hand(cards_2pair), (3, 3, 2, 10))

    # One pair: 22 + K J 9 7 5 → (2, 2, 13, 11, 9)
    test_eq(evaluate_hand(cards_2kind), (2, 2, 13, 11, 9))

    # High card: A Q 10 8 6 4 2 → (1, 14, 12, 10, 8, 6, 4, 2)
    test_eq(evaluate_hand(cards_highcard), (1, 14, 12, 10, 8, 6, 4, 2))

    # ace-high straight without flush should NOT be royal flush
    cards_ace_straight_noflush = [Card('♠','10'), Card('♥','J'), Card('♦','Q'), Card('♣','K'), Card('♠','A'), Card('♥','3'), Card('♦','6')]
    test_eq(evaluate_hand(cards_ace_straight_noflush), (5, 14))

    # straight + flush from different cards should NOT be straight flush
    cards_straight_and_flush = [Card('♠','2'), Card('♠','4'), Card('♠','6'), Card('♠','8'), Card('♠','10'), Card('♥','5'), Card('♥','7')]
    test_eq(evaluate_hand(cards_straight_and_flush), (6, 10, 8, 6, 4, 2))

    # ace-high straight + flush, but flush cards not a straight → flush, not royal flush
    cards_ace_straight_nonstraight_flush = [Card('♠','A'), Card('♠','K'), Card('♠','Q'), Card('♠','J'), Card('♠','5'), Card('♥','10'), Card('♥','3')]
    test_eq(evaluate_hand(cards_ace_straight_nonstraight_flush), (6, 14, 13, 12, 11, 5))

    # straight flush hidden when 6+ cards share a suit and top 5 don't form a straight
    cards_sf_hidden = [Card('♠','2'), Card('♠','3'), Card('♠','4'), Card('♠','5'), Card('♠','6'), Card('♠','8'), Card('♠','K')]
    test_eq(evaluate_hand(cards_sf_hidden), (9, 6))

test_evaluate_hand()
```

### compare_hands

`compare_hands(hands: List[List[Card]]) -> tuple` — Return the winning
hand rank among `hands`

------------------------------------------------------------------------

### compare_hands

``` python
def compare_hands(
    hands:list
)->tuple:
```

*Return the winning hand rank among `hands`.*

``` python
evaluate_hand(cards_flush)
```

    (6, 10, 8, 6, 4, 2)

``` python
def test_compare_hands():
    # Clear winner: flush beats one pair
    test_eq(compare_hands([cards_2kind, cards_flush]), (6, 10, 8, 6, 4, 2))

    # Tie: two identical hands → both should win
    test_eq(compare_hands([cards_flush, cards_flush]), (6, 10, 8, 6, 4, 2))

    # Three hands, two tie for best
    test_eq(compare_hands([cards_2kind, cards_flush, cards_royalflush]), (10,))
    
    # Reverse order shouldn't matter
    test_eq(compare_hands([cards_flush, cards_2kind]), (6, 10, 8, 6, 4, 2))

test_compare_hands()
```

### hand_name

------------------------------------------------------------------------

### hand_name

``` python
def hand_name(
    rank:tuple
)->str:
```

*Readable name for an `evaluate_hand` tuple, e.g. (7,3,2) -\> ‘full
house, threes over twos’.*

``` python
def test_hand_name():
    # Royal flush — only category with no rank mentioned
    test_eq(hand_name((10,)), 'royal flush')

    # Straight flush / straight — "X-high"
    test_eq(hand_name((9, 7)), 'straight flush, seven-high')
    test_eq(hand_name((5, 7)), 'straight, seven-high')
    test_eq(hand_name((5, 14)), 'straight, ace-high')     # ace-high straight
    test_eq(hand_name((5, 5)), 'straight, five-high')     # wheel (A-2-3-4-5)

    # Four of a kind / three of a kind / one pair — "Xs"
    test_eq(hand_name((8, 2, 12)), 'four of a kind, twos')
    test_eq(hand_name((8, 14, 2)), 'four of a kind, aces')
    test_eq(hand_name((4, 2, 11, 9)), 'three of a kind, twos')
    test_eq(hand_name((2, 2, 13, 11, 9)), 'one pair, twos')
    test_eq(hand_name((2, 14, 13, 11, 9)), 'one pair, aces')

    # Full house — "Xs over Ys"
    test_eq(hand_name((7, 3, 2)), 'full house, threes over twos')
    test_eq(hand_name((7, 14, 13)), 'full house, aces over kings')

    # Two pair — "Xs and Ys"
    test_eq(hand_name((3, 3, 2, 10)), 'two pair, threes and twos')
    test_eq(hand_name((3, 14, 13, 12)), 'two pair, aces and kings')

    # Flush / high card — "X-high"
    test_eq(hand_name((6, 10, 8, 6, 4, 2)), 'flush, ten-high')
    test_eq(hand_name((6, 14, 13, 12, 11, 5)), 'flush, ace-high')
    test_eq(hand_name((1, 14, 12, 10, 8, 6, 4, 2)), 'high card, ace-high')
    test_eq(hand_name((1, 2, 4, 6, 8, 10, 12)), 'high card, two-high')

test_hand_name()
```

## Deck

In my original plan, I thought of writing a `CardCollection` class as an
abstract class and create `Hand`, `Deck`, and `CommunityCards` by
inheriting from it as they all consist of cards. However, they don’t
really have methods that share. To keep things simple, I decided to
start implementing `Deck` as a list of `Card`s and see how it feels.

A deck consists of 52 cards. From that, we need to shuffle and withdraw
cards so the dealer can deal cards.

------------------------------------------------------------------------

### mk_deck

``` python
def mk_deck(
    suits:list='♠♥♦♣', ranks:list=['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
)->L:
```

*Create a deck with given suits and ranks*

------------------------------------------------------------------------

### shuffle_deck

``` python
def shuffle_deck(
    deck:list
)->list:
```

*Shuffle `deck` in place and return it*

``` python
random.seed(42)

deck = mk_deck()
shuffle_deck(deck)[:5]
```

    [♠J, ♥Q, ♥A, ♠5, ♥10]

``` python
def test_deck():
    # Standard deck: 52 cards, 4 suits × 13 ranks
    d = mk_deck()
    test_eq(len(d), 52)

    # No duplicates
    test_eq(len(set(d)), 52)

    # Custom: single suit, 5 ranks → 5 cards
    d5 = mk_deck(suits[0], ranks[:5])
    test_eq(len(d5), 5)
    test_eq(d5, [Card('♠','2'), Card('♠','3'), Card('♠','4'), Card('♠','5'), Card('♠','6')])

    # Two suits, three ranks → 6 cards
    d6 = mk_deck(suits[:2], ranks[:3])
    test_eq(len(d6), 6)
    
    # First card is first suit + first rank
    test_eq(d6[0], Card('♠','2'))
    
test_deck()
```

------------------------------------------------------------------------

### withdraw_card

``` python
def withdraw_card(
    deck:list, n:int=1
)->list:
```

*Withdraw `n` cards from `deck` and return them. Modifies `deck` in
place.*

``` python
def test_withdraw_card():
    # One card: returns it, deck shrinks by one, card was last in deck
    deck = mk_deck()
    last_card = deck[-1]
    got = withdraw_card(deck)
    test_eq(got, [last_card])
    test_eq(len(deck), 51)
    
    # n cards: returned in reverse deck order (pop from end)
    deck = mk_deck()
    expected = list(deck[-3:])[::-1]
    got = withdraw_card(deck, 3)
    test_eq(got, expected)
    test_eq(len(deck), 49)

    # Withdrawn cards are exactly the ones removed — nothing lost or duplicated
    deck = mk_deck()
    full = set(deck)
    got = withdraw_card(deck, 5)
    test_eq(len(set(got) & set(deck)), 0)
    test_eq(set(got) | set(deck), full)

    # n=0: no cards, deck unchanged
    deck = mk_deck()
    test_eq(withdraw_card(deck, 0), [])
    test_eq(len(deck), 52)

    # Withdraw the whole deck: works, deck now empty
    deck = mk_deck()
    test_eq(len(withdraw_card(deck, 52)), 52)
    test_eq(len(deck), 0)

    # Too many: raises, and the deck is untouched
    deck = mk_deck()
    test_fail(lambda: withdraw_card(deck, 53), contains='Not enough cards')
    test_eq(len(deck), 52)

    # Depleted deck: raises even for n=1
    deck = mk_deck()
    withdraw_card(deck, 52)
    test_fail(lambda: withdraw_card(deck), contains='Not enough cards')

test_withdraw_card()
```

## Monte Carlo

## equity

------------------------------------------------------------------------

### equity

``` python
def equity(
    hand:list, community:list=(), n_opponents:int=1, n_sims:int=10000, seed:int=None
)->dict:
```

*Monte Carlo estimate of win/tie/lose fractions for `hand` given
`community` vs `n_opponents`.* Pass `seed` for reproducible classroom
demos.

The `equity` function provides a great way to simulate what would happen
if the game goes on. It counts wins, ties, and loses from 10_000
simulations by default. It’s fun to play with. If I start with two aces,
what is a chance of winning against one opponent?

``` python
equity([Card('♠','A'), Card('♥','A')])
```

    {'win': 0.8571, 'tie': 0.0053, 'lose': 0.13759999999999994}

It is 85%, but think about what would happen with 2 opponents?

``` python
equity([Card('♠','A'), Card('♥','A')], n_opponents=2)
```

    {'win': 0.7305, 'tie': 0.0042, 'lose': 0.2653}

It dropped to 73%. Why would that be?

What would happen if we have three kings in the community card? How
would that change the probability of winning if it changes at all?

``` python
equity([Card('♠','A'), Card('♥','A')], [Card('♠','K'), Card('♥','K'), Card('♦','K')])
```

    {'win': 0.9522, 'tie': 0.0053, 'lose': 0.04249999999999998}

## Player

`Player`class:

- balance: int
- bet: int
- playing: bool
- hand: List[Card](#card) methods:
- take_action(round)
- call(round)
- check(round)
- fold(round)
- raise(round, amount).

------------------------------------------------------------------------

### Action

``` python
def Action(
    *args, **kwds
):
```

*Create a collection of name/value pairs.*

Example enumeration:

> > > class Color(Enum): … RED = 1 … BLUE = 2 … GREEN = 3

Access them by:

- attribute access:

  > > > Color.RED \<Color.RED: 1\>

- value lookup:

  > > > Color(1) \<Color.RED: 1\>

- name lookup:

  > > > Color\[‘RED’\] \<Color.RED: 1\>

Enumerations can be iterated over, and know how many members they have:

> > > len(Color) 3

> > > list(Color) \[\<Color.RED: 1\>, \<Color.BLUE: 2\>, \<Color.GREEN:
> > > 3\>\]

Methods can be added to enumerations, and members can have their own
attributes – see the documentation for details.

### Strategy

`Player` has a `strategy` attribute, which chooses actions.

------------------------------------------------------------------------

### human

``` python
def human():
```

*Call self as a function.*

------------------------------------------------------------------------

### always_raise

``` python
def always_raise(
    n
):
```

*Call self as a function.*

------------------------------------------------------------------------

### always_fold

``` python
def always_fold():
```

*Call self as a function.*

------------------------------------------------------------------------

### always_check

``` python
def always_check():
```

*Call self as a function.*

------------------------------------------------------------------------

### always_call

``` python
def always_call():
```

*Call self as a function.*

### player class

------------------------------------------------------------------------

### Player

``` python
def Player(
    username:str='new_user', balance:int=0, bet:int=0, playing:bool=True, hand:list=<factory>,
    strategy:Callable=always_call
)->None:
```

``` python
@patch
def __repr__(self: Player):
    return (f"Player(username={self.username!r}, balance={self.balance}, bet={self.bet}, "
            f"playing={self.playing}, hand={self.hand}, strategy={self.strategy.__name__})")
```

------------------------------------------------------------------------

### ActionHandler

``` python
def ActionHandler(
    player, stage_bet
):
```

*Initialize self. See help(type(self)) for accurate signature.*

``` python
stage_bet = 2
player1 = Player('galopy', balance=20, playing=True, strategy=always_raise(1))
need = player1.perform_action(stage_bet)
need, player1
```

    galopy intends to raise, stage_bet: 2, raise_amount: 1

    (3,
     Player(username='galopy', balance=17, bet=3, playing=True, hand=[], strategy=always_raise(1)))

``` python
def test_player():
    # call: normal case
    p = Player('t', balance=100, bet=10)
    test_eq(p.call(20), 20)
    test_eq(p.balance, 80)
    test_eq(p.bet, 30)

    # call: need = 0 (already matched)
    p = Player('t', balance=100, bet=10)
    test_eq(p.call(0), 0)
    test_eq(p.balance, 100)
    test_eq(p.bet, 10)

    # check: no state change
    p = Player('t', balance=100, bet=10)
    test_eq(p.check(), 0)
    test_eq(p.balance, 100)
    test_eq(p.bet, 10)

    # fold: playing → False, no money changes
    p = Player('t', balance=100, bet=10)
    test_eq(p.fold(), 0)
    test_eq(p.playing, False)
    test_eq(p.balance, 100)

    # raise_: normal case
    p = Player('t', balance=100, bet=10)
    test_eq(p.raise_(50), 50)
    test_eq(p.balance, 50)
    test_eq(p.bet, 60)

    # call + raise_ share _place_bet: bets accumulate
    p = Player('t', balance=100, bet=10)
    p.call(5); p.raise_(5)
    test_eq(p.balance, 90)
    test_eq(p.bet, 20)
    
    # reset_bet: only bet zeroed, balance untouched
    p = Player('t', balance=100, bet=10)
    p.reset_bet()
    test_eq(p.bet, 0)
    test_eq(p.balance, 100)

test_player()
```

``` python
def test_action_handler():
    # call: pays the difference between stage_bet and current bet
    p = Player('t', balance=20, bet=0, strategy=always_call)
    test_eq(p.perform_action(2), 2)
    test_eq(p.balance, 18)
    test_eq(p.bet, 2)
    test_eq(p.playing, True)

    # call: partial — player already bet 1, only needs 1 more
    p = Player('t', balance=20, bet=1, strategy=always_call)
    test_eq(p.perform_action(2), 1)
    test_eq(p.balance, 19)
    test_eq(p.bet, 2)

    # check: no state change, returns 0
    p = Player('t', balance=20, bet=0, strategy=always_check)
    test_eq(p.perform_action(2), 0)
    test_eq(p.balance, 20)
    test_eq(p.bet, 0)
    test_eq(p.playing, True)

    # fold: playing → False, no money moves
    p = Player('t', balance=20, bet=0, strategy=always_fold)
    test_eq(p.perform_action(2), 0)
    test_eq(p.playing, False)
    test_eq(p.balance, 20)
    test_eq(p.bet, 0)

    # raise: pays new_stage_bet - current bet
    p = Player('t', balance=20, bet=0, strategy=always_raise(5))
    test_eq(p.perform_action(2), 7)  # 2 + 5 = 7
    test_eq(p.balance, 13)
    test_eq(p.bet, 7)

    # AI folds when it can't afford the call
    p = Player('t', balance=1, bet=0, strategy=always_call)
    test_eq(p.perform_action(2), 0)
    test_eq(p.playing, False)
    test_eq(p.balance, 1)

    # AI folds when it can't afford the raise
    p = Player('t', balance=5, bet=0, strategy=always_raise(10))
    test_eq(p.perform_action(2), 0)
    test_eq(p.playing, False)
    test_eq(p.balance, 5)

test_action_handler()
```

    t intends to call, stage_bet: 2, raise_amount: 0
    t intends to call, stage_bet: 2, raise_amount: 0
    t intends to check, stage_bet: 2, raise_amount: 0
    t intends to fold, stage_bet: 2, raise_amount: 0
    t intends to raise, stage_bet: 2, raise_amount: 5
    t intends to call, stage_bet: 2, raise_amount: 0
    t intends to raise, stage_bet: 2, raise_amount: 10

## Round

Round Management

Attributes: - stage (pre-flop, flop, turn, river) - players - deck -
community_cards

Methods:

- `betting_round(round) -> None`: Manage one round of player actions
  (pre-flop, flop, turn, or river). Ends when all active players have
  called or checked.
- `distribute(pot, winners) -> None`: Distribute the pot to the
  winner(s).
- `start_round(round) -> None`: Orchestrate the whole hand: deal hole
  cards → blinds → betting round → flop → betting round → turn → betting
  round → river → betting round → showdown → distribute.

------------------------------------------------------------------------

### Stage

``` python
def Stage(
    *args, **kwds
):
```

*Create a collection of name/value pairs.*

Example enumeration:

> > > class Color(Enum): … RED = 1 … BLUE = 2 … GREEN = 3

Access them by:

- attribute access:

  > > > Color.RED \<Color.RED: 1\>

- value lookup:

  > > > Color(1) \<Color.RED: 1\>

- name lookup:

  > > > Color\[‘RED’\] \<Color.RED: 1\>

Enumerations can be iterated over, and know how many members they have:

> > > len(Color) 3

> > > list(Color) \[\<Color.RED: 1\>, \<Color.BLUE: 2\>, \<Color.GREEN:
> > > 3\>\]

Methods can be added to enumerations, and members can have their own
attributes – see the documentation for details.

Let’s go through playing a round of poker game!

------------------------------------------------------------------------

### Round

``` python
def Round(
    players:NoneType=None, deck:NoneType=None, community:NoneType=None, stage:Stage=<Stage.PREFLOP: 1>,
    stage_bet:int=0, pot:int=0
):
```

*Initialize self. See help(type(self)) for accurate signature.*

``` python
def test_distribute():
    # Single winner takes the whole pot
    r = Round(); r.pot = 10
    p = Player(balance=5)
    r._distribute([p])
    test_eq(p.balance, 15)

    # Even split: two winners
    r = Round(); r.pot = 10
    p1, p2 = Player(balance=5), Player(balance=5)
    r._distribute([p1, p2])
    test_eq(p1.balance, 10)
    test_eq(p2.balance, 10)

    # Odd chips: remainder dropped (documents current behavior)
    r = Round(); r.pot = 5
    p1, p2 = Player(balance=0), Player(balance=0)
    r._distribute([p1, p2])
    test_eq(p1.balance, 2)
    test_eq(p2.balance, 2)

test_distribute()
```

    Distributing $ 10 to [Player(username='new_user', balance=5, bet=0, playing=True, hand=[], strategy=always_call)]
    Distributing $ 10 to [Player(username='new_user', balance=5, bet=0, playing=True, hand=[], strategy=always_call), Player(username='new_user', balance=5, bet=0, playing=True, hand=[], strategy=always_call)]
    Distributing $ 5 to [Player(username='new_user', balance=0, bet=0, playing=True, hand=[], strategy=always_call), Player(username='new_user', balance=0, bet=0, playing=True, hand=[], strategy=always_call)]

``` python
def test_prep_round():
    # Fresh round: each player gets 2 cards, deck loses 2 per player
    players = [Player(balance=20) for _ in range(3)]
    r = Round(players=players)
    r._prep_round()
    test_eq([len(p.hand) for p in players], [2, 2, 2])
    test_eq(len(r.deck), 52 - 6)
    test_eq(r.stage, Stage.PREFLOP)
    test_eq(r.community, [])
    test_eq(r.pot, 0)

    # Called again: state resets, fresh hands dealt
    hands_before = [p.hand for p in players]
    r._prep_round()
    test_eq([len(p.hand) for p in players], [2, 2, 2])
    test_eq(len(r.deck), 52 - 6)
    test_ne([p.hand for p in players], hands_before)

test_prep_round()
```

``` python
def test_reset_betting_round():
    # Dirty state gets fully reset
    players = [Player(balance=20, bet=3) for _ in range(2)]
    r = Round(players=players)
    r._idx, r.stage_bet, r._raiser = 1, 10, 1
    r._reset_betting_round()
    test_eq([p.bet for p in players], [0, 0])
    test_eq(r.stage_bet, 0)
    test_eq(r._idx, 0)
    test_eq(r._raiser, -1)

    # Balances untouched — only bets reset
    test_eq([p.balance for p in players], [20, 20])

test_reset_betting_round()
```

``` python
def test_deal_community():
    # PREFLOP
    r = Round()
    r._deal_community()
    test_eq(len(r.community), 0)
    test_eq(len(r.deck), 52)

    # FLOP: 3 cards dealt, appended to community
    r.stage = Stage.FLOP
    r._deal_community()
    test_eq(len(r.community), 3)
    test_eq(len(r.deck), 52 - 3)

    # TURN: 1 more card appended
    r.stage = Stage.TURN
    r._deal_community()
    test_eq(len(r.community), 4)
    test_eq(len(r.deck), 52 - 4)

    # RIVER: same logic as TURN — 1 card
    r.stage = Stage.RIVER
    r._deal_community()
    test_eq(len(r.community), 5)
    test_eq(len(r.deck), 52 - 5)

test_deal_community()
```

    --- PREFLOP --- Community: []

    --- FLOP --- Community: [♣7, ♠J, ♦10]

    --- TURN --- Community: [♣7, ♠J, ♦10, ♠4]

    --- RIVER --- Community: [♣7, ♠J, ♦10, ♠4, ♥Q]

``` python
def test_showdown():
    # Clear winner: player1 flush; player2 high card; player3 folded with
    # a winning hand, excluded from evaluation
    community = [Card('♠','6'), Card('♠','8'), Card('♠','10'), Card('♥','5'), Card('♦','9')]
    p1 = Player(username='a', hand=[Card('♠','2'), Card('♠','4')])
    p2 = Player(username='b', hand=[Card('♥','3'), Card('♦','7')])
    p3 = Player(username='c', hand=[Card('♠','A'), Card('♠','K')], playing=False)
    r = Round(players=[p1,p2,p3], community=community, pot=10)
    r._showdown()
    test_eq(p1.balance, 10)
    test_eq(p2.balance, 0)
    test_eq(p3.balance, 0)  # folded: excluded even though hand would win

    # Tie: community royal flush — both players split the pot
    community = [Card('♠','10'), Card('♠','J'), Card('♠','Q'), Card('♠','K'), Card('♠','A')]
    p1 = Player(username='a', hand=[Card('♥','2'), Card('♦','3')])
    p2 = Player(username='b', hand=[Card('♣','4'), Card('♠','5')])
    r = Round(players=[p1,p2], community=community, pot=10)
    r._showdown()
    test_eq(p1.balance, 5)
    test_eq(p2.balance, 5)

test_showdown()
```

    Winners: ['a'] with (6, 10, 8, 6, 4, 2)
    Distributing $ 10 to [Player(username='a', balance=0, bet=0, playing=True, hand=[♠2, ♠4], strategy=always_call)]
    Winners: ['a', 'b'] with (10,)
    Distributing $ 10 to [Player(username='a', balance=0, bet=0, playing=True, hand=[♥2, ♦3], strategy=always_call), Player(username='b', balance=0, bet=0, playing=True, hand=[♣4, ♠5], strategy=always_call)]

------------------------------------------------------------------------

### Round.start_round

``` python
def start_round():
```

*Deal, start PREFLOP, and auto-advance to first human player.*

``` python
def test_start_round():
    # A raises 1 each betting round, B calls, C always folds
    a = Player('a', balance=20, strategy=always_raise(1))
    b = Player('b', balance=20, strategy=always_call)
    c = Player('c', balance=20, strategy=always_fold)
    r = Round(players=[a, b, c])
    list(r.start_round())  # consume the generator; all AI, so no yields

    # 4 betting rounds × (A:1 + B:1) = 8 chips, all paid out
    test_eq(sum(p.balance for p in r.players), 60)  # no chips created or lost
    test_eq(a.balance + b.balance, 40)              # 8 chips moved A+B → winner
    test_eq(c.balance, 20)                          # folded, never paid

test_start_round()
```

    --- PREFLOP --- Community: []
    a intends to raise, stage_bet: 0, raise_amount: 1
    b intends to call, stage_bet: 1, raise_amount: 0
    c intends to fold, stage_bet: 1, raise_amount: 0

    --- FLOP --- Community: [♥3, ♥K, ♣9]
    a intends to raise, stage_bet: 0, raise_amount: 1
    b intends to call, stage_bet: 1, raise_amount: 0

    --- TURN --- Community: [♥3, ♥K, ♣9, ♥2]
    a intends to raise, stage_bet: 0, raise_amount: 1
    b intends to call, stage_bet: 1, raise_amount: 0

    --- RIVER --- Community: [♥3, ♥K, ♣9, ♥2, ♠6]
    a intends to raise, stage_bet: 0, raise_amount: 1
    b intends to call, stage_bet: 1, raise_amount: 0
    Winners: ['a'] with (6, 13, 10, 9, 3, 2)
    Distributing $ 8 to [Player(username='a', balance=16, bet=1, playing=True, hand=[♥10, ♥9], strategy=always_raise(1))]

------------------------------------------------------------------------

### act

``` python
def act(
    game, player, round
):
```

*Call self as a function.*

``` python
player1 = Player('galopy', balance=20, playing=True, strategy=always_fold)
player2 = Player('jalopy', balance=20, playing=False)
player3 = Player('zalopy', balance=20, playing=True, strategy=always_call)
players = [player1, player2, player3]
round1 = Round(players = players)
game = round1.start_round()
player = next(game, None)

round1.state()
```

    --- PREFLOP --- Community: []
    galopy intends to fold, stage_bet: 0, raise_amount: 0
    jalopy intends to call, stage_bet: 0, raise_amount: 0
    zalopy intends to call, stage_bet: 0, raise_amount: 0

    --- FLOP --- Community: [♠9, ♣A, ♥8]
    jalopy intends to call, stage_bet: 0, raise_amount: 0
    zalopy intends to call, stage_bet: 0, raise_amount: 0

    --- TURN --- Community: [♠9, ♣A, ♥8, ♠8]
    jalopy intends to call, stage_bet: 0, raise_amount: 0
    zalopy intends to call, stage_bet: 0, raise_amount: 0

    --- RIVER --- Community: [♠9, ♣A, ♥8, ♠8, ♦K]
    jalopy intends to call, stage_bet: 0, raise_amount: 0
    zalopy intends to call, stage_bet: 0, raise_amount: 0
    Winners: ['zalopy'] with (4, 8, 14, 13)
    Distributing $ 0 to [Player(username='zalopy', balance=20, bet=0, playing=True, hand=[♣J, ♣8], strategy=always_call)]

    'Game is over'

``` python
player = act(game, player, round1)
player
```

    Game is over

## Game

We can create a `Game` class that simplifies playing the game.

------------------------------------------------------------------------

### Game

``` python
def Game(
    players
):
```

*Initialize self. See help(type(self)) for accurate signature.*

``` python
game = Game([Player('you', balance=20, strategy=always_call),
             Player('bot', balance=20, strategy=always_raise(1))])
game.start()
```

    --- PREFLOP --- Community: []
    you intends to call, stage_bet: 0, raise_amount: 0
    bot intends to raise, stage_bet: 0, raise_amount: 1
    you intends to call, stage_bet: 1, raise_amount: 0

    --- FLOP --- Community: [♦6, ♦J, ♦9]
    you intends to call, stage_bet: 0, raise_amount: 0
    bot intends to raise, stage_bet: 0, raise_amount: 1
    you intends to call, stage_bet: 1, raise_amount: 0

    --- TURN --- Community: [♦6, ♦J, ♦9, ♥J]
    you intends to call, stage_bet: 0, raise_amount: 0
    bot intends to raise, stage_bet: 0, raise_amount: 1
    you intends to call, stage_bet: 1, raise_amount: 0

    --- RIVER --- Community: [♦6, ♦J, ♦9, ♥J, ♦3]
    you intends to call, stage_bet: 0, raise_amount: 0
    bot intends to raise, stage_bet: 0, raise_amount: 1
    you intends to call, stage_bet: 1, raise_amount: 0
    Winners: ['bot'] with (6, 13, 11, 9, 6, 3)
    Distributing $ 8 to [Player(username='bot', balance=16, bet=1, playing=True, hand=[♦K, ♥Q], strategy=always_raise(1))]
    Game is over

``` python
game.act()
```

``` python
game.players
```

    [Player(username='you', balance=16, bet=1, playing=True, hand=[♥8, ♣5], strategy=always_call),
     Player(username='bot', balance=24, bet=1, playing=True, hand=[♦K, ♥Q], strategy=always_raise(1))]
