fertig-classic-games/src/games/craps/CrapsAI.js

93 lines
4.0 KiB
JavaScript
Raw 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.

// Heuristic Craps AI. Stateless helpers consumed by CrapsGame.
//
// Each opponent gets a stable "personality" derived from their name, which
// shapes what they bet and how big. Functions return plain bet specs / odds
// requests; the scene applies them through CrapsLogic (which guards affordability).
import { BET, POINT_NUMBERS, legalBetTypes, oddsEligibleBets, maxOddsFor } from './CrapsLogic.js';
const PROFILES = {
// unitPct: flat-bet size as a fraction of bankroll
// oddsMult: odds as a multiple of the flat bet
// fieldChance: probability of tossing a field bet on a come-out
// place: which numbers they like to place during the point phase
// dontChance: probability of betting the Don't side instead of Pass
conservative: { unitPct: 0.03, oddsMult: 1, fieldChance: 0.10, place: [], dontChance: 0.18, comeChance: 0.10 },
balanced: { unitPct: 0.05, oddsMult: 2, fieldChance: 0.25, place: [6, 8], dontChance: 0.10, comeChance: 0.25 },
aggressive: { unitPct: 0.08, oddsMult: 3, fieldChance: 0.55, place: [6, 8, 5, 9], dontChance: 0.05, comeChance: 0.45 },
gambler: { unitPct: 0.11, oddsMult: 3, fieldChance: 0.80, place: [6, 8, 4, 10],dontChance: 0.00, comeChance: 0.60 },
};
const PROFILE_KEYS = Object.keys(PROFILES);
function profileFor(player) {
const name = player.name ?? player.avatar?.id ?? 'bot';
let h = 0;
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
return PROFILES[PROFILE_KEYS[h % PROFILE_KEYS.length]];
}
const snap = (n, step) => Math.max(step, Math.round(n / step) * step);
function unitFor(player, prof) {
return Math.min(100, snap(player.chips * prof.unitPct, 5));
}
// Place-bet sizing must match the payout increment (6/8 → ×6, others → ×5).
function placeSize(player, prof, number) {
const step = number === 6 || number === 8 ? 6 : 5;
return Math.min(120, snap(player.chips * prof.unitPct, step));
}
// Flat bets to open for the current phase. Returns an array of bet specs.
export function chooseBets(player, state) {
const prof = profileFor(player);
const legal = legalBetTypes(state);
const specs = [];
let budget = player.chips;
const afford = (amt) => amt > 0 && amt <= budget;
if (state.point === null) {
// Come-out: take a line bet, maybe a field flyer.
const unit = unitFor(player, prof);
const hasLine = player.bets.some((b) => b.type === BET.PASS || b.type === BET.DONT_PASS);
if (!hasLine && afford(unit)) {
const dont = Math.random() < prof.dontChance && legal.has(BET.DONT_PASS);
specs.push({ type: dont ? BET.DONT_PASS : BET.PASS, amount: unit });
budget -= unit;
}
if (Math.random() < prof.fieldChance) {
const f = snap(unit / 2, 5);
if (afford(f)) { specs.push({ type: BET.FIELD, amount: f }); budget -= f; }
}
} else {
// Point phase: maybe a come bet, plus favourite place numbers.
if (Math.random() < prof.comeChance) {
const unit = unitFor(player, prof);
if (afford(unit)) { specs.push({ type: BET.COME, amount: unit }); budget -= unit; }
}
const placed = new Set(player.bets.filter((b) => b.type === BET.PLACE).map((b) => b.number));
for (const num of prof.place) {
if (placed.has(num) || num === state.point) continue;
if (!POINT_NUMBERS.includes(num)) continue;
const amt = placeSize(player, prof, num);
if (afford(amt)) { specs.push({ type: BET.PLACE, number: num, amount: amt }); budget -= amt; }
}
}
return specs;
}
// Odds to lay behind eligible line/come bets. Returns [{ betId, amount }].
export function chooseOdds(player, state) {
const prof = profileFor(player);
if (prof.oddsMult <= 0) return [];
const out = [];
let budget = player.chips;
for (const bet of oddsEligibleBets(player, state)) {
if (bet.oddsAmount > 0) continue;
const want = Math.min(bet.amount * prof.oddsMult, maxOddsFor(bet));
const amt = snap(want, 5);
if (amt > 0 && amt <= budget) { out.push({ betId: bet.id, amount: amt }); budget -= amt; }
}
return out;
}