• v0.5.3 2fbfe11977

    v0.5.3 — a table you size yourself, and games an administrator can see and end

    Jesse.Markowitz released this 2026-08-21 18:53:53 +00:00 | 71 commits to main since this release

    Both halves came out of playing the StartOS build. The wrapper's health
    check and admin actions consume this; they land separately.

    The host picks the table size (2-4) when creating a game, and the seats
    array is built at that length once. Before, it GREW as people joined, so
    the four rows on screen were partly fiction — a 2-player game just started
    with a 2-long array, while a host who dropped a bot into a later chair
    padded it with a null and silently disabled Start behind a one-line note.
    A gap can no longer be written down rather than merely being refused.

    That also avoided a trap. Compacting seats at Lobby.Start — the obvious
    way to support a "closed" chair — would have shifted the player index that
    every PlayerSession stamps at join time and that /api/stream and
    /api/intent both route by, handing a player somebody else's railroad with
    no error anywhere.

    And it fixed a live balance bug: minCombinedRevenue is derived from the
    player count, but the config was fixed at CREATE while the count wasn't
    known until START, so the lobby guessed 4. Every 2-player game ran against
    a floor of 60 instead of 30 — and missing the floor means everyone loses,
    so a 2-player competitive game was set up to fail for a UI artifact rather
    than a rule.

    /api/health gained games:{active,lobby}, read from a new cheap summary()
    on GameSession rather than exportSave(), which would copy every intent of
    every game to answer a question about none of them. Three admin routes are
    new behind an ADMIN_SECRET env var in an x-admin-secret header: GET
    /api/games, GET /api/games//save, DELETE /api/games/. Until now a
    started game could not be ended by anyone — no route, no player action, no
    resignation — so an abandoned game stayed active in the index and was
    faithfully resumed on every boot, forever.

    Three deliberate choices there: the admin secret is NOT the join secret,
    which every player holds and which would therefore let anyone at the table
    destroy anyone else's game; unset means the routes 404 exactly as any
    unknown path does, with or without a header, so a server never given an
    administrator doesn't advertise that it has one; and a delete returns the
    deleted game's save, since the intents are the game (D5) — nothing is
    destroyed without being handed to whoever destroyed it.

    SavedGame gained an optional lastMoveAt (falling back to createdAt) so
    "has this stalled?" survives a restart. Kept out of history for the same
    reason the turn timings are: a replay must reproduce a game from decisions
    alone, and wall-clock is not a decision.

    index.ts logs "Resuming N saved games..." before the loop rather than one
    line per game after it. Measured a full 4-player game at 100ms to replay,
    and only unfinished games are replayed, so listening before loading would
    have bought nothing for the cost of a "still loading" state everywhere.

    Verified: 667 tests pass (662 + 5), and the new session tests were checked
    against two mutations (lastMoveAt never advancing; resume dropping it) to
    confirm they fail without the code. Live against a running server: health
    counts tracking through the lobby->game transition, admin auth rejecting a
    missing and a wrong secret, list/export/delete, the deleted game's files
    and index entry actually gone from disk, a second delete 404ing, the admin
    routes invisible when ADMIN_SECRET is unset, and a 3-player table refusing
    a 4th player and a size of 5 refused at the door.

    Also carries the TODO items raised on 2026-08-21: the lobby offering no
    game parameters (the floor bug within it now fixed, the form still
    missing), and the four optionalRules — of which only reducedVisibility and
    emergencyToolbox are read by anything, while sisterTrains and
    employeeRotation are declared, defaulted, and consulted nowhere.

    Downloads