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
+16 -16
View File
@@ -27,7 +27,7 @@ import {
import { createGame } from '../src/engine/setup.ts';
import type { GameEvent } from '../src/engine/events.ts';
import type { 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 { snapshot } from '../src/sim/view.ts';
const config: GameConfig = {
@@ -81,8 +81,8 @@ function pinned(s: GameState, index: number, card: string) {
function drawTurn(s: GameState): void {
s.clock.phase = 'localOps';
s.clock.currentActor = 0;
s.turn.option = 'draw';
s.turn.drawnThisTurn = true;
turnOf(s, 0).option = 'draw';
turnOf(s, 0).drawnThisTurn = true;
}
// ---------------------------------------------------------------------------
@@ -343,12 +343,12 @@ describe('Flying Switch rolls a cut into an industry', () => {
id: 'crew', trainNumber: 1, trainIsExtra: false, engineAt: 0,
consist: [{ type: 'hopper', loaded: false }, { type: 'boxcar', loaded: false }],
direction: 'east', facing: 'e',
position: { at: 'grid', owner: 0, coord: at(-1, 2) }, movesUsed: 0,
position: { at: 'grid', seat: 0, coord: at(-1, 2) }, movesUsed: 0,
});
s.clock.phase = 'localOps';
s.clock.currentActor = 0;
s.turn.option = 'switch';
s.turn.movesRemaining = 6;
turnOf(s, 0).option = 'switch';
turnOf(s, 0).movesRemaining = 6;
return industry;
}
@@ -367,10 +367,10 @@ describe('Flying Switch rolls a cut into an industry', () => {
assert.equal(s.trays.get('crew')!.consist.length, 1, 'the cut leaves the consist');
assert.deepEqual(
s.trays.get('crew')!.position,
{ at: 'grid', owner: 0, coord: at(-1, 2) },
{ at: 'grid', seat: 0, coord: at(-1, 2) },
'the engine never enters the industry',
);
assert.equal(s.turn.movesRemaining, 5, 'the manoeuvre costs a Move');
assert.equal(turnOf(s, 0).movesRemaining, 5, 'the manoeuvre costs a Move');
});
it('only reaches an adjacent card', () => {
@@ -454,7 +454,7 @@ describe('the Limits sign moves with the Running Track (§2.1, Gap 4a)', () => {
// — track on the far side of the sign, and a second sign planted beyond that.
const s = game();
const area = areaOf(s, 0);
s.turn.option = 'draw';
turnOf(s, 0).option = 'draw';
const straight = trackInHand(s, 'straight', 'none');
const beyondEast = { row: area.runningRow, col: area.limitsEast.col + 1 };
@@ -508,7 +508,7 @@ describe('the Limits sign moves with the Running Track (§2.1, Gap 4a)', () => {
// player's Limits is what makes a collision his fault.
const s = game();
const area = areaOf(s, 0);
s.turn.option = 'draw';
turnOf(s, 0).option = 'draw';
for (let n = 0; n < 4; n++) {
const target = { row: area.runningRow, col: area.limitsWest.col };
@@ -549,7 +549,7 @@ describe('a turnout may be laid on top of a card already down', () => {
// straight along the main and then wanted to branch there had no move at all — the piece had to
// have been a turnout when it went down.
const s = game();
s.turn.option = 'draw';
turnOf(s, 0).option = 'draw';
const at = layStraight(s);
for (const hand of ['left', 'right'] as const) {
@@ -575,7 +575,7 @@ describe('a turnout may be laid on top of a card already down', () => {
// opposite edges, so swapping one for the other would move the leg off whatever it joined.
const s = game();
const area = areaOf(s, 0);
s.turn.option = 'draw';
turnOf(s, 0).option = 'draw';
// A turnout on the main, then its matching curve on the row below — the start of every siding.
const turnoutAt = { row: area.runningRow, col: area.limitsEast.col };
@@ -609,7 +609,7 @@ describe('a turnout may be laid on top of a card already down', () => {
it('refuses to swap the track out from under a car, or out from under an Interlocking', () => {
const s = game();
const area = areaOf(s, 0);
s.turn.option = 'draw';
turnOf(s, 0).option = 'draw';
const at = layStraight(s);
const square = area.grid.get(`${at.row},${at.col}`)!;
@@ -633,7 +633,7 @@ describe('a turnout may be laid on top of a card already down', () => {
// The Office is east-west track like a straight, but it is not track to build over.
const s = game();
const area = areaOf(s, 0);
s.turn.option = 'draw';
turnOf(s, 0).option = 'draw';
assert.equal(
check(s, 0, {
type: 'card.play', cardId: trackInHand(s, 'turnout', 'right'),
@@ -646,7 +646,7 @@ describe('a turnout may be laid on top of a card already down', () => {
it('does not let a straight or a curve upgrade anything — only a turnout may', () => {
const s = game();
s.turn.option = 'draw';
turnOf(s, 0).option = 'draw';
const at = layStraight(s);
assert.equal(
@@ -665,7 +665,7 @@ describe('a turnout may be laid on top of a card already down', () => {
// The 18 Enhancement cards were permanently dead for weeks for exactly this reason: the engine
// allowed the play and `legalIntents` never once offered a square it could go on.
const s = game();
s.turn.option = 'draw';
turnOf(s, 0).option = 'draw';
const at = layStraight(s);
const cardId = trackInHand(s, 'turnout', 'right');