96 lines
2.8 KiB
JavaScript
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;
|
|
}
|