Two setup screens, not three — the in-game dialog is deleted
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cf018b4a5f
commit
131538dc7c
+48
-64
@@ -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 = <T extends HTMLElement>(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<HTMLInputElement>('ng-seed').value = '';
|
||||
field<HTMLInputElement>('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 `<dialog>` answers with an empty `returnValue` and no submit event at all.
|
||||
*/
|
||||
dlg.addEventListener('close', () => {
|
||||
if (dlg.returnValue !== 'deal') return;
|
||||
commitNewGame(ng, field<HTMLInputElement>('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 ?? '');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user