fertig-classic-games/src/games/rummikub/RummikubAI.js

48 lines
2.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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