From 131538dc7c1bc4468865646d1824c210cda55525 Mon Sep 17 00:00:00 2001 From: "Jesse.Markowitz" Date: Sun, 30 Aug 2026 07:58:56 -0400 Subject: [PATCH] =?UTF-8?q?Two=20setup=20screens,=20not=20three=20?= =?UTF-8?q?=E2=80=94=20the=20in-game=20dialog=20is=20deleted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jesse: "it should not go to a separate screen. We should reuse the Solitaire New Game Screen… in general we should reuse what we already have." #newgamedlg was a third copy of the same questions and the one that drifted: shown only to a solitaire player, it asked "Everyone loses if COMBINED Revenue…" and explained Employee Rotation in full multiplayer terms beside a control it had disabled. Both were on the list to re-word; deleting the screen removes the drift instead of restating it. New game opens the setup screen IN PLACE rather than navigating, so the live session stays in memory: the fields open on the rules actually being played (what the dialog was good for), and Continue Existing Saved Game puts the board back with no reload. render() calls save() every frame, so nothing is at risk either way. The two remaining screens now match below their headers — same three parameters in the same order, same seed note, same chair note. Solitaire shows Players at the table locked at 1 rather than omitting it: a fixed control says "same form, table of one", a missing one made it a different form sharing a rules block. Drift guard drops to two prefixes and now fails if any ng- id returns. Stays in the unshipped v0.7.9. 874 tests pass. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX --- CHANGELOG.md | 30 +++++++ src/web/main.ts | 112 +++++++++++-------------- src/web/play.html | 208 ++++++---------------------------------------- test/web.test.ts | 139 +++++++++++++++---------------- 4 files changed, 173 insertions(+), 316 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cb62d2..a8d59e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,6 +124,36 @@ orientation**, 276 east / 259 west. `setup.ts` is the only place a Mainline node rolls the direction from the seed for every grade, so the `?? 'east'` fallbacks in `view.ts` and `advance.ts` are unreachable. +### Two setup screens, not three — the in-game dialog is deleted + +Jesse, 2026-08-30: "If you are in the Solitaire game and you click the new game dialog, it should not +go to a separate screen. We should reuse the Solitaire New Game Screen… in general we should reuse +what we already have." + +`#newgamedlg` was a third copy of the same questions and the one that drifted. It was shown ONLY to +a solitaire player, and it asked "Everyone loses if **combined** Revenue at the end is under" — a +table's question, put to one person — and explained Employee Rotation in full, in multiplayer terms, +beside a control it had itself disabled. Both were on the list to re-word. Deleting the screen +removes the drift instead of restating it, which is the cheaper fix and the one that cannot drift +again. + +**New game now opens the setup screen in place.** Not a navigation: the live session stays in memory, +so the fields open on the rules actually being played — which is what the dialog was good for, and +losing "change one dial, redeal, compare" would have been a real loss — and **Continue Existing Saved +Game** puts the board straight back with no reload and no replay. Nothing is at risk either way: +`render()` calls `save()` every frame, so the game in progress is always on disk. + +**And the two remaining screens now match below their headers.** Each keeps its own opening — the +lobby's join/secret section and "Create a new game" are multiplayer's alone — but from the parameters +down they are one form: the same three fields in the same order (seed, players at the table, days), +the same note about a seed and settings dealing the same railroad, and the same note about every +chair being taken. Solitaire shows **Players at the table** too, locked at one, rather than omitting +it — a control that is present and fixed says "this is the same form, at a table of one", where a +missing one just made it a different form that happened to share a rules block. + +The drift guard that had been checking three prefixes now checks two, and a new assertion fails if +any `ng-` id ever reappears. + ### Three wording and layout fixes - **The collision entries** on all three screens now read "The game ends immediately and results in diff --git a/src/web/main.ts b/src/web/main.ts index a4d3b77..d3d361e 100644 --- a/src/web/main.ts +++ b/src/web/main.ts @@ -2137,65 +2137,26 @@ function commitNewGame(wired: WiredGameType, seedFieldValue: string): void { else location.search = next; } +/** + * THE IN-GAME "NEW GAME" BUTTON GOES TO THE SETUP SCREEN (Jesse, 2026-08-30 — "it should not go to + * a separate screen. We should reuse the Solitaire New Game Screen"). + * + * `#newgamedlg` used to be a third copy of the same questions and the one that drifted: it carried + * multiplayer wording on a screen only a solitaire player ever sees. It is deleted; this navigates + * to the screen that already asks these questions properly. + * + * Nothing is lost on the way: `render()` calls `save()` every frame, so the game in progress is + * always on disk, and the setup screen offers "Continue Existing Saved Game" to come back to it. + */ const newBtn = document.getElementById('newgame'); -const dlg = document.getElementById('newgamedlg') as HTMLDialogElement | null; -if (newBtn && dlg) { - const field = (id: string): T => document.getElementById(id) as T; - - /** - * THE SAME FIVE GAME TYPES THE LOBBY OFFERS, and the same shared rules block under them. - * - * The dialog used to carry its own copy of the questions and its own idea of the defaults, which - * is how it ended up with "where an Extra may start" that the lobby did not have and none of the - * three optional rules that it did. Every screen now reads `presets.ts` and drives its block - * through `settings-form.ts`; only Solitaire can actually be DEALT here, so the three multiplayer - * types are shown disabled rather than hidden — what this screen offers and what the lobby offers - * should read as one list, not two. - */ - const ng = wireGameTypeBlock('ng-', dlg); - - /** - * ASK FOR ALL OF IT, rather than documenting URL parameters in the title bar. - * - * It asked for the seed alone, through `prompt()`. The opening hand and the three revenue rates - * were constants in the source, so trying a variation meant an edit and a rebuild — and balance is - * the open question this game has (`TODO.md`). A dialog is what lets a playtest be a playtest. - * - * The dialog OPENS ON THE RULES IN PLAY rather than on the defaults: dealing a second game to - * compare against the first is the common case, and re-entering settings each time is how a - * comparison silently stops comparing. Which TYPE that is comes out of the comparison — a game - * dealt at the Solitaire defaults reopens on Solitaire, and one that was tuned reopens on Custom - * with every changed field marked. - */ +if (newBtn) { newBtn.onclick = () => { - // The button itself is hidden for a session that cannot deal (`applyCapabilities`), but the - // dialog's whole answer-reading/URL-navigating flow below assumes a LocalSession throughout, so - // the guard is repeated — and `local` is captured as a `const` so the narrowing survives the - // closures below it (see `renderUndo`'s identical note on why `session` itself cannot be). - if (!isLocal(session)) return; - const local = session; - const f = local.view(); - const day = f.day; - const started = f.status === 'active' && (day > 1 || f.stage > 1); - if (started && !confirm(`Forget this game (seed ${local.seed()}, Day ${day}) and deal a new one?`)) return; - - field('ng-seed').value = ''; - field('ng-days').value = String(f.days); - ng.setBase('solitaire', 'solitaire'); - // The rules actually in play, then the comparison decides what to call them. - ng.form.write(settingsOf(configFromFrame(f)), presetSettings('solitaire', 1, f.days)); - ng.refresh(); - dlg.showModal(); + showScreen('solitairesetup'); + // IN PLACE, not a navigation: the live session stays in memory, so the fields can open on the + // rules actually being played and "Continue Existing Saved Game" is just showing the board + // again rather than a reload and a replay. + runSolitaireSetup(new URLSearchParams(), true, session.view()); }; - - /** - * One handler for every way the dialog can close — the Deal button, the Cancel button, and Esc, - * which `` answers with an empty `returnValue` and no submit event at all. - */ - dlg.addEventListener('close', () => { - if (dlg.returnValue !== 'deal') return; - commitNewGame(ng, field('ng-seed').value); - }); } /** @@ -2208,7 +2169,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, hasSave = false): void { +function runSolitaireSetup(params: URLSearchParams, hasSave = false, live: Frame | null = null): void { const screen = document.getElementById('solitairesetup'); const dealBtn = document.getElementById('ss-deal'); if (!screen || !dealBtn) return; @@ -2220,25 +2181,48 @@ function runSolitaireSetup(params: URLSearchParams, hasSave = false): void { const seedField = document.getElementById('ss-seed') as HTMLInputElement | null; if (seedField) seedField.value = params.get('seed') ?? ''; + /** + * WHAT THE FIELDS OPEN ON, and it is not the same question in both directions. + * + * Reached mid-game from "New game", this opens on the rules CURRENTLY IN PLAY — that is what the + * deleted dialog was good for, and losing it would make "change one dial and redeal to compare" + * impossible. Reached from the splash, there is no game to read, so it opens on the plain + * Solitaire defaults. + */ + if (live) { + const daysField = document.getElementById('ss-days') as HTMLInputElement | null; + if (daysField) daysField.value = String(live.days); + ss.setBase('solitaire', 'solitaire'); + ss.form.write(settingsOf(configFromFrame(live)), presetSettings('solitaire', 1, live.days)); + ss.refresh(); + } else { + ss.selectPreset('solitaire'); + } + /** * 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. + * Mid-game the game is still in memory, so going back is just showing it again. From the splash + * there is nothing loaded yet, so it is a navigation to the bare URL and `start()` restores the + * save — one place that turns a URL into a game, either way. */ const resumeBtn = document.getElementById('ss-resume'); const savedNote = document.getElementById('ss-saved-note'); + const canResume = hasSave || live !== null; if (resumeBtn) { - resumeBtn.hidden = !hasSave; - resumeBtn.onclick = () => void (location.search = ''); + resumeBtn.hidden = !canResume; + resumeBtn.onclick = live + ? () => { + showScreen('gameui'); + render(); + } + : () => void (location.search = ''); } - if (savedNote) savedNote.hidden = !hasSave; + if (savedNote) savedNote.hidden = !canResume; - ss.selectPreset('solitaire'); dealBtn.onclick = () => commitNewGame(ss, seedField?.value ?? ''); } diff --git a/src/web/play.html b/src/web/play.html index c10e338..21589ce 100644 --- a/src/web/play.html +++ b/src/web/play.html @@ -318,9 +318,10 @@ ul.blocked li{padding:2px 0} + `main.ts` only decides whether THIS div, `#solitairesetup` or `#gameui` is the one visible. + `#solitairesetup` asks the same questions of a solitaire player, through the same shared module + (`settings-form.ts`) — the two blocks are generated from one template, and since 2026-08-30 + they are the ONLY two: the in-game dialog that was a third copy is gone. -->