Trimming the stage alone does nothing, and that is worth writing down: the scale is snapped, so at 3x the game is 1512 device pixels wide whether the stage is 560 or 512. Shrinking the surround only pays when it crosses a step. So both halves land together. ── the stage ── 560x400 down to 512x352, which is expert (504x342, measured) plus four pixels a side. There is nothing left to give — intermediate is 342 tall too, so anything shorter clips a board people actually play. ── half steps from 2x ── Not a retreat from whole-number scaling. The unit that has to stay regular is the CELL, not the pixel: this is a 16px grid, so at 3.5x a cell is exactly 56 screen pixels — as even as 3x or 4x — and only sub-cell pixels alternate between three and four. Below 2x that alternation is a third of a pixel rather than a seventh, so whole numbers still apply there. The case that started all this, a fractional devicePixelRatio at nominal 1:1, put the CELL boundaries at 26.4px, which is a different thing entirely. Together, on a 1800x1400 device-pixel panel: 3.0x becomes 3.5x, and the game goes from 1512 to 1764 device pixels wide. Known cost, and it is in the README: the Extras > Auto Options dialog is 328 wide and opens 25px right of the game, so it needs a 554-wide stage to fit whole from beginner. It cannot be closed from the panel anyway — no window manager, no title bar, no OK button — so that is the thing to fix before this number is revisited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
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 — 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 firstPOST /start. Deleting it is still the standard fix for a wedged wine and the next start rebuilds it — but it now takes the replays with it, see below.prefix/drive_c/Minesweeper/— where the game RUNS from, and where its replays land. The executable is copied here on every start;C:is the only drive the prefix has, so this is the only directory the game has a writable path to. Launched from the plugin checkout instead, its own path came back as\\?\unix\home\…and finishing a game raisedRun-time error '76': Path not foundwhile it tried to auto-save the recording.run/— pid state, logs, andvnc.sock. Mode 0700, and that is load-bearing.
Two gaps, both from having no window manager
Worth knowing before you conclude something is broken. Neither is hard to fix; neither is fixed.
Some dialogs cannot be closed. There is no window manager, so no window has a title bar. A dialog with its own OK button is fine — the run-time error box is — but Extras → Auto Options has neither an OK button nor a Cancel, and its only exit was the title bar. Restarting the sidecar clears it.
The panel sends no keyboard events at all. MinesweeperPanel forwards pointer events and nothing else,
so Escape does not reach the game and a Custom board cannot be typed in. Adding keys to the RFB client is
the smaller half of this; wine also expects a window manager to hand it focus.
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 512x352
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 snaps the scale, in DEVICE pixels, to whole numbers below 2x and half steps above it. Halves are safe
because the unit that has to stay regular is the CELL: at 3.5x a cell is exactly 56 screen pixels, as even
as at 3x, and only sub-cell pixels alternate.
That makes the size QUANTISED, which is the thing to understand before changing the stage: at 3x the game is the same width whether the stage is 512 or 560. Trimming the surround is worth nothing on its own — it only helps when it crosses a step. 512x352 is expert (504x342) plus four pixels a side, and there is no room left: intermediate is 342 tall too. A custom board bigger than that is clipped at the right and bottom rather than shrunk, with the menu bar kept reachable, and the Auto Options dialog needs a 554-wide stage to fit whole from beginner.
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
xdotoolclick 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.