93 lines
4.0 KiB
JavaScript
93 lines
4.0 KiB
JavaScript
// 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;
|
||
}
|