Files
lifegame/shared/game.test.js
T
2026-07-21 16:17:12 -07:00

98 lines
4.8 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createInitialState, reduce, getLegalIntents, isPlayersTurn } from './game.js';
function join(state, playerId, name, seat) {
return reduce(state, { type: 'JOIN', playerId, name, seat, color: '#000' });
}
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('full game: both choice points, race to finish', () => {
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.deepEqual(getLegalIntents(state, 'p1'), ['REQUEST_ROLL']);
assert.deepEqual(getLegalIntents(state, 'p2'), []);
assert.equal(isPlayersTurn(state, 'p2'), false);
// Not p2's turn yet.
assert.throws(() => reduce(state, { type: 'ROLL', playerId: 'p2', value: 3 }), /Not your turn/);
// p1 rolls onto the first choice space; movement stops immediately even
// though only 1 of the roll's pips was needed.
state = reduce(state, { type: 'ROLL', playerId: 'p1', value: 3 });
assert.equal(state.players.p1.position, 'crossroads');
assert.deepEqual(state.players.p1.pendingChoice, { atSpace: 'crossroads', options: ['career_1', 'edu_1'] });
assert.equal(state.currentTurn, 'p1', 'turn stays with the player until they choose');
assert.deepEqual(getLegalIntents(state, 'p1'), ['REQUEST_CHOOSE']);
assert.throws(() => reduce(state, { type: 'ROLL', playerId: 'p1', value: 2 }), /Resolve pending choice/);
assert.throws(() => reduce(state, { type: 'CHOOSE', playerId: 'p1', spaceId: 'finish' }), /Illegal choice/);
state = reduce(state, { type: 'CHOOSE', playerId: 'p1', spaceId: 'career_1' });
assert.equal(state.players.p1.position, 'career_1');
assert.equal(state.players.p1.pendingChoice, null);
assert.equal(state.currentTurn, 'p2', 'choosing resolves the turn');
// p2 takes the education branch.
state = reduce(state, { type: 'ROLL', playerId: 'p2', value: 3 });
assert.equal(state.players.p2.position, 'crossroads');
state = reduce(state, { type: 'CHOOSE', playerId: 'p2', spaceId: 'edu_1' });
assert.equal(state.players.p2.position, 'edu_1');
assert.equal(state.currentTurn, 'p1');
// p1: career_1 -> life_crossroads is exactly 6 steps; only the landed
// space's cash effect applies, not spaces merely passed through.
state = reduce(state, { type: 'ROLL', playerId: 'p1', value: 6 });
assert.equal(state.players.p1.position, 'life_crossroads');
assert.equal(state.players.p1.cash, 0, 'passed-through Paycheck/Drama/Review do not apply');
state = reduce(state, { type: 'CHOOSE', playerId: 'p1', spaceId: 'investment_1' });
assert.equal(state.players.p1.position, 'investment_1');
assert.equal(state.players.p1.cash, -150);
assert.equal(state.currentTurn, 'p2');
// p2: edu_1 -> life_crossroads is also exactly 6 steps.
state = reduce(state, { type: 'ROLL', playerId: 'p2', value: 6 });
assert.equal(state.players.p2.position, 'life_crossroads');
state = reduce(state, { type: 'CHOOSE', playerId: 'p2', spaceId: 'relationship_1' });
assert.equal(state.players.p2.position, 'relationship_1');
assert.equal(state.currentTurn, 'p1');
// p1: investment_1 -> high_risk_choice is exactly 4 steps.
state = reduce(state, { type: 'ROLL', playerId: 'p1', value: 4 });
assert.equal(state.players.p1.position, 'high_risk_choice');
state = reduce(state, { type: 'CHOOSE', playerId: 'p1', spaceId: 'safe_1' });
assert.equal(state.players.p1.cash, -50); // -150 + 100
assert.equal(state.currentTurn, 'p2');
// p2: relationship_1 -> high_risk_choice is also exactly 4 steps.
state = reduce(state, { type: 'ROLL', playerId: 'p2', value: 4 });
assert.equal(state.players.p2.position, 'high_risk_choice');
state = reduce(state, { type: 'CHOOSE', playerId: 'p2', spaceId: 'high_risk_1' });
assert.equal(state.players.p2.cash, 500);
assert.equal(state.currentTurn, 'p1');
// p1: safe_1 -> finish is exactly 3 steps. First arrival ends the game.
assert.equal(state.status, 'active');
state = reduce(state, { type: 'ROLL', playerId: 'p1', value: 3 });
assert.equal(state.players.p1.position, 'finish');
assert.equal(state.status, 'finished');
assert.equal(state.winnerId, 'p1');
assert.equal(state.currentTurn, null);
assert.deepEqual(getLegalIntents(state, 'p2'), []);
assert.throws(() => reduce(state, { type: 'ROLL', playerId: 'p2', value: 1 }), /not active/);
});