6749777b45
game.test.js: rewritten for the new {type, rolls} action shape and 5-stat
player model. The full-playthrough test now walks the real 212-space board
turn by turn (deterministic dummy rolls — always the low end of each die's
range) until finish, asserting every stat stays numeric and every one of
the 9 real tile types actually gets resolved along the way. board-shape
assertions updated to match (212 spaces, no `choices` populated yet).
tileEffects.test.js (new): unit tests per tile type in isolation, including
exact deterministic assertions against real placeholder-table content (roll
a known value, assert the exact banded effect) and a multi-table
roll_table_ref cross-check computed independently from the raw table data —
no seeded-rng machinery anywhere, same explicit-action-value pattern the
reducer tests already used. Also a sweep resolving every real tile across
the full range of its die to catch any type/die combination that throws.
package.json: test script now runs shared/*.test.js instead of naming one
file.
130 lines
5.1 KiB
JavaScript
130 lines
5.1 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createInitialState, reduce, getLegalIntents, isPlayersTurn } from './game.js';
|
|
import { board } from './board.js';
|
|
import { rollsNeededFor } from './tileEffects.js';
|
|
|
|
function join(state, playerId, name, seat) {
|
|
return reduce(state, { type: 'JOIN', playerId, name, seat, color: '#000' });
|
|
}
|
|
|
|
// Deterministic dummy rolls — always the low end of each die's range, so
|
|
// these tests exercise the reducer's mechanics without depending on real
|
|
// (or synthesized-placeholder) table content. shared/tileEffects.test.js
|
|
// covers the actual banding/effect math.
|
|
function rollsFor(space) {
|
|
return rollsNeededFor(space).map(() => 1);
|
|
}
|
|
|
|
function takeTurn(state, playerId) {
|
|
const player = state.players[playerId];
|
|
if (player.pendingChoice) {
|
|
const spaceId = player.pendingChoice.options[0];
|
|
return reduce(state, { type: 'CHOOSE', playerId, spaceId, rolls: rollsFor(board.spaces[spaceId]) });
|
|
}
|
|
const nextSpaceId = board.spaces[player.position].next;
|
|
return reduce(state, { type: 'ROLL', playerId, rolls: rollsFor(board.spaces[nextSpaceId]) });
|
|
}
|
|
|
|
test('lobby: join validation', () => {
|
|
let state = createInitialState();
|
|
state = join(state, 'p1', 'Alice', 1);
|
|
assert.throws(() => join(state, 'p1', 'Alice again', 2), /already joined/);
|
|
assert.throws(() => join(state, 'p2', 'Bob', 1), /Seat already taken/);
|
|
assert.throws(() => reduce(state, { type: 'START_GAME' }), /Not enough players/);
|
|
});
|
|
|
|
test('join gives every player the five starting stats', () => {
|
|
let state = createInitialState();
|
|
state = join(state, 'p1', 'Alice', 1);
|
|
const p1 = state.players.p1;
|
|
assert.deepEqual(
|
|
{ cash: p1.cash, love: p1.love, education: p1.education, wealth: p1.wealth, age: p1.age },
|
|
state.config.startingStats
|
|
);
|
|
});
|
|
|
|
test('turn order and illegal actions', () => {
|
|
let state = createInitialState();
|
|
state = join(state, 'p1', 'Alice', 1);
|
|
state = join(state, 'p2', 'Bob', 2);
|
|
state = reduce(state, { type: 'START_GAME' });
|
|
|
|
assert.equal(state.status, 'active');
|
|
assert.deepEqual(state.turnOrder, ['p1', 'p2']);
|
|
assert.equal(state.currentTurn, 'p1');
|
|
assert.equal(isPlayersTurn(state, 'p1'), true);
|
|
assert.equal(isPlayersTurn(state, 'p2'), false);
|
|
assert.deepEqual(getLegalIntents(state, 'p1'), ['REQUEST_ROLL']);
|
|
assert.deepEqual(getLegalIntents(state, 'p2'), []);
|
|
|
|
assert.throws(() => reduce(state, { type: 'ROLL', playerId: 'p2', rolls: [] }), /Not your turn/);
|
|
assert.throws(() => reduce(state, { type: 'CHOOSE', playerId: 'p1', spaceId: 'anything' }), /No pending choice/);
|
|
});
|
|
|
|
test('a single roll advances exactly one tile and applies its effect', () => {
|
|
let state = createInitialState();
|
|
state = join(state, 'p1', 'Alice', 1);
|
|
state = join(state, 'p2', 'Bob', 2);
|
|
state = reduce(state, { type: 'START_GAME' });
|
|
|
|
const expectedNext = board.spaces[board.startSpaceId].next;
|
|
state = takeTurn(state, 'p1');
|
|
assert.equal(state.players.p1.position, expectedNext);
|
|
assert.equal(state.currentTurn, 'p2', 'turn advances since no choice is pending yet');
|
|
assert.equal(state.log.at(-1).landedOn, expectedNext);
|
|
});
|
|
|
|
test('full board: walking every tile to the temporary finish never crashes and always terminates', () => {
|
|
let state = createInitialState();
|
|
state = join(state, 'p1', 'Alice', 1);
|
|
state = join(state, 'p2', 'Bob', 2);
|
|
state = reduce(state, { type: 'START_GAME' });
|
|
|
|
const seenTypes = new Set();
|
|
let guard = 0;
|
|
while (state.status === 'active') {
|
|
if (++guard > 1000) throw new Error('game did not finish in a reasonable number of turns');
|
|
const turnPlayerId = state.currentTurn;
|
|
state = takeTurn(state, turnPlayerId);
|
|
const after = state.players[turnPlayerId];
|
|
seenTypes.add(board.spaces[after.position].type);
|
|
for (const stat of ['cash', 'love', 'education', 'wealth', 'age']) {
|
|
assert.equal(typeof after[stat], 'number', `${stat} stays numeric`);
|
|
}
|
|
assert.ok(board.spaces[after.position], 'player is always on a real space');
|
|
}
|
|
|
|
assert.equal(state.status, 'finished');
|
|
assert.ok(state.winnerId);
|
|
assert.equal(state.currentTurn, null);
|
|
assert.equal(state.players[state.winnerId].position, 'finish');
|
|
assert.deepEqual(getLegalIntents(state, 'p1'), []);
|
|
|
|
// Every distinct tile type in the real board actually got resolved along the way.
|
|
for (const type of [
|
|
'payday', 'action_space', 'dice_space', 'roll_table_ref',
|
|
'inline_table', 'cash_bonus', 'event', 'choice', 'stop',
|
|
]) {
|
|
assert.ok(seenTypes.has(type), `expected to land on a "${type}" tile during a full playthrough`);
|
|
}
|
|
|
|
assert.throws(() => reduce(state, { type: 'ROLL', playerId: 'p1', rolls: [] }), /not active/);
|
|
});
|
|
|
|
test('board graph is well-formed', () => {
|
|
const ids = Object.keys(board.spaces);
|
|
assert.equal(ids.length, 212, '211 real tiles + 1 synthetic finish');
|
|
let nonFinishCount = 0;
|
|
for (const id of ids) {
|
|
const space = board.spaces[id];
|
|
if (space.type === 'finish') {
|
|
assert.equal(space.next, undefined);
|
|
continue;
|
|
}
|
|
nonFinishCount++;
|
|
assert.ok(board.spaces[space.next], `${id} -> missing ${space.next}`);
|
|
}
|
|
assert.equal(nonFinishCount, 211);
|
|
});
|