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

220 lines
6.5 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.

// Pure game logic — no Phaser dependency.
// Convention: point indices 023 (0 = point 1, White's home ace-point).
// White moves high→low (23→0), bears off past index 0.
// Black moves low→high (0→23), bears off past index 23.
// White home board: indices 05. Black home board: indices 1823.
export function createInitialState() {
const points = Array.from({ length: 24 }, () => ({ color: null, count: 0 }));
points[23] = { color: 'white', count: 2 };
points[12] = { color: 'white', count: 5 };
points[7] = { color: 'white', count: 3 };
points[5] = { color: 'white', count: 5 };
points[0] = { color: 'black', count: 2 };
points[11] = { color: 'black', count: 5 };
points[16] = { color: 'black', count: 3 };
points[18] = { color: 'black', count: 5 };
return {
points,
bar: { white: 0, black: 0 },
borneOff: { white: 0, black: 0 },
dice: null,
movesLeft: [],
currentPlayer: 'white',
phase: 'roll',
winner: null,
};
}
export function cloneState(state) {
return JSON.parse(JSON.stringify(state));
}
export function rollDice(state) {
const d1 = Math.ceil(Math.random() * 6);
const d2 = Math.ceil(Math.random() * 6);
return rollSpecificDice(state, d1, d2);
}
export function rollSpecificDice(state, d1, d2) {
const s = cloneState(state);
s.dice = [d1, d2];
s.movesLeft = d1 === d2 ? [d1, d1, d1, d1] : [d1, d2];
s.phase = 'move';
return s;
}
export function endTurn(state) {
const s = cloneState(state);
s.currentPlayer = s.currentPlayer === 'white' ? 'black' : 'white';
s.dice = null;
s.movesLeft = [];
s.phase = 'roll';
return s;
}
export function allCheckersInHome(state, player) {
if (state.bar[player] > 0) return false;
const [homeMin, homeMax] = player === 'white' ? [0, 5] : [18, 23];
for (let i = 0; i < 24; i++) {
if (i < homeMin || i > homeMax) {
if (state.points[i].color === player && state.points[i].count > 0) return false;
}
}
return true;
}
// Furthest checker from the bear-off edge (by index):
// White: highest index in 05 (index 5 = 6 pips away)
// Black: lowest index in 1823 (index 18 = 6 pips away)
function furthestHomeChecker(state, player) {
const { points } = state;
if (player === 'white') {
for (let i = 5; i >= 0; i--) {
if (points[i].color === 'white' && points[i].count > 0) return i;
}
} else {
for (let i = 18; i <= 23; i++) {
if (points[i].color === 'black' && points[i].count > 0) return i;
}
}
return -1;
}
export function getValidMoves(state) {
const { currentPlayer: player, movesLeft, points, bar } = state;
const opp = player === 'white' ? 'black' : 'white';
const uniqueDice = [...new Set(movesLeft)];
const inBearOff = allCheckersInHome(state, player);
const moves = [];
const isBlocked = (idx) => points[idx]?.color === opp && points[idx].count >= 2;
const isHit = (idx) => points[idx]?.color === opp && points[idx].count === 1;
// Bar entry takes priority — only these moves if bar has checkers
if (bar[player] > 0) {
for (const die of uniqueDice) {
// White enters opposite side: die 1 → index 23, die 6 → index 18
// Black enters near side: die 1 → index 0, die 6 → index 5
const entryIdx = player === 'white' ? (24 - die) : (die - 1);
if (!isBlocked(entryIdx)) {
moves.push({ from: 'bar', to: entryIdx, dieUsed: die, hit: isHit(entryIdx) });
}
}
return dedup(moves);
}
for (const die of uniqueDice) {
for (let i = 0; i < 24; i++) {
if (points[i].color !== player || points[i].count === 0) continue;
if (player === 'white') {
const dest = i - die;
if (dest >= 0) {
if (!isBlocked(dest)) moves.push({ from: i, to: dest, dieUsed: die, hit: isHit(dest) });
} else if (inBearOff) {
if (dest === -1) {
// Exact bear-off (die == i + 1)
moves.push({ from: i, to: 'off', dieUsed: die, hit: false });
} else {
// Overshoot — only the furthest checker may be borne off
if (furthestHomeChecker(state, 'white') === i) {
moves.push({ from: i, to: 'off', dieUsed: die, hit: false });
}
}
}
} else {
const dest = i + die;
if (dest <= 23) {
if (!isBlocked(dest)) moves.push({ from: i, to: dest, dieUsed: die, hit: isHit(dest) });
} else if (inBearOff) {
if (dest === 24) {
// Exact bear-off (die == 24 - i)
moves.push({ from: i, to: 'off', dieUsed: die, hit: false });
} else {
// Overshoot
if (furthestHomeChecker(state, 'black') === i) {
moves.push({ from: i, to: 'off', dieUsed: die, hit: false });
}
}
}
}
}
}
return dedup(moves);
}
function dedup(moves) {
const seen = new Set();
return moves.filter((m) => {
const key = `${m.from}|${m.to}|${m.dieUsed}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
export function applyMove(state, move) {
let s = cloneState(state);
const player = s.currentPlayer;
const opp = player === 'white' ? 'black' : 'white';
// Remove from source
if (move.from === 'bar') {
s.bar[player] = Math.max(0, s.bar[player] - 1);
} else {
s.points[move.from].count--;
if (s.points[move.from].count === 0) s.points[move.from].color = null;
}
// Handle hit — send opponent blot to bar
if (move.hit && move.to !== 'off') {
s.points[move.to].count = 0;
s.points[move.to].color = null;
s.bar[opp]++;
}
// Place on destination
if (move.to === 'off') {
s.borneOff[player]++;
} else {
s.points[move.to].color = player;
s.points[move.to].count++;
}
// Consume the die
const dieIdx = s.movesLeft.indexOf(move.dieUsed);
if (dieIdx !== -1) s.movesLeft.splice(dieIdx, 1);
// Win check
if (s.borneOff[player] >= 15) {
s.winner = player;
s.phase = 'game_over';
return s;
}
// Auto-end turn if no dice remain or no legal moves remain
if (s.movesLeft.length === 0 || getValidMoves(s).length === 0) {
s = endTurn(s);
}
return s;
}
export function hasAnyMove(state) {
return getValidMoves(state).length > 0;
}
export function computePipCount(state, player) {
let pips = 0;
for (let i = 0; i < 24; i++) {
const pt = state.points[i];
if (pt.color !== player || pt.count === 0) continue;
const dist = player === 'white' ? (i + 1) : (24 - i);
pips += dist * pt.count;
}
pips += state.bar[player] * 25;
return pips;
}