Loading…
a327ex.com

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_move are 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-scaled run_time clock; 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_parse validates 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 seals score + #captured_pending and 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.txt persists 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 own devices.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_ts tracks 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: dedicated location = /api/games/submit with client_max_body_size 256k (30Hz cursor tracks outgrow the API-wide 16k), installed live + in deploy/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 DESCnow_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=false on the engine default shader; set_draw_shader is async on web). Consequence: gate the game's effect_setup (it drives zero effect axes) but do NOT no-op set_draw_shader in the host — that's the sanctioned path for the future 062026 slice.
  • renderer/games/knightvspawns/ is GENERATED by tools/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_HOSTED gating in game source: F3 tuner/F5 lab requires (+ sound_tuner_update stub), file I/O, effect_setup. sfx normalizes sound_get_path to assets/... so the desktop-authored DSP tuning table still matches hosted paths.
  • game_host extensions: sound_load/shader_load_file/spritesheet_load dir-prefix shadows, mouse_set_visible no-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-frame effect_clear with a nil effect_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 __index fallback 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_LAYERS 32 → 128 (Anchor3 bfe435f): 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_result pattern (id range 30000+), token polled from web_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_get EXISTS on desktop as a C no-op — test platform ~= 'web'), field reads ui_typed_text (the toolkit's ui_begin drains the engine buffer every frame), host gates engine_get_typed_text on capture (an on-screen game must not eat the site chat's keystrokes), game_host_capturing_text() suspends page shortcuts (beside chat_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 global g_font_embolden re-rendered every glyph FT_RENDER_MODE_NORMAL (8-bit gray) while font_blit_to_buffer unpacked rough fonts as packed 1-bit mono — gray bytes bit-tested = blobs. Fixed in Anchor3 c130be9: 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 real pixel_mode. Ships to the live site with the next --engine deploy.

Dev-loop infrastructure (now permanent):

  • serve-web.py proxies /api/* to production (forwarding User-Agent — Cloudflare 1010-bans urllib's default) and web-shell.html mints 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 live game_scores; wiped during the session and again at launch.
  • The local test message (::game knightvspawns in 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.