71 lines
3.8 KiB
TypeScript
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);
|
|
});
|