import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ // `:` — the upstream half tracks the bundled Station Master tag exactly // (`versions.md`'s consistency checklist), so bumping the submodule pin and bumping this are one // action. The downstream half moves only for packaging-only changes, and RESETS to `:0` when the // upstream half moves. // // FOUR COMPONENTS IS FINE. ExVer's upstream half is an arbitrary-length dotted number, not semver // — checked against the SDK's own parser rather than assumed, since every example in `versions.md` // has three: `0.7.9.8:0` parses to upstream `[0,7,9,8]`, which sorts above `[0,7,9]`, so a box on // 0.7.9:0 sees this as an update rather than refusing it as a downgrade. version: '0.8.0.12:0', releaseNotes: { en_US: 'Bundles Station Master v0.8.0.12 — a player who has lost their browser storage can be put ' + 'back in their seat. **Games already running on this server resume normally**: no rule ' + 'changed. **The problem this fixes.** The only proof of who you are is kept by your own ' + 'browser, so clearing your browsing data, returning in a private window, or sitting down at a ' + 'different browser leaves you looking like a stranger — an empty lobby, no game code, no name ' + '— while the game is still running on the server and your seat is still yours. It happened at ' + 'a real table: of two players in one game, one reloaded straight back in and the other could ' + 'not get in at all. **The fix is a new Restore a Seat action.** Pick the player from a list of ' + 'whoever is actually holding a seat, and it hands you a link. Send it to them, and opening it ' + 'in their own browser puts them straight back where they were. The link works once and expires ' + 'after 30 minutes, and whoever opens it takes that seat — so send it to that person rather ' + 'than posting it somewhere public. If it expires first, just run the action again. The link ' + 'carries a one-time code and never the seat itself, so a link left in a chat window is worth ' + 'nothing once it has been used.', es_ES: 'Incluye Station Master v0.8.0.12 — un jugador que haya perdido los datos de su navegador ' + 'puede volver a su asiento. **Las partidas en curso en este servidor se reanudan con ' + 'normalidad**: no ha cambiado ninguna regla. **El problema que corrige.** La única prueba de ' + 'quién eres la guarda tu propio navegador: si borras los datos de navegación, vuelves en una ' + 'ventana privada o te sientas ante otro navegador, apareces como un desconocido — una sala ' + 'vacía, sin código de partida ni nombre — aunque la partida siga en el servidor y el asiento ' + 'siga siendo tuyo. Ocurrió en una mesa real: de dos jugadores, uno volvió sin problemas y el ' + 'otro no pudo entrar. **La solución es la nueva acción Restaurar un asiento.** Elige al ' + 'jugador de la lista de quienes ocupan un asiento y se te entrega un enlace. Envíaselo y, al ' + 'abrirlo en su propio navegador, volverá justo donde estaba. El enlace funciona una sola vez y ' + 'caduca a los 30 minutos, y quien lo abra ocupará ese asiento: envíaselo a esa persona en ' + 'lugar de publicarlo. Si caduca antes, ejecuta la acción de nuevo. El enlace lleva un código ' + 'de un solo uso y nunca el asiento en sí, así que un enlace olvidado en un chat no sirve de ' + 'nada una vez usado.', de_DE: 'Enthält Station Master v0.8.0.12 — wer die Daten seines Browsers verloren hat, kann wieder ' + 'auf seinen Platz gesetzt werden. **Laufende Spiele auf diesem Server werden normal ' + 'fortgesetzt**: keine Regel wurde geändert. **Das behobene Problem.** Der einzige Nachweis, ' + 'wer Sie sind, liegt in Ihrem eigenen Browser. Browserdaten gelöscht, im privaten Fenster ' + 'zurückgekehrt oder an einem anderen Browser gesessen — und Sie erscheinen als Fremder: leere ' + 'Lobby, kein Spielcode, kein Name, obwohl das Spiel weiterläuft und der Platz Ihnen gehört. An ' + 'einem echten Tisch geschehen: von zwei Spielern kam einer sofort zurück, der andere gar ' + 'nicht. **Die Lösung ist die neue Aktion „Einen Platz wiederherstellen".** Wählen Sie den ' + 'Spieler aus der Liste der tatsächlich besetzten Plätze, und Sie erhalten einen Link. ' + 'Schicken Sie ihn; wird er im eigenen Browser geöffnet, sitzt der Spieler wieder genau dort, ' + 'wo er war. Der Link funktioniert einmal und verfällt nach 30 Minuten, und wer ihn öffnet, ' + 'nimmt diesen Platz ein — schicken Sie ihn also dieser Person und posten Sie ihn nicht ' + 'öffentlich. Verfällt er vorher, führen Sie die Aktion einfach erneut aus. Der Link enthält ' + 'einen Einmal-Code und niemals den Platz selbst; ein Link in einem Chatfenster ist nach dem ' + 'Einlösen wertlos.', pl_PL: 'Zawiera Station Master v0.8.0.12 — gracz, który stracił dane swojej przeglądarki, może ' + 'wrócić na swoje miejsce. **Gry w toku na tym serwerze wznawiają się normalnie**: żadna zasada ' + 'się nie zmieniła. **Naprawiony problem.** Jedynym dowodem tożsamości jest to, co trzyma twoja ' + 'przeglądarka. Wyczyść dane, wróć w oknie prywatnym albo usiądź przy innej przeglądarce, a ' + 'wyglądasz jak ktoś obcy — pusta poczekalnia, brak kodu gry i nazwy — chociaż gra nadal działa ' + 'na serwerze, a miejsce wciąż należy do ciebie. Zdarzyło się to przy prawdziwym stole: z ' + 'dwóch graczy jeden wrócił od razu, a drugi w ogóle nie mógł wejść. **Rozwiązaniem jest nowa ' + 'akcja „Przywróć miejsce".** Wybierz gracza z listy osób faktycznie zajmujących miejsca, a ' + 'otrzymasz link. Wyślij mu go — po otwarciu we własnej przeglądarce wróci dokładnie tam, gdzie ' + 'był. Link działa raz i wygasa po 30 minutach, a kto go otworzy, zajmie to miejsce — wyślij ' + 'go więc tej osobie, zamiast publikować. Jeśli wygaśnie, po prostu uruchom akcję ponownie. ' + 'Link zawiera jednorazowy kod, a nigdy samo miejsce, więc link zostawiony na czacie po ' + 'wykorzystaniu jest bezwartościowy.', fr_FR: 'Inclut Station Master v0.8.0.12 — un joueur qui a perdu les données de son navigateur peut ' + 'être remis à sa place. **Les parties en cours sur ce serveur reprennent normalement** : ' + 'aucune règle n’a changé. **Le problème corrigé.** La seule preuve de votre identité est ' + 'conservée par votre propre navigateur : effacez vos données, revenez dans une fenêtre privée ' + 'ou installez-vous devant un autre navigateur, et vous apparaissez comme un inconnu — salle ' + 'vide, ni code de partie ni nom — alors que la partie tourne toujours et que la place est ' + 'toujours la vôtre. C’est arrivé à une vraie table : de deux joueurs, l’un est revenu ' + 'directement, l’autre n’a pas pu entrer. **La solution est la nouvelle action « Restaurer une ' + 'place ».** Choisissez le joueur dans la liste de ceux qui occupent réellement une place, et ' + 'vous obtenez un lien. Envoyez-le-lui : ouvert dans son propre navigateur, il le remet ' + 'exactement où il était. Le lien fonctionne une seule fois et expire au bout de 30 minutes, et ' + 'quiconque l’ouvre prend cette place — envoyez-le donc à cette personne plutôt que de le ' + 'publier. S’il expire avant, relancez simplement l’action. Le lien porte un code à usage ' + 'unique et jamais la place elle-même : un lien laissé dans une conversation ne vaut plus rien ' + 'une fois utilisé.', }, migrations: { /** * NOTHING TO CARRY, AND CHECKED THE SAME NARROW WAY v0.7.9.8's ENTRY WAS. * * The rule this package lives by is in the game's README: a save is a seed plus the moves * played, replayed through the CURRENT rules, so the only thing that strands an older save is a * change that makes a once-legal move illegal. So the question is never "how big was the * release" — v0.8.0 is a large one — but "did any predicate change its answer". * * Measured with `git diff v0.7.9.8..v0.8.0.1 -- src/engine/`. The WHOLE engine diff is eight * lines: * four `GameEvent` types (`trayMoved`, `carsCoupled`, `carsDropped`, `consistSorted`) gained a * `player` field, and the four places that build them pass it. `check()` is untouched, `legal.ts` * and `advance.ts` are untouched, and no predicate anywhere was edited. Games in progress resume, * and the release notes say so in all five locales rather than leaving it to be discovered. * * EVENTS ARE NOT PERSISTED, so widening four of them cannot invalidate anything on disk: a * `SavedGame` is `{ seed, config, history: Intent[], … }` and the events are derived by replaying * it. The narration TEXT of two lines changed with them ("Moved Train 3 …" where it used to read * "CREW moved …"), which a resumed game simply regenerates — the log is not stored either. * * WHAT IS NOT COVERED BY THAT ARGUMENT, and why it still does not need a migration: the wire * between this server and a browser gained a lot. `Push` now carries ordered presentation steps * and a public-board baseline, and the board deltas changed shape. None of it is on disk, and a * browser reloads onto the new page as part of the update, so there is no version skew to * migrate — the same reasoning v0.7.9.8 used for its own `Frame` changes. * * v0.8.0.1 ADDED NOTHING TO THIS ARGUMENT: `git diff v0.8.0..v0.8.0.1 -- src/engine/` is empty. * It changed `sim/pacing.ts`, `web/step-queue.ts` and `web/main.ts` — how long a step is shown * and when the queue calls itself idle — which is presentation and touches neither the rules nor * anything on disk. * * v0.8.0.2 DOES TOUCH THE ENGINE, and it is worth being exact about why that is still nothing to * migrate. `git diff v0.8.0.1..v0.8.0.2 -- src/engine/` is three added lines: `collisionsPrevDay` * declared on `GameState`, initialised to 0 in `setup.ts`, and assigned at the Day rollover in * `advance.ts`. It is a counter that nothing checks — no predicate reads it, so no once-legal move * became illegal. * * v0.8.0.3 THROUGH v0.8.0.7 ADDED NOTHING: `git diff v0.8.0.2..v0.8.0.7 -- src/engine/` is empty. * One moved the Skip button, changed what the waiting row says, and stopped the speed control * stretching the clock; one took a hostname out of comments; the last drew the Home Office deck * and lit the pile a watched move touched, which is a diff of two frames the client already * holds. None of it is a rule, and none of it is on disk. * * v0.8.0.8 IS DIFFERENT AND IS THE ONE TO READ CAREFULLY: it changes a RULE (Gitea#23). When the * Home Office deck is reshuffled, a timetabled train sitting in the SALVAGE YARD is no longer * recovered — it was played, its number is on the timetable, and it cannot be run twice. The same * card in a DEPARTMENT was discarded rather than played and still comes back, as does any Extra. * A duplicate went with it: `trainScheduled` had been pushing a synthetic `train-` beside the * real card, so four scheduled trains left eight entries in a pile of four. * * GAMES IN PROGRESS STILL RESUME, and the reason is the narrow one this package always asks * (README § Design notes): a save is a seed plus the moves played, replayed through the CURRENT * rules, so what strands one is a change that makes a once-legal move ILLEGAL. Nothing here does. * `check()` is untouched; a draw is a draw whatever lies on top of the deck. What differs is the * Salvage Yard's depth, which was double-counting, and which cards a reshuffle returns — and a * reshuffle is effectively unreachable in ordinary play: eight games driven to 4000 moves across * eight seeds produced ZERO. A game that had somehow reshuffled would replay to a different deck * order rather than refusing to replay, which is worth knowing and is not the same as breaking. * * v0.8.0.9 TOUCHES THE ENGINE FOR SPEED, AND PROVED IT CHANGED NOTHING. `git diff v0.8.0.8..v0.8.0.9 * -- src/engine/` adds a route cache scoped to one unchanged position (`withRouteCache`), splits * `applyIntent` into `prepareIntent` + `commitEvents`, trims allocation in `exploreMoves`, adds * `legalSwitchingActions`, and exports `badlyMadeUp`, `protoCard` and `extendLimitsIfNeeded` for the * bot. No predicate's answer changed: every event and intent of 32 seeded games hashed identical before * and after each step, and the game's `test/route-cache.test.ts` pins the split. Everything else is the * developer bot, whose past moves are already intents in a save. Games in progress resume. * * v0.8.0.10 IS THE FIRST BUMP THAT BREAKS RESUMES, AND IT DOES SO DELIBERATELY. It corrects WHEN §8.1 * asks the Superintendent to rule (Gitea#26): a train on a card BEHIND the one departing is nobody's * concern, and was being counted. A save is the seed and the moves replayed through the CURRENT rules, * so a game holding a `mainline.clearance` the rules no longer ask for is refused at that move with * NO_PENDING_DECISION — `tryResumeSession` reports which move, and `index.ts` logs the refusal and * leaves the file alone. Measured before shipping: 40 four-seat co-op games recorded by bots under * v0.8.0.9 and replayed under this release — 28 stop, usually 5-40% of the way in. Jesse's call * (2026-09-15): no games in progress need keeping, so this ships without a per-game switch for the old * rule, and the release notes say so in all five locales. A game worth finishing can be finished by * putting v0.8.0.9 back — the save file is untouched. * * The rest of the release is the screen and the log — who the board says it is waiting on, what the * history says and when it says it, a flash on a realigned Mainline card — plus one new read-only * route (`GET /api/save?token=…`) that hands a seated player their own save. None of it is on disk. * * A NEW STATE FIELD CANNOT STRAND A SAVE HERE, which is the property worth naming rather than * re-deriving: a `SavedGame` holds `{ seed, config, history }` and NOT a serialised `GameState`. * Resuming calls `createGame` — which fills the new field from `setup.ts` — and then replays the * intents through the current rules, so the field is populated on the way past. There is no old * shape on disk for a new field to be missing from. * * THE ONE THING THAT WOULD HAVE NEEDED CARE, had it been built the way the plan first described: * the display-step collector runs inside `submit()`, and the resume path (`fromMultiplayerSave`) * deliberately does not go through `submit()`. So replaying a saved game emits no steps and burns * no sequence numbers. A test in the game pins that, because it is a property rather than an * accident. * * v0.8.0.11 RESUMES, AND IT WAS MEASURED THE SAME NARROW WAY — which matters more than usual * here, because the release immediately before it is the one that did NOT. * `git diff v0.8.0.10..v0.8.0.11 -- src/engine/` is three files and nineteen insertions: * `trainArrived` gains an `owner` field (`events.ts`), the one line in `advance.ts` that fills * it from `playerAtSeat`, and a Mainline card's printed description losing a sentence about a * speed that is not drawn on the card (`content.ts`). `check()` and `legal.ts` are untouched and * no predicate was edited, so nothing once legal became illegal. Events are not persisted — a * `SavedGame` is `{ seed, config, history }` and the events are derived by replaying it — so * widening one of them cannot invalidate anything on disk, exactly as the v0.8.0.1 entry above * argues for four of them. * * THE REST OF THE RELEASE IS THE SCREEN: a Pause control for the catch-up queue, a one-render * look at another player's Office Area, the turn chart following the step being replayed rather * than the live game, "still needs" on the make-up panel, the amber on an addable yard chip, and * save filenames carrying the join code. None of it is a rule and none of it is on disk. * * v0.8.0.12 TOUCHES THE ENGINE NOT AT ALL, which is the shortest entry this comment has ever * had a right to: `git diff v0.8.0.11..v0.8.0.12 -- src/engine/` is EMPTY. The release is the * server's HTTP surface, the browser client and the docs — a seat recovery flow (Gitea#33) for * a player whose browser has lost the token that is their only identity. * * WHAT IT ADDS, AND WHY NONE OF IT IS A MIGRATION: two routes (`POST /api/games//claim` * behind the admin secret, `POST /api/claim` for the player following the link), a `?claim=` * entry path in the client, and `seatedPlayers` on the administrative game listing. The codes * themselves live in memory and are deliberately NOT persisted — minted on demand and spent * within minutes, so a restart dropping them is the right failure rather than a loss. Nothing * new is written to the volume, no predicate was edited, and `check()` and `legal.ts` are * untouched, so no once-legal move became illegal. Games in progress resume, and the release * notes say so in all five locales. */ up: async ({ effects }) => {}, down: IMPOSSIBLE, }, })