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. -->