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/); });