feat(levels): rename the "smart" animal tile to "acrobatic" (#48)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
84cceb3
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(levels): rename a tile and drop the lion target on the animal board (#47)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
0f9f5dc
Swap the "flying" tile for "smart" and remove "lion" from the animal
board's targets/labels (now 9). Sync the CLAUDE.md target count.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(onboarding): tidy the welcome modal layout (#46)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
3037fcd
Visual polish on the welcome card:
- Drop the inline ⏎ glyph; the sentence now reads "the Enter button" in
words, leaving a single carriage-return icon (the keycap) so the two no
longer compete.
- Balance the title across its two lines (text-wrap: balance) and give the
boxed "targets" room to breathe.
- Lighten the "targets" box (2px rim, 1° tilt) so it echoes the HUD target
chips instead of overpowering them.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(onboarding): add a line-art welcome modal on game open (#45)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
8429f6a
Greet the player with a hand-drawn paper-and-ink card over the
dropping-in board:
- Title "Get the LLM to guess all the targets", with "targets" boxed
like the real HUD target chips.
- Body "Jump on the ⏎ button to submit your description", with an inline
carriage-return keycap echoing the close button.
- A ⏎ close button rendered like the actual submit tile (soft ink,
dashed "press me" rim, centred glyph).
Closes on the ⏎ button, Enter/Escape, or a backdrop tap; gameplay hops
are held off (handleDir guard) until it's dismissed. Pure DOM/CSS over
the existing overlay pattern — no Modal creds needed.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(levels): add the countries board with the column-shuffle mechanic (#44)
Ben Blaker
Unverified signature
3663d53
## What & why
Board four — **`countries`** (`order=3`, reached after the subjects board). The
judge reads which country a sentence sounds like: USA, Russia, Italy, France,
India, China, Korea, England.
### New mechanic — the `shift` / column-shuffle tile
The user's `[swap]` tile, named **`shift`** in code so it doesn't collide with
the existing off-edge board-navigation *swap tiles*.
- Shift tiles render as hand-drawn keycaps stamped with **two stacked accent ⇨
arrows** (wordless, dashed "press me" rim).
- The top row is all structural: a blank `start` in the middle flanked by four
shift tiles. Hopping onto one **rotates every row UNDER the top row one column
to the right**; the rightmost cap of each row **wraps around to become the
leftmost**, arcing up-and-over the caps that slide in beneath it. The top row
never moves.
- Landing on a shift tile costs one budget hop and appends no word (consistent
with `wings` / every land, and routed through #42's new `settleIdle()` …
feat(layout): centre the sentence and unify the desktop/tablet board (#43)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
cfa0c79
Centre the in-progress sentence and anchor it a fixed gap below the
target checklist (tracking the measured HUD height --hud-h) instead of
the fixed top-left position.
Extend the desktop layout down to phone widths: move the compact
media-query breakpoint from 1240px to 560px so tablet/medium windows
keep the generous desktop fonts rather than the cramped compact layout.
To stop the board colliding with the wrapped checklist + sentence at
those widths, the stage drops below the chrome adaptively — full height
when the HUD is a single row (biggest board on wide screens), lowering
only as --hud-h grows. The camera re-centres the board in that band on
wide/tall windows; mobile and landscape framing are unchanged.
Verified with Playwright on the emotion and animal boards at 1280 /
1000 / 900 / 640px and on portrait + landscape phones: the sentence is
centred and clear of the targets, the board clears the sentence, no
collisions, and the mobile layout is preserved.
Co-authored-by: Claude…
feat(portal): spinning-spiral teleport tiles on the animal board (#42)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
8d3ebb8
Three spinning ink-spiral portals on the animal board (board 2), each linking to
the next clockwise (top→right→bottom→top). Hopping onto one shrinks the doodle
into the source spiral and expands it out of the destination. "portal" is a new
reserved tile word (wordless, never reaches the judge); the landing hop costs
budget. Spirals spin in the render loop (mesh spin, no texture redraw).
Rebased onto #41 (the wings board): #41 reserved the word "wings", which collided
with the literal "wings" the layout placed at animal (3,2) — renamed that cell to
"winged" so the animal board stays portal-only.
Verified in-browser (Playwright): all three clockwise links walked live, spirals
spinning, shrink-in/expand-out teleport, "winged" renders as a normal word, 0
console errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(levels): add the subjects board with the wings flight mechanic (#41)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
45107cc
Board three (subjects, order=2), reached after the animal board. The judge
reads which academic subject a sentence sounds like (maths, science,
philosophy, economics, literature, languages, art, music).
New mechanic — wings:
- `[wings]` tiles render as hand-drawn accent wing keycaps (wordless, like
start/⏎).
- Hopping onto one lifts the doodle into the air: it sprouts wings (new "fly"
pose) and rises to cruise altitude, wings slowly flapping on their own timer
(independent of the idle "boil", so they keep beating mid-hop).
- The next two hops glide FREE — airborne, no word appended, no budget spent.
- The third hop descends "down from the sky" and lands for real, e.g. onto a
⏎ to press submit. Wings are a clean exit: lock in the sentence you've
written, then soar over the rest of the keys to the button without typing
them.
Flight state resets on board change, death, fresh attempt, and swap-tile
transitions. Adds takeoff (soar) and wing-beat (flap) sounds.
Verified locall…
- Targets checklist wraps to a full-width second row (title + counter keep row 1); board/prompt drop below via a --hud-h CSS var.
- Compact breakpoint raised 560px → 1240px so mid-width windows/tablets stop wrapping big chips over the board; desktop ≥1240px unchanged.
- Portrait board trims the camera side-margin to fill ~85% of the width without clipping edge columns.
- Framebuffer pixel-ratio cap raised 2 → 3 so the board isn't soft on dpr-3 phones (textures stay at 2×).
- Added CLAUDE.md (agent orientation).
feat(emotion): replace sarcastic and surprised targets with scared (#39)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
9cebd73
Drop the "sarcastic" and "surprised" emotion targets/labels from the
home board and add a single "scared" target in their place, taking the
list from 8 targets to 7. The grid's "yikes"/"hurt"/"disturb"/"angry"
words make a scared read reachable.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge pull request #38 from benjosaur/fix-gradio-modal-bugs
Ben Blaker
Unverified signature
d39d214
fix: Safari mixer, sentence-clipping and animation jank on the HF Space
Merge pull request #37 from benjosaur/board-bang-tile-drop-tests
Ben Blaker
Unverified signature
d1dd7b9
feat(emotion): restore the "!" tile; drop the test suite
Remove tests/, the pytest config, the pytest dev dependency, and the README
line documenting the test commands. Nothing references them anymore.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(emotion): restore the "!" punctuation tile at (1,3)
Swap the right-hand "little" back to "!" so the feelings board keeps a
punctuation tile that attaches to words (e.g. "great!"). start (1,1) and
the ⏎ submit (2,3) are unchanged, so the parity that keeps two-word reads
reachable still holds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
perf(render): smooth animation by gating and staggering the sketch boil
The boil redrew all ~16 hi-dpi (768px) tile canvases and re-uploaded their
textures to the GPU in a single setInterval tick, 3x/second. Measured on
HF/Safari that was a ~70ms main-thread stall every ~340ms — frame spikes of
40-94ms spaced one boil-interval apart — which dropped frames mid-animation
and read as choppy, jagged, disjointed motion.
Two changes, no loss of the hand-drawn liveliness:
- Only boil while idle: during a hop / judge bob / verdict the motion
already carries the life, and a stalled frame mid-motion is precisely what
reads as jank, so those frames are left for the animation.
- Round-robin a small fixed slice (2 of ~18 surfaces) per tick instead of
the whole board at once, so no tick redraws/uploads more than ~2 textures.
Re-measured: severe (>40ms) spikes during motion essentially gone, jank
frames (>25ms) cut ~3x, median steady at ~17ms (59fps).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(emotion): reshape the feelings board and expand to 8 targets (#36)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
3402747
Redraw the home board's grid (slowly→yikes, the old "!" tile → little) and
grow the collectible set from 3 to 8 emotions: happy, elated, irked, sarcastic,
distraught, furious, betrayed, surprised. The judge's candidate labels track
the targets.
Update the solution-path tests that referenced the removed "!" tile and refresh
the judge live-eval's board sentences for the same reason. All level invariants
still hold — grid rectangular, start blank, every target scoreable, solvable
within budget.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(prompt): render the written sentence un-clipped in Safari
The chip write-on revealed text by animating clip-path with negative
insets, then removed the clip with clearProps. Inside HF's iframe under
Safari/WebKit that loses both ways: Safari clamps negative clip-path insets
to 0, and it won't repaint a clip-path that's later removed until a manual
reflow. So the just-written word stayed shaved off on the right until you
inspect-element — every fresh load.
Reveal by sweeping the right inset 100%->0 and END on a plain inset(0): no
negative values, never removed. Pad .sq-chip-text (cancelled in layout) so
inset(0) wraps every glyph, slant and descender with room to spare.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(fonts): preload both Caveat weights before boot
document.fonts.ready only waits for faces already in active use, and the
Google Fonts stylesheet loads async — so at boot Caveat 600 (the verdict
card) wasn't loaded yet, and the card rendered in a fallback face then
reflowed once the webfont landed. Explicitly request every weight we use
(Patrick Hand 400, Caveat 600 + 700) so all text has final metrics before
anything lays out or animates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(mixer): keep the audio mixer open while you use its controls
Two ways the mixer dismissed itself mid-use:
- A quick close→reopen landed in the 0.15s close fade: the stale fade's
onComplete still ran and re-hid the popover (display:none) while popOpen
stayed true. The next tap on a now-invisible slider fell through to the
board, and the outside-tap handler closed the mixer. openPop() now kills
any in-flight close, and the close onComplete bails if we've reopened.
- Inside HF's iframe under Safari/WebKit a slider's pointer capture can
retarget the pointerdown so the outside-tap handler's .contains() check
reads false and a drag dismisses the mixer. Stop pointerdowns that start
in the popover from reaching that handler at all.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge pull request #35 from benjosaur/debug-modal-consistency
Replace the few-shot "<sentence> is the same as" -> "<label>" framing with
a single system instruction: name the targets and ask for the one most
similar to the player's message, sent the bare sentence as the user turn.
Drops the per-board few-shot examples and the Level.examples field along
with all the "is the same as" suffix plumbing.
The Modal endpoint is prompt-agnostic (it scores whatever messages arrive),
so no redeploy is needed for this to take effect in the app.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(judge): drop the JUDGE_FAKE offline stub
The stub favoured the first uncollected target with hardcoded logprobs —
useful before the Modal judge existed, now just a divergent code path that
masked real misconfiguration. Local dev points at the deployed endpoint via
.env; a missing MODAL_JUDGE_URL now errors clearly instead of silently
falling back to fake verdicts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(judge): condition prompt on full target set, not remaining
The judge system message was built from remainingTargets(), which
shrinks as the player collects targets — so the same sentence got a
different prompt (and could flip verdict) within a session. Condition
on the stable level.targets; keep the remaining set only for the
winner-in-targets win check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ci(modal): auto-deploy the judge on push to main (#34)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
17c3e5f
A deployed Modal app is a snapshot of the code at `modal deploy` time, so the
GPU judge doesn't update until it's redeployed. Wire that into CI: on push to
main touching modal_judge.py (and via manual workflow_dispatch), run
`modal deploy modal_judge.py`.
- Path-filtered to modal_judge.py so unrelated UI/audio commits don't trigger
pointless redeploys.
- Runner only installs the `modal` CLI; torch/transformers build remotely on
Modal's builders, so the job is cheap.
- The stable *.modal.run URL is unchanged by redeploys, so the HF Space's
MODAL_JUDGE_URL secret needs no coordination.
Requires repo secrets MODAL_TOKEN_ID / MODAL_TOKEN_SECRET (Modal dashboard →
Settings → API Tokens).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(modal): bake the judge model into the image so snapshot restore can't break (#33)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
9bca9a5
The memory snapshot loaded MiniCPM3-4B from a Modal Volume (HF_HOME=/cache),
so the snapshot held mmap/FD references walking into the Volume's `hub` dir.
Volumes are mutable and not kept in lockstep with snapshots, so when the cache
changed (e.g. the unpinned trust_remote_code re-fetch rewriting the modeling
files) restore failed re-walking that path:
vfs.CompleteRestore() failed: failed to walk ".../hub": no such file or
directory -> exit 128
Fix: move the files the snapshot points at onto the immutable image layer.
- Download weights at image-build time (`run_function(download_model)`); drop
the `semantique-hf-cache` Volume entirely. The snapshot now references only
the content-addressed image, which can't drift or be deleted.
- Pin the revision (d6b14dda) and load offline (HF_HUB_OFFLINE /
TRANSFORMERS_OFFLINE) so trust_remote_code never phones HF or rewrites the
cache at load time.
- Add @modal.concurrent(max_inputs=4): let one warm L4 serve a small burst of
…
fix(embed): keep the HF board fit scroll-invariant (#32)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
8d256bc
The embed-fit `avail` folded in `info.scrollTop`, so the board refit on
every parent scroll tick. Across a scroll only `scrollTop` changes
(`clientHeight`/`offsetTop` are constant), so each tick swung the height
and called `renderer.setSize()`, reallocating the WebGL drawing buffer —
the "lags much more on HF" jank, with the board reframing mid-scroll
clipping the canvas-baked keycap text. A real HF page is always
scrollable, so it fired constantly; localhost has no iframe and never
reproduced.
Drop `scrollTop` from `avail` (now `max(0, clientHeight - offsetTop)`,
preserving the "fit below HF's header" intent) and guard `apply` with a
`lastKey` so pure-scroll getPageInfo events early-return before any
fit/reflow. Genuine viewport changes (resize/rotate) still shift
`clientHeight` and refit.
Verified in a cross-origin iframe-resizer-v4 repro: buffer churn while
scrolling drops from 4 distinct sizes to 1 stable size, the resize path
still adapts (738->532->892), and the embedded board …
chore: ignore local audio source masters (#31)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
d01c1a5
The root-level Cipher2.mp3 and pencil-scratch.mp3 are high-bitrate source
masters kept locally; only their downsampled copies under static/ ship.
They were untracked but not ignored, so a stray `git add .` could have
committed the 9.2 MB master. Anchor the ignore rules to the repo root so
static/*.mp3 stays tracked.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(audio): replace synth bg music with the Cipher2 track (#28)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
fc31f3c
Swap the procedural C-pentatonic synth loop for a recorded track,
downsampled to 128 kbps (~3.7 MB). It's too big to inline as a data URI
like the pen-scratch SFX (that would bloat every page load), so it
streams from static/ via gr.set_static_paths and the file route, with
the URL resolved against the Gradio root so it holds up inside the HF
Spaces iframe.
A looping <audio> element feeds the existing musicGain bus, so the
in-game mixer (slider, mute, localStorage persistence, first-gesture
unlock) drives it unchanged. The pause-on-mute is deferred until the
pending play() settles, so a fast mute->unmute mid-load can't wedge the
element.
Verified locally (Gradio 6.18, Playwright): the file route serves
audio/mpeg with range support, the track plays and loops in real time
through the bus, and mute/unmute -- including rapid toggles during load
-- behaves correctly, with no console errors. HF-iframe streaming still
needs a real deploy to confirm.
Co-authored-by: Claude Opus 4.8 (1M con…
fix(ui): enlarge target words and center them when they wrap (#30)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
c949a43
The target words in the HUD rendered at 20px on laptop/desktop, which was
too small to read at a glance. Bump them to 32px (the dominant HUD text),
keep phones at 20px via the existing mobile media query so the compact
single-line HUD is unchanged, and add justify-content:center so wrapped
lines center under each other instead of left-aligning.
Verified in-browser at 1440x900 (computed 20px->32px, no HUD collision),
390x844 (still 20px, no overflow), and a wrap-forcing 580x900 (two lines,
both centered on the same x).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(layout): keep the grid centred when the forward swap tile appears (#29)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
a5f7f73
Previously the board shifted left by half a gap whenever a next board
existed, so the grid only re-centred after the swap tile was added. Pin
the board at x=0 instead; the camera already frames x=0 with the swap
tile's width, so the grid reads centred from the start and the lone tile
sits in the right margin.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(ui): retitle the out-of-hops stamp "context window exceeded" (#27)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
efe291e
Leans into the LLM theming: the death card's red stamp now reads
"context window exceeded" instead of "out of hops!".
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(audio): pen-scratch + keycap-press sound as each word is written (#26)
Ben Blaker
Unverified signature
6f2eb74
Play a real pen-nib recording (trimmed ~0.37s clip, inlined as a data URI) on each word's handwriting reveal, alongside the keycap-press thock. Rides the sfx bus so the in-game mixer levels/mutes it. Verified in-browser (sample plays once per word, no console errors); 18 unit tests pass.
feat(judge): self-host emotion judge on Modal GPU with exact logprobs (#24)
Ben Blaker
Unverified signature
9f36d31
Replace the HF Inference Providers call (top-20 logprob cap + missing-label floor) with a self-hosted MiniCPM3-4B on a Modal GPU that reads exact full-vocab probabilities. judge.py builds the prompt and POSTs {messages, labels} to a scale-to-zero Modal endpoint that scores each label's whole token sequence in one batched forward pass. Reconciled with the modular levels/ architecture on main.
feat(nav): swap boards by hopping a one-way progression tile (#25)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
b9f18ee
Replace the two bottom-corner nav buttons with an in-world keycap that
sits just off the board edge. Once every target on a board is collected,
a "→ <next>" tile appears; hopping onto it transports the doodle to the
next board's start tile and rains the rest of the keycaps in from the
sky, staggered by distance, before play resumes. Progression is one-way
(no back tile). The same drop-in plays as the board intro.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(levels): retheme the home board to happy/betrayed/surprised (#23)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
aa58329
Swap the home board's targets from happy/sad/angry/scared to
happy/betrayed/surprised, change the (1,0) tile from "apple" to "yikes",
and recalibrate the judge few-shot examples to the new labels
(hurt→betrayed, yikes→surprised). The start tile and ⏎ submit tile keep
their positions, so grid parity is unchanged.
The live board-sentence eval was coupled to the old labels and stripped
the few-shot examples to measure zero-shot discrimination — which never
surfaces "betrayed"/"surprised" as a first answer token. Rework it to
mirror the actual in-game judge() call (passing level.examples), which is
the discrimination players really get and what makes those labels
reachable.
Verified: full suite green (30 passed incl. live HF eval); rendered the
board in-app and confirmed grid + target chips match.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(ui): stop clipping the ! and ? sentence chips (#22)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
8633182
The chip write-on reveal animates a clip-path inset; gsap leaves the end
state (`inset(-20% -10% -20% -10%)`) applied permanently. The right inset
is a percentage of the chip's box, so for a single-character `!`/`?` tile
(~12px wide) it's ~1px — too little to clear Caveat's forward slant, and
the lean stays clipped forever.
Clear the inline clip-path once the reveal finishes (gsap `clearProps`),
so no residual clip lingers on any chip.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(audio): background music and an in-game volume mixer (#21)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
7d3cc83
Add a fully procedural background music loop (generative C-pentatonic
melody over a soft I–vi–IV–V bass, on its own Web Audio bus) and an
in-game volume mixer. Tapping either top-right icon opens a hand-drawn
popover with two wobbly-ink sliders — music and sfx — each with a mute
icon; it closes on an outside tap or Escape.
Each channel carries a 0..1 volume on an independent bus, squared into a
perceptual loudness curve; dragging snaps the gain live while mute/unmute
ramps it and restores the prior level. Levels persist in localStorage and
the bar icons show a red ink slash when a channel is at zero. Music
defaults on and starts on the player's first gesture.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(ui): restore the ⏎ submit tile and a solid start outline (#20)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
48c6837
Revert the submit tile's label from "=" back to "⏎" (the carriage-return
glyph) across the level grids, board engine, tests, and docs. The judge
prompt is "<sentence> is the same as" now, so the tile is a purely visual
marker — the rename touches no scoring logic.
Also drop the dashed rim on the start tile so it reads as a normal solid
keycap; only the submit tile keeps its dashed "press me" cue.
23 fast tests pass; verified both boards render in-browser (fake judge).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(layout): keep tiles clear of the hop hint line (#19)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
491673f
The stage was full-bleed (inset: 0) and the board frames itself hugging
the stage bottom, so the bottom tile row rendered straight under the
"arrow keys / WASD to hop" (or "swipe to hop") line. Reserve that bottom
band as non-play area; the existing camera framing reflows the board into
the shorter stage, and the death-slide now exits the canvas above the hint.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(layout): remove residual vertical overflow in the HF Spaces iframe (#18)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
da194a2
The previous embed fit filled the parent's full viewport, but the app iframe
sits below HF's header (offsetTop) and Gradio's shell (.main.fillable/.wrap) is
a constant few px taller than our component — so the page still scrolled ~96px.
Fill the actual slot instead: parent viewport minus the iframe's offset below
the top, then subtract the measured shell chrome (body height − root height) so
the whole document equals one screen. Clamp to one viewport and cache the chrome
to avoid oscillation. Verified 0 overflow on desktop (818=882−64) and mobile
(780=844−64); the full board, HUD and hint fit on a single page either way.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(judge): enrich prompt with targets and per-level few-shot examples (#17)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
d39c87a
Reshape the judge prompt that scores a board sentence against the level's
candidate labels:
- Add a system message listing the still-needed `targets`, threaded through
judge() -> score_labels().
- Replace the bare "=" in the user message with "is the same as", reading as
an equivalence while still ending right where the answer token is predicted
(constrained-decoding logprob mechanism unchanged).
- New optional `Level.examples` field: per-level (sentence, label) pairs
expanded into few-shot user/assistant turns to calibrate the judge. Kept
server-side only (not in client_value) so they don't spoil the puzzle.
emotion and animal boards each get four board-vocabulary examples.
Update the check_logprobs diagnostic to mirror the new prompt shape, and the
live eval test to pass all labels as unbiased targets. All non-slow tests pass.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(layout): keep the board on-screen in the HF Spaces iframe (#16)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
83e6847
HF embeds the Space in an iframe driven by iframe-resizer, which sizes the
iframe to its content. The board's `100dvh` root then resolves to the iframe's
own height, so each measure-and-grow cycle made it taller without bound — the
vertically centered 3D board ended up thousands of px below the fold ("it all
drags down"). Local runs have no iframe, so `100dvh` was the real window and the
bug never showed.
When embedded, let the document be content-height and pin the game to a fixed
pixel height (the parent's visible viewport via iframe-resizer's getPageInfo,
with an instant innerHeight/screen fallback) so the iframe settles instead of
feeding back on itself. The non-embedded path is untouched.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(ui): drop redundant labels and the extra check tick (#15)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
ff1852e
The "targets:", "sentence:", and "hops:" labels stated the obvious — the
checklist pills, the prompt strip, and the "N/budget" counter each read
clearly on their own — so remove them and the now-dead CSS (including the
tiny-phone rule that only hid the "targets:" word).
A collected target already reads as collected via the fade + strikethrough,
so drop the redundant green ✓ tick. checkOff now gives a brief scale pop on
the pill itself for feedback in place of the tick animation.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge pull request #14 from benjosaur/add-level-modular-api
Ben Blaker
Unverified signature
37c9a17
feat(levels): modular drop-in level API + animal board
refactor(judge): simplify to a "<sentence> =" prompt; rename ⏎ tile to =
The judge prompt is now just the assembled sentence followed by " =", and the
board's candidate labels alone constrain the answer (emotions on one board,
animals on another). The submit tile changes from ⏎ to = so the tile you hop
onto is literally the prompt's operator.
- levels: Level drops category/few_shot/question, keeping grid + labels. Grids
use "=" for submit tiles.
- judge: score_labels(sentence, labels) sends one message, no examples.
- engine / tests / README / logprobs probe: ⏎ -> =.
23 fast tests pass; verified both boards submit onto "=" in-browser (fake judge).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(levels): modular drop-in level API + animal board
Make adding a board a one-file drop-in, and ship a second level.
- levels/ package: each board is a self-contained Level (grid + its own judge
spec — category, few-shot, labels), auto-discovered from its file; order/home
set play order and the boot board. Replaces the single level.py.
- judge is now category-agnostic: score_labels takes a Level, and judge()
resolves the board from the level_id the client sends (labels/few-shot stay
server-side).
- engine: empty "" tiles are walkable but wordless; board build factored into
buildTiles/buildTargets/loadLevel so boards swap client-side; camera reframes
to board size; checked targets persist per board.
- nav: hand-drawn ink arrows walk between boards — "next" unlocks after the
first target is collected, "back" returns to the previous board.
- animal board: 5x5, central start, four ⏎ corners, walkable blanks, ten
animals to collect.
25 fast tests pass; verified both boards in-browser with the offline judge.
Co-Authored-By:…
feat(mobile): swipe controls and responsive layout (#13)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
a808a72
Make the game playable on phones. The board was keyboard-only and assumed a
desktop window.
Controls: a swipe maps 1:1 onto the existing arrow keys. Pointer Events on the
stage read the gesture's dominant axis on release and feed the same DIRS vector
through a shared handleDir() (keyboard + swipe), so mid-hop buffering still
applies. Touch/pen only — mouse stays on the keyboard. A <28px drag is a tap and
is ignored.
Layout: 100dvh, touch-action/overscroll locked so swipes don't scroll/zoom;
safe-area insets; a compact pass for narrow (portrait) and short (landscape)
viewports keeps the HUD on one line and recenters the board into the clear band;
tiny phones (<=340px) drop the "targets:" word so the hop counter never clips.
Viewport meta is locked from JS (single tag, no pinch/double-tap zoom) and
Gradio's wrapper padding is zeroed for a true full-bleed.
Verified with Playwright at 320/360/390 portrait + 844x390 landscape: all four
swipes hit the correct tile, tap/mouse ignored, keyb…
refactor(ui): replace LLM jargon with friendlier game labels (#12)
Ben Blaker+1: Claude Opus 4.8 (1M context)
Unverified signature
27f427c
Make the game read like a puzzle, not a model-internals demo:
- HUD "context window: N/10" -> "hops: N/10"; overflow copy reworded
("last hop\! one more and you're out…", death/stamp "out of hops…")
- <bos> tile -> blank home square; <eos> tile -> ⏎ (carriage-return symbol)
- "prompt:" strip -> "sentence:", enlarged + bolder, start marker hidden
Tokens stay "start"/"⏎" internally (level grid + JS comparisons); only the
player-facing text changed. README and level tests updated to match.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(targets): replace single target with collect-them-all checklist (#11)
Ben Blaker+1: Claude Fable 5
Unverified signature
557ba69
All four emotions now show as a checklist in the HUD. Winning a round
checks off the emotion the judge picked; checks persist across rounds.
The judge payload sends the still-unchecked targets, so a repeat emotion
stamps "<emotion>, again." instead of a win, and the verdict bars
highlight only the targets still left to collect.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
feat(prompt): move prompt strip from bottom to just under the HUD (#9)
Ben Blaker+1: Claude Fable 5
Unverified signature
3d6fe70
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>