98 lines
4.8 KiB
JavaScript
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/);
|
|
});
|