487 lines
25 KiB
JavaScript
487 lines
25 KiB
JavaScript
#!/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);
|