Commit Graph
4 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 035c374563 quit the game cleanly on stop, so best times survive
Where they live: `HKCU\Software\Brightsoft\Minesweeper X` in the prefix's
`user.reg`. `Time1/2/3` are beginner, intermediate and expert; `Name1/2/3` are
the names, which is why the name you type sticks as the default. Nothing is
stored beside the executable except the recordings.

Why they were always empty: the game holds that whole key in memory and commits
it ONLY on a clean exit. `stop()` sent SIGTERM. So every restart of this sidecar
threw away every record set since the last one — six wins in one evening left a
Best Times table reading 9999.99, which is what started this.

`stop()` now drives Game > Exit and waits for the process to go before reaping.
It runs FIRST, ahead of even reaping x11vnc, because it needs the display alive
and it is racing PM2's clock.

── how it was pinned down ──

Not by reading. `user.reg` grew by 848 bytes at the exact moment the game
exited, which named the file. Writing `Time1="12.34"` / `Name1="Pastilhas"`
into it by hand and restarting made the game display them, which proved the key
and the load path. Then switching difficulty and stopping cleanly wrote
Width/Height/Mines through from 30/16/99 to 8/8/10, which proved the write path
carries in-memory state — beginner is 8x8 here, not the 9x9 I had assumed.

── the part this does NOT fix ──

`pm2 restart` will usually still cut it short: PM2 SIGKILLs 1600ms after
SIGTERM by default and a full stop takes about 5.6 seconds. That is not new —
this sidecar's teardown has always exceeded that budget — but it does mean the
fix only reliably applies to an explicit POST /stop. Raising it needs a
`kill_timeout` on `Pm2App` in the platform's plugins/ecosystem.ts, which has no
such field. Left alone rather than decided quietly; it is in the README.

Also reverted: the 12.34 "Pastilhas" beginner record was my probe, not a game.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
2026-08-23 23:50:37 +00:00
pastilhasandClaude Opus 5 34fad8be35 refuse a command when a dialog is in the way
Found by `POST /difficulty` failing three times against a "you have the fastest
time for intermediate level" prompt that had been sitting there waiting for a
name.

Failing was not the problem. What the three attempts DID was: a modal dialog
swallows the click meant for the menu bar, the menu never opens, and the second
click — aimed at where a menu item would have been — lands on the BOARD. Three
times. On a live game that reveals cells and can lose it.

So both commands now look for any named top-level window that is not the game,
and refuse with its title rather than clicking harder. Named windows only, since
wine litters the display with 1x1 stubs.

Verified against a real one: with the Open Recording dialog up, both
/difficulty and /replay return `"Open Recording" is open on the board — dismiss
it first`, and the board is untouched.

The lesson generalises past this plugin: driving another program's UI by
synthetic clicks means every command needs to check the world is still the shape
it assumed. A retry loop without that check is not resilience, it is the same
mistake three times.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
2026-08-23 23:34:05 +00:00
pastilhasandClaude Opus 5 5d62668883 difficulty and replay panels in the left column
Top panel: Expert / Intermediate / Beginner. Bottom: that difficulty's saved
games, best first, click to play one on the board.

── the selection lives in the URL ──

`/minesweeper/expert`, not a channel and not panel state. It survives a
refresh, it can be linked, and the two panels never pass anything between
themselves — both read `useParams`. The plugin section route already existed;
nothing had used it.

The side effect hangs off the URL too: an effect on the selected level posts
the difficulty, so arriving at `/minesweeper/expert` by click, deep link or
back button all leave the game on expert. `POST /difficulty` verifies and is
idempotent, so a repeat changes nothing. One `<Navigate replace>` after the
hooks canonicalises both the bare route and a bogus section, and it waits for
`/status` so the URL lands on what the game IS rather than a guess — which is
why `difficulty` is now part of that payload.

Levels are NavLinks so cmd-click works and the active style comes from the
router. Replay rows are buttons, because they do not navigate anywhere, they
make the game do something. That is the line navigation-audit.md draws.

── two things on the server ──

`GET /replays` reads the directory rather than any index, because the filename
IS the metadata — Minesweeper X names a recording `<Lvl> <secs> - <date>.mvr`
and keeps no catalogue. A name that does not parse is skipped rather than
guessed at. Ordering is the server's: a loss is never "best" however short it
was, so wins rank by time and losses fall to the end, still watchable.

`POST /replay` drives Extras > Play Recording... and TYPES the filename into a
file dialog, which is why it only accepts a name `isKnownReplay` just matched
against the directory. An unchecked client string reaching `xdotool type` can
carry a path, a wildcard, or a newline that submits something else. Verified by
the dialog closing, not by the window size — a beginner replay leaves the
window beginner-sized, which is also what nothing-happened looks like.

Verified end to end against the running session: the expert list comes back
ranked 58.63 through 82.06 with losses after, playing 58.63 starts on the
board, and `{"file":"../../../etc/passwd"}` is a 404.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
2026-08-23 23:23:44 +00:00
pastilhasandClaude Opus 5 977448868c POST /difficulty — set the board from outside the game
First command in what will presumably be several, so the shape matters more
than the feature: name the OUTCOME, verify it against the world, and report
what the game now IS rather than that a click was sent.

    POST /api/minesweeper/difficulty  { "level": "expert" }
    -> { "ok": true, "level": "expert", "changed": true }

The proxy forwards <prefix>/*, so this needed no platform-side change.

── clicks, not keys, and that was measured ──

The obvious mechanism is `alt+g` then the underlined letter. It does not work
reliably: the same sequence gave a different difficulty depending on what was
selected before, and `alt+g e` landed on Intermediate more than once. Arrow
navigation was no better. A menu keystroke is a SEQUENCE, so it depends on
timing and on where the highlight already was; a click is one event that names
its target. Six scrambled clicks in a row hit six correct items.

There is a second reason keys are wrong here. With no window manager, X input
focus is PointerRoot — keys go to whatever the pointer is over — so a sequence
that RESIZES the window can move it out from under the pointer and silently
drop the rest of its own keystrokes. That is exactly what the first attempt
did, and it looked like a mapping problem.

── two things that make it a command rather than a click ──

`withStableWindow` holds off the re-centring watcher for the duration. A menu
interaction is two clicks ~450ms apart and the watcher ticks every second, so
it WILL move the window between them, and the second click then lands where
the item used to be — which on this game is the board. A stray click on a live
board reveals a cell and can lose the game.

And the check is the window's SIZE, not that the click was sent: 152x214,
280x342, 504x342 are distinct, so the result is verifiable. That is what makes
retrying safe, and what makes a repeat a no-op (`changed: false`) rather than a
second difficulty change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
2026-08-23 23:02:59 +00:00