You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

166 lines
5.0 KiB

import { randomInt } from 'node:crypto';
import { nextPlayingSeat, withdrawParchis } from './turns.mjs';
export const starts = [4, 21, 38, 55];
const safe = new Set([4, 11, 16, 21, 28, 33, 38, 45, 50, 55, 62, 67]);
const square = (seat, progress) => (starts[seat] + progress) % 68;
const occupants = (state, cell) =>
state.pieces.flatMap((pieces, seat) =>
pieces.flatMap((p, piece) =>
p >= 0 && p < 64 && square(seat, p) === cell ? [{ seat, piece }] : [],
),
);
function canMove(state, seat, piece) {
const p = state.pieces[seat][piece];
const distance = state.die;
if (
!distance ||
p === 71 ||
(p === -1 && distance !== 5) ||
p + distance > 71
)
return false;
const target = p === -1 ? 0 : p + distance;
if (p !== -1)
for (let step = p + 1; step <= Math.min(target, 63); step++) {
const group = occupants(state, square(seat, step));
if (group.length >= 2 && group[0].seat === group[1].seat) return false;
}
if (target < 64) {
const group = occupants(state, square(seat, target));
if (group.length >= 2) return false;
if (
group.length &&
group[0].seat !== seat &&
safe.has(square(seat, target))
)
return false;
} else if (target < 71 && state.pieces[seat].includes(target)) return false;
return true;
}
export const parchis = {
withdraw: withdrawParchis,
id: 'parchis',
version: '1.0.0',
minPlayers: 2,
maxPlayers: 4,
/** @returns {{pieces: number[][], turn: number, die: number|null, rolled: number|null, bonus: boolean, extraTurn: boolean}} */
create(players = 4) {
return {
pieces: Array.from({ length: players }, () => [-1, -1, -1, -1]),
turn: 0,
die: null,
rolled: null,
bonus: false,
extraTurn: false,
};
},
/** @returns {{type: string, piece?: number}[]} */
legal(state, seat) {
if (seat !== state.turn) return [];
if (state.die === null) return [{ type: 'roll' }];
let moves = state.pieces[seat].flatMap((_, piece) =>
canMove(state, seat, piece) ? [{ type: 'move', piece }] : [],
);
// In this explicit basic variant, leaving home on a five is compulsory.
if (
!state.bonus &&
state.die === 5 &&
moves.some((m) => state.pieces[seat][m.piece] === -1)
)
moves = moves.filter((m) => state.pieces[seat][m.piece] === -1);
return moves.length ? moves : [{ type: 'pass' }];
},
apply(state, action, seat, rng = () => randomInt(1, 7)) {
if (
!this.legal(state, seat).some(
(a) =>
a.type === action.type &&
(a.type !== 'move' || a.piece === action.piece),
)
)
throw new Error('Esa acción no está disponible.');
const next = structuredClone(state);
if (action.type === 'roll') {
const die = rng();
if (!Number.isInteger(die) || die < 1 || die > 6)
throw new Error('Dado no válido.');
next.die = die;
next.rolled = die;
next.extraTurn = die === 6;
if (this.legal(next, seat).every((a) => a.type === 'pass')) {
next.die = null;
next.turn = next.extraTurn ? seat : nextPlayingSeat(next, seat);
next.extraTurn = false;
}
return {
state: next,
label: `Dado: ${die}`,
result: null,
sound: 'roll',
};
}
let reward = null;
let label = 'Sin movimientos: pasa el turno';
if (action.type === 'move') {
const piece = action.piece;
const p = state.pieces[seat][piece];
const target = p === -1 ? 0 : p + state.die;
next.pieces[seat][piece] = target;
label = `Ficha ${piece + 1}: ${p === -1 ? 'sale de casa' : `avanza ${state.die}`}`;
if (target < 64 && !safe.has(square(seat, target))) {
for (const other of occupants(next, square(seat, target)))
if (other.seat !== seat) {
next.pieces[other.seat][other.piece] = -1;
reward = 20;
label += ' · captura';
}
}
if (target === 71) {
reward = 10;
label += ' · llega a meta';
}
if (next.pieces[seat].every((p) => p === 71))
return {
state: next,
label,
result: { winner: seat, reason: 'cuatro fichas en meta' },
sound: 'finish',
};
}
if (reward !== null) {
next.die = reward;
next.bonus = true;
if (this.legal(next, seat).every((a) => a.type === 'pass')) {
next.die = null;
next.bonus = false;
next.turn = next.extraTurn ? seat : nextPlayingSeat(next, seat);
next.extraTurn = false;
}
} else {
next.die = null;
next.bonus = false;
next.turn = state.extraTurn ? seat : nextPlayingSeat(next, seat);
next.extraTurn = false;
}
return {
state: next,
label,
result: null,
sound:
action.type === 'pass'
? 'pass'
: reward === 20
? 'capture'
: reward === 10
? 'goal'
: 'move',
};
},
view(state) {
return { ...state, starts, safe: [...safe] };
},
};

Powered by TurnKey Linux.