// Blackjack pure game logic — no Phaser dependencies import { SUITS, RANKS } from '../cards/Deck.js'; // ─── Shoe ───────────────────────────────────────────────────────────────────── const RANK_VALUE = { '2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,'T':10,'J':10,'Q':10,'K':10,'A':11, }; function makeCard(rank, suit) { 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}`, }; } export function buildShoe(numDecks = 6) { const cards = []; for (let d = 0; d < numDecks; d++) { for (const suit of SUITS) { for (const rank of RANKS) { cards.push(makeCard(rank, suit)); } } } for (let i = cards.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [cards[i], cards[j]] = [cards[j], cards[i]]; } return cards; } // ─── Hand evaluation ────────────────────────────────────────────────────────── export function handValue(cards) { let total = 0; let aces = 0; for (const c of cards) { if (c.rank === 'A') { aces++; total += 11; } else total += Math.min(c.value, 10); } while (total > 21 && aces > 0) { total -= 10; aces--; } return { score: total, soft: aces > 0 && total <= 21 }; } export function isBlackjack(hand) { return hand.length === 2 && handValue(hand).score === 21; } export function isBust(hand) { return handValue(hand).score > 21; } export function canDouble(player) { const hand = player.activeHand === 1 ? player.hand2 : player.hand; return (hand ?? []).length === 2; } export function canSplit(player) { return ( player.hand.length === 2 && player.hand[0].rank === player.hand[1].rank && !player.hand2 ); } // ─── State creation ─────────────────────────────────────────────────────────── export function createInitialState(opponents, chips) { const players = [ { seat: 0, name: 'You', isHuman: true, active: true, opponent: null, chips, bet: 0, bet2: null, hand: [], hand2: null, doubled: false, doubled2: false, insuranceBet: 0, activeHand: 0, status: 'waiting', status2: null, result: null, result2: null, chipsWon: 0, }, ]; for (let i = 0; i < 6; i++) { const opp = opponents[i] ?? null; players.push({ seat: i + 1, name: opp?.name ?? '', isHuman: false, active: !!opp, opponent: opp, chips: 1000, bet: 0, bet2: null, hand: [], hand2: null, doubled: false, doubled2: false, insuranceBet: 0, activeHand: 0, status: 'waiting', status2: null, result: null, result2: null, chipsWon: 0, }); } return { phase: 'betting', dealer: { hand: [], revealed: false }, players, currentSeat: 0, insurancePending: false, roundNumber: 0, }; } // ─── Round start ────────────────────────────────────────────────────────────── export function prepareRound(gs) { const players = gs.players.map(p => ({ ...p, bet: 0, bet2: null, hand: [], hand2: null, doubled: false, doubled2: false, insuranceBet: 0, activeHand: 0, status: p.active ? 'waiting' : 'waiting', status2: null, result: null, result2: null, chipsWon: 0, })); return { ...gs, phase: 'betting', dealer: { hand: [], revealed: false }, players, currentSeat: 0, insurancePending: false, roundNumber: gs.roundNumber + 1, }; } export function applyBet(gs, seat, amount) { const players = gs.players.map(p => p.seat === seat ? { ...p, bet: amount } : p); return { ...gs, players }; } // Deal all cards into state from the shoe (shoe is mutated). // Returns new gs with hands populated, phase set appropriately. export function dealAllCards(gs, shoe) { const players = gs.players.map(p => ({ ...p })); const dealer = { hand: [], revealed: false }; // Standard deal order: each player card 1, dealer upcard, each player card 2, dealer hole const activePlayers = players.filter(p => p.active); // Round 1 for (const p of activePlayers) p.hand.push(shoe.pop()); dealer.hand.push(shoe.pop()); // upcard (index 0) // Round 2 for (const p of activePlayers) p.hand.push(shoe.pop()); dealer.hand.push(shoe.pop()); // hole card (index 1) // Detect player blackjacks immediately for (const p of activePlayers) { if (isBlackjack(p.hand)) p.status = 'blackjack'; } const firstActiveSeat = activePlayers[0]?.seat ?? 0; const dealerUpcard = dealer.hand[0]; const insurancePending = dealerUpcard.rank === 'A'; return { ...gs, phase: insurancePending ? 'insurance' : 'player_turn', dealer, players, currentSeat: firstActiveSeat, insurancePending, }; } // ─── Insurance ──────────────────────────────────────────────────────────────── export function applyInsurance(gs, seat, accept) { const players = gs.players.map(p => { if (p.seat !== seat) return p; const insuranceBet = accept ? Math.floor(p.bet / 2) : 0; return { ...p, insuranceBet }; }); return { ...gs, players }; } // After all insurance decisions: check dealer blackjack export function resolveInsurance(gs) { const dealerBJ = isBlackjack(gs.dealer.hand); const players = gs.players.map(p => { if (!p.active || p.insuranceBet === 0) return p; // Insurance bet: pays 2:1 if dealer has BJ, else lost const insuranceResult = dealerBJ ? p.insuranceBet * 2 : -p.insuranceBet; return { ...p, chipsWon: (p.chipsWon ?? 0) + insuranceResult }; }); return { ...gs, players, insurancePending: false }; } // ─── Player actions ─────────────────────────────────────────────────────────── export function applyHit(gs, seat, shoe) { const players = gs.players.map(p => { if (p.seat !== seat) return p; const isH2 = p.activeHand === 1; const newCard = shoe.pop(); if (isH2) { const hand2 = [...p.hand2, newCard]; const bust = isBust(hand2); return { ...p, hand2, status2: bust ? 'bust' : 'playing' }; } else { const hand = [...p.hand, newCard]; const bust = isBust(hand); return { ...p, hand, status: bust ? 'bust' : 'playing' }; } }); return { ...gs, players }; } export function applyStand(gs, seat) { const players = gs.players.map(p => { if (p.seat !== seat) return p; if (p.activeHand === 1) return { ...p, status2: 'standing' }; // If split exists and hand 1 not yet played, move to hand 2 if (p.hand2 !== null && p.status2 === null) return { ...p, status: 'standing', activeHand: 1 }; return { ...p, status: 'standing' }; }); return { ...gs, players }; } export function applyDouble(gs, seat, shoe) { const players = gs.players.map(p => { if (p.seat !== seat) return p; const isH2 = p.activeHand === 1; const newCard = shoe.pop(); if (isH2) { const hand2 = [...p.hand2, newCard]; const bust = isBust(hand2); return { ...p, hand2, bet2: p.bet2 * 2, doubled2: true, status2: bust ? 'bust' : 'standing' }; } else { const hand = [...p.hand, newCard]; const bust = isBust(hand); const doubled = true; // If split exists and not yet played hand 2 if (p.hand2 !== null && p.status2 === null) { return { ...p, hand, bet: p.bet * 2, doubled, status: bust ? 'bust' : 'standing', activeHand: 1 }; } return { ...p, hand, bet: p.bet * 2, doubled, status: bust ? 'bust' : 'standing' }; } }); return { ...gs, players }; } export function applySplit(gs, seat, shoe) { const players = gs.players.map(p => { if (p.seat !== seat) return p; const [c1, c2] = p.hand; const hand = [c1, shoe.pop()]; const hand2 = [c2, shoe.pop()]; const isAceSplit = c1.rank === 'A'; // Ace splits: each hand gets one card only, immediately stands return { ...p, hand, hand2, bet2: p.bet, doubled: false, doubled2: false, status: isAceSplit ? 'standing' : 'playing', status2: isAceSplit ? 'standing' : null, activeHand: 0, }; }); return { ...gs, players }; } // ─── Dealer turn ────────────────────────────────────────────────────────────── // Returns new gs with dealer hand fully played out. shoe is mutated. export function runDealerTurn(gs, shoe) { const dealer = { ...gs.dealer, revealed: true }; while (true) { const { score } = handValue(dealer.hand); if (score >= 17) break; dealer.hand = [...dealer.hand, shoe.pop()]; } return { ...gs, phase: 'resolved', dealer }; } // ─── Resolve round ──────────────────────────────────────────────────────────── export function resolveRound(gs) { const { score: dealerScore } = handValue(gs.dealer.hand); const dealerBJ = isBlackjack(gs.dealer.hand); const dealerBust = dealerScore > 21; const players = gs.players.map(p => { if (!p.active) return p; function resolveHand(hand, bet, status, doubled) { if (!hand || hand.length === 0) return { result: null, net: 0 }; const { score: playerScore } = handValue(hand); const playerBJ = isBlackjack(hand); if (status === 'bust') return { result: 'lose', net: -bet }; if (playerBJ && dealerBJ) return { result: 'push', net: 0 }; if (playerBJ) return { result: 'blackjack', net: Math.floor(bet * 1.5) }; // 3:2 if (dealerBJ) return { result: 'lose', net: -bet }; if (dealerBust) return { result: 'win', net: bet }; if (playerScore > dealerScore) return { result: 'win', net: bet }; if (playerScore === dealerScore) return { result: 'push', net: 0 }; return { result: 'lose', net: -bet }; } const h1 = resolveHand(p.hand, p.bet, p.status, p.doubled); const h2 = p.hand2 ? resolveHand(p.hand2, p.bet2, p.status2, p.doubled2) : { result: null, net: 0 }; const totalNet = h1.net + h2.net + (p.chipsWon ?? 0); // chipsWon may have insurance delta const newChips = p.chips + totalNet; return { ...p, chips: newChips, result: h1.result, result2: h2.result, chipsWon: totalNet, }; }); return { ...gs, phase: 'resolved', players }; } // ─── Turn advancement ───────────────────────────────────────────────────────── // Returns the seat number of the next player who still needs to act, or null if done. export function nextActiveSeat(gs) { const activePlayers = gs.players.filter(p => p.active); const currentIdx = activePlayers.findIndex(p => p.seat === gs.currentSeat); for (let i = currentIdx + 1; i < activePlayers.length; i++) { const p = activePlayers[i]; if (p.status === 'playing' || p.status === 'waiting') return p.seat; } return null; // dealer's turn } export function isPlayerDone(player) { // Hand 1 is done const h1done = ['standing','bust','blackjack','done'].includes(player.status); // Hand 2 (if split): must also be done const h2done = player.hand2 === null || ['standing','bust','blackjack','done'].includes(player.status2); return h1done && h2done; }