Files
Dune/tests/two-player-engine.test.ts
T

71 lines
3.8 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, createGame, joinControlledFaction, joinGame, newPlayer, viewGame, type Game } from '../game/engine';
function lobby(): Game {
let game = createGame('TWOPEOPLE', newPlayer('a', 'A', 'atreides'));
game = applyAction(game, 'a', { type: 'twoPlayerMode', enabled: true });
joinControlledFaction(game, newPlayer('b', 'A second', 'emperor'), 'a');
joinGame(game, newPlayer('c', 'C', 'guild'));
joinControlledFaction(game, newPlayer('d', 'C second', 'harkonnen'), 'c');
return game;
}
void test('two people can independently ready their two physical factions and finish setup', () => {
let game = lobby();
assert.deepEqual(game.twoPlayerControllers, { version: 1, controllers: [
{ id: 'a', seats: ['a', 'b'] }, { id: 'c', seats: ['c', 'd'] },
] });
assert.deepEqual(viewGame(game, 'b').controlledFactionSeats.map(seat => seat.id), ['a', 'b']);
assert.deepEqual(viewGame(game, 'd').controlledFactionSeats.map(seat => seat.id), ['c', 'd']);
for (const id of ['a', 'b', 'c', 'd']) game = applyAction(game, id, { type: 'ready' });
game = applyAction(game, 'a', { type: 'start' });
assert.equal(game.status, 'setup');
for (const id of ['a', 'b', 'c', 'd']) {
const privateView = viewGame(game, id);
for (const seat of privateView.players) {
assert.equal('traitorChoices' in seat, seat.id === id);
assert.equal('hand' in seat, seat.id === id);
}
}
// Harkonnen automatically keeps all four dealt traitors; the other factions
// each choose one before play begins.
assert.equal(game.players.find(player => player.id === 'd')!.traitors.length, 4);
assert.equal(game.players.find(player => player.id === 'd')!.traitorChoices.length, 0);
for (const id of ['a', 'b', 'c']) {
const player = game.players.find(seat => seat.id === id)!;
game = applyAction(game, id, { type: 'traitor', leader: player.traitorChoices[0] });
}
assert.equal(game.status, 'playing');
assert.equal(game.players.every(player => player.ally === null), true);
if (game.phaseOpening) {
for (const id of ['a', 'b', 'c', 'd']) game = applyAction(game, id, { type: 'ready' });
}
const dialers = [...game.stormDialers];
for (const id of dialers) game = applyAction(game, id, { type: 'stormDial', amount: 1 });
assert.equal(game.stormPending, 2);
});
void test('lobby preserves separate controller seats and refuses incomplete, excess and mixed ownership', () => {
let game = createGame('TWOPEOPLE', newPlayer('a', 'A', 'atreides'));
game = applyAction(game, 'a', { type: 'twoPlayerMode', enabled: true });
assert.throws(() => joinControlledFaction(game, newPlayer('x', 'X', 'emperor'), 'other'), /controller/);
joinControlledFaction(game, newPlayer('b', 'B', 'emperor'), 'a');
assert.throws(() => joinControlledFaction(game, newPlayer('x', 'X', 'guild'), 'b'), /two faction/);
assert.throws(() => applyAction(game, 'a', { type: 'twoPlayerMode', enabled: false }), /cannot be disabled/);
assert.throws(() => applyAction(game, 'a', { type: 'addControlledBot', controller: 'a', faction: 'guild', difficulty: 'Easy' }), /AI controller/);
game = applyAction(game, 'a', { type: 'ready' });
game = applyAction(game, 'b', { type: 'ready' });
assert.throws(() => applyAction(game, 'a', { type: 'start' }), /exactly two controllers/);
assert.equal(game.players.length, 2);
});
void test('forged or missing controller assignments reject without mutating the game', () => {
const game = lobby();
game.twoPlayerControllers!.controllers[1].seats[1] = 'b';
const before = structuredClone(game);
assert.throws(() => viewGame(game, 'a'), /only one controller/);
assert.throws(() => applyAction(game, 'a', { type: 'ready' }), /only one controller/);
assert.deepEqual(game, before);
});