ABA Games Variations — KNIGHT VS. PAWNS 2
Fable 5 xhigh
Summary
Second KNIGHT VS. PAWNS session: built the complete scoreboard + replay system end to end (in-game recording/playback → live server API → site embed → scoreboard UI), debugged the first-ever full web-game publish on a327ex.com through ~10 real bugs, and wrote the reusable publishing guide (renderer/games/README.md). Items were deferred to the next session; the public launch (homepage message + deploy) waits until items are done.
Step 1 — Replay recording + playback (in-game, desktop-first):
- Chose event-log replay over deterministic resimulation: the entire gameplay-relevant history is three event kinds — pawn spawn (column), march beat, move commit (L-move index) — because
march_pawns/commit_moveare deterministic given board state and all other randomness is VFX-only. Playback re-runs the same functions by pumping the log against the same juice-scaledrun_timeclock; slow-mo re-derives from replayed leaks. - Wire format
KVP1|score|duration_ms|events|cursor: delta-encoded ms timestamps, events as<dt><kind><arg>tokens, cursor as<dt>,<dx>,<dy>.rec_parsevalidates everything (out-of-range columns/moves reject). - Cursor track at 30Hz with idle suppression (owner bumped from my initial 5Hz — "on fast movements it doesn't capture everything"): idle ticks emit nothing, a hold keyframe lands when motion resumes so interpolation doesn't drift through pauses. Ghost cursor = full-color emoji hand; the live cursor isn't drawn during replays (owner call).
- Sealing subtlety: captures still pending at death resolve after
die()(their countdown runs while dead), so the recording sealsscore + #captured_pendingand the determinism check (replay check: score N vs recorded N — OK) waits for pendings to empty. - W on the game-over screen rewatches through the serialized form (exercises the wire format every use);
replay_last.txtpersists a run across desktop restarts (gated off hosted).
Identity design ("sign your run") — the owner wanted fixed device identities plus per-game/per-run names:
- Privacy wrinkle surfaced:
devices.name(chat) is private (visible only to the visitor + owner console); a scoreboard is public. So scoreboards get their owndevices.game_name(auto adjective-noun at first score contact, born public). - Score rows key
(game, device_id, name)with NULL name = the fixed identity (display via COALESCE at read time, renames propagate) and non-NULL = a frozen alias. Best-run upsert per identity;last_tstracks the per-game prefill ("the name you last played under"); aliases are sticky per game. - Submit fires once per run: enter, new-run, or a 10s idle timeout (the mobile/walk-away catch-all — no soft-keyboard path in-engine v1).
Step 2 — Server API (live on a327ex.com):
- Migration 12:
game_scores(+ expression unique index on(game, device_id, COALESCE(name,''))) +devices.game_name. server/games.lua: registry + the KVP1 validator that re-simulates every replay with pure grid/integer logic (mirror of spawn/march/commit; trailing same-frame post-death spawns allowed — the game really records those; loose cadence heuristics kill sparse-pawn forgeries). Forged scores can't land; every board entry is watchable by construction.- Endpoints
POST /api/games/submit(device token, 60/hr per owner call, upsert-on-improvement),GET /api/games/scores(top N +you= prefill/best/rank/alias),GET /api/games/replay?id=. Muted devices submit normally but vanish from public reads (the chat mute reused). nginx: dedicatedlocation = /api/games/submitwithclient_max_body_size 256k(30Hz cursor tracks outgrow the API-wide 16k), installed live + indeploy/nginx-a327ex-engine.conf. - Tested on a VPS scratch instance (A327EX_PORT/A327EX_SECRETS): 15 tests — honest submit validates against the independent simulator first try, byte-identical replay round-trip, tampered score/header, post-death-event forgery, alias rows, mute, prefill tiebreak (
last_ts DESC, id DESC—now_ms()is whole-second). Then a live smoke test; test rows wiped. - Incident (reported honestly): a scratch-restart command ran the server with default secrets for a moment — it crashed on the port bind but had already applied migration 12 to the LIVE DB. Verified harmless (additive schema, live server clean, deploy later found version 12 and skipped).
Step 3 — Site embed (the first emoji-family ::game):
- Owner redirected the draw-shader plan to the 2026-07-18 "Website" session (whole-browser freeze: the 41KB ricochet ubershader cold-compiling ~8.7s in Chrome's GPU process; site now runs
effects=falseon the engine default shader;set_draw_shaderis async on web). Consequence: gate the game'seffect_setup(it drives zero effect axes) but do NOT no-opset_draw_shaderin the host — that's the sanctioned path for the future 062026 slice. renderer/games/knightvspawns/is GENERATED bytools/sync_knightvspawns.sh(asset whitelist; LanaPixel subset 3.15MB → 30KB Latin+European per owner choice; no draw_shader.frag). 2.1MB package inside renderer.data.GAME_HOSTEDgating in game source: F3 tuner/F5 lab requires (+sound_tuner_updatestub), file I/O, effect_setup.sfxnormalizessound_get_pathtoassets/...so the desktop-authored DSP tuning table still matches hosted paths.- game_host extensions:
sound_load/shader_load_file/spritesheet_loaddir-prefix shadows,mouse_set_visibleno-op,layer_draw(dx,dy) offsets carried through the composite queue, GAME_DEFS entry (480×270 pixel, restart chip).
The embed debug saga (each fix now permanent host/engine infrastructure):
anchor/layer.lua:232 bad argument #2 to 'shader_set_int' (got nil)—ui_begin's per-frameeffect_clearwith a nileffect_draw_shader; the game's emoji/effect.lua lacked the nil-guards ricochet got on 2026-07-18. Ported (effect_set/effect_clear/effect_write_deco).game_host.lua: attempt to call a nil value (layer_draw_into)— renderer/anchor.exe was the July-1 cutover binary, predating every ::game engine feature; embedded games had never run on the desktop renderer at all (wasm always built from Anchor3). Rebuilt Anchor3 desktop → renderer exe now tracks Anchor3 HEAD.- Total black screen including the site once the game started, sounds still playing. Diagnosed with temporary F6/F7/F8 stage-skip toggles + reading the F7 misrender as geometry (sidebar at 3× scale ≈ the site drawn into a 640×270 buffer): the emoji pipeline writes layer globals via explicit
_G[...]—_G['ui_layer'] = layer_new('ui')punched through the env's__indexfallback and clobbered the site's ui_layer with the game's HUD layer. Fix:env._G = env(the sandbox env IS the game's global table). Also fixed text.lua's_G[tag]color lookups resolving against the site palette. - Engine
MAX_LAYERS32 → 128 (Anchor3bfe435f): an emoji-family game registers ~28 layers; switched-out instances keep theirs registered.
Step 4 — Scoreboard UI + wiring:
- Death screen: HIGH SCORES panel (top 8, your identity green-striped, click a row → fetch + watch that replay in-feed with a WATCHING banner; exit returns to the board, arcade-style) + auto-focused name field.
- HTTP via the site's
web_http_get(id,...)/web_http_resultpattern (id range 30000+), token polled fromweb_boot_param('deviceToken'); submit = fire-and-forget POST + 0.6s board refetch for the outcome (the chat-send pattern). Desktop = MOCK MODE (fabricated board, mock submits update it like the real refetch). - First test round fixes: mock detection (
web_http_getEXISTS on desktop as a C no-op — testplatform ~= 'web'), field readsui_typed_text(the toolkit'sui_begindrains the engine buffer every frame), host gatesengine_get_typed_texton capture (an on-screen game must not eat the site chat's keystrokes),game_host_capturing_text()suspends page shortcuts (besidechat_capturing), text focus latches capture. - Owner UX round: select-all prefill (blue band; first keystroke replaces — "if I type a new name the old name remains" bug), mock submits update the mock board.
- Web round: the 'a32' auto-save bug = the 10s timeout firing mid-typing → any field interaction disarms it; silent no-save = 'a327ex' is a RESERVED name substring → server 400s invisibly to a fire-and-forget POST. Client now mirrors the rule ("that name is reserved" in red), non-enter paths fall back to the default identity, and the server logs every submit rejection. Note: the owner literally cannot sign his own board (owner-pass = possible follow-up).
- Panel/game "z-fighting" (web only) — on web the engine renders frames with ZERO update steps (fixed timestep vs rAF jitter) and update-queued widget draws vanish for those frames. Panel visuals moved to draw-side paint (
sb_panel_draw), interactions stay update-side against recorded rects (the site chat's pattern). Panel widened to 300px (past the 240px board); strip pixel-cap 0.8 → 0.9 of viewport height (ordinary windows were stuck at k=2). - Font antialiasing → white-blob glyphs → the real engine bug: hosted,
set_filter_mode('rough')is a no-op (site owns the global filter), so the host defaults pixel-game fonts/textures to rough. Rough fonts then rendered as solid white outlined blobs — NOT web-specific but embolden-specific: the site's globalg_font_emboldenre-rendered every glyphFT_RENDER_MODE_NORMAL(8-bit gray) whilefont_blit_to_bufferunpacked rough fonts as packed 1-bit mono — gray bytes bit-tested = blobs. Fixed in Anchor3c130be9: embolden skips rough fonts (mono has no AA thinning to counter; pixel fonts keep their exact standalone look) and the blit branches on the bitmap's realpixel_mode. Ships to the live site with the next--enginedeploy.
Dev-loop infrastructure (now permanent):
serve-web.pyproxies/api/*to production (forwarding User-Agent — Cloudflare 1010-bans urllib's default) andweb-shell.htmlmints the device token (engine-boot's identify bridge mirrored; no sessionId so analytics stays inert in dev) — the full submit/fetch/watch loop tests locally. Dev rows land in the livegame_scores; wiped during the session and again at launch.- The local test message (
::game knightvspawnsin home.md + posts mirror) was staged for each test round and reverted before this seal — deploy.sh rsyncs the working tree, so staged test content must never sit across sessions.
Documentation: a327ex-site/renderer/games/README.md — the full publishing process (hosted gating → generated package → GAME_DEFS → test tiers → scoreboard wiring → launch order) + a symptom-indexed gotcha catalog of everything above. Memory topics updated (project_abagames, project_anchor_website).
State / next: all four layers verified (desktop standalone + desktop renderer + local web full loop, owner-confirmed). NOT public: no ::game element in published content; the live wasm predates the engine fixes until a --engine deploy. Next session: items; then launch = wipe game_scores + game_name, owner's homepage message, deploy.sh --engine --content.
🔒 Only the summary of this log is public. Private because it contains release internal details.