Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
def48201e4 |
@@ -19,6 +19,27 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
---
|
||||
|
||||
## 0.8.6 — 2026-09-29
|
||||
|
||||
### A Competitive seat gets its save when the game is over
|
||||
|
||||
`/api/save` handed any seat its save — seed and all — while the game ran. The comment said every
|
||||
move was already on the player's screen, which was true of the moves and false of the seed, and the
|
||||
seed is every rival's hand and the order of the deck: the exact thing the shared log strips. But a
|
||||
save without a seed is not a save. **Jesse's ruling: accept the leak in Co-op; otherwise, save only
|
||||
at the end of the game.** A Competitive seat is refused with `SAVE_AFTER_FINISH` until the game is
|
||||
finished, and the Save replay button says why and waits; a Co-op table, which has nothing to hide
|
||||
from itself, and a one-seat game download at any time.
|
||||
|
||||
### A correction to the 0.8.3 entry
|
||||
|
||||
It says the Second Section card went into the deck "without a line in its notes". 0.8.2's release
|
||||
notes did name it — "ordering a Second Section costs the card, which was declared in the rules and
|
||||
never actually dealt". What was missing was not the line but the save check after the change; the
|
||||
process rule in `TODO.md` stands as written.
|
||||
|
||||
---
|
||||
|
||||
## 0.8.5 — 2026-09-29
|
||||
|
||||
The third release from the audit: housekeeping. Nothing a player can see changes; what changes is
|
||||
|
||||
@@ -695,7 +695,10 @@ were NOT fixed, each with the reason, so nothing quietly evaporates.
|
||||
(f) `package.json`'s `test/**/*.test.ts` only works because dash has no globstar and there is
|
||||
exactly one nesting level; spell it `test/*/*.test.ts`.
|
||||
|
||||
- [ ] **#117** — **`/api/save` hands every seat the seed mid-game — NEEDS JESSE.** The seat's own
|
||||
- [x] **#117** — **RULED 2026-09-29 (Jesse): "accept the leak in coop. otherwise save only at end of
|
||||
game."** Built the same day: `/api/save` answers `403 SAVE_AFTER_FINISH` to a Competitive seat
|
||||
while the game runs and serves a Co-op or one-seat game at any time; the page's Save replay
|
||||
button says so and stays disabled until the end. Originally: **`/api/save` hands every seat the seed mid-game — NEEDS JESSE.** The seat's own
|
||||
save download returns `seed` while the game is active; in Competitive that is every rival's
|
||||
hand and the deck order, the exact leak `game.ts` strips from the log. But a save without the
|
||||
seed cannot replay, which is the whole point of a save. Jesse (2026-09-29): "We should discuss
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Station Master — Components and Markers
|
||||
|
||||
**Version 0.8.5** · 2026-09-29
|
||||
**Version 0.8.6** · 2026-09-29
|
||||
|
||||
**Scope:** non-card physical components and supplies. Card-created facilities, workers, deck piles,
|
||||
hand state, timetable state and other markers are documented with their cards or in the
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Station Master — Home Deck
|
||||
|
||||
**Version 0.8.5** · 2026-09-29
|
||||
**Version 0.8.6** · 2026-09-29
|
||||
|
||||
**Scope:** the Home Office deck — how it is dealt, drawn, discarded and reshuffled, and what the
|
||||
rules are for playing each kind of card out of it.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Station Master — Mainline Deck
|
||||
|
||||
**Version 0.8.5** · 2026-09-29
|
||||
**Version 0.8.6** · 2026-09-29
|
||||
|
||||
**Scope:** the tarot-sized Mainline cards placed between Offices — how the deck is dealt, what a
|
||||
card does to a train crossing it, and the Home Deck cards played onto one.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Station Master — Quickstart
|
||||
|
||||
**Version 0.8.5** · 2026-09-29
|
||||
**Version 0.8.6** · 2026-09-29
|
||||
|
||||
For a player who has never played.
|
||||
|
||||
|
||||
+6
-1
@@ -1,6 +1,6 @@
|
||||
# Station Master — Rules
|
||||
|
||||
**Version 0.8.5** · 2026-09-29
|
||||
**Version 0.8.6** · 2026-09-29
|
||||
|
||||
**Authority:** observed code paths and tests. Where a card face, a prototype document and executable
|
||||
behaviour differ, this document reports **executable behaviour** and marks unimplemented material.
|
||||
@@ -372,6 +372,11 @@ stay where they are.
|
||||
So multiplayer changes shared traffic, scores, turn order and how the game is won or lost — not card
|
||||
attacks, which do not exist in any mode.
|
||||
|
||||
**Saving a multiplayer game.** The server holds the save; **Save replay** downloads your copy. A save
|
||||
carries the seed, and the seed is the whole deal — every hand and the order of the deck — so in a
|
||||
**Competitive** game the download is available once the game is over. In **Co-op** it is available
|
||||
at any time.
|
||||
|
||||
## 7. Frequently asked questions
|
||||
|
||||
### Is the same seed always the same game?
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "station-master",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Station Master — a railroad operations game",
|
||||
|
||||
+16
-3
@@ -845,8 +845,15 @@ export function startServer(opts: ServerOptions): Server {
|
||||
* The administrative export at `/api/games/<id>/save` is gated on the admin secret, which a player
|
||||
* does not have and should not need: a save is the seed and the moves, and every one of those moves
|
||||
* is already on this player's screen. So the seat's own session token is the gate, exactly as it is
|
||||
* for `/api/stream` and `/api/intent` — it proves which game and which chair, and nothing else is
|
||||
* disclosed. The page turns the JSON into a file (`main.ts`'s `downloadSave`).
|
||||
* for `/api/stream` and `/api/intent` — it proves which game and which chair. The page turns the
|
||||
* JSON into a file (`main.ts`'s `downloadSave`).
|
||||
*
|
||||
* NOT WHILE A COMPETITIVE GAME IS RUNNING (Jesse's ruling, 2026-09-29, TODO #117). "Every one of
|
||||
* those moves is already on this player's screen" was true of the moves and false of the SEED,
|
||||
* which a save has to carry to be a save — and the seed is every rival's hand and the deck order
|
||||
* for the rest of the game, the exact thing `game.ts` strips from the shared log. So a
|
||||
* Competitive seat gets its save once the game is over; a Co-op table has nothing to hide from
|
||||
* itself and gets it at any time, as does a one-seat game.
|
||||
*/
|
||||
if (url.pathname === '/api/save' && req.method === 'GET') {
|
||||
const ps = sessions.get(url.searchParams.get('token') ?? '');
|
||||
@@ -855,7 +862,13 @@ export function startServer(opts: ServerOptions): Server {
|
||||
sendJson(res, 404, { error: 'no such game' });
|
||||
return;
|
||||
}
|
||||
sendJson(res, 200, { gameId: ps.gameId, save: session.exportSave() });
|
||||
const save = session.exportSave();
|
||||
const competitive = save.config.mode === 'competitive' && save.playerNames.length > 1;
|
||||
if (competitive && save.status !== 'finished') {
|
||||
sendJson(res, 403, { error: 'SAVE_AFTER_FINISH' });
|
||||
return;
|
||||
}
|
||||
sendJson(res, 200, { gameId: ps.gameId, save });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1497,6 +1497,19 @@ function render(): void {
|
||||
const f = session.view();
|
||||
const menu = session.menu();
|
||||
noteFirstFrame(f, rejoiningRemote);
|
||||
/**
|
||||
* A COMPETITIVE SAVE WAITS FOR THE END (Jesse's ruling, TODO #117). The file carries the seed,
|
||||
* which is every rival's hand, so `/api/save` refuses it while the game runs — and the button
|
||||
* says so here rather than silently doing nothing when clicked.
|
||||
*/
|
||||
const saveBtn = document.getElementById('savefile') as HTMLButtonElement | null;
|
||||
if (saveBtn && !isLocal(session)) {
|
||||
const locked = f.mode === 'competitive' && f.status !== 'finished';
|
||||
saveBtn.disabled = locked;
|
||||
saveBtn.title = locked
|
||||
? 'In a Competitive game the save file carries the whole deal, so it can be downloaded once the game is over'
|
||||
: 'Download this game as a save file you can replay or share';
|
||||
}
|
||||
|
||||
// Which squares the selected card or track piece may go on. Highlighting them is what turns the
|
||||
// coordinate list into a board: you pick the thing, then click where it goes.
|
||||
|
||||
@@ -151,6 +151,25 @@ describe('the HTTP layer (v0.8.4)', () => {
|
||||
assert.equal(reconnect['lastSeq'], 1, 'the reconnect push does not carry the count');
|
||||
});
|
||||
|
||||
it('hands a Competitive seat its save only once the game is over, and a Co-op seat at any time', async () => {
|
||||
// Jesse's ruling on TODO #117: the seed in a save is every rival's hand, so a Competitive
|
||||
// download waits for the end; a co-operative table has nothing to hide from itself.
|
||||
const { host } = await table();
|
||||
assert.equal((await post('/api/lobby/start', { token: host.token })).status, 200);
|
||||
const refused = await fetch(base + `/api/save?token=${host.token}`);
|
||||
assert.equal(refused.status, 403, 'a running Competitive game handed out its seed');
|
||||
assert.deepEqual(await refused.json(), { error: 'SAVE_AFTER_FINISH' });
|
||||
|
||||
const coop = await post('/api/lobby/create', { secret: SECRET, config: { ...config, mode: 'coop' }, displayName: 'Host', players: 2 });
|
||||
const coopHost = coop.json as unknown as Seat;
|
||||
assert.equal((await post('/api/lobby/join', { secret: SECRET, gameCode: coopHost.gameCode, displayName: 'Guest' })).status, 200);
|
||||
assert.equal((await post('/api/lobby/start', { token: coopHost.token })).status, 200);
|
||||
const allowed = await fetch(base + `/api/save?token=${coopHost.token}`);
|
||||
assert.equal(allowed.status, 200, 'a Co-op seat could not download its save');
|
||||
const body = (await allowed.json()) as { save: { seed: number } };
|
||||
assert.equal(typeof body.save.seed, 'number');
|
||||
});
|
||||
|
||||
it('applies a burst of concurrent moves one at a time and leaves the save readable', async () => {
|
||||
const { host, guest } = await table();
|
||||
assert.equal((await post('/api/lobby/start', { token: host.token })).status, 200);
|
||||
|
||||
Reference in New Issue
Block a user