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.
225 lines
7.6 KiB
225 lines
7.6 KiB
import { describe, it, expect } from 'vitest';
|
|
import { chess } from '../server/games/chess.mjs';
|
|
import { checkers } from '../server/games/checkers.mjs';
|
|
import { getGame } from '../server/games/registry.mjs';
|
|
import { parchis } from '../server/games/parchis.mjs';
|
|
|
|
describe('ajedrez: reglas en el servidor', () => {
|
|
it('rechaza movimientos ilegales y jugar fuera de turno', () => {
|
|
expect(() =>
|
|
chess.apply(chess.create(), { type: 'move', from: 'e2', to: 'e5' }, 0),
|
|
).toThrow();
|
|
expect(chess.legal(chess.create(), 1)).toEqual([]);
|
|
expect(() =>
|
|
chess.apply(chess.create(), { type: 'move', from: 'e7', to: 'e5' }, 1),
|
|
).toThrow();
|
|
});
|
|
it('detecta el mate y preserva el historial para reconstruir la partida', () => {
|
|
let state = chess.create();
|
|
let result;
|
|
for (const [from, to, seat] of [
|
|
['f2', 'f3', 0],
|
|
['e7', 'e5', 1],
|
|
['g2', 'g4', 0],
|
|
['d8', 'h4', 1],
|
|
] as const) {
|
|
const move = chess.apply(state, { type: 'move', from, to }, seat);
|
|
state = move.state;
|
|
result = move.result;
|
|
}
|
|
expect(result).toEqual({ winner: 1, reason: 'jaque mate' });
|
|
expect(chess.view(state).pgn).toContain('Qh4#');
|
|
});
|
|
it('conserva la repetición triple al reconstruir desde movimientos', () => {
|
|
let state = chess.create();
|
|
let result;
|
|
for (let round = 0; round < 2; round++)
|
|
for (const [from, to, seat] of [
|
|
['g1', 'f3', 0],
|
|
['g8', 'f6', 1],
|
|
['f3', 'g1', 0],
|
|
['f6', 'g8', 1],
|
|
] as const) {
|
|
const applied = chess.apply(state, { type: 'move', from, to }, seat);
|
|
state = applied.state;
|
|
result = applied.result;
|
|
}
|
|
expect(result?.winner).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('damas con recorrido largo', () => {
|
|
it('aplica las correcciones también a las partidas ya creadas', () => {
|
|
expect(getGame('damas', '1.0.0')).toBe(checkers);
|
|
expect(getGame('damas', '2.0.0')).toBe(checkers);
|
|
});
|
|
const position = () => ({
|
|
board: Array(64).fill(0),
|
|
turn: 0,
|
|
chain: null,
|
|
quietMoves: 0,
|
|
});
|
|
it('fuerza capturas y encadena saltos con la misma pieza', () => {
|
|
const state = position();
|
|
state.board[42] = 1;
|
|
state.board[33] = 2;
|
|
state.board[17] = 2;
|
|
state.board[46] = 1;
|
|
expect(checkers.legal(state, 0)).toEqual([
|
|
{ type: 'move', from: 42, to: 24, capture: 33 },
|
|
]);
|
|
const first = checkers.apply(state, { type: 'move', from: 42, to: 24 }, 0);
|
|
expect(first.state.turn).toBe(0);
|
|
expect(first.state.chain).toBe(24);
|
|
expect(() =>
|
|
checkers.apply(first.state, { type: 'move', from: 46, to: 37 }, 0),
|
|
).toThrow();
|
|
const next = checkers.apply(
|
|
first.state,
|
|
{ type: 'move', from: 24, to: 10 },
|
|
0,
|
|
);
|
|
expect(next.state.board[17]).toBe(0);
|
|
expect(next.result?.winner).toBe(0);
|
|
});
|
|
it('la dama recorre diagonales completas y una ficha propia bloquea el paso', () => {
|
|
const state = position();
|
|
state.board[42] = -1;
|
|
expect(checkers.legal(state, 0).map((m) => m.to)).toEqual(
|
|
expect.arrayContaining([33, 24, 35, 28, 21, 14, 7, 49, 56, 51, 60]),
|
|
);
|
|
state.board[35] = 1;
|
|
expect(
|
|
checkers
|
|
.legal(state, 0)
|
|
.filter((m) => m.from === 42)
|
|
.map((m) => m.to),
|
|
).not.toEqual(expect.arrayContaining([28]));
|
|
expect(
|
|
checkers.legal(state, 0).some((m) => m.from === 42 && m.to === 7),
|
|
).toBe(false);
|
|
});
|
|
it('la dama captura a distancia y conserva la pieza en una segunda captura', () => {
|
|
const state = position();
|
|
state.board[42] = -1;
|
|
state.board[35] = 2;
|
|
state.board[19] = 2;
|
|
state.board[5] = 2;
|
|
expect(
|
|
checkers
|
|
.legal(state, 0)
|
|
.filter((m) => m.capture === 35)
|
|
.map((m) => m.to),
|
|
).toEqual([28, 21, 14, 7]);
|
|
const first = checkers.apply(state, { type: 'move', from: 42, to: 28 }, 0);
|
|
expect(first.state.chain).toBe(28);
|
|
expect(first.state.turn).toBe(0);
|
|
expect(checkers.legal(first.state, 0)).toEqual([
|
|
{ type: 'move', from: 28, to: 10, capture: 19 },
|
|
{ type: 'move', from: 28, to: 1, capture: 19 },
|
|
]);
|
|
const second = checkers.apply(
|
|
first.state,
|
|
{ type: 'move', from: 28, to: 1 },
|
|
0,
|
|
);
|
|
expect(second.state.board[35]).toBe(0);
|
|
expect(second.state.board[19]).toBe(0);
|
|
expect(second.state.board[1]).toBe(-1);
|
|
expect(second.state.chain).toBeNull();
|
|
expect(second.state.turn).toBe(1);
|
|
});
|
|
it('no permite saltar dos adversarios en un solo salto', () => {
|
|
const state = position();
|
|
state.board[42] = -1;
|
|
state.board[35] = 2;
|
|
state.board[28] = 2;
|
|
expect(
|
|
checkers
|
|
.legal(state, 0)
|
|
.some((m) => m.to === 21 || m.capture !== undefined),
|
|
).toBe(false);
|
|
});
|
|
it('termina la captura al coronar y permite que las damas retrocedan', () => {
|
|
const state = position();
|
|
state.board[17] = 1;
|
|
state.board[10] = 2;
|
|
state.board[12] = 2;
|
|
const result = checkers.apply(state, { type: 'move', from: 17, to: 3 }, 0);
|
|
expect(result.state.board[3]).toBe(-1);
|
|
expect(result.state.chain).toBeNull();
|
|
expect(result.state.turn).toBe(1);
|
|
expect(
|
|
checkers.legal({ ...result.state, turn: 0 }, 0).some((m) => m.to > 3),
|
|
).toBe(true);
|
|
});
|
|
it('declara tablas por inactividad y rechaza entradas arbitrarias', () => {
|
|
const state = position();
|
|
state.board[42] = -1;
|
|
state.board[5] = -2;
|
|
state.quietMoves = 79;
|
|
expect(
|
|
checkers.apply(state, { type: 'move', from: 42, to: 33 }, 0).result
|
|
?.winner,
|
|
).toBeNull();
|
|
expect(() =>
|
|
checkers.apply(state, { type: 'move', from: -1, to: 100 }, 0),
|
|
).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('parchís básico', () => {
|
|
it('el dado procede del servidor y salir con un cinco es obligatorio', () => {
|
|
const state = parchis.create(2);
|
|
const rolled = parchis.apply(state, { type: 'roll', die: 6 }, 0, () => 5);
|
|
expect(rolled.state.die).toBe(5);
|
|
rolled.state.pieces[0][1] = 10;
|
|
expect(parchis.legal(rolled.state, 0).every((a) => a.piece !== 1)).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
parchis.apply(rolled.state, { type: 'move', piece: 0 }, 0).state
|
|
.pieces[0][0],
|
|
).toBe(0);
|
|
expect(() => parchis.apply(state, { type: 'roll' }, 1, () => 5)).toThrow();
|
|
});
|
|
it('bloquea el paso a través de barreras', () => {
|
|
const state = parchis.create(2);
|
|
state.pieces[0] = [0, 2, 2, -1];
|
|
state.die = 4;
|
|
expect(parchis.legal(state, 0).some((a) => a.piece === 0)).toBe(false);
|
|
});
|
|
it('captura fuera de un seguro y concede 20 pasos', () => {
|
|
const state = parchis.create(2);
|
|
state.pieces[0][0] = 20;
|
|
state.pieces[1][0] = 5;
|
|
state.die = 2;
|
|
const result = parchis.apply(state, { type: 'move', piece: 0 }, 0);
|
|
expect(result.state.pieces[1][0]).toBe(-1);
|
|
expect(result.state.die).toBe(20);
|
|
expect(result.state.bonus).toBe(true);
|
|
});
|
|
it('protege los seguros y exige entrada exacta a meta', () => {
|
|
const state = parchis.create(2);
|
|
state.pieces[0][0] = 15;
|
|
state.pieces[1][0] = 0;
|
|
state.die = 2;
|
|
expect(parchis.legal(state, 0).some((a) => a.piece === 0)).toBe(false);
|
|
state.pieces[0][0] = 70;
|
|
state.die = 2;
|
|
expect(parchis.legal(state, 0)).toEqual([{ type: 'pass' }]);
|
|
});
|
|
it('acaba al llevar las cuatro fichas a meta y conserva el turno extra', () => {
|
|
const state = parchis.create(2);
|
|
state.pieces[0] = [71, 71, 71, 70];
|
|
state.die = 1;
|
|
expect(parchis.apply(state, { type: 'move', piece: 3 }, 0).result).toEqual({
|
|
winner: 0,
|
|
reason: 'cuatro fichas en meta',
|
|
});
|
|
const roll = parchis.apply(parchis.create(2), { type: 'roll' }, 0, () => 6);
|
|
expect(roll.state.turn).toBe(0);
|
|
expect(roll.state.die).toBeNull();
|
|
});
|
|
});
|