Metadata-Version: 2.4
Name: py-poker-equity
Version: 0.1.2
Summary: Fast Texas Hold'em equity calculator with precomputed preflop lookup table
Author: Mitch Tabian
License-Expression: MIT
Project-URL: Homepage, https://github.com/mitchtabian/py-poker-equity
Project-URL: Repository, https://github.com/mitchtabian/py-poker-equity
Project-URL: Issues, https://github.com/mitchtabian/py-poker-equity/issues
Keywords: poker,equity,texas-holdem,hand-evaluator,preflop
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Games/Entertainment
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"

# py-poker-equity

Fast Texas Hold'em equity calculator for Python. Computes win/loss/tie percentages for heads-up matchups at any stage of a hand.

- **Preflop**: Instant lookup from a precomputed table (all 169x169 canonical matchups)
- **Flop/Turn/River**: Exact enumeration of remaining board cards (no Monte Carlo, no approximation)
- **Zero dependencies**: Pure Python, no C/Rust compilation, nothing to break

## Installation

```bash
pip install py-poker-equity
```

Or install from source:

```bash
git clone https://github.com/mitchtabian/py-poker-equity.git
cd py-poker-equity
pip install -e .
```

## Quick Start

```python
from py_poker_equity import get_equity

# Preflop: AK suited vs pocket queens
result = get_equity(["Ah", "Kh"], ["Qd", "Qs"])
# {'a_win': 46.15, 'b_win': 53.15, 'tie': 0.70}

# Flop: AK suited vs pocket queens on a friendly board
result = get_equity(["Ah", "Kh"], ["Qd", "Qs"], board=["7h", "4h", "2c"])
# {'a_win': 54.55, 'b_win': 43.94, 'tie': 1.52}

# Turn
result = get_equity(["Ah", "Kh"], ["Qd", "Qs"], board=["7h", "4h", "2c", "Jd"])
# {'a_win': 43.18, 'b_win': 56.82, 'tie': 0.0}

# River (deterministic — one player wins or it's a tie)
result = get_equity(["Ah", "Kh"], ["Qd", "Qs"], board=["7h", "4h", "2c", "Jd", "Ac"])
# {'a_win': 100.0, 'b_win': 0.0, 'tie': 0.0}
```

## API Reference

### Card Notation

Cards are strings with rank + suit:
- **Ranks**: `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`, `10`, `J`, `Q`, `K`, `A`
- **Suits**: `h` (hearts), `d` (diamonds), `c` (clubs), `s` (spades)

Examples: `"Ah"` (ace of hearts), `"10s"` (ten of spades), `"2c"` (two of clubs)

---

### `get_equity(hand_a, hand_b, board=None)`

Calculate win/loss/tie percentages for a heads-up matchup.

**Parameters:**
- `hand_a` — list of 2 card strings (player A's hole cards)
- `hand_b` — list of 2 card strings (player B's hole cards)
- `board` — list of 0, 3, 4, or 5 card strings (community cards). Default `None` = preflop.

**Returns:** dict with keys `a_win`, `b_win`, `tie` (percentages 0-100, rounded to 2 decimal places)

**Raises:** `ValueError` if duplicate cards are detected or board has invalid number of cards.

```python
from py_poker_equity import get_equity

# Preflop (uses lookup table — instant)
get_equity(["Ah", "Kh"], ["Qd", "Qs"])

# Flop (enumerates ~946 remaining board combos)
get_equity(["Ah", "Kh"], ["Qd", "Qs"], board=["7h", "4h", "2c"])

# Turn (enumerates ~44 remaining cards)
get_equity(["Ah", "Kh"], ["Qd", "Qs"], board=["7h", "4h", "2c", "Jd"])

# River (just evaluates — one comparison)
get_equity(["Ah", "Kh"], ["Qd", "Qs"], board=["7h", "4h", "2c", "Jd", "Ac"])
```

---

### `get_preflop_equity(hand_a, hand_b)`

Look up preflop equity directly from the precomputed table. This is called automatically by `get_equity` when no board is provided, but you can call it directly if you want.

**Parameters:**
- `hand_a` — list of 2 card strings
- `hand_b` — list of 2 card strings

**Returns:** dict with keys `a_win`, `b_win`, `tie`

```python
from py_poker_equity import get_preflop_equity

get_preflop_equity(["Ah", "Kh"], ["Qd", "Qs"])
# {'a_win': 46.15, 'b_win': 53.15, 'tie': 0.70}
```

---

### `evaluate_hand(hole_cards, board)`

Evaluate the best 5-card poker hand from hole cards + board.

**Parameters:**
- `hole_cards` — list of 2 card strings
- `board` — list of 3-5 card strings

**Returns:** `HandResult` namedtuple with:
- `hand_rank` — int (1 = Royal Flush, 10 = High Card)
- `hand_name` — string (e.g. "Full House", "Pair")
- `best_five` — list of the 5 `Card` namedtuples that make the best hand
- `tiebreaker` — tuple used for comparing hands of the same rank

```python
from py_poker_equity import evaluate_hand

result = evaluate_hand(["Ah", "Kh"], ["Qh", "Jh", "10h", "2c", "3d"])
print(result.hand_name)   # "Royal Flush"
print(result.hand_rank)   # 1

result = evaluate_hand(["7s", "7d"], ["7c", "Ks", "Kh", "2c", "3d"])
print(result.hand_name)   # "Full House"
print(result.hand_rank)   # 4
```

**Hand rank reference:**

| Rank | Hand |
|------|------|
| 1 | Royal Flush |
| 2 | Straight Flush |
| 3 | Four of a Kind |
| 4 | Full House |
| 5 | Flush |
| 6 | Straight |
| 7 | Three of a Kind |
| 8 | Two Pair |
| 9 | Pair |
| 10 | High Card |

---

### `compute_winner(hand_a, hand_b)`

Determine the winner between two `HandResult` objects.

**Parameters:**
- `hand_a` — `HandResult` from `evaluate_hand`
- `hand_b` — `HandResult` from `evaluate_hand`

**Returns:** The winning `HandResult` object, or `None` if it's a tie.

```python
from py_poker_equity import evaluate_hand, compute_winner

board = ["9c", "7s", "5h", "2c", "3d"]
aces = evaluate_hand(["Ah", "Ad"], board)
kings = evaluate_hand(["Kh", "Kd"], board)

winner = compute_winner(aces, kings)

if winner is aces:
    print("Aces win!")
elif winner is kings:
    print("Kings win!")
else:
    print("It's a tie!")
# Output: Aces win!
```

## Preflop Table

The preflop lookup table ships with the package (752 KB). It contains exact equity for all 14,365 canonical hand matchups, sourced from the [oscar6echo/Poker2](https://github.com/oscar6echo/Poker2) dataset which was computed via exhaustive enumeration of all possible boards.

If the table gets deleted or you want to regenerate it:

```bash
python -m py_poker_equity
```

This converts the reference CSV (included in the test fixtures) into the JSON lookup format. Takes about 2 seconds.

## How It Works

**Preflop**: There are 169 canonical starting hand types in Texas Hold'em (13 pairs + 78 suited + 78 offsuit). The preflop table stores the exact equity for every possible matchup between these hand types. At runtime, hole cards are mapped to their canonical type and the result is looked up — no computation needed.

**Post-flop**: With 3+ community cards dealt, the remaining unknown cards are few enough to enumerate exhaustively:
- **Flop** (2 cards to come): ~946 possible board completions
- **Turn** (1 card to come): ~44 possible river cards
- **River** (board complete): Just evaluate and compare

For each possible board completion, both hands are evaluated and the winner is tallied. This gives exact results — no simulation, no approximation.

## Performance

| Stage | Method | Speed |
|-------|--------|-------|
| Preflop | Lookup table | Instant |
| Flop | Enumerate ~946 boards | ~50ms |
| Turn | Enumerate ~44 cards | ~2ms |
| River | Single evaluation | <1ms |

## Limitations

This library is **heads-up only** (2 players). It does not support multi-way equity calculations (3+ players).

**Why?** The preflop lookup table is the constraint. For 2 players there are ~14,000 canonical matchups — a few hundred KB. For 3 players it's 169^3 = ~4.8 million matchups (~115 MB). For 4 players it's ~815 million. It blows up exponentially and becomes impractical to ship as a pip package.

**Post-flop multi-way is still feasible** using the `evaluate_hand` and `compute_winner` functions directly. The enumeration logic is the same — you just evaluate 3+ hands per board runout instead of 2. On the flop that's ~946 board completions, which is fast regardless of player count. If you need multi-way equity post-flop, you can write a small wrapper around `evaluate_hand`:

```python
from itertools import combinations
from py_poker_equity.evaluator import evaluate_hand, compute_winner, parse_card

def get_multiway_equity(hands, board):
    """
    hands: list of hole card lists, e.g. [["Ah", "Kh"], ["Qd", "Qs"], ["Jc", "10c"]]
    board: list of 3-5 board cards
    """
    all_known = [c for h in hands for c in h] + board
    known_set = set((parse_card(c).rank, parse_card(c).suit) for c in all_known)

    RANKS = ['2','3','4','5','6','7','8','9','10','J','Q','K','A']
    SUITS = ['h','d','c','s']
    remaining = [f"{r}{s}" for r in RANKS for s in SUITS
                 if (parse_card(f"{r}{s}").rank, parse_card(f"{r}{s}").suit) not in known_set]

    cards_needed = 5 - len(board)
    wins = [0] * len(hands)
    ties = 0

    for extra in combinations(remaining, cards_needed):
        full_board = board + list(extra)
        results = [evaluate_hand(h, full_board) for h in hands]
        # Find the best hand
        best = results[0]
        best_indices = [0]
        for i in range(1, len(results)):
            w = compute_winner(best, results[i])
            if w is results[i]:
                best = results[i]
                best_indices = [i]
            elif w is None:
                best_indices.append(i)
        if len(best_indices) == 1:
            wins[best_indices[0]] += 1
        else:
            ties += 1

    total = sum(wins) + ties
    return {
        'wins': [round(w / total * 100, 2) for w in wins],
        'tie': round(ties / total * 100, 2),
    }
```

The only thing this library can't do for multi-way is **preflop** — because there's no lookup table for 3+ players. If you know the stage of the all-in (flop, turn, or river), multi-way works fine.

## License

MIT
