// Farkel — heuristic opponent. No Phaser, no timers, no state mutation. After a // roll the AI always takes the greedy best-scoring set (FarkelLogic.bestScoring); // the only real decision is reroll vs. bank, made with an expected-value model // over the standard Farkle odds, shaded by a 1-5 skill profile for human-like // pacing and the occasional greedy blunder. import { WIN_TARGET, ON_BOARD_MIN } from './FarkelData.js'; // Probability a roll of N dice scores nothing (standard Farkle figures). const FARKLE_PROB = { 1: 0.667, 2: 0.444, 3: 0.278, 4: 0.157, 5: 0.077, 6: 0.023 }; // Rough expected points added by scoring dice in a roll of N. const AVG_GAIN = { 1: 25, 2: 50, 3: 75, 4: 113, 5: 150, 6: 200 }; const SKILL_PROFILES = { 1: { greed: 1.6, noise: 0.5, delay: [700, 1200] }, 2: { greed: 1.3, noise: 0.35, delay: [650, 1100] }, 3: { greed: 1.1, noise: 0.2, delay: [600, 1000] }, 4: { greed: 1.0, noise: 0.1, delay: [520, 900] }, 5: { greed: 1.0, noise: 0.0, delay: [440, 820] }, }; function profileFor(skill) { return SKILL_PROFILES[Math.max(1, Math.min(5, skill | 0))] ?? SKILL_PROFILES[3]; } export function nextThinkDelay(skill) { const [lo, hi] = profileFor(skill).delay; return lo + Math.random() * (hi - lo); } // Decide whether to reroll the remaining dice (true) or bank (false). Called // after the AI has already set aside its scoring dice for the current roll. export function decideReroll(state, skill = 3) { const prof = profileFor(skill); const t = state.turn; const me = state.players[state.current]; // Below the on-board minimum, banking would score nothing — so the kept total // is worthless until it reaches 500. Always push on (there's nothing to lose). if (!me.onBoard && t.kept < ON_BOARD_MIN) return true; // If banking now wins the game, take it. if (me.score + t.kept >= WIN_TARGET) return false; const prob = FARKLE_PROB[t.available] ?? 0.5; const gain = (AVG_GAIN[t.available] ?? 50) * prof.greed; let ev = (1 - prob) * gain - prob * t.kept; ev += (Math.random() * 2 - 1) * prof.noise * 100; // If an opponent is on the brink, gamble harder to keep pace. let oppMax = 0; state.players.forEach((p) => { if (p.seat !== me.seat && p.score > oppMax) oppMax = p.score; }); if (oppMax >= WIN_TARGET * 0.8 && me.score + t.kept < oppMax) ev += 150; return ev > 0; }