fertig-classic-games/public/src/games/backgammon/BackgammonAI.js

96 lines
2.8 KiB
JavaScript

import {
cloneState, getValidMoves, applyMove, computePipCount,
} from './BackgammonLogic.js';
// Returns an ordered array of moves for the AI (Black) to execute.
export function chooseMoves(state) {
const sequences = generateSequences(state);
if (sequences.length === 0) return [];
let best = null;
let bestScore = -Infinity;
for (const seq of sequences) {
const score = evaluateState(seq.finalState, 'black');
if (score > bestScore) { bestScore = score; best = seq; }
}
return best ? best.moves : [];
}
function generateSequences(state, depth = 0) {
if (depth > 6) return [{ moves: [], finalState: state }];
const validMoves = getValidMoves(state);
if (validMoves.length === 0 || state.movesLeft.length === 0) {
return [{ moves: [], finalState: state }];
}
const results = [];
const seenBoards = new Set();
for (const move of validMoves) {
const next = applyMove(state, move);
// If applyMove ended the turn, no further moves are possible in this sequence
const finished = next.currentPlayer !== state.currentPlayer || next.phase !== 'move';
if (finished) {
results.push({ moves: [move], finalState: next });
} else {
const subSeqs = generateSequences(next, depth + 1);
for (const sub of subSeqs) {
const key = boardHash(sub.finalState);
if (!seenBoards.has(key)) {
seenBoards.add(key);
results.push({ moves: [move, ...sub.moves], finalState: sub.finalState });
}
}
}
}
return results.length > 0 ? results : [{ moves: [], finalState: state }];
}
function boardHash(state) {
return state.points.map((p) => `${p.color?.[0] ?? '-'}${p.count}`).join('') +
`|b${state.bar.black}w${state.bar.white}`;
}
function evaluateState(state, player) {
const opp = player === 'white' ? 'black' : 'white';
let score = 0;
// Pip count advantage
const ownPips = computePipCount(state, player);
const oppPips = computePipCount(state, opp);
score += (oppPips - ownPips) * 1.5;
// Borne off bonus
score += state.borneOff[player] * 25;
score -= state.borneOff[opp] * 25;
// Opponent checkers on bar (we sent them there)
score += state.bar[opp] * 20;
// Own blots (lone exposed checkers)
for (let i = 0; i < 24; i++) {
const pt = state.points[i];
if (pt.color === player && pt.count === 1) score -= 8;
if (pt.color === player && pt.count >= 2) {
// Home board anchors
const inHome = player === 'white' ? (i <= 5) : (i >= 18);
if (inHome) score += 10;
}
}
// Prime bonus — runs of 3+ consecutive blocked points
let run = 0;
for (let i = 0; i < 24; i++) {
if (state.points[i].color === player && state.points[i].count >= 2) {
run++;
if (run >= 3) score += 15;
} else {
run = 0;
}
}
return score;
}