// Rummikub — heuristic AI. No Phaser, no state mutation. `planTurn` inspects an // (already-current) engine state and returns a plain plan describing what tiles // to lay down (and the resulting table arrangement) or a decision to draw. The // scene applies & animates the plan; the engine validates the commit. import { INITIAL_MELD_MIN } from './RummikubData.js'; import { bestMeldDecomposition } from './RummikubSolver.js'; // Skill 1–5: lower skill is slower, less likely to manipulate the table, and // adds "noise" (sometimes declines an available meld to draw instead). const PROFILE = { 1: { delay: [780, 1250], manipulate: false, declineChance: 0.30 }, 2: { delay: [680, 1100], manipulate: false, declineChance: 0.18 }, 3: { delay: [600, 980], manipulate: false, declineChance: 0.08 }, 4: { delay: [500, 860], manipulate: true, declineChance: 0.03 }, 5: { delay: [430, 740], manipulate: true, declineChance: 0.0 }, }; function profile(skill) { return PROFILE[Math.max(1, Math.min(5, skill | 0))] || PROFILE[3]; } export function thinkDelay(skill) { const [lo, hi] = profile(skill).delay; return lo + Math.floor(Math.random() * (hi - lo)); } // Returns { type:'commit', tilesPlayed, newTable, firstMeld } or { type:'draw' }. export function planTurn(state, seat, skill) { const p = profile(skill); const player = state.players[seat]; const rack = player.rack; const table = state.table.map((s) => s.slice()); const plan = bestMeldDecomposition(rack, table, { mustReach: player.hasMelded ? 0 : INITIAL_MELD_MIN, alreadyMelded: player.hasMelded, manipulate: p.manipulate, }); if (!plan || plan.tilesPlayed.length === 0) return { type: 'draw' }; // Low-skill players sometimes sit on a small play and draw instead — but never // decline a chance to go out. const goesOut = plan.tilesPlayed.length === rack.length; if (!goesOut && Math.random() < p.declineChance) return { type: 'draw' }; return { type: 'commit', tilesPlayed: plan.tilesPlayed, newTable: plan.newTable, firstMeld: plan.firstMeld }; }