fertig-classic-games/src/games/holdem/HoldemAI.js

114 lines
4.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { evaluateHand, callAmount, canCheck, minRaise } from './HoldemLogic.js';
// Per-seat personalities indexed by seat number (17 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: 020 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 (01) from evaluation rank + board texture
function handStrength(player, community) {
if (community.length === 0) return null;
const result = evaluateHand([...player.hand, ...community]);
// rank 08; normalize to 01 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 01
} 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));
}