"""Poker hands as a structured domain — a harder problem for the formalism. Type: Rank = ordered 13-set (2..A; the order is part of the type) Suit = bare 4-set Card = Rank x Suit Hand = 5-subset of Card (the positions are fully interchangeable: a hand IS a point of Sym^5(Card)) Class = {HighCard, Pair, TwoPair, Trips, Straight, Flush, FullHouse, Quads, StraightFlush, RoyalFlush} Derived structure (the same constructions as tic-tac-toe): - membership atoms: the evaluation maps of the multiset (contains card c); - rank multiplicities m_r : Hand -> {0..4} (multiplicity quotient over the projection Card -> Rank); - suit multiplicities s_u : Hand -> {0..5}; - multiplicities of multiplicities N_k = #{ranks with m_r = k} — the same second-order construction as tic-tac-toe's line-profile counts, and the coordinates of the partition {m_r} of 5; - existential lifts (max rank multiplicity, max suit multiplicity); - run structure: because Rank is *ordered*, windows of 5 consecutive ranks are canonical subsets of Rank (with the ace also low, the standard convention); "the rank support contains a full window with top r" gives the straight family. |Hand| = C(52,5) = 2,598,960; all extensions are materialized as bitmasks. """ from __future__ import annotations import itertools import numpy as np from .core import Atom, Domain, choice, dedupe_atoms N_HANDS = 2598960 CLASSES = ["HighCard", "Pair", "TwoPair", "Trips", "Straight", "Flush", "FullHouse", "Quads", "StraightFlush", "RoyalFlush"] KNOWN_COUNTS = [1302540, 1098240, 123552, 54912, 10200, 5108, 3744, 624, 36, 4] RANK_NAMES = ["2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A"] SUIT_NAMES = ["c", "d", "h", "s"] def bits_to_mask(arr: np.ndarray) -> int: return int.from_bytes(np.packbits(arr, bitorder="little").tobytes(), "little") def mask_to_bits(mask: int, n: int = N_HANDS) -> np.ndarray: nbytes = (n + 7) // 8 b = np.frombuffer(mask.to_bytes(nbytes, "little"), dtype=np.uint8) return np.unpackbits(b, bitorder="little")[:n].astype(bool) class PokerData: def __init__(self): hands = np.empty((N_HANDS, 5), dtype=np.uint8) for i, combo in enumerate(itertools.combinations(range(52), 5)): hands[i] = combo self.hands = hands # card presence (cards are distinct, so presence == multiplicity) cp = np.zeros((N_HANDS, 52), dtype=bool) rows = np.arange(N_HANDS) for j in range(5): cp[rows, hands[:, j]] = True self.card_present = cp by = cp.reshape(N_HANDS, 4, 13) # card = suit*13 + rank rc = by.sum(axis=1, dtype=np.uint8) # rank multiplicities sc = by.sum(axis=2, dtype=np.uint8) # suit multiplicities self.rank_counts = rc self.suit_counts = sc self.mult_counts = np.stack([(rc == k).sum(axis=1) for k in (1, 2, 3, 4)], axis=1).astype(np.uint8) # N_1..N_4 self.max_rank_mult = rc.max(axis=1) self.max_suit = sc.max(axis=1) # straight windows: 9 high windows (tops 5..A) + the low-ace wheel support = rc > 0 runtop = np.full(N_HANDS, 255, dtype=np.uint8) wheel = (support[:, 12] & support[:, 0] & support[:, 1] & support[:, 2] & support[:, 3]) # A-2-3-4-5, top '5'=idx 3 runtop[wheel] = 3 for s in range(9): # tops idx 4..12 win = support[:, s:s + 5].all(axis=1) runtop[win] = s + 4 # higher window wins self.runtop = runtop self.straight = runtop != 255 def labels_poker(d: PokerData) -> np.ndarray: n1, n2, n3, n4 = (d.mult_counts[:, k] for k in range(4)) flush = d.max_suit == 5 straight = d.straight lab = np.zeros(N_HANDS, dtype=np.int8) # HighCard lab[n2 == 1] = 1 # Pair lab[n2 == 2] = 2 # TwoPair lab[n3 == 1] = 3 # Trips lab[straight] = 4 # Straight lab[flush] = 5 # Flush lab[(n3 == 1) & (n2 == 1)] = 6 # FullHouse lab[n4 == 1] = 7 # Quads lab[straight & flush] = 8 # StraightFlush lab[straight & flush & (d.runtop == 12)] = 9 # RoyalFlush return lab def build_domain(d: PokerData) -> Domain: atoms: list[Atom] = [] nfam = 6 # member, rankcount, suitcount, multcount, max, run tag = choice(nfam) # 1. membership: contains card (r, u) mem_cost = tag + choice(52) member_atoms: dict[int, Atom] = {} for c in range(52): a = Atom(f"has {RANK_NAMES[c % 13]}{SUIT_NAMES[c // 13]}", mem_cost, bits_to_mask(d.card_present[:, c])) atoms.append(a) member_atoms[c] = a d.card_present = None # free ~135 MB # 2. rank multiplicities: rank (13), kind (2: =, >=), c (0..4) for r in range(13): base = tag + choice(13) col = d.rank_counts[:, r] for cth in range(5): cost = base + choice(2) + choice(5) atoms.append(Atom(f"m[{RANK_NAMES[r]}]={cth}", cost, bits_to_mask(col == cth))) atoms.append(Atom(f"m[{RANK_NAMES[r]}]>={cth}", cost, bits_to_mask(col >= cth))) # 3. suit multiplicities: suit (4), kind (2), c (0..5) for u in range(4): base = tag + choice(4) col = d.suit_counts[:, u] for cth in range(6): cost = base + choice(2) + choice(6) atoms.append(Atom(f"s[{SUIT_NAMES[u]}]={cth}", cost, bits_to_mask(col == cth))) atoms.append(Atom(f"s[{SUIT_NAMES[u]}]>={cth}", cost, bits_to_mask(col >= cth))) # 4. multiplicities of multiplicities: k (1..4), kind (2), c (0..5) for k in range(1, 5): base = tag + choice(4) col = d.mult_counts[:, k - 1] for cth in range(6): cost = base + choice(2) + choice(6) atoms.append(Atom(f"N{k}={cth}", cost, bits_to_mask(col == cth))) atoms.append(Atom(f"N{k}>={cth}", cost, bits_to_mask(col >= cth))) # 5. existential lifts: which max (2), kind (2), c for name, col, rng in (("maxmult", d.max_rank_mult, range(1, 5)), ("maxsuit", d.max_suit, range(1, 6))): base = tag + choice(2) for cth in rng: cost = base + choice(2) + choice(len(rng)) atoms.append(Atom(f"{name}={cth}", cost, bits_to_mask(col == cth))) atoms.append(Atom(f"{name}>={cth}", cost, bits_to_mask(col >= cth))) # 6. run structure: kind (3: straight / top= / top>=), top (10 windows) base = tag atoms.append(Atom("straight", base + choice(3), bits_to_mask(d.straight))) for top in range(3, 13): cost = base + choice(3) + choice(10) atoms.append(Atom(f"runtop={RANK_NAMES[top]}", cost, bits_to_mask(d.runtop == top))) atoms.append(Atom(f"runtop>={RANK_NAMES[top]}", cost, bits_to_mask((d.runtop != 255) & (d.runtop >= top)))) universe = (1 << N_HANDS) - 1 atoms = dedupe_atoms(atoms, universe) hands = d.hands def singleton_atoms(x: int) -> list[Atom]: return [member_atoms[int(c)] for c in hands[x]] return Domain(N_HANDS, CLASSES, atoms, singleton_atoms, "poker")