Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
193800a649 |
@@ -19,6 +19,47 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
---
|
||||
|
||||
## 0.7.8 — 2026-08-30
|
||||
|
||||
### The setup screen was unreachable for anyone who had ever played
|
||||
|
||||
Third report of the same symptom, and this time the build was confirmed current on screen
|
||||
(`0.7.7-mtf7hyxc`), which ruled out the caching fault v0.7.7 had just fixed and left the actual
|
||||
cause with nowhere to hide.
|
||||
|
||||
**v0.7.5 skipped the setup screen whenever `load()` found a save**, reasoned in its own comment as
|
||||
"a saved game is a game to resume". The consequence went unnoticed: a browser that has ever played
|
||||
solitaire *always* has a save, so the door could never reach the screen again. Only a browser that
|
||||
had never played would see it — which is exactly why a fresh private window appeared to prove
|
||||
v0.7.7's cache fix. The private window had no save. Two genuine faults were stacked, the caching one
|
||||
was real and is fixed, and it masked this one.
|
||||
|
||||
**The door now outranks a saved game.** `?solitaire` is an explicit request to set a game up;
|
||||
clicking "Play solitaire" is not a request to resume. A BARE reload still resumes, which is the
|
||||
zero-friction case D11 is about and is pinned by its own test.
|
||||
|
||||
**Dealing from the door would have destroyed a game in progress**, since `commitNewGame` calls
|
||||
`clearSave()` — so the screen now carries `#ss-resume` ("Continue saved game") and states plainly
|
||||
that dealing replaces the save. Resuming navigates to the bare URL rather than building a session
|
||||
on the spot, so `start()` stays the only place that turns a URL into a game.
|
||||
|
||||
**Recorded because the failure was diagnostic, not technical.** The first two attempts each fixed
|
||||
something real that was not this, and both were reported as verified. The routing fix in v0.7.6 was
|
||||
verified by reading what the server served; v0.7.7's by the same. Neither ever exercised the actual
|
||||
path with the actual state a returning player has. The reproduction here is a failing test asserting
|
||||
the door with a save present — written before the fix, and it failed with "a saved game swallowed
|
||||
the door".
|
||||
|
||||
### The splash footer names both ways to play
|
||||
|
||||
Was "solitaire runs entirely in your browser — no server code required", written when solitaire was
|
||||
the only door. Now: "Multiplayer runs on StartOS server. Solitaire runs entirely in your browser."
|
||||
(Jesse, 2026-08-30, asked to ride along with the next change rather than take a release of its own.)
|
||||
|
||||
868 tests pass, four of them new.
|
||||
|
||||
---
|
||||
|
||||
## 0.7.7 — 2026-08-30
|
||||
|
||||
### Two releases shipped to a browser that never received them
|
||||
|
||||
@@ -333,6 +333,19 @@ Queued 2026-08-29, from building Gitea#11 and #16 (both shipped in v0.7.3, main
|
||||
**The lesson worth keeping: when a fix appears to have had no effect, check that it arrived
|
||||
before re-diagnosing it.** A hard-reload would have answered it on the first report.
|
||||
|
||||
**And a THIRD report, 2026-08-30, with the build confirmed current on screen — which is what
|
||||
finally found it. Fixed in v0.7.8.** v0.7.5 skipped the setup screen whenever a save existed
|
||||
("a saved game is a game to resume"), so any browser that had ever played solitaire could never
|
||||
reach it again; the private window that seemed to vindicate v0.7.7 simply had no save. The door
|
||||
outranks a save now, a bare reload still resumes, and `#ss-resume` keeps the game in progress
|
||||
one button away since Deal clears it.
|
||||
|
||||
**Three attempts, two of them fixing something real that was not the reported fault.** Each was
|
||||
reported as verified, and each verification read what the SERVER served rather than exercising
|
||||
the path with the state a returning player actually has. The thing that worked was a failing
|
||||
test written before the fix. Worth remembering next time a report repeats: reproduce the user's
|
||||
state first, and treat "I verified it" as unearned until something failed the way they described.
|
||||
|
||||
Still not verified past that: nobody has clicked all the way through the setup screen's own
|
||||
fields and confirmed the dealt game matches what was chosen. Worth being an early item in the
|
||||
next play session, alongside #39's four unplayed v0.7.4 features.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "station-master",
|
||||
"version": "0.7.7",
|
||||
"version": "0.7.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Station Master — a railroad operations game",
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ footer{margin-top:26px;color:var(--dim);font-size:11px;display:flex;gap:18px;fle
|
||||
|
||||
<footer>
|
||||
<span>build <span id="build">__BUILD__</span></span>
|
||||
<span>solitaire runs entirely in your browser — no server code required</span>
|
||||
<span>Multiplayer runs on StartOS server. Solitaire runs entirely in your browser.</span>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
|
||||
+35
-9
@@ -773,16 +773,24 @@ function start(): void {
|
||||
* (Jesse, 2026-08-29 — "let the user choose their options like the start of a multiplayer game";
|
||||
* "asking first is the only path").
|
||||
*
|
||||
* A saved game or an explicit `seed=` both mean this visit is not "no plan yet" — a saved game is
|
||||
* a game to resume, and a seed names a specific deal someone already chose to share or bookmark,
|
||||
* the same reasoning `?lobby` already uses to skip past the doors on an invite link. `hand` is the
|
||||
* one field every `commitNewGame` write always sets (`rulesToUrl`), so its presence means this
|
||||
* navigation IS the setup screen's own Deal button, landing back here to actually deal — checking
|
||||
* it is what stops the screen asking itself the question a second time.
|
||||
* Three things answer the question and so skip the screen, in this order of precedence:
|
||||
* `hand` (every `commitNewGame` write sets it, so this navigation IS the Deal button landing back
|
||||
* here to deal), `seed` (a specific deal someone chose to share or bookmark), and — only when the
|
||||
* player did not explicitly ask to set one up — an existing save, which is a game to resume.
|
||||
*
|
||||
* THE DOOR OUTRANKS A SAVED GAME, and getting that wrong is what made this feature unreachable
|
||||
* for three releases. v0.7.5 skipped the screen whenever `load()` found ANYTHING, reasoned as "a
|
||||
* saved game is a game to resume" — but a browser that has ever played solitaire always has one,
|
||||
* so the door could never reach the screen again. Reported three times (Jesse, 2026-08-29 twice
|
||||
* and 2026-08-30); a private window appeared to absolve it only because it had never played and
|
||||
* so had no save. Clicking "Play solitaire" is a request to set a game up, not to resume one — a
|
||||
* BARE reload is the resume case, and still is. `#ss-resume` is what keeps the save reachable, so
|
||||
* this costs nobody the game they were playing.
|
||||
*/
|
||||
if (!saved && requested === null && !params.has('hand')) {
|
||||
const askedToSetUp = params.get('solitaire') !== null;
|
||||
if (requested === null && !params.has('hand') && (askedToSetUp || !saved)) {
|
||||
showScreen('solitairesetup');
|
||||
runSolitaireSetup(params);
|
||||
runSolitaireSetup(params, saved !== null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2173,7 +2181,7 @@ if (newBtn && dlg) {
|
||||
* Solitaire defaults, since there is no live game to compare against yet, and reuses the identical
|
||||
* `wireGameTypeBlock`/`commitNewGame` pair the in-game dialog uses — the two are one design, not two.
|
||||
*/
|
||||
function runSolitaireSetup(params: URLSearchParams): void {
|
||||
function runSolitaireSetup(params: URLSearchParams, hasSave = false): void {
|
||||
const screen = document.getElementById('solitairesetup');
|
||||
const dealBtn = document.getElementById('ss-deal');
|
||||
if (!screen || !dealBtn) return;
|
||||
@@ -2185,6 +2193,24 @@ function runSolitaireSetup(params: URLSearchParams): void {
|
||||
const seedField = document.getElementById('ss-seed') as HTMLInputElement | null;
|
||||
if (seedField) seedField.value = params.get('seed') ?? '';
|
||||
|
||||
/**
|
||||
* THE WAY BACK TO A GAME IN PROGRESS, and the reason the door is allowed to outrank a save at all.
|
||||
* Dealing from here calls `clearSave()`, so a player who reached this screen from the splash — by
|
||||
* clicking "Play solitaire", which nobody reads as "throw away what I was playing" — needs their
|
||||
* game one button away and needs to be told what Deal costs.
|
||||
*
|
||||
* Resuming is a navigation to the BARE url rather than a session built here: `start()` already
|
||||
* resumes a save on a bare load, and routing both paths through it keeps one place that turns a
|
||||
* URL into a game.
|
||||
*/
|
||||
const resumeBtn = document.getElementById('ss-resume');
|
||||
const savedNote = document.getElementById('ss-saved-note');
|
||||
if (resumeBtn) {
|
||||
resumeBtn.hidden = !hasSave;
|
||||
resumeBtn.onclick = () => void (location.search = '');
|
||||
}
|
||||
if (savedNote) savedNote.hidden = !hasSave;
|
||||
|
||||
ss.selectPreset('solitaire');
|
||||
dealBtn.onclick = () => commitNewGame(ss, seedField?.value ?? '');
|
||||
}
|
||||
|
||||
@@ -765,7 +765,15 @@ ul.blocked li{padding:2px 0}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Shown only when `station-master.save.v1` holds a game. Dealing from this screen CLEARS that
|
||||
save (`commitNewGame` calls `clearSave`), so without a way back the door would be a way to
|
||||
lose a game in progress — and the door is reached by clicking "Play solitaire", which nobody
|
||||
reads as "discard what I was playing". -->
|
||||
<p class="ng-note" id="ss-saved-note" hidden>You have a solitaire game in progress. Dealing a new
|
||||
one below replaces it — there is no undo for that.</p>
|
||||
|
||||
<menu class="ng-buttons">
|
||||
<button id="ss-resume" type="button" hidden>Continue saved game</button>
|
||||
<button id="ss-deal" type="button">Deal</button>
|
||||
</menu>
|
||||
</section>
|
||||
|
||||
@@ -4192,6 +4192,49 @@ describe('the New Game dialog', () => {
|
||||
assert.equal(els.get('gameui')!['hidden'], true, 'a game was dealt before anyone chose anything');
|
||||
});
|
||||
|
||||
it('the solitaire door reaches the setup screen even when a solitaire game is saved', async () => {
|
||||
/**
|
||||
* REPORTED THREE TIMES BY JESSE (2026-08-29, twice, and 2026-08-30). v0.7.5 skipped the setup
|
||||
* screen whenever `load()` found a save, reasoned as "a saved game is a game to resume" — but
|
||||
* that means ANY browser that has ever played solitaire can never reach the setup screen from
|
||||
* the door again, which is the whole feature. The private window that appeared to prove the
|
||||
* caching fix had simply never played, so its `localStorage` was empty.
|
||||
*
|
||||
* The door is an explicit request to set a game up. A BARE reload still resumes (below).
|
||||
*/
|
||||
const save = JSON.stringify({ seed: 12345, history: [] });
|
||||
const { els } = await load('?solitaire', { 'station-master.save.v1': save });
|
||||
assert.equal(els.get('solitairesetup')!['hidden'], false, 'a saved game swallowed the door');
|
||||
assert.equal(els.get('gameui')!['hidden'], true, 'the saved game was resumed instead of asking');
|
||||
});
|
||||
|
||||
it('offers a way back to the saved game, since dealing from the door destroys it', async () => {
|
||||
// Deal calls `clearSave()`. The door is reached by clicking "Play solitaire", which nobody reads
|
||||
// as "discard what I was playing" — so the save has to be one button away, and the cost of Deal
|
||||
// has to be stated. Resuming navigates to the bare URL and lets `start()` do it.
|
||||
const save = JSON.stringify({ seed: 12345, history: [] });
|
||||
const { els, nav } = await load('?solitaire', { 'station-master.save.v1': save });
|
||||
assert.equal(els.get('ss-resume')!['hidden'], false, 'no way back to the game in progress');
|
||||
assert.equal(els.get('ss-saved-note')!['hidden'], false, "Deal's cost to the save is not stated");
|
||||
(els.get('ss-resume')!['onclick'] as () => void)();
|
||||
assert.equal(nav.search, '', 'resuming did not go back to the plain resume path');
|
||||
});
|
||||
|
||||
it('hides the resume button when there is no saved game to go back to', async () => {
|
||||
const { els } = await load('?solitaire');
|
||||
assert.equal(els.get('ss-resume')!['hidden'], true, 'a resume button with nothing to resume');
|
||||
assert.equal(els.get('ss-saved-note')!['hidden'], true, 'warns about replacing a save that does not exist');
|
||||
});
|
||||
|
||||
it('a bare reload still resumes a saved solitaire game rather than asking again', async () => {
|
||||
// The other half: `?solitaire` is what changed, not resuming itself. Reopening the tab must not
|
||||
// put a question in front of somebody who just wants their game back (D11's zero-friction case).
|
||||
const save = JSON.stringify({ seed: 12345, history: [] });
|
||||
const { els } = await load('', { 'station-master.save.v1': save });
|
||||
assert.equal(els.get('gameui')!['hidden'], false, 'a bare reload did not resume the saved game');
|
||||
assert.equal(els.get('solitairesetup')!['hidden'], true, 'the setup screen interrupted a resume');
|
||||
});
|
||||
|
||||
it('the solitaire door reaches solitaire even when this browser remembers a multiplayer game', async () => {
|
||||
// Found 2026-08-29 verifying v0.7.5 on phoenix.local: a browser with ANY remembered multiplayer
|
||||
// seat (`station-master.remote.v1`) could never reach solitaire's setup screen at all — a bare
|
||||
|
||||
Reference in New Issue
Block a user