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
|
## 0.7.7 — 2026-08-30
|
||||||
|
|
||||||
### Two releases shipped to a browser that never received them
|
### 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
|
**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.
|
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
|
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
|
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.
|
next play session, alongside #39's four unplayed v0.7.4 features.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "station-master",
|
"name": "station-master",
|
||||||
"version": "0.7.7",
|
"version": "0.7.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Station Master — a railroad operations game",
|
"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>
|
<footer>
|
||||||
<span>build <span id="build">__BUILD__</span></span>
|
<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>
|
</footer>
|
||||||
</main>
|
</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";
|
* (Jesse, 2026-08-29 — "let the user choose their options like the start of a multiplayer game";
|
||||||
* "asking first is the only path").
|
* "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
|
* Three things answer the question and so skip the screen, in this order of precedence:
|
||||||
* a game to resume, and a seed names a specific deal someone already chose to share or bookmark,
|
* `hand` (every `commitNewGame` write sets it, so this navigation IS the Deal button landing back
|
||||||
* the same reasoning `?lobby` already uses to skip past the doors on an invite link. `hand` is the
|
* here to deal), `seed` (a specific deal someone chose to share or bookmark), and — only when the
|
||||||
* one field every `commitNewGame` write always sets (`rulesToUrl`), so its presence means this
|
* player did not explicitly ask to set one up — an existing save, which is a game to resume.
|
||||||
* 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.
|
* 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');
|
showScreen('solitairesetup');
|
||||||
runSolitaireSetup(params);
|
runSolitaireSetup(params, saved !== null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2173,7 +2181,7 @@ if (newBtn && dlg) {
|
|||||||
* Solitaire defaults, since there is no live game to compare against yet, and reuses the identical
|
* 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.
|
* `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 screen = document.getElementById('solitairesetup');
|
||||||
const dealBtn = document.getElementById('ss-deal');
|
const dealBtn = document.getElementById('ss-deal');
|
||||||
if (!screen || !dealBtn) return;
|
if (!screen || !dealBtn) return;
|
||||||
@@ -2185,6 +2193,24 @@ function runSolitaireSetup(params: URLSearchParams): void {
|
|||||||
const seedField = document.getElementById('ss-seed') as HTMLInputElement | null;
|
const seedField = document.getElementById('ss-seed') as HTMLInputElement | null;
|
||||||
if (seedField) seedField.value = params.get('seed') ?? '';
|
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');
|
ss.selectPreset('solitaire');
|
||||||
dealBtn.onclick = () => commitNewGame(ss, seedField?.value ?? '');
|
dealBtn.onclick = () => commitNewGame(ss, seedField?.value ?? '');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -765,7 +765,15 @@ ul.blocked li{padding:2px 0}
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</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">
|
<menu class="ng-buttons">
|
||||||
|
<button id="ss-resume" type="button" hidden>Continue saved game</button>
|
||||||
<button id="ss-deal" type="button">Deal</button>
|
<button id="ss-deal" type="button">Deal</button>
|
||||||
</menu>
|
</menu>
|
||||||
</section>
|
</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');
|
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 () => {
|
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
|
// 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
|
// seat (`station-master.remote.v1`) could never reach solitaire's setup screen at all — a bare
|
||||||
|
|||||||
Reference in New Issue
Block a user