#!/usr/bin/env node // verifyPaiGowPoker.js — engine tests for Pai Gow Poker import { HAND_RANK, makeJoker, buildDeck, evaluate5Card, evaluate2Card, compare5Card, compare2Card, isFoul, houseWay, createInitialState, prepareRound, applyBet, dealHands, applyHouseWaySplit, applyDealerHouseWay, applyHumanSplit, resolveRound, rankLabel, } from '../src/games/paigow/PaiGowPokerLogic.js'; import { chooseBet } from '../src/games/paigow/PaiGowPokerAI.js'; import { SUITS, RANKS } from '../src/games/cards/Deck.js'; let pass = 0, fail = 0; function ok(label, cond) { if (cond) { console.log(` ✓ ${label}`); pass++; } else { console.error(` ✗ ${label}`); fail++; } } function card(rank, suit) { const RANK_VALUE = Object.fromEntries(RANKS.map((r, i) => [r, i + 2])); return { rank, suit, value: RANK_VALUE[rank], label: rank === 'T' ? '10' : rank, isRed: suit === 'h' || suit === 'd', suitSymbol: { s:'♠',h:'♥',d:'♦',c:'♣' }[suit], key: `${rank}${suit}`, isJoker: false }; } // ── Deck ────────────────────────────────────────────────────────────────────── console.log('\nDeck'); const deck = buildDeck(); ok('deck has 53 cards', deck.length === 53); ok('deck contains one Joker', deck.filter(c => c.isJoker).length === 1); ok('Joker has correct key', deck.find(c => c.isJoker)?.key === 'JK'); const nonJoker = deck.filter(c => !c.isJoker); ok('52 standard cards present', nonJoker.length === 52); const keys = new Set(nonJoker.map(c => c.key)); ok('all standard keys unique', keys.size === 52); // Deck should be shuffled (not in insertion order) const firstRank = deck[0].rank; const allSame = deck.slice(0, 13).every(c => c.rank === firstRank); ok('deck is shuffled (not all same rank in first 13)', !allSame); // ── 5-card eval — standard hands ───────────────────────────────────────────── console.log('\n5-card evaluation — standard hands'); const royalFlush = [card('A','s'), card('K','s'), card('Q','s'), card('J','s'), card('T','s')]; const ev = evaluate5Card(royalFlush); ok('Royal Flush detected', ev.rank === HAND_RANK.ROYAL_FLUSH); ok('Royal Flush name', ev.name === 'Royal Flush'); const sf = [card('9','h'), card('8','h'), card('7','h'), card('6','h'), card('5','h')]; const sfEv = evaluate5Card(sf); ok('Straight Flush detected', sfEv.rank === HAND_RANK.STRAIGHT_FLUSH); ok('Straight Flush tiebreaker = 9', sfEv.tiebreakers[0] === 9); const quads = [card('A','s'), card('A','h'), card('A','d'), card('A','c'), card('K','s')]; const qEv = evaluate5Card(quads); ok('Four of a Kind detected', qEv.rank === HAND_RANK.FOUR_OF_A_KIND); ok('Quads tiebreakers [14,13]', qEv.tiebreakers[0] === 14 && qEv.tiebreakers[1] === 13); const fh = [card('K','s'), card('K','h'), card('K','d'), card('Q','s'), card('Q','h')]; ok('Full House detected', evaluate5Card(fh).rank === HAND_RANK.FULL_HOUSE); const flush = [card('A','h'), card('J','h'), card('9','h'), card('6','h'), card('2','h')]; ok('Flush detected', evaluate5Card(flush).rank === HAND_RANK.FLUSH); const straight = [card('9','s'), card('8','h'), card('7','d'), card('6','c'), card('5','s')]; ok('Straight detected', evaluate5Card(straight).rank === HAND_RANK.STRAIGHT); ok('Straight tiebreaker = 9', evaluate5Card(straight).tiebreakers[0] === 9); // Wheel (A-2-3-4-5) const wheel = [card('A','s'), card('2','h'), card('3','d'), card('4','c'), card('5','s')]; const wheelEv = evaluate5Card(wheel); ok('Wheel straight detected', wheelEv.rank === HAND_RANK.STRAIGHT); ok('Wheel tiebreaker = 5', wheelEv.tiebreakers[0] === 5); const trips = [card('Q','s'), card('Q','h'), card('Q','d'), card('7','c'), card('3','s')]; ok('Three of a Kind detected', evaluate5Card(trips).rank === HAND_RANK.THREE_OF_A_KIND); const twoPair = [card('A','s'), card('A','h'), card('K','d'), card('K','c'), card('J','s')]; ok('Two Pair detected', evaluate5Card(twoPair).rank === HAND_RANK.TWO_PAIR); const onePair = [card('T','s'), card('T','h'), card('A','d'), card('K','c'), card('Q','s')]; ok('One Pair detected', evaluate5Card(onePair).rank === HAND_RANK.ONE_PAIR); const hiCard = [card('A','s'), card('K','h'), card('Q','d'), card('J','c'), card('9','s')]; ok('High Card detected', evaluate5Card(hiCard).rank === HAND_RANK.HIGH_CARD); // Flush SF distinction (non-SF flush) const sfDistinct = [card('K','s'), card('Q','s'), card('J','s'), card('T','s'), card('8','s')]; ok('Non-consecutive flush is Flush not SF', evaluate5Card(sfDistinct).rank === HAND_RANK.FLUSH); // ── 5-card eval — Joker ─────────────────────────────────────────────────────── console.log('\n5-card evaluation — Joker'); const joker = makeJoker(); // Five Aces const fiveAces = [card('A','s'), card('A','h'), card('A','d'), card('A','c'), joker]; const faEv = evaluate5Card(fiveAces); ok('Five Aces detected', faEv.rank === HAND_RANK.FIVE_ACES); ok('Five Aces name', faEv.name === 'Five Aces'); // Joker completes a flush const flushWithJoker = [card('K','h'), card('J','h'), card('9','h'), card('6','h'), joker]; const fjEv = evaluate5Card(flushWithJoker); ok('Joker completes flush', fjEv.rank === HAND_RANK.FLUSH); // Joker completes a straight const stWithJoker = [card('9','s'), card('8','h'), card('7','d'), card('6','c'), joker]; const sjEv = evaluate5Card(stWithJoker); ok('Joker completes straight', sjEv.rank === HAND_RANK.STRAIGHT); // Joker completes a straight flush const sfWithJoker = [card('9','h'), card('8','h'), card('7','h'), card('6','h'), joker]; const sfJEv = evaluate5Card(sfWithJoker); ok('Joker completes straight flush', sfJEv.rank === HAND_RANK.STRAIGHT_FLUSH); // Joker as Ace (no better use in a scattered hand) const jokerAlone = [card('K','s'), card('Q','h'), card('J','d'), card('9','c'), joker]; const jaEv = evaluate5Card(jokerAlone); // K-Q-J-9 + Joker can complete K-Q-J-T-9 straight with Joker as T → STRAIGHT ok('Joker completes a straight when possible', jaEv.rank >= HAND_RANK.STRAIGHT); // Joker with one Ace in a hand that can make a straight (AKQJ+Joker=T → STRAIGHT) const jokerAceStr = [card('A','s'), card('K','h'), card('Q','d'), card('J','c'), joker]; const jaStr = evaluate5Card(jokerAceStr); ok('Joker+AKQJ makes a straight (Joker=T)', jaStr.rank === HAND_RANK.STRAIGHT); // Joker with one Ace in a hand that cannot make straight or flush → pair of Aces const jokerAcePair = [card('A','s'), card('9','h'), card('7','d'), card('5','c'), joker]; const jaPair = evaluate5Card(jokerAcePair); ok('Joker + Ace (no straight/flush) = pair of Aces', jaPair.rank === HAND_RANK.ONE_PAIR && jaPair.tiebreakers[0] === 14); // ── 2-card evaluation ───────────────────────────────────────────────────────── console.log('\n2-card evaluation'); const pairKings = [card('K','s'), card('K','h')]; const hiCardAQ = [card('A','s'), card('Q','h')]; ok('pair beats high card', compare2Card(evaluate2Card(pairKings), evaluate2Card(hiCardAQ)) > 0); const pairAces = [card('A','s'), card('A','h')]; ok('pair of Aces beats pair of Kings', compare2Card(evaluate2Card(pairAces), evaluate2Card(pairKings)) > 0); const AK = [card('A','s'), card('K','h')]; const AQ = [card('A','h'), card('Q','d')]; ok('A-K beats A-Q', compare2Card(evaluate2Card(AK), evaluate2Card(AQ)) > 0); const jokerK = [joker, card('K','s')]; const jokerKEv = evaluate2Card(jokerK); ok('Joker in 2-card hand = Ace (Ace-King)', jokerKEv.rank === 0 && jokerKEv.tiebreakers[0] === 14); // Pair of Joker+Ace (Joker treated as Ace) const jokerAceLow2 = [joker, card('A','s')]; const jokerAceEv = evaluate2Card(jokerAceLow2); ok('Joker + Ace in 2-card = pair of Aces', jokerAceEv.rank === 1 && jokerAceEv.tiebreakers[0] === 14); // ── compare5Card / compare2Card ──────────────────────────────────────────────── console.log('\nComparison'); const sfEval = evaluate5Card(sf); const flushEval = evaluate5Card(flush); ok('compare5Card: SF beats Flush', compare5Card(sfEval, flushEval) > 0); const pairAcesEv = evaluate5Card([card('A','s'), card('A','h'), card('K','d'), card('Q','c'), card('J','s')]); const pairKingsEv = evaluate5Card([card('K','s'), card('K','h'), card('A','d'), card('Q','c'), card('J','s')]); ok('compare5Card: pair Aces > pair Kings', compare5Card(pairAcesEv, pairKingsEv) > 0); ok('compare5Card: tie returns 0', compare5Card(pairAcesEv, pairAcesEv) === 0); const p2 = evaluate2Card([card('A','s'), card('K','h')]); const p3 = evaluate2Card([card('A','h'), card('K','d')]); ok('compare2Card: equal high cards returns 0', compare2Card(p2, p3) === 0); // ── Foul detection ───────────────────────────────────────────────────────────── console.log('\nFoul detection'); // Valid: 5-card pair of 2s, 2-card A-K const valid5 = [card('2','s'), card('2','h'), card('A','d'), card('K','c'), card('Q','s')]; const valid2 = [card('A','s'), card('K','h')]; ok('pair of 2s vs A-K high card: NOT foul', !isFoul(valid5, valid2)); // Foul: 5-card A-K-Q-J-9 high card, 2-card pair of Kings const foul5 = [card('A','s'), card('K','h'), card('Q','d'), card('J','c'), card('9','s')]; const foul2 = [card('K','s'), card('K','d')]; ok('high card 5-card vs pair of Kings 2-card: IS foul', isFoul(foul5, foul2)); // Foul: 5-card pair of Kings, 2-card pair of Aces const foul5b = [card('K','s'), card('K','h'), card('Q','d'), card('J','c'), card('9','s')]; const foul2b = [card('A','s'), card('A','h')]; ok('pair of Kings high vs pair of Aces low: IS foul', isFoul(foul5b, foul2b)); // Valid: 5-card pair of Aces, 2-card pair of Kings const valid5b = [card('A','s'), card('A','h'), card('K','d'), card('Q','c'), card('J','s')]; const valid2b = [card('K','s'), card('K','h')]; ok('pair of Aces vs pair of Kings: NOT foul', !isFoul(valid5b, valid2b)); // Equal pairs: NOT a foul (5-card hand wins on kickers; this case only arises with quads) const equalP5 = [card('K','d'), card('K','c'), card('Q','s'), card('J','h'), card('9','s')]; const equalP2 = [card('K','s'), card('K','h')]; ok('pair of Kings vs pair of Kings: NOT foul (equal rank, 5-card wins on kickers)', !isFoul(equalP5, equalP2)); // ── House Way ────────────────────────────────────────────────────────────────── console.log('\nHouse Way — No Pair'); const noPair7 = [card('A','s'), card('Q','h'), card('J','d'), card('9','c'), card('8','s'), card('6','h'), card('3','d')]; const hwNP = houseWay(noPair7); ok('no pair: high hand has 5 cards', hwNP.highHand.length === 5); ok('no pair: low hand has 2 cards', hwNP.lowHand.length === 2); ok('no pair: Ace in high hand', hwNP.highHand.some(c => c.rank === 'A')); // 2nd/3rd best (Q, J) should be in low hand ok('no pair: Q in low hand', hwNP.lowHand.some(c => c.rank === 'Q')); ok('no pair: split not foul', !isFoul(hwNP.highHand, hwNP.lowHand)); console.log('\nHouse Way — One Pair'); // Hand with no straight/flush possible so pair rule is exercised cleanly const onePair7 = [card('7','s'), card('7','h'), card('A','d'), card('K','c'), card('9','s'), card('5','h'), card('3','d')]; const hwOP = houseWay(onePair7); ok('one pair: high hand has pair', evaluate5Card(hwOP.highHand).rank >= HAND_RANK.ONE_PAIR); ok('one pair: A in low hand (best 2 singletons)', hwOP.lowHand.some(c => c.rank === 'A')); ok('one pair: K in low hand', hwOP.lowHand.some(c => c.rank === 'K')); ok('one pair: split not foul', !isFoul(hwOP.highHand, hwOP.lowHand)); console.log('\nHouse Way — Two Pair'); // Both pairs ≤ 6s: keep together const tp1 = [card('6','s'), card('6','h'), card('4','d'), card('4','c'), card('A','s'), card('K','h'), card('Q','d')]; const hwTP1 = houseWay(tp1); ok('two pair (≤6): both pairs in high', evaluate5Card(hwTP1.highHand).rank === HAND_RANK.TWO_PAIR); ok('two pair (≤6): not foul', !isFoul(hwTP1.highHand, hwTP1.lowHand)); // High pair ≥ JJ: split — HIGH pair → HIGH hand, LOW pair → LOW hand const tp2 = [card('J','s'), card('J','h'), card('5','d'), card('5','c'), card('A','s'), card('K','h'), card('Q','d')]; const hwTP2 = houseWay(tp2); ok('two pair (JJ+): JJ in HIGH hand', hwTP2.highHand.some(c => c.rank === 'J')); ok('two pair (JJ+): low pair (55) in LOW hand', hwTP2.lowHand.every(c => c.rank === '5')); ok('two pair (JJ+): not foul', !isFoul(hwTP2.highHand, hwTP2.lowHand)); // AA + 22: split — AA in HIGH hand, 22 in LOW hand const tp3 = [card('A','s'), card('A','h'), card('2','d'), card('2','c'), card('K','s'), card('Q','h'), card('J','d')]; const hwTP3 = houseWay(tp3); ok('two pair (AA+22): AA in HIGH hand', hwTP3.highHand.some(c => c.rank === 'A')); ok('two pair (AA+22): 22 in LOW hand', hwTP3.lowHand.every(c => c.rank === '2')); ok('two pair (AA+22): not foul', !isFoul(hwTP3.highHand, hwTP3.lowHand)); console.log('\nHouse Way — Three Pair'); const threePair = [card('A','s'), card('A','h'), card('K','d'), card('K','c'), card('Q','s'), card('Q','h'), card('J','d')]; const hwThreePair = houseWay(threePair); ok('three pair: low hand is a pair', evaluate2Card(hwThreePair.lowHand).rank === 1); ok('three pair: low hand has highest pair (Aces)', hwThreePair.lowHand.some(c => c.rank === 'A')); ok('three pair: not foul', !isFoul(hwThreePair.highHand, hwThreePair.lowHand)); console.log('\nHouse Way — Three of a Kind'); const trips7 = [card('8','s'), card('8','h'), card('8','d'), card('A','c'), card('K','s'), card('Q','h'), card('J','d')]; const hwTrips = houseWay(trips7); ok('trips (888): trips in high hand', evaluate5Card(hwTrips.highHand).rank === HAND_RANK.THREE_OF_A_KIND); ok('trips: A in low hand', hwTrips.lowHand.some(c => c.rank === 'A')); ok('trips: not foul', !isFoul(hwTrips.highHand, hwTrips.lowHand)); // Three Aces const tripsAces = [card('A','s'), card('A','h'), card('A','d'), card('K','c'), card('Q','s'), card('J','h'), card('T','d')]; const hwTA = houseWay(tripsAces); ok('three Aces: one Ace in low hand', hwTA.lowHand.some(c => c.rank === 'A')); ok('three Aces: not foul', !isFoul(hwTA.highHand, hwTA.lowHand)); console.log('\nHouse Way — Full House'); const fullHouse7 = [card('K','s'), card('K','h'), card('K','d'), card('Q','s'), card('Q','h'), card('A','c'), card('J','d')]; const hwFH = houseWay(fullHouse7); ok('full house: pair in low hand', evaluate2Card(hwFH.lowHand).rank === 1); ok('full house: pair of Queens in low', hwFH.lowHand.every(c => c.rank === 'Q')); ok('full house: trips in high', evaluate5Card(hwFH.highHand).rank === HAND_RANK.THREE_OF_A_KIND); ok('full house: not foul', !isFoul(hwFH.highHand, hwFH.lowHand)); console.log('\nHouse Way — Four of a Kind'); // Low quads (2s-6s): keep together const quads6 = [card('6','s'), card('6','h'), card('6','d'), card('6','c'), card('A','s'), card('K','h'), card('Q','d')]; const hwQ6 = houseWay(quads6); ok('quads of 6s: all 4 in high hand', hwQ6.highHand.filter(c => c.rank === '6').length === 4); ok('quads of 6s: not foul', !isFoul(hwQ6.highHand, hwQ6.lowHand)); // High quads (Aces): always split const quadsA = [card('A','s'), card('A','h'), card('A','d'), card('A','c'), card('K','s'), card('Q','h'), card('J','d')]; const hwQA = houseWay(quadsA); ok('quads of Aces: split (2 Aces in low)', hwQA.lowHand.filter(c => c.rank === 'A').length === 2); ok('quads of Aces: 2 Aces in high', hwQA.highHand.filter(c => c.rank === 'A').length === 2); ok('quads of Aces: not foul', !isFoul(hwQA.highHand, hwQA.lowHand)); // Jack quads: split const quadsJ = [card('J','s'), card('J','h'), card('J','d'), card('J','c'), card('K','s'), card('Q','h'), card('T','d')]; const hwQJ = houseWay(quadsJ); ok('quads of Jacks: split', hwQJ.lowHand.filter(c => c.rank === 'J').length === 2); ok('quads of Jacks: not foul', !isFoul(hwQJ.highHand, hwQJ.lowHand)); console.log('\nHouse Way — Five Aces'); const fiveAces7 = [card('A','s'), card('A','h'), card('A','d'), card('A','c'), makeJoker(), card('K','s'), card('Q','h')]; const hwFA = houseWay(fiveAces7); ok('Five Aces: 5 ace-equiv cards in high', hwFA.highHand.length === 5); ok('Five Aces: Five Aces rank', evaluate5Card(hwFA.highHand).rank === HAND_RANK.FIVE_ACES); ok('Five Aces: not foul', !isFoul(hwFA.highHand, hwFA.lowHand)); // ── State management ─────────────────────────────────────────────────────────── console.log('\nState management'); const opponents = Array.from({ length: 3 }, (_, i) => ({ name: `AI${i+1}`, id: i+1 })); let gs = createInitialState(opponents, 2000); ok('createInitialState: 6 seats total', gs.players.length === 6); ok('seat 0 is human', gs.players[0].isHuman); ok('3 opponents active', gs.players.filter(p => p.active && !p.isHuman).length === 3); ok('2 empty seats inactive', gs.players.filter(p => !p.active).length === 2); gs = prepareRound(gs); ok('prepareRound: phase = betting', gs.phase === 'betting'); ok('prepareRound: deck has 53 cards', gs.deck.length === 53); ok('prepareRound: bets reset', gs.players.every(p => p.bet === 0)); gs = applyBet(gs, 0, 25); ok('applyBet: human bet = 25', gs.players[0].bet === 25); gs = dealHands(gs); ok('dealHands: phase = setting', gs.phase === 'setting'); ok('human has 7 cards', gs.players[0].hand.length === 7); ok('AI1 has 7 cards', gs.players[1].hand.length === 7); ok('dealer has 7 cards', gs.dealer.hand.length === 7); // 4 active players × 7 + dealer × 7 = 35 cards dealt; 53 - 35 = 18 remaining ok('4 active players + dealer = 35 cards dealt, 18 remaining', gs.deck.length === 53 - 35); // Apply house way to AI gs = applyHouseWaySplit(gs, 1); ok('AI1 has highHand of 5', gs.players[1].highHand.length === 5); ok('AI1 has lowHand of 2', gs.players[1].lowHand.length === 2); ok('AI1 not foul', !gs.players[1].isFoul); gs = applyDealerHouseWay(gs); ok('dealer has highHand of 5', gs.dealer.highHand.length === 5); ok('dealer has lowHand of 2', gs.dealer.lowHand.length === 2); // Human sets hands (use house way for simplicity in test) const hwHuman = houseWay(gs.players[0].hand); gs = applyHumanSplit(gs, hwHuman.highHand, hwHuman.lowHand); ok('human has highHand of 5', gs.players[0].highHand.length === 5); ok('human not foul', !gs.players[0].isFoul); // Apply house way to remaining AI seats too for (let s = 2; s <= 5; s++) { if (gs.players[s].active) gs = applyHouseWaySplit(gs, s); } gs = resolveRound(gs); ok('resolveRound: phase = resolved', gs.phase === 'resolved'); ok('human has a result', ['win','push','lose','foul'].includes(gs.players[0].result)); // ── Resolution — chips math ─────────────────────────────────────────────────── console.log('\nResolution — chips math'); // Simulate a known win (human wins both hands) { let testGs = createInitialState([{ name: 'Bot' }], 1000); testGs = prepareRound(testGs); testGs = applyBet(testGs, 0, 100); testGs = dealHands(testGs); // Force-set human and dealer hands for a guaranteed win const winHigh = [card('A','s'), card('A','h'), card('A','d'), card('K','s'), card('K','h')]; // FH const winLow = [card('Q','s'), card('Q','h')]; // pair of Qs const looseHigh = [card('2','s'), card('3','h'), card('5','d'), card('7','c'), card('9','s')]; // garbage const looseLow = [card('4','s'), card('6','h')]; testGs = { ...testGs, players: testGs.players.map(p => p.seat === 0 ? { ...p, highHand: winHigh, lowHand: winLow, highEval: { rank: HAND_RANK.FULL_HOUSE, name: 'Full House', tiebreakers: [14, 13] }, lowEval: { rank: 1, tiebreakers: [12] }, isFoul: false } : p), }; testGs = { ...testGs, dealer: { ...testGs.dealer, highHand: looseHigh, lowHand: looseLow, highEval: evaluate5Card(looseHigh), lowEval: evaluate2Card(looseLow) } }; testGs = applyHouseWaySplit(testGs, 1); testGs = resolveRound(testGs); ok('win result: chipsWon = 95 (100 - 5% commission)', testGs.players[0].chipsWon === 95); ok('win result: chips increased', testGs.players[0].chips === 1095); } // Simulate a known loss { let testGs = createInitialState([{ name: 'Bot' }], 1000); testGs = prepareRound(testGs); testGs = applyBet(testGs, 0, 50); testGs = dealHands(testGs); const loseHigh = [card('2','s'), card('3','h'), card('5','d'), card('7','c'), card('9','s')]; const loseLow = [card('4','s'), card('6','h')]; const winHigh = [card('A','s'), card('A','h'), card('A','d'), card('K','s'), card('K','h')]; const winLow = [card('Q','s'), card('Q','h')]; testGs = { ...testGs, players: testGs.players.map(p => p.seat === 0 ? { ...p, highHand: loseHigh, lowHand: loseLow, highEval: evaluate5Card(loseHigh), lowEval: evaluate2Card(loseLow), isFoul: false } : p) }; testGs = { ...testGs, dealer: { ...testGs.dealer, highHand: winHigh, lowHand: winLow, highEval: { rank: HAND_RANK.FULL_HOUSE, name: 'Full House', tiebreakers: [14, 13] }, lowEval: { rank: 1, tiebreakers: [12] } } }; testGs = applyHouseWaySplit(testGs, 1); testGs = resolveRound(testGs); ok('lose result: chipsWon = -50', testGs.players[0].chipsWon === -50); ok('lose result: chips = 950', testGs.players[0].chips === 950); } // Push: player wins high, loses low { let testGs = createInitialState([{ name: 'Bot' }], 1000); testGs = prepareRound(testGs); testGs = applyBet(testGs, 0, 25); testGs = dealHands(testGs); const pHigh = [card('K','s'), card('K','h'), card('K','d'), card('Q','s'), card('Q','h')]; // FH KKK-QQ const pLow = [card('3','s'), card('4','h')]; const dHigh = [card('2','s'), card('3','h'), card('5','d'), card('7','c'), card('9','s')]; // high card const dLow = [card('A','s'), card('A','h')]; // pair of Aces testGs = { ...testGs, players: testGs.players.map(p => p.seat === 0 ? { ...p, highHand: pHigh, lowHand: pLow, highEval: evaluate5Card(pHigh), lowEval: evaluate2Card(pLow), isFoul: false } : p), dealer: { ...testGs.dealer, highHand: dHigh, lowHand: dLow, highEval: evaluate5Card(dHigh), lowEval: evaluate2Card(dLow) } }; testGs = applyHouseWaySplit(testGs, 1); testGs = resolveRound(testGs); ok('push result: chipsWon = 0', testGs.players[0].chipsWon === 0); ok('push result: result = push', testGs.players[0].result === 'push'); } // Foul auto-lose { let testGs = createInitialState([{ name: 'Bot' }], 1000); testGs = prepareRound(testGs); testGs = applyBet(testGs, 0, 40); testGs = dealHands(testGs); const foulHighHand = [card('A','s'), card('K','h'), card('Q','d'), card('J','c'), card('9','s')]; const foulLowHand = [card('A','h'), card('A','d')]; // pair Aces in low, but high hand is high card = foul testGs = applyHumanSplit(testGs, foulHighHand, foulLowHand); testGs = applyDealerHouseWay(testGs); testGs = applyHouseWaySplit(testGs, 1); testGs = resolveRound(testGs); ok('foul: auto-lose', testGs.players[0].result === 'foul'); ok('foul: chipsWon = -40', testGs.players[0].chipsWon === -40); } // Copy rule: tied high hand → dealer wins (push if player wins low) { // Force both high hands to the same rank/tiebreakers let testGs = createInitialState([{ name: 'Bot' }], 1000); testGs = prepareRound(testGs); testGs = applyBet(testGs, 0, 20); testGs = dealHands(testGs); const sameHighEval = { rank: HAND_RANK.FLUSH, name: 'Flush', tiebreakers: [14, 12, 10, 8, 6] }; const winLowEval = { rank: 1, tiebreakers: [13] }; // pair Kings const loseLowEval = { rank: 0, tiebreakers: [12, 10] }; // Q-T high testGs = { ...testGs, players: testGs.players.map(p => p.seat === 0 ? { ...p, highHand: [], lowHand: [], highEval: sameHighEval, lowEval: winLowEval, isFoul: false } : p), dealer: { ...testGs.dealer, highHand: [], lowHand: [], highEval: sameHighEval, lowEval: loseLowEval } }; testGs = applyHouseWaySplit(testGs, 1); testGs = resolveRound(testGs); // Player wins low (pair K > Q-T), dealer wins high (tie → dealer). Push overall. ok('copy rule: tied high + player wins low = push', testGs.players[0].result === 'push'); } // ── AI bet ──────────────────────────────────────────────────────────────────── console.log('\nAI bet'); const aiPlayer = { chips: 500 }; const bets = Array.from({ length: 100 }, () => chooseBet(aiPlayer)); ok('chooseBet: all in range [5, 100]', bets.every(b => b >= 5 && b <= 100)); ok('chooseBet: never exceeds chips (500)', bets.every(b => b <= 500)); const poorPlayer = { chips: 7 }; const poorBets = Array.from({ length: 20 }, () => chooseBet(poorPlayer)); ok('chooseBet: never exceeds player chips (7)', poorBets.every(b => b <= 7)); // ── Deck coverage ───────────────────────────────────────────────────────────── console.log('\nDeck coverage'); // 6 players + dealer × 7 = 49 cards ≤ 53 ok('max 5 AI opponents: 6 players + dealer = 49 cards dealt ≤ 53', 6 * 7 + 7 <= 53); // ── Summary ──────────────────────────────────────────────────────────────────── console.log(`\n ${pass} passed, ${fail} failed\n`); if (fail > 0) process.exit(1);