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
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] };
|
|
},
|
|
};
|