48 lines
2.0 KiB
JavaScript
48 lines
2.0 KiB
JavaScript
// 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 };
|
||
}
|