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:
+13
-7
@@ -34,6 +34,7 @@ import type {
|
||||
Card,
|
||||
CardId,
|
||||
DivisionNode,
|
||||
SeatIndex,
|
||||
GameConfig,
|
||||
GameState,
|
||||
OfficeArea,
|
||||
@@ -42,7 +43,7 @@ import type {
|
||||
TrackCard,
|
||||
TrayId,
|
||||
} from './state.ts';
|
||||
import { coordKey, freshTurn } from './state.ts';
|
||||
import { coordKey, freshTurns } from './state.ts';
|
||||
|
||||
export type SetupOptions = {
|
||||
id: string;
|
||||
@@ -129,7 +130,7 @@ export function buildRollingStock(): RollingStock[] {
|
||||
* branch from the opening Stage. The stubs are NOT turnouts: §A.1's directional rule governs
|
||||
* drawn turnout cards only.
|
||||
*/
|
||||
function buildOfficeArea(owner: PlayerIndex): OfficeArea {
|
||||
function buildOfficeArea(seat: SeatIndex): OfficeArea {
|
||||
const row = 0;
|
||||
const officeCoord = { row, col: 0 };
|
||||
const limitsWest = { row, col: -1 };
|
||||
@@ -159,7 +160,7 @@ function buildOfficeArea(owner: PlayerIndex): OfficeArea {
|
||||
grid.set(coordKey(limitsEast), limitsCard());
|
||||
|
||||
return {
|
||||
owner,
|
||||
seat,
|
||||
tier: 'whistlePost',
|
||||
grid,
|
||||
officeCoord,
|
||||
@@ -230,7 +231,7 @@ function buildDivision(players: number, rng: Rng): DivisionNode[] {
|
||||
nodes.push({ kind: 'divisionPoint', side: 'west', holding: [] });
|
||||
for (let p = 0; p < players; p++) {
|
||||
nodes.push(mainline());
|
||||
nodes.push({ kind: 'office', owner: p });
|
||||
nodes.push({ kind: 'office', seat: p });
|
||||
}
|
||||
nodes.push(mainline());
|
||||
nodes.push({ kind: 'divisionPoint', side: 'east', holding: [] });
|
||||
@@ -250,8 +251,12 @@ export function createGame(opts: SetupOptions): GameState {
|
||||
|
||||
const players = playerNames.map((name, index) => ({ index, name, revenue: 0 }));
|
||||
|
||||
const officeAreas = new Map<PlayerIndex, OfficeArea>();
|
||||
for (let p = 0; p < playerCount; p++) officeAreas.set(p, buildOfficeArea(p));
|
||||
// Offices are keyed by SEAT — a fixed position in the west-to-east chain. `seating` maps seats to
|
||||
// the players occupying them, and starts as the identity mapping, which is what makes the
|
||||
// seat/player split behaviour-neutral. Employee Rotation would rotate this array and nothing else.
|
||||
const officeAreas = new Map<SeatIndex, OfficeArea>();
|
||||
for (let seat = 0; seat < playerCount; seat++) officeAreas.set(seat, buildOfficeArea(seat));
|
||||
const seating: PlayerIndex[] = Array.from({ length: playerCount }, (_, seat) => seat);
|
||||
|
||||
// §4.4 - highest D12 takes the Eastern Division Point; §4.5 - highest begins as Superintendent.
|
||||
// Both rolls are drawn even in solitaire so the RNG stream stays identical across player counts.
|
||||
@@ -334,6 +339,7 @@ export function createGame(opts: SetupOptions): GameState {
|
||||
seed,
|
||||
rngState: rng.getState(),
|
||||
players,
|
||||
seating,
|
||||
division: { nodes: buildDivision(playerCount, rng) },
|
||||
officeAreas,
|
||||
trays: new Map(),
|
||||
@@ -354,7 +360,7 @@ export function createGame(opts: SetupOptions): GameState {
|
||||
superintendent,
|
||||
actorOffset: 0,
|
||||
},
|
||||
turn: freshTurn(MOVES_PER_LOCAL_OPS),
|
||||
turns: freshTurns(playerCount, MOVES_PER_LOCAL_OPS),
|
||||
movedThisPhase: new Set(),
|
||||
collisionsToday: 0,
|
||||
status: 'active',
|
||||
|
||||
Reference in New Issue
Block a user