v0.7.0 — four game types, a lobby you can read and leave, and a multiplayer game that makes a sound

The multiplayer set-up, the lobby, the start of a game, and four signals a remote client had never
been sent. Reasoning, the preset table and what was verified how: CHANGELOG.md.

- Co-op, Competitive, Cutthroat, Solitaire and Custom, on both screens, from one shared block —
  they had drifted, and each was missing a question the other asked.
- A player reads the whole rule set before taking a seat, may leave a lobby or a running game, and
  keeps a seat across a reload. The host may clear a chair. The browser remembers every game it is
  in, not just the last one.
- The start of a game is drawn: a handoff beat, an announcement, the code and type in the header.
- Sound, the timetable flash, announcements and the just-drawn badge now reach a remote client;
  justDrawn goes to the seat that drew it and nobody else.
- Played on StartOS, which found the rest: an Extra belongs to the player who played it, the board
  never named the Superintendent, bot seats were reported as absent players, and rule section
  numbers are out of every string a player reads.

Also carries the previous session's Heavy Grade documentation work — asked again, answer unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016JczK5i33ZNSf2PtzZqdhS
This commit is contained in:
Jesse.Markowitz
2026-08-23 05:46:42 -04:00
co-authored by Claude Opus 5
parent 42adfda390
commit 06db36e5b5
41 changed files with 4387 additions and 588 deletions
+18 -3
View File
@@ -266,8 +266,14 @@ function newTrainPhase(s: GameState, events: GameEvent[]): AdvanceResult {
* chooser is the acting player; §7 says the player who PLAYED the card, which is the same person
* in solitaire and needs `pendingExtras` to carry a player before it is not.
*/
if (s.pendingExtras.length > 0 && s.freeTrays.length > 0) {
s.clock.currentActor = actorAt(s, s.clock.actorOffset % s.players.length);
const extra = s.pendingExtras[0];
if (extra !== undefined && s.freeTrays.length > 0) {
// §7 — the player who PLAYED the card places it. `pendingExtras` carries them (2026-08-23);
// before that it was a bare train number and this asked whoever the acting order was on, which
// is the same person in solitaire and the wrong one at every table. Reported by Jesse from a
// two-player game.
if (s.clock.currentActor !== extra.player) events.push({ type: 'actorChanged', player: extra.player });
s.clock.currentActor = extra.player;
return { events, needsInput: true };
}
@@ -285,7 +291,16 @@ function newTrainPhase(s: GameState, events: GameEvent[]): AdvanceResult {
const filling = trainNeedingCars(s);
if (filling) {
const tray = s.trays.get(filling)!;
const nextActor = actorAt(s, tray.consist.length % s.players.length);
/**
* TWO DIFFERENT RULES, and §7 states them a paragraph apart. A TIMETABLED train's consist is
* built by the table — "starting with the Superintendent and working left, each player may place
* ONE car" — while an EXTRA is loaded by the player who played it, "as he chooses". So an Extra
* does not enter the round at all; it belongs to `builtBy` until it is full.
*/
const nextActor =
tray.trainIsExtra && tray.builtBy !== undefined
? tray.builtBy
: actorAt(s, tray.consist.length % s.players.length);
if (s.clock.currentActor !== nextActor) events.push({ type: 'actorChanged', player: nextActor });
s.clock.currentActor = nextActor;
return { events, needsInput: true };