// 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; }