v0.4.1 - bug fixes. initial d12 rolls determine what player in which seat.

This commit is contained in:
Jesse
2026-08-13 15:28:53 -04:00
parent 49f8504b05
commit a7221dcf20
22 changed files with 736 additions and 237 deletions
+29 -4
View File
@@ -256,14 +256,35 @@ export function createGame(opts: SetupOptions): GameState {
// 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.
const divisionRolls = players.map(() => rng.d12());
const superRolls = players.map(() => rng.d12());
const superintendent = argmax(superRolls);
void divisionRolls; // seating is fixed by array order; the roll is recorded for the event log
/**
* §4.4 — THE OPENING D12 DECIDES WHO SITS WHERE.
*
* `seating[seat] = player`, and seat 0 is the WESTERN end of the chain (`buildDivision` lays the
* Western Division Point, then office 0, and finishes at the Eastern one). So the highest roll
* takes the last seat — "highest is the Eastern Division Point" — and the lowest ends up beside
* the Western Division Point, which is the rule's other named position.
*
* The rule names only those two ends, because at a table the players are already sitting in a
* chain and the roll only says which way round it is. There is no physical table here, so the
* roll orders everybody: ascending by roll, west to east. It uses a number every player is
* already told to roll, and it makes the roll matter to more than the winner.
*
* Ties break toward the LOWER player index sitting further east, which is the same convention
* `argmax` uses for the Superintendent roll on the line above — first max wins.
*
* This was `void divisionRolls` until v0.4.1: the roll was drawn and discarded, and seating was
* the identity mapping. Turning it on is what makes seat and player genuinely different at
* runtime rather than only in the type names.
*/
const seating: PlayerIndex[] = players
.map((_, p) => p)
.sort((a, b) => divisionRolls[a]! - divisionRolls[b]! || b - a);
/**
* §4.6-4.7 — THE OPENING DEAL, dealt from two piles rather than one.
@@ -297,8 +318,11 @@ export function createGame(opts: SetupOptions): GameState {
const hands = new Map<PlayerIndex, CardId[]>();
let trackCursor = 0;
let otherCursor = 0;
// §4.7 — "starting from the Superintendent, deal each player…", which proceeds round the table
// and is therefore seat order, not player order.
const superSeat = seating.indexOf(superintendent);
for (let i = 0; i < playerCount; i++) {
const p = (superintendent + i) % playerCount;
const p = seating[(superSeat + i) % playerCount]!;
hands.set(p, [
...trackPile.slice(trackCursor, trackCursor + OPENING_TRACK),
...otherPile.slice(otherCursor, otherCursor + OPENING_OTHER),
@@ -340,6 +364,7 @@ export function createGame(opts: SetupOptions): GameState {
rngState: rng.getState(),
players,
seating,
openingRolls: { division: divisionRolls, superintendent: superRolls },
division: { nodes: buildDivision(playerCount, rng) },
officeAreas,
trays: new Map(),