Two halves of the same problem: the picture was sharp everywhere and wrong overall. ── whole-number scaling ── The canvas is now sized to the largest WHOLE number of device pixels per source pixel that fits the panel, measured with a ResizeObserver and the device pixel ratio. Below 1x it takes a unit fraction instead, so the rows that survive are evenly spaced rather than dropped in clumps. This is a 16px-per-cell bitmap from 1995. At a fractional scale one cell gets three screen pixels and its neighbour gets two, the grid goes uneven and the digits wobble — which reads as "not quite right" rather than as blur, and `image-rendering: pixelated` does not help because unevenness is not blur. A fractional devicePixelRatio produces exactly this at what looks like 1:1. It is also allowed to scale UP past the framebuffer's own size, which was the other half of it: `max-w-full` only ever shrinks, so on a wide panel the canvas sat at its intrinsic size using a fraction of the pixels available. ── a smaller stage ── 800x600 down to 560x400. The stage is what gets scaled to fill the panel, so every pixel of empty surround is panel the game does not get, and the whole- number steps are far apart: in an 1866-device-pixel panel, 800 wide lands on 2x and 560 wide lands on 3x. Same panel, half again as much game. 560x400 is expert (504x342, measured on this machine) plus a margin. A custom board bigger than the stage is now clipped at the right and bottom rather than shrunk; `centre` clamps at zero so the menu bar stays reachable. Widen the two constants if that turns out to be the wrong trade. Verified: beginner centres at 204,93 on the new stage, which is exact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
128 lines
7.3 KiB
Markdown
128 lines
7.3 KiB
Markdown
# Minesweeper
|
|
|
|
Minesweeper X, running under wine on a headless X display, streamed into an Officer panel.
|
|
|
|
It is a game. It is also the smallest honest proof that Officer can put an arbitrary X11 program in a
|
|
panel — a framebuffer, a process on it, a VNC server reading it, a client in the browser — and it is the
|
|
first plugin to use the platform's per-plugin websocket relay.
|
|
|
|
## What it needs
|
|
|
|
Six host binaries, declared in `manifest.ts` and checked before install: `wine`, `Xvfb`, `x11vnc`,
|
|
`xdotool`, `xdpyinfo`, `winetricks`.
|
|
|
|
The awkward one is wine. Minesweeper X is a **32-bit** PE (machine `0x014c`, Visual Basic 6), so the host
|
|
needs i386 multiarch and a `win32` prefix. A 64-bit-only wine cannot load it, and the failure is a bare
|
|
`Bad EXE format`. On Debian or Ubuntu:
|
|
|
|
```
|
|
sudo dpkg --add-architecture i386
|
|
sudo apt update
|
|
sudo apt install wine wine32:i386 xvfb x11vnc x11-utils xdotool winetricks
|
|
```
|
|
|
|
## The game
|
|
|
|
[Minesweeper X](https://minesweepergame.com/download/minesweeper-x.zip) — the competition build, with
|
|
timing and replay extras. `game/Minesweeper X.exe` is committed; it is 68 KB and it is the thing being
|
|
run rather than state produced by running it.
|
|
|
|
| | |
|
|
| --- | --- |
|
|
| zip sha256 | `21089193924919ef876a5b464f06bed9cc149867c51e40c259288ebe623014f0` |
|
|
| exe sha256 | `e414c207adaa14999b0464c3d0a701f20ed11282d5ed0f84b27b62d25389f661` |
|
|
|
|
The download needs a browser `User-Agent`; the site answers `403` to curl's default.
|
|
|
|
## Where its state lives
|
|
|
|
`DATA_PATH/minesweeper/` — never in this directory, which `plugins:update` will `git pull` over.
|
|
|
|
- `prefix/` — the wine prefix, ~600 MB, built on first `POST /start`. Deleting it is the standard fix for
|
|
a wedged wine, and it is safe: the next start rebuilds it.
|
|
- `run/` — pid state, logs, and `vnc.sock`. **Mode 0700, and that is load-bearing.**
|
|
|
|
## Two things worth knowing before you change anything
|
|
|
|
**x11vnc has no password, and that is correct — but only because it also has no port.** It binds a unix
|
|
socket inside `run/`, mode 0700. The kernel refuses every other account before x11vnc is asked anything,
|
|
so a password would be a second lock on a door only one person can reach. The version this replaced bound
|
|
`127.0.0.1:5907` with `-nopw`, which is a different claim entirely — every local account could open it and
|
|
take the display.
|
|
|
|
**`-unixsock` does not remove the TCP listener, and the first alpha shipped believing it did.** It adds a
|
|
unix socket beside the port; x11vnc was on `0.0.0.0:5900` and `[::]:5900` with no password, reachable from
|
|
the whole tailnet and the whole LAN, under a comment saying there was no port at all. Three flags close it
|
|
and each one earns its place: `-rfbport 0` for IPv4, `-rfbportv6 0` for IPv6 — `-rfbport 0` alone leaves
|
|
`[::]:5900` up — and `-noipv6` for outgoing sockets.
|
|
|
|
The check that matters is `ss -lntp` against the **running** process. Reading the arguments cannot show a
|
|
listener that exists because a flag is missing, and running the same arguments by hand did not reproduce
|
|
it either.
|
|
|
|
**The game is centred on a fixed stage, and the whole stage is what you see.** The framebuffer is 560x400
|
|
whatever the difficulty, `sidecar/session.ts` keeps the window in the middle of it, and the panel draws the
|
|
framebuffer entire — scaled to fit, never cropped. So changing difficulty moves the board rather than
|
|
resizing the panel, and the surround is teal (`-solid`) rather than the root's black.
|
|
|
|
**The stage is small because the panel scales it by whole numbers.** Pixel art at a fractional scale gets
|
|
uneven cells — some three screen pixels wide, their neighbours two — which reads as "not quite right"
|
|
rather than as blur, and `image-rendering: pixelated` does not help because unevenness is not blur. So the
|
|
panel picks the largest whole number of DEVICE pixels per source pixel and every pixel of empty stage is
|
|
panel the game does not get: in an 1866-device-pixel panel, 800 wide lands on 2x and 560 wide lands on 3x.
|
|
560x400 is expert (504x342) plus a margin. A custom board bigger than that is clipped at the right and
|
|
bottom rather than shrunk, with the menu bar kept reachable.
|
|
|
|
It used to park the window at the origin and have the client crop to the window's size, polled every four
|
|
seconds. That resized the panel under the player, and it was off by the 3x29 wine puts between the root
|
|
origin and the window — a black strip along the top and a row of cut-off cells along the bottom, worst on
|
|
expert. `GET /status` still reports the window geometry; nothing consumes it now, it is a diagnostic.
|
|
|
|
**The wine prefix is confined to `C:` on every start.** A fresh prefix maps the whole filesystem to `Z:`
|
|
and symlinks the profile into the real home, and Minesweeper X has Open and Save dialogs for its replays.
|
|
That is why this was an ungrantable `execution` capability in the platform. `sidecar/prefix.ts` closes it
|
|
instead, which is what makes the plugin's permission safe to grant. Wine restores `z:` whenever it updates
|
|
a prefix, so the confinement is re-asserted rather than done once.
|
|
|
|
## Contract
|
|
|
|
```
|
|
GET /api/minesweeper/status { installed, running, window, error? }
|
|
POST /api/minesweeper/start idempotent; builds the prefix on a cold install (minutes)
|
|
POST /api/minesweeper/stop
|
|
/api/plugin-ws/minesweeper the display, as RFB
|
|
```
|
|
|
|
## Where this came from
|
|
|
|
Built first as a standalone directory, `officer-dev/wine-minesweeper`, and folded in here when it became a
|
|
plugin. Two things from that work are worth keeping, because both are judgement calls somebody will
|
|
otherwise make again from scratch.
|
|
|
|
**Wine came from Ubuntu noble, not the WineHQ repository.** The distribution package is older — 9.0 — and
|
|
that is fine for a 2017 VB6 application. The alternative was adding a third-party apt source, with its key
|
|
and its update cadence, to a machine that runs a bitcoin wallet and a mail store, for a game of Minesweeper.
|
|
If a future version genuinely needs newer wine, that trade is worth revisiting; until then it is not.
|
|
|
|
**Enabling i386 pulled four krb5 libraries forward** within noble-updates. That was checked before
|
|
installing rather than after: nothing moved near libc, systemd, openssl or postgres. Worth re-checking on
|
|
any host where `dpkg --add-architecture i386` is new, since it is the one step here that changes how the
|
|
whole machine resolves packages and is never undone by uninstalling the plugin.
|
|
|
|
### The original verification baseline
|
|
|
|
From the standalone build, on Ubuntu noble with wine 9.0. Useful as a comparison when something feels wrong
|
|
rather than as a test to run:
|
|
|
|
- The game launches in about a second and draws the classic beginner board at **152x214**.
|
|
- A synthetic `xdotool` click opens a region and starts the timer — so **input reaches the game**, which is
|
|
the part that matters for a playable stream and the part a screenshot cannot tell you.
|
|
- x11vnc completes an **RFB 003.008** handshake and reports an 800x600 32bpp framebuffer.
|
|
- Cold start, display through to game window, in under two seconds. That excludes building the prefix,
|
|
which happens once and takes minutes.
|
|
|
|
The shell prototype that ran all this (`bin/session.sh`) is not kept. Everything it knew — waiting for a
|
|
usable display rather than a live pid, parking the window at the origin, and clearing the whole wine
|
|
process family on stop — is in `sidecar/session.ts`, with the reasoning attached to the code that depends
|
|
on it.
|