0.8.0.12:0 — bundle Station Master v0.8.0.12, and a Restore a Seat action

A player who has lost their browser storage can be put back in their seat
(Gitea#33 in the game repo). The session token is the only identity the game
has and it lives in one browser's localStorage, scoped to the origin joined at,
so a cleared profile, a private window or a different browser locks a player
out of a game that is still running with their session still on disk. Seen at a
real table: of two humans in one game, the host reloaded straight back in and
the joiner met an empty lobby.

New action, restore-seat: pick a seat from a dropdown of the players actually
holding a session — bots never appear, since seatedPlayers is read from the
server's session map rather than guessed from display names — and get back a
link built from this interface's own address. serverApi gained mintClaim; the
dictionary gained 57-64 in all five locales.

THE LINK CARRIES A CODE, NOT THE TOKEN. lobby-and-sessions.md §1 says to keep
the token out of URLs so it is not shoulder-surfed or pasted into a chat, and a
recovery link is exactly what gets pasted into a chat. The code is single-use,
expires in 30 minutes, and the page trades it for the real token over a POST.
Minting is admin-gated because deciding that somebody has lost a seat is a
judgement no route can make safely; spending needs no secret, because the
player following the link holds none.

GAMES IN PROGRESS RESUME NORMALLY, measured rather than assumed:
`git diff v0.8.0.11..v0.8.0.12 -- src/engine/` is EMPTY. The release is the
server's HTTP surface, the client and the docs. The codes live in memory and
are deliberately not persisted, so nothing new reaches the volume and there is
nothing to migrate. README, instructions.md and the release notes in all five
locales say so.

NOT YET EXERCISED END TO END: the two routes and the action are typechecked and
the claim store is unit-tested, but nothing has run them against a live server.
This build is what makes that possible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nnuCv8UodHucFfx3LWEoX
This commit is contained in:
Jesse
2026-09-16 20:21:15 -04:00
co-authored by Claude Opus 5
parent da0e5d49d6
commit a70337cf8b
9 changed files with 341 additions and 121 deletions
+2
View File
@@ -2,8 +2,10 @@ import { sdk } from '../sdk'
import { gamesInProgress } from './gamesInProgress'
import { getJoinSecret } from './getJoinSecret'
import { manageGame } from './manageGame'
import { restoreSeat } from './restoreSeat'
export const actions = sdk.Actions.of()
.addAction(getJoinSecret)
.addAction(gamesInProgress)
.addAction(manageGame)
.addAction(restoreSeat)
+124
View File
@@ -0,0 +1,124 @@
import { i18n } from '../i18n'
import { sdk } from '../sdk'
import { listGames, mintClaim } from '../serverApi'
import { uiPort } from '../utils'
const { InputSpec, Value } = sdk
/**
* PUT A PLAYER BACK IN THEIR SEAT — Gitea#33.
*
* A session token is the only identity the game has, and it lives in exactly one place the player
* controls: their browser's local storage, scoped to the address they joined at. Lose it — another
* browser, a cleared profile, a private window — and the seat is unreachable, because nothing else on
* the server will accept a claim to it. Seen at a real table: the joining player came back to an
* empty lobby while their token sat intact in the server's own files, and the only way in was an
* administrator reading it off the data volume and the player pasting it into a devtools console.
*
* THE LINK CARRIES A CODE, NOT THE TOKEN. The game's `lobby-and-sessions.md` §1 says to keep the
* token out of URLs so it is not shoulder-surfed or pasted into a chat — and a recovery link is
* exactly what gets pasted into a chat. The code is single-use and expires in thirty minutes; the
* page trades it for the real token over a POST as it loads.
*
* ADMINISTRATIVE ON PURPOSE. Deciding that a particular person has lost a particular seat is a
* judgement, and there is no safe way to automate it — anyone able to mint their own code could take
* any chair at the table.
*/
export const restoreSeat = sdk.Action.withInput(
'restore-seat',
async () => ({
name: i18n('Restore a Seat'),
description: i18n(
'Create a one-time link that puts a player back into their seat',
),
warning: i18n(
'A restore link works once and expires in 30 minutes. Whoever opens it takes that seat — send it to the right person, not to a public channel.',
),
// Nothing about a live game is readable from disk, so the server has to be up to answer which
// seats exist — the same reason `manageGame` declares this.
allowedStatuses: 'only-running',
group: null,
visibility: 'enabled',
}),
async ({ effects }) => {
const games = await listGames(effects)
const values: Record<string, string> = {}
for (const g of games) {
// A lobby has no seats to restore: nobody has a game to be put back into yet.
if (g.state !== 'running') continue
for (const player of g.seatedPlayers ?? []) {
const name = g.playerNames[player] ?? `${i18n('Seat')} ${player + 1}`
values[`${g.gameId}:${player}`] = `${g.gameCode ?? g.gameId} — ${name}`
}
}
// A select needs a default from its own options, and a server with no seated players has none.
if (Object.keys(values).length === 0)
values['none'] = i18n('No seated players on this server')
return InputSpec.of({
seat: Value.select({
name: i18n('Seat'),
description: i18n('Which player to restore'),
default: Object.keys(values)[0]!,
values,
}),
})
},
// Nothing to pre-fill: which seat you mean is the whole question being asked.
async () => null,
async ({ effects, input }) => {
if (input.seat === 'none') {
return {
version: '1',
title: i18n('Restore a Seat'),
message: i18n('No seated players on this server'),
result: null,
}
}
const [gameId, seatText] = input.seat.split(':')
const { code } = await mintClaim(effects, gameId!, Number(seatText))
/**
* The link is built from the interface's OWN address, so it is clickable rather than a code the
* administrator has to graft onto a URL by hand — which was the whole complaint about the
* devtools workaround this replaces.
*
* Public addresses first, then any other network-reachable one: a player who joined over the
* internet cannot use a `.local` name, and preferring the address the table actually uses is the
* difference between a link that works and one that needs explaining. `once()` rather than
* `const()` — an action wants today's answer, not a subscription that re-runs it.
*/
const host = await sdk.host.getOwn(effects, 'ui-multi').once()
const addresses = host?.bindings[uiPort]?.interfaces['ui']?.addressInfo
const urls = addresses
? [...addresses.public.format('urlstring'), ...addresses.nonLocal.format('urlstring')]
: []
const base = urls[0]
// No address to hand? Return the path anyway: the administrator knows the address they use, and
// a code with nowhere to go is still better than no code at all.
const link = `${(base ?? '').replace(/\/+$/, '')}/play.html?claim=${encodeURIComponent(code)}`
return {
version: '1',
title: i18n('Restore Link'),
message: i18n(
'Send this to the player and have them open it in their own browser. It works once, expires in 30 minutes, and puts them back in their seat. It does not contain their session token.',
),
result: {
type: 'single',
name: i18n('Restore Link'),
description: null,
value: link,
// Not masked: it is meant to be read out, copied and sent, and it is worthless once spent.
masked: false,
copyable: true,
qr: true,
},
}
},
)
+9
View File
@@ -64,6 +64,15 @@ const dict = {
'the server answered': 54,
'Join Secret': 55,
"Share this with anyone you want to be able to create or join a game on this server. It doesn't identify a person or a seat, just who's allowed at the lobby door. Running this action again generates a new one and restarts the server with it; anyone already seated in a game keeps playing, but the old secret stops working for new games and joins.": 56,
// actions/restoreSeat.ts
'Restore a Seat': 57,
'Create a one-time link that puts a player back into their seat': 58,
'A restore link works once and expires in 30 minutes. Whoever opens it takes that seat — send it to the right person, not to a public channel.': 59,
Seat: 60,
'Which player to restore': 61,
'No seated players on this server': 62,
'Restore Link': 63,
'Send this to the player and have them open it in their own browser. It works once, expires in 30 minutes, and puts them back in their seat. It does not contain their session token.': 64,
} as const
/**
+32
View File
@@ -59,6 +59,14 @@ export default {
54: 'el servidor respondi\u00f3',
55: 'Secreto de acceso',
56: 'Comp\u00e1rtelo con quien quieras que pueda crear una partida o unirse a una en este servidor. No identifica a una persona ni un asiento, solo a qui\u00e9n se le permite entrar. Ejecutar esta acci\u00f3n de nuevo genera otro y reinicia el servidor con \u00e9l; quien ya est\u00e9 sentado en una partida sigue jugando, pero el secreto antiguo deja de servir para nuevas partidas y uniones.',
57: 'Restaurar un asiento',
58: 'Crea un enlace de un solo uso que devuelve a un jugador a su asiento',
59: 'Un enlace de restauraci\u00f3n funciona una sola vez y caduca en 30 minutos. Quien lo abra ocupar\u00e1 ese asiento: env\u00edalo a la persona correcta y no a un canal p\u00fablico.',
60: 'Asiento',
61: 'Qu\u00e9 jugador restaurar',
62: 'No hay jugadores sentados en este servidor',
63: 'Enlace de restauraci\u00f3n',
64: 'Env\u00edaselo al jugador y que lo abra en su propio navegador. Funciona una sola vez, caduca en 30 minutos y lo devuelve directamente a su asiento. No contiene su token de sesi\u00f3n.',
},
de_DE: {
0: 'Starte Station Master!',
@@ -118,6 +126,14 @@ export default {
54: 'der Server antwortete mit',
55: 'Beitrittsgeheimnis',
56: 'Weitergeben an alle, die auf diesem Server ein Spiel erstellen oder einem beitreten sollen. Es benennt weder eine Person noch einen Platz, nur wer an der T\u00fcr eingelassen wird. Diese Aktion erneut auszuf\u00fchren erzeugt ein neues und startet den Server damit neu; wer bereits an einem Spiel sitzt, spielt weiter, doch das alte Geheimnis funktioniert f\u00fcr neue Spiele und Beitritte nicht mehr.',
57: 'Einen Platz wiederherstellen',
58: 'Erzeugt einen einmalig g\u00fcltigen Link, der einen Spieler zur\u00fcck auf seinen Platz bringt',
59: 'Ein Wiederherstellungslink funktioniert genau einmal und verf\u00e4llt nach 30 Minuten. Wer ihn \u00f6ffnet, nimmt diesen Platz ein \u2014 schicken Sie ihn der richtigen Person und nicht in einen \u00f6ffentlichen Kanal.',
60: 'Platz',
61: 'Welcher Spieler wiederhergestellt wird',
62: 'Keine sitzenden Spieler auf diesem Server',
63: 'Wiederherstellungslink',
64: 'Schicken Sie dies dem Spieler, der es in seinem eigenen Browser \u00f6ffnet. Es funktioniert einmal, verf\u00e4llt nach 30 Minuten und setzt ihn direkt wieder auf seinen Platz. Es enth\u00e4lt nicht sein Sitzungs-Token.',
},
pl_PL: {
0: 'Uruchamianie Station Master!',
@@ -177,6 +193,14 @@ export default {
54: 'serwer odpowiedzia\u0142',
55: 'Sekret do\u0142\u0105czania',
56: 'Udost\u0119pnij je ka\u017cdemu, kto ma m\u00f3c utworzy\u0107 gr\u0119 na tym serwerze lub do niej do\u0142\u0105czy\u0107. Nie wskazuje osoby ani miejsca \u2014 tylko to, kogo wpuszcza si\u0119 do poczekalni. Ponowne uruchomienie tej akcji tworzy nowy sekret i restartuje z nim serwer; kto ju\u017c siedzi przy grze, gra dalej, ale stary sekret przestaje dzia\u0142a\u0107 przy nowych grach i do\u0142\u0105czeniach.',
57: 'Przywr\u00f3\u0107 miejsce',
58: 'Tworzy jednorazowy link, kt\u00f3ry przywraca gracza na jego miejsce',
59: 'Link przywracaj\u0105cy dzia\u0142a tylko raz i wygasa po 30 minutach. Kto go otworzy, zajmie to miejsce \u2014 wy\u015blij go w\u0142a\u015bciwej osobie, a nie na kana\u0142 publiczny.',
60: 'Miejsce',
61: 'Kt\u00f3rego gracza przywr\u00f3ci\u0107',
62: 'Brak graczy przy stole na tym serwerze',
63: 'Link przywracaj\u0105cy',
64: 'Wy\u015blij to graczowi i popro\u015b, by otworzy\u0142 link we w\u0142asnej przegl\u0105darce. Dzia\u0142a raz, wygasa po 30 minutach i przywraca go prosto na jego miejsce. Nie zawiera jego tokenu sesji.',
},
fr_FR: {
0: 'Démarrage de Station Master !',
@@ -236,5 +260,13 @@ export default {
54: 'le serveur a r\u00e9pondu',
55: 'Secret de connexion',
56: "Partagez-le avec toute personne qui doit pouvoir cr\u00e9er une partie sur ce serveur ou en rejoindre une. Il n'identifie ni une personne ni une place, seulement qui est admis \u00e0 la porte. Relancer cette action en g\u00e9n\u00e8re un nouveau et red\u00e9marre le serveur avec ; qui est d\u00e9j\u00e0 assis \u00e0 une partie continue de jouer, mais l'ancien secret cesse de fonctionner pour les nouvelles parties et les arriv\u00e9es.",
57: 'Restaurer une place',
58: 'Cr\u00e9e un lien \u00e0 usage unique qui remet un joueur \u00e0 sa place',
59: "Un lien de restauration fonctionne une seule fois et expire au bout de 30 minutes. Quiconque l'ouvre prend cette place : envoyez-le \u00e0 la bonne personne, et non sur un canal public.",
60: 'Place',
61: 'Quel joueur restaurer',
62: 'Aucun joueur assis sur ce serveur',
63: 'Lien de restauration',
64: "Envoyez-le au joueur pour qu'il l'ouvre dans son propre navigateur. Il fonctionne une fois, expire au bout de 30 minutes et le remet directement \u00e0 sa place. Il ne contient pas son jeton de session.",
},
} satisfies Record<string, LangDict>
+36
View File
@@ -21,6 +21,11 @@ export type GameRow = {
playerCount: number
playerNames: string[]
createdAt: number
/**
* The seats a HUMAN holds a session token for — absent on a lobby. Bots never appear: the server
* reads this from its session map rather than guessing from `playerNames` (Gitea#33).
*/
seatedPlayers?: number[]
// Absent on a lobby — it has no game to be part-way through.
lastMoveAt?: number
status?: 'active' | 'finished'
@@ -96,3 +101,34 @@ export async function deleteGame(
})
return (await res.json()) as { gameCode: string | null; save: unknown }
}
/**
* Mint a one-time code that puts one player back into their seat — Gitea#33.
*
* A session token is the only identity the game has, and it lives in one browser's local storage.
* Lose it and the seat is unreachable: nothing else on the server will accept a claim to it. So the
* administrator mints a code for a named seat, and the player opens a link carrying it.
*
* THE CODE IS NOT THE TOKEN. The game's own `lobby-and-sessions.md` §1 says to keep the token out of
* URLs, and this code is going into one — so it is short-lived, single-use, and exchanged for the
* real token by the page over a POST. `Content-Type` is set here because `adminFetch` spreads these
* headers alongside the admin secret rather than assuming a body.
*/
export async function mintClaim(
effects: T.Effects,
gameId: string,
player: number,
): Promise<{ code: string; expiresAt: number; player: number; displayName: string; gameCode: string | null }> {
const res = await adminFetch(effects, `/api/games/${gameId}/claim`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ player }),
})
return (await res.json()) as {
code: string
expiresAt: number
player: number
displayName: string
gameCode: string | null
}
}
+86 -106
View File
@@ -10,118 +10,84 @@ export const current = VersionInfo.of({
// — 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.11:0',
version: '0.8.0.12:0',
releaseNotes: {
en_US:
'Bundles Station Master v0.8.0.11 — sixteen fixes from the second multiplayer playtest. ' +
'**Games already running on this server resume normally.** No rule changed: the whole engine ' +
'difference is one field added to one event and one card description reworded, so nothing ' +
'that was legal has become illegal. **Making up a train is far clearer.** The panel now says ' +
'what the train STILL needs rather than only what its card calls for, says that you add ONE ' +
'car before the round passes to the next player, marks the train being loaded on the Division ' +
'map, and highlights the cars you may add in the same amber every other button uses — the ' +
'cars in the Division Yard have always been the buttons, but they did not look like it. **A ' +
'train’s arrival now says whose Office it reached**, and no longer tells every player they ' +
'can work it when it is standing in somebody else’s district. **While your board catches up** ' +
'on other players’ moves you can now PAUSE it and look as long as you like — a paused move ' +
'keeps the time it had left — and you can open any other player’s Office Area for a look. The ' +
'Day, Stage and phase on screen now match the move you are watching instead of running ahead ' +
'of it, so being five moves behind looks the same everywhere on the page. When nobody is ' +
'being waited on, the screen says what the Division is doing — “the Division is moving ' +
'trains” — instead of “waiting on nobody”. **Saved games download named after the game**, ' +
'with the join code, Day and Stage in the filename. Also: the LIMITS sign is printed below ' +
'its card instead of through the middle of the word, the dividing line on a Mainline card can ' +
'actually be seen, a Restaurant or Hotel next to a Whistle Post says it is dormant until the ' +
'Office is upgraded rather than appearing to do nothing, only the one card that reads ' +
'FAST/SLOW mentions it, and the version number appears once in the page header instead of ' +
'twice.',
'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.11 — dieciséis correcciones de la segunda partida de prueba ' +
'multijugador. **Las partidas en curso en este servidor se reanudan con normalidad**: no ha ' +
'cambiado ninguna regla. **Formar un tren es mucho más claro.** El panel indica ahora lo que ' +
'al tren le FALTA, y no solo lo que pide su carta; explica que añades UN vagón y el turno ' +
'pasa al siguiente jugador; marca en el mapa de la División el tren que se está formando; y ' +
'resalta los vagones que puedes añadir con el mismo ámbar que los demás botones — los vagones ' +
'del Patio de División siempre fueron los botones, pero no lo parecían. **La llegada de un ' +
'tren dice a qué Oficina llegó**, y ya no le dice a todo el mundo que puede trabajarlo cuando ' +
'está en el distrito de otro jugador. **Mientras tu tablero se pone al día** con los ' +
'movimientos de otros, ahora puedes PAUSARLO el tiempo que quieras — el movimiento pausado ' +
'conserva el tiempo que le quedaba — y abrir el Área de Oficina de cualquier otro jugador ' +
'para verla. El Día, la Etapa y la fase en pantalla coinciden con el movimiento que estás ' +
'viendo, así que ir cinco movimientos por detrás se ve igual en toda la página. Cuando no se ' +
'espera a nadie, la pantalla dice qué está haciendo la División — «la División está moviendo ' +
'trenes» — en lugar de «esperando a nadie». **Las partidas guardadas se descargan con el ' +
'nombre de la partida**: el código de acceso, el Día y la Etapa en el nombre del archivo. ' +
'Además: el letrero LIMITS se imprime debajo de su carta y no atravesando la palabra, la ' +
'línea divisoria de una carta de Línea Principal se ve de verdad, un Restaurante u Hotel ' +
'junto a un Whistle Post indica que está inactivo hasta que se mejore la Oficina, solo la ' +
'única carta que lee RÁPIDO/LENTO lo menciona, y el número de versión aparece una vez en la ' +
'cabecera en lugar de dos.',
'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.11 — sechzehn Korrekturen aus der zweiten Mehrspieler-' +
'Testpartie. **Laufende Spiele auf diesem Server werden normal fortgesetzt**: keine Regel ' +
'wurde geändert. **Das Zusammenstellen eines Zuges ist viel klarer.** Das Panel nennt jetzt, ' +
'was dem Zug noch FEHLT, statt nur, was seine Karte verlangt; es sagt, dass Sie EINEN Wagen ' +
'anhängen und die Runde dann zum nächsten Spieler weitergeht; es markiert den Zug, der gerade ' +
'beladen wird, auf der Divisionskarte; und die Wagen, die Sie anhängen dürfen, leuchten im ' +
'selben Bernstein wie jede andere Schaltfläche — die Wagen im Divisionsbahnhof waren immer ' +
'schon die Schaltflächen, sahen aber nicht danach aus. **Die Ankunft eines Zuges nennt jetzt, ' +
'wessen Office er erreicht hat**, und sagt nicht mehr allen, sie könnten ihn bearbeiten, wenn ' +
'er im Bezirk eines anderen steht. **Während Ihr Spielfeld die Züge der anderen nachholt**, ' +
'können Sie es nun PAUSIEREN, so lange Sie wollen — der pausierte Zug behält die Zeit, die ' +
'ihm blieb — und das Office-Gebiet jedes anderen Spielers ansehen. Tag, Etappe und Phase auf ' +
'dem Bildschirm stimmen mit dem Zug überein, den Sie gerade sehen; fünf Züge im Rückstand zu ' +
'sein sieht überall auf der Seite gleich aus. Wartet das Spiel auf niemanden, zeigt der ' +
'Bildschirm, was die Division tut — „die Division bewegt Züge“ — statt „wartet auf niemanden“. ' +
'**Gespeicherte Spiele werden nach dem Spiel benannt**: Beitrittscode, Tag und Etappe im ' +
'Dateinamen. Außerdem: Das LIMITS-Schild steht unter seiner Karte statt mitten im Wort, die ' +
'Trennlinie auf einer Hauptstreckenkarte ist tatsächlich zu sehen, ein Restaurant oder Hotel ' +
'neben einem Whistle Post meldet sich als ruhend, bis das Office ausgebaut wird, nur die eine ' +
'Karte, die SCHNELL/LANGSAM liest, erwähnt es, und die Versionsnummer erscheint einmal in der ' +
'Kopfzeile statt zweimal.',
'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.11 — szesnaście poprawek z drugiej testowej partii ' +
'wieloosobowej. **Gry w toku na tym serwerze wznawiają się normalnie**: żadna zasada się nie ' +
'zmieniła. **Kompletowanie pociągu jest dużo jaśniejsze.** Panel pokazuje teraz, czego ' +
'pociągowi jeszcze BRAKUJE, a nie tylko czego wymaga jego karta; mówi, że dodajesz JEDEN ' +
'wagon, po czym kolejka przechodzi do następnego gracza; zaznacza kompletowany pociąg na ' +
'mapie Dywizji; a wagony, które możesz dodać, świecą tym samym bursztynem co pozostałe ' +
'przyciski — wagony na Stacji Dywizyjnej zawsze były przyciskami, ale tak nie wyglądały. ' +
'**Przyjazd pociągu podaje, czyje Biuro osiągnął**, i nie mówi już wszystkim, że mogą go ' +
'obsłużyć, gdy stoi w dzielnicy innego gracza. **Gdy twoja plansza nadrabia ruchy innych**, ' +
'możesz ją teraz ZATRZYMAĆ na dowolnie długo — zatrzymany ruch zachowuje pozostały mu czas — ' +
'i obejrzeć Biuro dowolnego innego gracza. Dzień, Etap i faza na ekranie zgadzają się z ' +
'ruchem, który oglądasz, więc pięć ruchów w tyle wygląda tak samo na całej stronie. Gdy gra ' +
'na nikogo nie czeka, ekran mówi, co robi Dywizja — „Dywizja przesuwa pociągi” — zamiast ' +
'„czeka na nikogo”. **Zapisane gry pobierają się nazwane po grze**: kod dołączenia, Dzień i ' +
'Etap w nazwie pliku. Ponadto: napis LIMITS jest drukowany pod kartą, a nie przez środek ' +
'słowa, linia podziału na karcie Magistrali jest wreszcie widoczna, Restauracja lub Hotel ' +
'obok Whistle Post informuje, że jest uśpiony do czasu rozbudowy Biura, tylko ta jedna karta, ' +
'która czyta SZYBKO/WOLNO, o tym wspomina, a numer wersji pojawia się w nagłówku raz zamiast ' +
'dwa razy.',
'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.11 — seize corrections issues de la deuxième partie de test ' +
'multijoueurs. **Les parties en cours sur ce serveur reprennent normalement** : aucune règle ' +
'n’a changé. **Composer un train est bien plus clair.** Le panneau indique désormais ce qu’il ' +
'MANQUE au train, et non plus seulement ce que sa carte demande ; il précise que vous ajoutez ' +
'UN wagon avant que le tour ne passe au joueur suivant ; il marque sur la carte de la ' +
'Division le train en cours de composition ; et les wagons que vous pouvez ajouter ' +
's’allument dans le même ambre que tous les autres boutons — les wagons du Dépôt de Division ' +
'ont toujours été les boutons, mais n’en avaient pas l’air. **L’arrivée d’un train indique à ' +
'quel Office il est parvenu**, et ne dit plus à tout le monde qu’il peut le manœuvrer alors ' +
'qu’il stationne dans le district d’un autre. **Pendant que votre plateau rattrape** les ' +
'coups des autres, vous pouvez maintenant le METTRE EN PAUSE aussi longtemps que vous voulez ' +
'— le coup mis en pause conserve le temps qu’il lui restait — et ouvrir l’Aire de Bureau de ' +
'n’importe quel autre joueur. Le Jour, l’Étape et la phase affichés correspondent au coup que ' +
'vous regardez : avoir cinq coups de retard se voit de la même façon partout sur la page. ' +
'Quand la partie n’attend personne, l’écran dit ce que fait la Division — « la Division fait ' +
'circuler les trains » — au lieu de « en attente de personne ». **Les parties sauvegardées ' +
'portent le nom de la partie** : code d’accès, Jour et Étape dans le nom du fichier. ' +
'Également : le panneau LIMITS est imprimé sous sa carte au lieu de traverser le mot, la ' +
'ligne de séparation d’une carte de Ligne principale est enfin visible, un Restaurant ou un ' +
'Hôtel à côté d’un Whistle Post indique qu’il est en sommeil jusqu’à l’amélioration de ' +
'l’Office, seule la carte qui lit RAPIDE/LENT le mentionne, et le numéro de version apparaît ' +
'une fois dans l’en-tête au lieu de deux.',
'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: {
/**
@@ -233,6 +199,20 @@ export const current = VersionInfo.of({
* 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/<id>/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,