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:
@@ -13,19 +13,18 @@ Station Master is a railroad operations board game with an authoritative multipl
|
|||||||
solitaire needs no server and is not what this package is for. This package runs that server —
|
solitaire needs no server and is not what this package is for. This package runs that server —
|
||||||
the browser client, the lobby, and the intent/SSE API — as a single StartOS service.
|
the browser client, the lobby, and the intent/SSE API — as a single StartOS service.
|
||||||
|
|
||||||
**Bundled version: 0.8.0.11.** Sixteen fixes from the second multiplayer playtest. Making up a train
|
**Bundled version: 0.8.0.12.** A player who has lost their browser storage can be put back in their
|
||||||
now says what the consist STILL needs, that a player adds one car before the round passes on, marks the
|
seat, through the new **Restore a Seat** action — see Actions below. The session token is the only
|
||||||
train being loaded on the Division map, and gives an addable car in the yard the page's action colour. A
|
identity the game has and it lives in one browser's `localStorage`, so a cleared profile, a private
|
||||||
train's arrival names whose Office it reached. A player watching the board catch up can pause it, look at
|
window or a different browser locks a player out of a game that is still running with their session
|
||||||
another player's Office Area, and sees one consistent Day/Stage/phase rather than a chart running ahead of
|
still on disk. v0.8.0.11 before it was sixteen playtest fixes, including the New Train panel saying what
|
||||||
the board. Saves download named after the game. v0.8.0.10 before it corrected when the Superintendent is
|
a consist still needs; v0.8.0.10 corrected when the Superintendent is asked to rule.
|
||||||
asked to rule; v0.8.0.9 rebuilt the bot and made the engine about three times faster.
|
|
||||||
|
|
||||||
**Games in progress resume normally on this one**, and it was checked the narrow way rather than assumed:
|
**Games in progress resume normally on this one**, and it was checked the narrow way rather than
|
||||||
`git diff v0.8.0.10..v0.8.0.11 -- src/engine/` is three files, nineteen insertions — an `owner` field on
|
assumed: `git diff v0.8.0.11..v0.8.0.12 -- src/engine/` is **empty**. The release is the server's HTTP
|
||||||
the `trainArrived` event, the one line that fills it, and a reworded card description. No predicate
|
surface, the browser client and the docs — no predicate was edited, so no once-legal move became
|
||||||
changed its answer, so no once-legal move became illegal. (0.8.0.10 was the exception: it corrected a
|
illegal. (0.8.0.10 was the exception: it corrected a ruling and deliberately stranded games holding
|
||||||
ruling and deliberately stranded games holding one. That warning does not apply here.)
|
one. That warning does not apply here.)
|
||||||
|
|
||||||
The paragraphs below were written for the 0.7.9.8 bump and still describe how a bump is checked.
|
The paragraphs below were written for the 0.7.9.8 bump and still describe how a bump is checked.
|
||||||
|
|
||||||
@@ -285,8 +284,8 @@ progress on every update, including updates that changed only how the board is d
|
|||||||
|
|
||||||
## Actions
|
## Actions
|
||||||
|
|
||||||
Two of the three read or change the games on the server, and both are `only-running`: what they
|
Three of the four read or change the games on the server, and all three are `only-running`: what
|
||||||
report exists only inside the live server process. A save is a seed plus a list of moves, so
|
they report exists only inside the live server process. A save is a seed plus a list of moves, so
|
||||||
"whose turn is it" is answerable only by replaying the game through the engine — which lives in
|
"whose turn is it" is answerable only by replaying the game through the engine — which lives in
|
||||||
the game repo, not in this package. The server has already done that work and is asked for the
|
the game repo, not in this package. The server has already done that work and is asked for the
|
||||||
answer.
|
answer.
|
||||||
@@ -313,6 +312,19 @@ answer.
|
|||||||
worth replaying. This is the only way a game ends other than being played to a finish: an
|
worth replaying. This is the only way a game ends other than being played to a finish: an
|
||||||
abandoned game otherwise stays active and is resumed on every restart indefinitely.
|
abandoned game otherwise stays active and is resumed on every restart indefinitely.
|
||||||
|
|
||||||
|
- **Restore a Seat** (`restore-seat`) — pick a seat from a dropdown of the players actually holding
|
||||||
|
a session, and get back a one-time link that puts that player into it. **This is the answer to a
|
||||||
|
lost seat token** (Gitea#33 in the game repo): the token is the only identity the game has and it
|
||||||
|
lives in one browser's `localStorage`, so a cleared profile, a private window or a different
|
||||||
|
browser leaves a player locked out of a game that is still running with their session still on
|
||||||
|
disk. The link carries a **code, never the token** — single-use, expires in 30 minutes — which the
|
||||||
|
page trades for the real token over a POST as it loads (`lobby-and-sessions.md` §1: keep the token
|
||||||
|
out of URLs). Anyone who opens the link takes that seat, so it is sent to one person and not to a
|
||||||
|
public channel. Minting is administrative because deciding that somebody has lost a seat is a
|
||||||
|
judgement; spending needs no secret, because the player following the link holds none. The
|
||||||
|
dropdown lists only seats a human holds a token for — bots never appear, read from the server's
|
||||||
|
session map rather than guessed from player names.
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
- **Get the join secret to share with players** — raised on install, severity `critical`. Points
|
- **Get the join secret to share with players** — raised on install, severity `critical`. Points
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ watches the table — there is nothing else to open.
|
|||||||
Solitaire runs entirely in your own browser and needs nothing from the server, so it works here
|
Solitaire runs entirely in your own browser and needs nothing from the server, so it works here
|
||||||
exactly as it does on the public Station Master site.
|
exactly as it does on the public Station Master site.
|
||||||
|
|
||||||
|
**What changed in 0.8.0.12. Games already running on this server are not affected** — they resume
|
||||||
|
normally; no rule changed.
|
||||||
|
|
||||||
|
**A player who cannot get back into their game can be let back in.** The only proof of who you are is
|
||||||
|
kept by your own browser, so clearing your browsing data, coming back in a private window, or sitting
|
||||||
|
down at a different browser makes you look like a stranger — an empty lobby, no game code, no name —
|
||||||
|
even though the game is still running and the seat is still yours. The new **Restore a Seat** action
|
||||||
|
hands you a link for that player; opening it in their own browser puts them straight back where they
|
||||||
|
were. It works once, expires after 30 minutes, and whoever opens it takes that seat, so send it to that
|
||||||
|
person rather than posting it somewhere public. See **Restore a Seat** below.
|
||||||
|
|
||||||
**What changed in 0.8.0.11. Games already running on this server are not affected** — they resume
|
**What changed in 0.8.0.11. Games already running on this server are not affected** — they resume
|
||||||
normally; no rule changed.
|
normally; no rule changed.
|
||||||
|
|
||||||
@@ -284,6 +295,20 @@ a game that will never be finished — otherwise it sits there, and comes back e
|
|||||||
service restarts. It cannot be undone, so the game's save is handed back to you when it happens:
|
service restarts. It cannot be undone, so the game's save is handed back to you when it happens:
|
||||||
that text is the only copy left, so keep it if the game was worth watching.
|
that text is the only copy left, so keep it if the game was worth watching.
|
||||||
|
|
||||||
|
### Restore a Seat
|
||||||
|
|
||||||
|
Use this when a player cannot get back into a game they are already in — they open the page and find
|
||||||
|
an empty lobby, with no game code and no name filled in, as though they had never joined.
|
||||||
|
|
||||||
|
That happens because the only proof of who you are is kept by your own browser. Clear your browsing
|
||||||
|
data, come back in a private window, or sit down at a different browser, and that proof is gone —
|
||||||
|
even though the game is still running on the server and the seat is still yours.
|
||||||
|
|
||||||
|
Pick the player from the list and this hands you a link. Send it to them, and opening it in their own
|
||||||
|
browser puts them straight back in their seat. **The link works once and expires after 30 minutes**,
|
||||||
|
and whoever opens it takes that seat — so send it to that person directly rather than posting it
|
||||||
|
somewhere public. If it expires before they get to it, just run the action again.
|
||||||
|
|
||||||
> **Games in progress normally survive an update of this service.** When the update changes the
|
> **Games in progress normally survive an update of this service.** When the update changes the
|
||||||
> rules of the game itself, a half-played game cannot be resumed under rules it was not played by,
|
> rules of the game itself, a half-played game cannot be resumed under rules it was not played by,
|
||||||
> and that game is set aside rather than continued — you would see it disappear from the lobby.
|
> and that game is set aside rather than continued — you would see it disappear from the lobby.
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { sdk } from '../sdk'
|
|||||||
import { gamesInProgress } from './gamesInProgress'
|
import { gamesInProgress } from './gamesInProgress'
|
||||||
import { getJoinSecret } from './getJoinSecret'
|
import { getJoinSecret } from './getJoinSecret'
|
||||||
import { manageGame } from './manageGame'
|
import { manageGame } from './manageGame'
|
||||||
|
import { restoreSeat } from './restoreSeat'
|
||||||
|
|
||||||
export const actions = sdk.Actions.of()
|
export const actions = sdk.Actions.of()
|
||||||
.addAction(getJoinSecret)
|
.addAction(getJoinSecret)
|
||||||
.addAction(gamesInProgress)
|
.addAction(gamesInProgress)
|
||||||
.addAction(manageGame)
|
.addAction(manageGame)
|
||||||
|
.addAction(restoreSeat)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -64,6 +64,15 @@ const dict = {
|
|||||||
'the server answered': 54,
|
'the server answered': 54,
|
||||||
'Join Secret': 55,
|
'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,
|
"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
|
} as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ export default {
|
|||||||
54: 'el servidor respondi\u00f3',
|
54: 'el servidor respondi\u00f3',
|
||||||
55: 'Secreto de acceso',
|
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.',
|
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: {
|
de_DE: {
|
||||||
0: 'Starte Station Master!',
|
0: 'Starte Station Master!',
|
||||||
@@ -118,6 +126,14 @@ export default {
|
|||||||
54: 'der Server antwortete mit',
|
54: 'der Server antwortete mit',
|
||||||
55: 'Beitrittsgeheimnis',
|
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.',
|
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: {
|
pl_PL: {
|
||||||
0: 'Uruchamianie Station Master!',
|
0: 'Uruchamianie Station Master!',
|
||||||
@@ -177,6 +193,14 @@ export default {
|
|||||||
54: 'serwer odpowiedzia\u0142',
|
54: 'serwer odpowiedzia\u0142',
|
||||||
55: 'Sekret do\u0142\u0105czania',
|
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.',
|
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: {
|
fr_FR: {
|
||||||
0: 'Démarrage de Station Master !',
|
0: 'Démarrage de Station Master !',
|
||||||
@@ -236,5 +260,13 @@ export default {
|
|||||||
54: 'le serveur a r\u00e9pondu',
|
54: 'le serveur a r\u00e9pondu',
|
||||||
55: 'Secret de connexion',
|
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.",
|
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>
|
} satisfies Record<string, LangDict>
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ export type GameRow = {
|
|||||||
playerCount: number
|
playerCount: number
|
||||||
playerNames: string[]
|
playerNames: string[]
|
||||||
createdAt: number
|
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.
|
// Absent on a lobby — it has no game to be part-way through.
|
||||||
lastMoveAt?: number
|
lastMoveAt?: number
|
||||||
status?: 'active' | 'finished'
|
status?: 'active' | 'finished'
|
||||||
@@ -96,3 +101,34 @@ export async function deleteGame(
|
|||||||
})
|
})
|
||||||
return (await res.json()) as { gameCode: string | null; save: unknown }
|
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
@@ -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`
|
// — 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
|
// 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.
|
// 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: {
|
releaseNotes: {
|
||||||
en_US:
|
en_US:
|
||||||
'Bundles Station Master v0.8.0.11 — sixteen fixes from the second multiplayer playtest. ' +
|
'Bundles Station Master v0.8.0.12 — a player who has lost their browser storage can be put ' +
|
||||||
'**Games already running on this server resume normally.** No rule changed: the whole engine ' +
|
'back in their seat. **Games already running on this server resume normally**: no rule ' +
|
||||||
'difference is one field added to one event and one card description reworded, so nothing ' +
|
'changed. **The problem this fixes.** The only proof of who you are is kept by your own ' +
|
||||||
'that was legal has become illegal. **Making up a train is far clearer.** The panel now says ' +
|
'browser, so clearing your browsing data, returning in a private window, or sitting down at a ' +
|
||||||
'what the train STILL needs rather than only what its card calls for, says that you add ONE ' +
|
'different browser leaves you looking like a stranger — an empty lobby, no game code, no name ' +
|
||||||
'car before the round passes to the next player, marks the train being loaded on the Division ' +
|
'— while the game is still running on the server and your seat is still yours. It happened at ' +
|
||||||
'map, and highlights the cars you may add in the same amber every other button uses — the ' +
|
'a real table: of two players in one game, one reloaded straight back in and the other could ' +
|
||||||
'cars in the Division Yard have always been the buttons, but they did not look like it. **A ' +
|
'not get in at all. **The fix is a new Restore a Seat action.** Pick the player from a list of ' +
|
||||||
'train’s arrival now says whose Office it reached**, and no longer tells every player they ' +
|
'whoever is actually holding a seat, and it hands you a link. Send it to them, and opening it ' +
|
||||||
'can work it when it is standing in somebody else’s district. **While your board catches up** ' +
|
'in their own browser puts them straight back where they were. The link works once and expires ' +
|
||||||
'on other players’ moves you can now PAUSE it and look as long as you like — a paused move ' +
|
'after 30 minutes, and whoever opens it takes that seat — so send it to that person rather ' +
|
||||||
'keeps the time it had left — and you can open any other player’s Office Area for a look. The ' +
|
'than posting it somewhere public. If it expires first, just run the action again. The link ' +
|
||||||
'Day, Stage and phase on screen now match the move you are watching instead of running ahead ' +
|
'carries a one-time code and never the seat itself, so a link left in a chat window is worth ' +
|
||||||
'of it, so being five moves behind looks the same everywhere on the page. When nobody is ' +
|
'nothing once it has been used.',
|
||||||
'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.',
|
|
||||||
es_ES:
|
es_ES:
|
||||||
'Incluye Station Master v0.8.0.11 — dieciséis correcciones de la segunda partida de prueba ' +
|
'Incluye Station Master v0.8.0.12 — un jugador que haya perdido los datos de su navegador ' +
|
||||||
'multijugador. **Las partidas en curso en este servidor se reanudan con normalidad**: no ha ' +
|
'puede volver a su asiento. **Las partidas en curso en este servidor se reanudan con ' +
|
||||||
'cambiado ninguna regla. **Formar un tren es mucho más claro.** El panel indica ahora lo que ' +
|
'normalidad**: no ha cambiado ninguna regla. **El problema que corrige.** La única prueba de ' +
|
||||||
'al tren le FALTA, y no solo lo que pide su carta; explica que añades UN vagón y el turno ' +
|
'quién eres la guarda tu propio navegador: si borras los datos de navegación, vuelves en una ' +
|
||||||
'pasa al siguiente jugador; marca en el mapa de la División el tren que se está formando; y ' +
|
'ventana privada o te sientas ante otro navegador, apareces como un desconocido — una sala ' +
|
||||||
'resalta los vagones que puedes añadir con el mismo ámbar que los demás botones — los vagones ' +
|
'vacía, sin código de partida ni nombre — aunque la partida siga en el servidor y el asiento ' +
|
||||||
'del Patio de División siempre fueron los botones, pero no lo parecían. **La llegada de un ' +
|
'siga siendo tuyo. Ocurrió en una mesa real: de dos jugadores, uno volvió sin problemas y el ' +
|
||||||
'tren dice a qué Oficina llegó**, y ya no le dice a todo el mundo que puede trabajarlo cuando ' +
|
'otro no pudo entrar. **La solución es la nueva acción Restaurar un asiento.** Elige al ' +
|
||||||
'está en el distrito de otro jugador. **Mientras tu tablero se pone al día** con los ' +
|
'jugador de la lista de quienes ocupan un asiento y se te entrega un enlace. Envíaselo y, al ' +
|
||||||
'movimientos de otros, ahora puedes PAUSARLO el tiempo que quieras — el movimiento pausado ' +
|
'abrirlo en su propio navegador, volverá justo donde estaba. El enlace funciona una sola vez y ' +
|
||||||
'conserva el tiempo que le quedaba — y abrir el Área de Oficina de cualquier otro jugador ' +
|
'caduca a los 30 minutos, y quien lo abra ocupará ese asiento: envíaselo a esa persona en ' +
|
||||||
'para verla. El Día, la Etapa y la fase en pantalla coinciden con el movimiento que estás ' +
|
'lugar de publicarlo. Si caduca antes, ejecuta la acción de nuevo. El enlace lleva un código ' +
|
||||||
'viendo, así que ir cinco movimientos por detrás se ve igual en toda la página. Cuando no se ' +
|
'de un solo uso y nunca el asiento en sí, así que un enlace olvidado en un chat no sirve de ' +
|
||||||
'espera a nadie, la pantalla dice qué está haciendo la División — «la División está moviendo ' +
|
'nada una vez usado.',
|
||||||
'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.',
|
|
||||||
de_DE:
|
de_DE:
|
||||||
'Enthält Station Master v0.8.0.11 — sechzehn Korrekturen aus der zweiten Mehrspieler-' +
|
'Enthält Station Master v0.8.0.12 — wer die Daten seines Browsers verloren hat, kann wieder ' +
|
||||||
'Testpartie. **Laufende Spiele auf diesem Server werden normal fortgesetzt**: keine Regel ' +
|
'auf seinen Platz gesetzt werden. **Laufende Spiele auf diesem Server werden normal ' +
|
||||||
'wurde geändert. **Das Zusammenstellen eines Zuges ist viel klarer.** Das Panel nennt jetzt, ' +
|
'fortgesetzt**: keine Regel wurde geändert. **Das behobene Problem.** Der einzige Nachweis, ' +
|
||||||
'was dem Zug noch FEHLT, statt nur, was seine Karte verlangt; es sagt, dass Sie EINEN Wagen ' +
|
'wer Sie sind, liegt in Ihrem eigenen Browser. Browserdaten gelöscht, im privaten Fenster ' +
|
||||||
'anhängen und die Runde dann zum nächsten Spieler weitergeht; es markiert den Zug, der gerade ' +
|
'zurückgekehrt oder an einem anderen Browser gesessen — und Sie erscheinen als Fremder: leere ' +
|
||||||
'beladen wird, auf der Divisionskarte; und die Wagen, die Sie anhängen dürfen, leuchten im ' +
|
'Lobby, kein Spielcode, kein Name, obwohl das Spiel weiterläuft und der Platz Ihnen gehört. An ' +
|
||||||
'selben Bernstein wie jede andere Schaltfläche — die Wagen im Divisionsbahnhof waren immer ' +
|
'einem echten Tisch geschehen: von zwei Spielern kam einer sofort zurück, der andere gar ' +
|
||||||
'schon die Schaltflächen, sahen aber nicht danach aus. **Die Ankunft eines Zuges nennt jetzt, ' +
|
'nicht. **Die Lösung ist die neue Aktion „Einen Platz wiederherstellen".** Wählen Sie den ' +
|
||||||
'wessen Office er erreicht hat**, und sagt nicht mehr allen, sie könnten ihn bearbeiten, wenn ' +
|
'Spieler aus der Liste der tatsächlich besetzten Plätze, und Sie erhalten einen Link. ' +
|
||||||
'er im Bezirk eines anderen steht. **Während Ihr Spielfeld die Züge der anderen nachholt**, ' +
|
'Schicken Sie ihn; wird er im eigenen Browser geöffnet, sitzt der Spieler wieder genau dort, ' +
|
||||||
'können Sie es nun PAUSIEREN, so lange Sie wollen — der pausierte Zug behält die Zeit, die ' +
|
'wo er war. Der Link funktioniert einmal und verfällt nach 30 Minuten, und wer ihn öffnet, ' +
|
||||||
'ihm blieb — und das Office-Gebiet jedes anderen Spielers ansehen. Tag, Etappe und Phase auf ' +
|
'nimmt diesen Platz ein — schicken Sie ihn also dieser Person und posten Sie ihn nicht ' +
|
||||||
'dem Bildschirm stimmen mit dem Zug überein, den Sie gerade sehen; fünf Züge im Rückstand zu ' +
|
'öffentlich. Verfällt er vorher, führen Sie die Aktion einfach erneut aus. Der Link enthält ' +
|
||||||
'sein sieht überall auf der Seite gleich aus. Wartet das Spiel auf niemanden, zeigt der ' +
|
'einen Einmal-Code und niemals den Platz selbst; ein Link in einem Chatfenster ist nach dem ' +
|
||||||
'Bildschirm, was die Division tut — „die Division bewegt Züge“ — statt „wartet auf niemanden“. ' +
|
'Einlösen wertlos.',
|
||||||
'**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.',
|
|
||||||
pl_PL:
|
pl_PL:
|
||||||
'Zawiera Station Master v0.8.0.11 — szesnaście poprawek z drugiej testowej partii ' +
|
'Zawiera Station Master v0.8.0.12 — gracz, który stracił dane swojej przeglądarki, może ' +
|
||||||
'wieloosobowej. **Gry w toku na tym serwerze wznawiają się normalnie**: żadna zasada się nie ' +
|
'wrócić na swoje miejsce. **Gry w toku na tym serwerze wznawiają się normalnie**: żadna zasada ' +
|
||||||
'zmieniła. **Kompletowanie pociągu jest dużo jaśniejsze.** Panel pokazuje teraz, czego ' +
|
'się nie zmieniła. **Naprawiony problem.** Jedynym dowodem tożsamości jest to, co trzyma twoja ' +
|
||||||
'pociągowi jeszcze BRAKUJE, a nie tylko czego wymaga jego karta; mówi, że dodajesz JEDEN ' +
|
'przeglądarka. Wyczyść dane, wróć w oknie prywatnym albo usiądź przy innej przeglądarce, a ' +
|
||||||
'wagon, po czym kolejka przechodzi do następnego gracza; zaznacza kompletowany pociąg na ' +
|
'wyglądasz jak ktoś obcy — pusta poczekalnia, brak kodu gry i nazwy — chociaż gra nadal działa ' +
|
||||||
'mapie Dywizji; a wagony, które możesz dodać, świecą tym samym bursztynem co pozostałe ' +
|
'na serwerze, a miejsce wciąż należy do ciebie. Zdarzyło się to przy prawdziwym stole: z ' +
|
||||||
'przyciski — wagony na Stacji Dywizyjnej zawsze były przyciskami, ale tak nie wyglądały. ' +
|
'dwóch graczy jeden wrócił od razu, a drugi w ogóle nie mógł wejść. **Rozwiązaniem jest nowa ' +
|
||||||
'**Przyjazd pociągu podaje, czyje Biuro osiągnął**, i nie mówi już wszystkim, że mogą go ' +
|
'akcja „Przywróć miejsce".** Wybierz gracza z listy osób faktycznie zajmujących miejsca, a ' +
|
||||||
'obsłużyć, gdy stoi w dzielnicy innego gracza. **Gdy twoja plansza nadrabia ruchy innych**, ' +
|
'otrzymasz link. Wyślij mu go — po otwarciu we własnej przeglądarce wróci dokładnie tam, gdzie ' +
|
||||||
'możesz ją teraz ZATRZYMAĆ na dowolnie długo — zatrzymany ruch zachowuje pozostały mu czas — ' +
|
'był. Link działa raz i wygasa po 30 minutach, a kto go otworzy, zajmie to miejsce — wyślij ' +
|
||||||
'i obejrzeć Biuro dowolnego innego gracza. Dzień, Etap i faza na ekranie zgadzają się z ' +
|
'go więc tej osobie, zamiast publikować. Jeśli wygaśnie, po prostu uruchom akcję ponownie. ' +
|
||||||
'ruchem, który oglądasz, więc pięć ruchów w tyle wygląda tak samo na całej stronie. Gdy gra ' +
|
'Link zawiera jednorazowy kod, a nigdy samo miejsce, więc link zostawiony na czacie po ' +
|
||||||
'na nikogo nie czeka, ekran mówi, co robi Dywizja — „Dywizja przesuwa pociągi” — zamiast ' +
|
'wykorzystaniu jest bezwartościowy.',
|
||||||
'„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.',
|
|
||||||
fr_FR:
|
fr_FR:
|
||||||
'Inclut Station Master v0.8.0.11 — seize corrections issues de la deuxième partie de test ' +
|
'Inclut Station Master v0.8.0.12 — un joueur qui a perdu les données de son navigateur peut ' +
|
||||||
'multijoueurs. **Les parties en cours sur ce serveur reprennent normalement** : aucune règle ' +
|
'être remis à sa place. **Les parties en cours sur ce serveur reprennent normalement** : ' +
|
||||||
'n’a changé. **Composer un train est bien plus clair.** Le panneau indique désormais ce qu’il ' +
|
'aucune règle n’a changé. **Le problème corrigé.** La seule preuve de votre identité est ' +
|
||||||
'MANQUE au train, et non plus seulement ce que sa carte demande ; il précise que vous ajoutez ' +
|
'conservée par votre propre navigateur : effacez vos données, revenez dans une fenêtre privée ' +
|
||||||
'UN wagon avant que le tour ne passe au joueur suivant ; il marque sur la carte de la ' +
|
'ou installez-vous devant un autre navigateur, et vous apparaissez comme un inconnu — salle ' +
|
||||||
'Division le train en cours de composition ; et les wagons que vous pouvez ajouter ' +
|
'vide, ni code de partie ni nom — alors que la partie tourne toujours et que la place est ' +
|
||||||
's’allument dans le même ambre que tous les autres boutons — les wagons du Dépôt de Division ' +
|
'toujours la vôtre. C’est arrivé à une vraie table : de deux joueurs, l’un est revenu ' +
|
||||||
'ont toujours été les boutons, mais n’en avaient pas l’air. **L’arrivée d’un train indique à ' +
|
'directement, l’autre n’a pas pu entrer. **La solution est la nouvelle action « Restaurer une ' +
|
||||||
'quel Office il est parvenu**, et ne dit plus à tout le monde qu’il peut le manœuvrer alors ' +
|
'place ».** Choisissez le joueur dans la liste de ceux qui occupent réellement une place, et ' +
|
||||||
'qu’il stationne dans le district d’un autre. **Pendant que votre plateau rattrape** les ' +
|
'vous obtenez un lien. Envoyez-le-lui : ouvert dans son propre navigateur, il le remet ' +
|
||||||
'coups des autres, vous pouvez maintenant le METTRE EN PAUSE aussi longtemps que vous voulez ' +
|
'exactement où il était. Le lien fonctionne une seule fois et expire au bout de 30 minutes, et ' +
|
||||||
'— le coup mis en pause conserve le temps qu’il lui restait — et ouvrir l’Aire de Bureau de ' +
|
'quiconque l’ouvre prend cette place — envoyez-le donc à cette personne plutôt que de le ' +
|
||||||
'n’importe quel autre joueur. Le Jour, l’Étape et la phase affichés correspondent au coup que ' +
|
'publier. S’il expire avant, relancez simplement l’action. Le lien porte un code à usage ' +
|
||||||
'vous regardez : avoir cinq coups de retard se voit de la même façon partout sur la page. ' +
|
'unique et jamais la place elle-même : un lien laissé dans une conversation ne vaut plus rien ' +
|
||||||
'Quand la partie n’attend personne, l’écran dit ce que fait la Division — « la Division fait ' +
|
'une fois utilisé.',
|
||||||
'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.',
|
|
||||||
},
|
},
|
||||||
migrations: {
|
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
|
* 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
|
* 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.
|
* 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 }) => {},
|
up: async ({ effects }) => {},
|
||||||
down: IMPOSSIBLE,
|
down: IMPOSSIBLE,
|
||||||
|
|||||||
+1
-1
Submodule station-master updated: 9a9e50b3c6...7c9ef8797d
Reference in New Issue
Block a user