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.
119 lines
3.7 KiB
119 lines
3.7 KiB
import { randomInt } from 'node:crypto';
|
|
import { geese } from '../../shared/oca-board.mjs';
|
|
export { geese };
|
|
function advanceTurn(state, seat) {
|
|
let candidate = seat;
|
|
// Each skipped seat consumes one turn automatically; a full circuit may still have penalties.
|
|
for (let n = 0; n < state.positions.length * 4; n++) {
|
|
candidate = (candidate + 1) % state.positions.length;
|
|
if (state.withdrawn?.includes(candidate)) continue;
|
|
if (state.skips[candidate] > 0) {
|
|
state.skips[candidate]--;
|
|
continue;
|
|
}
|
|
state.turn = candidate;
|
|
return;
|
|
}
|
|
throw new Error('Estado de turnos no válido.');
|
|
}
|
|
export const oca = {
|
|
withdraw(state, seat) {
|
|
const next = structuredClone(state);
|
|
next.withdrawn = [...(next.withdrawn ?? []), seat];
|
|
next.positions[seat] = 0;
|
|
next.skips[seat] = 0;
|
|
if (next.turn === seat) advanceTurn(next, seat);
|
|
return next;
|
|
},
|
|
id: 'oca',
|
|
version: '1.0.0',
|
|
minPlayers: 2,
|
|
maxPlayers: 4,
|
|
create(players = 4) {
|
|
return {
|
|
positions: Array(players).fill(0),
|
|
skips: Array(players).fill(0),
|
|
turn: 0,
|
|
rolled: null,
|
|
lastPath: [],
|
|
lastPlayer: null,
|
|
note: 'Llegad al jardín de la casilla 63.',
|
|
};
|
|
},
|
|
legal(state, seat) {
|
|
return seat === state.turn ? [{ type: 'roll' }] : [];
|
|
},
|
|
apply(state, action, seat, rng = () => randomInt(1, 7)) {
|
|
if (seat !== state.turn || action.type !== 'roll')
|
|
throw new Error('No es tu turno.');
|
|
const die = rng();
|
|
if (!Number.isInteger(die) || die < 1 || die > 6)
|
|
throw new Error('Dado no válido.');
|
|
const next = structuredClone(state),
|
|
start = state.positions[seat];
|
|
let target = start + die,
|
|
repeat = false,
|
|
sound = 'roll';
|
|
next.rolled = die;
|
|
next.lastPlayer = seat;
|
|
next.lastEffect = null;
|
|
next.lastPath = Array.from({ length: die }, (_, i) =>
|
|
start + i + 1 > 63 ? 126 - start - i - 1 : start + i + 1,
|
|
);
|
|
if (target > 63) target = 126 - target;
|
|
let label = `Dado: ${die} · casilla ${target}`;
|
|
if (target === 63) {
|
|
next.positions[seat] = 63;
|
|
next.lastEffect = 'goal';
|
|
next.note = '¡Has llegado al jardín!';
|
|
return {
|
|
state: next,
|
|
label: `${label} · llega al jardín`,
|
|
sound,
|
|
result: { winner: seat, reason: 'llega exactamente a la casilla 63' },
|
|
};
|
|
}
|
|
if (geese.includes(target)) {
|
|
target = geese[geese.indexOf(target) + 1] ?? target;
|
|
repeat = true;
|
|
label += ` · de oca a oca: ${target}`;
|
|
} else if (target === 6 || target === 12) {
|
|
target = target === 6 ? 12 : 6;
|
|
repeat = true;
|
|
label += ` · puente a ${target}`;
|
|
} else if (target === 26 || target === 53) {
|
|
target = target === 26 ? 53 : 26;
|
|
repeat = true;
|
|
label += ` · de dado a dado: ${target}`;
|
|
} else if (target === 19) {
|
|
next.skips[seat] = 1;
|
|
label += ' · posada: descansa un turno';
|
|
} else if (target === 31) {
|
|
next.skips[seat] = 3;
|
|
label += ' · pozo: descansa tres turnos';
|
|
} else if (target === 42) {
|
|
target = 30;
|
|
label += ' · laberinto: vuelve a 30';
|
|
} else if (target === 52) {
|
|
next.skips[seat] = 2;
|
|
label += ' · cárcel: descansa dos turnos';
|
|
} else if (target === 58) {
|
|
target = 0;
|
|
label += ' · vuelve al inicio';
|
|
}
|
|
next.lastEffect = repeat
|
|
? 'bonus'
|
|
: next.skips[seat] > 0 || target < next.lastPath.at(-1)
|
|
? 'penalty'
|
|
: null;
|
|
next.positions[seat] = target;
|
|
next.lastPath.push(target);
|
|
next.note = label;
|
|
if (!repeat) advanceTurn(next, seat);
|
|
return { state: next, label, sound, result: null };
|
|
},
|
|
view(state) {
|
|
return { ...state, geese };
|
|
},
|
|
};
|