v0.3.1 — multiplayer groundwork: 13 engine tests covering 2-4 player games, a seat parameter on snapshot and actionMenu so a player sees their own railroad rather than seat 0's, and the 22 opponent-directed cards held out of every deck until they are built

This commit is contained in:
Jesse
2026-08-12 22:37:10 -04:00
parent 1bf1e95058
commit 216006b091
11 changed files with 460 additions and 44 deletions
+17 -6
View File
@@ -53,9 +53,20 @@ export type SetupOptions = {
/** Builds the 52-card Home Office deck (§12.1). Unshuffled; caller shuffles with the seeded RNG. */
export function buildDeck(mode: GameConfig['mode'] = 'competitive'): Card[] {
// Q6 — the 22 opponent-directed cards have no legal target in a one-player game, so a solitaire
// deck omits them entirely rather than leaving 19% of draws dead.
const solitaire = mode === 'solitaire';
/**
* THE 22 OPPONENT-DIRECTED CARDS ARE OUT OF EVERY DECK FOR NOW, not just the solitaire one.
*
* Q6 took them out of solitaire because they have no legal target in a one-player game. They are
* out of the competitive deck too because `checkPlay` answers both categories `NOT_IMPLEMENTED`:
* leaving them in would make ~9% of draws reject outright, which is worse than not dealing them.
* Three Enhancements — Facing Point Locks, Water Column and Overpass — exist only to answer these,
* and stay dormant until they come back. Recorded in TODO.md as multiplayer work.
*
* `mode` is still taken so the signature does not move when they return; it is deliberately unused
* for these two categories today.
*/
void mode;
const opponentCardsInDeck = false;
const cards: Card[] = [];
let n = 0;
const push = (kind: Card['kind']): void => {
@@ -73,7 +84,7 @@ export function buildDeck(mode: GameConfig['mode'] = 'competitive'): Card[] {
for (const m of MODIFIER_PROFILES) {
for (let i = 0; i < m.copies; i++) push({ kind: 'modifier', modifier: m.kind });
}
if (!solitaire) {
if (opponentCardsInDeck) {
for (const c of SPACE_USE_CARDS) {
for (let i = 0; i < c.copies; i++) push({ kind: 'spaceUse', key: c.key });
}
@@ -87,12 +98,12 @@ export function buildDeck(mode: GameConfig['mode'] = 'competitive'): Card[] {
for (const c of MANEUVER_CARDS) {
for (let i = 0; i < c.copies; i++) push({ kind: 'maneuver', key: c.key });
}
if (!solitaire) {
if (opponentCardsInDeck) {
for (const c of ACTION_CARDS) {
for (let i = 0; i < c.copies; i++) push({ kind: 'action', key: c.key });
}
}
// Track is IN the deck, 104 cards of it — the single largest category, and the reason building a
// Track is IN the deck, 96 cards of it — the single largest category, and the reason building a
// district costs you the industry or train you did not draw instead.
for (const t of TRACK_CARDS) {
for (let i = 0; i < t.copiesInDeck; i++) push({ kind: 'track', geometry: t.geometry, hand: t.hand });