import { evaluateHand, callAmount, canCheck, minRaise } from './HoldemLogic.js'; // Per-seat personalities indexed by seat number (1–7 for AI seats) const PERSONALITIES = { 1: { style: 'aggressive', raiseFactor: 0.55, bluffRate: 0.22 }, 2: { style: 'tight', raiseFactor: 0.28, bluffRate: 0.10 }, 3: { style: 'loose', raiseFactor: 0.44, bluffRate: 0.27 }, 4: { style: 'aggressive', raiseFactor: 0.62, bluffRate: 0.30 }, 5: { style: 'tight', raiseFactor: 0.22, bluffRate: 0.06 }, 6: { style: 'loose', raiseFactor: 0.50, bluffRate: 0.18 }, 7: { style: 'aggressive', raiseFactor: 0.48, bluffRate: 0.14 }, }; // Chen formula approximation: 0–20 score for pre-flop hand strength function chenScore(hand) { const [a, b] = [...hand].sort((x, y) => y.value - x.value); const hi = a.value; let score = hi >= 14 ? 10 : hi >= 13 ? 8 : hi >= 12 ? 7 : hi >= 11 ? 6 : hi / 2; if (a.value === b.value) { score = Math.max(score * 2, 5); } else { const gap = a.value - b.value - 1; if (gap === 0) score += 1; // connected else if (gap === 1) score -= 1; // 1-gap else if (gap === 2) score -= 2; // 2-gap else if (gap === 3) score -= 4; // 3-gap else score -= 5; // bigger gap if (b.value >= 2 && b.value <= 7 && gap <= 1) score += 1; // straight bonus if (a.suit === b.suit) score += 2; // suited bonus } return Math.max(0, score); } // Post-flop: rough hand percentile (0–1) from evaluation rank + board texture function handStrength(player, community) { if (community.length === 0) return null; const result = evaluateHand([...player.hand, ...community]); // rank 0–8; normalize to 0–1 with exponential curve return Math.min(1, (result.rank + 1) / 9 + result.tiebreakers[0] / 120); } // Pot odds: ratio of call amount to total pot after calling function potOdds(state, seat) { const toCall = callAmount(state, seat); if (toCall === 0) return 1; // free to check return toCall / (state.pot + toCall); } export function chooseAction(state, seat) { const player = state.players[seat]; const pers = PERSONALITIES[seat] ?? PERSONALITIES[1]; const isCheck = canCheck(state, seat); const toCall = callAmount(state, seat); const odds = potOdds(state, seat); // Determine strength metric let strength; if (state.phase === 'preflop') { const chen = chenScore(player.hand); strength = chen / 20; // normalize to 0–1 } else { strength = handStrength(player, state.community) ?? 0.5; } // Bluffing: occasionally treat weak hand as strong if (Math.random() < pers.bluffRate) strength = Math.min(1, strength + 0.40); // Fold threshold: fold if strength is below the price of calling let foldThreshold = odds * 0.88; // When the pot hasn't been raised pre-flop it only costs the big blind to see // the flop, so opponents limp in with almost anything instead of folding. const unraisedPreflop = state.phase === 'preflop' && state.roundBet <= (state.blind?.big ?? state.roundBet); if (unraisedPreflop) foldThreshold *= 0.25; if (isCheck) { // Check or bet if (strength > 0.55 && Math.random() < pers.raiseFactor) { const betAmount = computeRaiseAmount(state, seat, player, strength, pers); return { type: 'raise', amount: betAmount }; } return { type: 'check' }; } // There's a bet to call if (strength < foldThreshold && !isCheck) { return { type: 'fold' }; } // Strong enough to continue — raise or call? if (strength > 0.62 && Math.random() < pers.raiseFactor) { if (player.chips <= toCall * 1.5) { return { type: 'allin' }; } const betAmount = computeRaiseAmount(state, seat, player, strength, pers); return { type: 'raise', amount: betAmount }; } // Go all-in if call would leave almost nothing if (toCall >= player.chips) return { type: 'allin' }; return { type: 'call' }; } function computeRaiseAmount(state, seat, player, strength, pers) { const base = minRaise(state); // Scale raise size with hand strength and personality const multiplier = 1 + strength * pers.raiseFactor * 3; const raw = Math.round(base * multiplier / 5) * 5; // round to $5 return Math.min(raw, player.chips + (state.players[seat].bet)); }