v0.4.0 — multiplayer Phases 0 and 1: seat and player split apart, turn state per player, the page behind a Session, and eight seat/player mix-ups fixed with tests that fail without them

This commit is contained in:
Jesse
2026-08-13 14:06:02 -04:00
parent 216006b091
commit 49f8504b05
34 changed files with 1743 additions and 526 deletions
+9 -9
View File
@@ -12,7 +12,7 @@ import type { Intent } from '../src/engine/intents.ts';
import { legalActions } from '../src/engine/legal.ts';
import { createGame } from '../src/engine/setup.ts';
import type { CrewTray, GameConfig, GameState, GridCoord, TrackCard } from '../src/engine/state.ts';
import { coordKey } from '../src/engine/state.ts';
import { coordKey, turnOf } from '../src/engine/state.ts';
import { cardDescription, snapshot } from '../src/sim/view.ts';
const config: GameConfig = {
@@ -42,7 +42,7 @@ function placeTray(s: GameState, coord: GridCoord, consist: CrewTray['consist']
engineAt: 0,
consist,
direction: 'east',
position: { at: 'grid', owner: 0, coord },
position: { at: 'grid', seat: 0, coord },
movesUsed: 0,
});
return id;
@@ -90,7 +90,7 @@ describe('Local Operations: the three-way exclusive choice (§6)', () => {
const s = game();
const r = applyIntent(s, 0, { type: 'localOps.choose', option: 'draw' });
assert.ok(r.ok);
assert.equal(s.turn.option, 'draw');
assert.equal(turnOf(s, 0).option, 'draw');
});
it('forecloses the other two options for the Stage', () => {
@@ -366,7 +366,7 @@ describe('Local Operations: switching (§6.1, Appendix A)', () => {
applyIntent(s, 0, { type: 'localOps.choose', option: 'switch' });
const r = applyIntent(s, 0, { type: 'switch.move', trayId: tray, to: at(0, 2), reverse: false });
assert.ok(r.ok);
assert.equal(s.turn.movesRemaining, MOVES_PER_LOCAL_OPS - 1);
assert.equal(turnOf(s, 0).movesRemaining, MOVES_PER_LOCAL_OPS - 1);
});
it('couples standing cars automatically and mandatorily', () => {
@@ -395,7 +395,7 @@ describe('Local Operations: switching (§6.1, Appendix A)', () => {
const s = game();
const tray = placeTray(s, at(0, 1));
applyIntent(s, 0, { type: 'localOps.choose', option: 'switch' });
s.turn.movesRemaining = 0;
turnOf(s, 0).movesRemaining = 0;
assert.equal(
check(s, 0, { type: 'switch.move', trayId: tray, to: at(0, 0), reverse: false }),
'NO_MOVES_REMAINING',
@@ -993,7 +993,7 @@ describe('Industry cards go on a stub, and lock each other out', () => {
}));
s.clock.phase = 'localOps';
s.clock.currentActor = 0;
s.turn.option = 'draw';
turnOf(s, 0).option = 'draw';
return at(area.runningRow - 1, col + 1);
}
@@ -1151,7 +1151,7 @@ describe('the Crew Tray is a train, and must be made up to leave (§8.2, Appendi
s.trays.get(id)!.engineAt = 1;
s.clock.phase = 'localOps';
s.clock.currentActor = 0;
s.turn.option = 'switch';
turnOf(s, 0).option = 'switch';
// One car ahead of the engine and one behind: at most one may come off either end.
assert.equal(check(s, 0, { type: 'switch.dropCars', trayId: id, count: 1, fromNose: true }), null);
assert.equal(check(s, 0, { type: 'switch.dropCars', trayId: id, count: 1 }), null);
@@ -1185,7 +1185,7 @@ describe('only the train being made up may take cars (§7)', () => {
// Point. `check` did not, so the Division Yard would hand cars to a train sitting on a siding
// in your own district: cars appeared on a train nobody was making up.
const s = game();
trayAt(s, { at: 'grid', owner: 0, coord: at(0, 1) });
trayAt(s, { at: 'grid', seat: 0, coord: at(0, 1) });
assert.equal(check(s, 0, addHopper), 'NOT_BEING_MADE_UP');
});
@@ -1225,7 +1225,7 @@ describe('an Office upgrade keeps what Modifiers added (§9)', () => {
for (const [id, card] of s.cards) {
if (card.kind.kind !== 'office' || card.kind.tier !== tier) continue;
s.decks.hands.set(0, [id]);
s.turn.option = null;
turnOf(s, 0).option = null;
applyIntent(s, 0, { type: 'localOps.choose', option: 'draw' });
const r = applyIntent(s, 0, { type: 'card.play', cardId: id });
assert.ok(r.ok, `${tier} upgrade should apply`);