Skip to content

Add an image tile view to Sliding Puzzle - #575

Merged
mpiorowski merged 5 commits into
mpiorowski:mainfrom
mojoro:feat/sliding-puzzle-image-mode
Sep 8, 2026
Merged

mpiorowski merged 5 commits into
mpiorowski:mainfrom
mojoro:feat/sliding-puzzle-image-mode

Conversation

@mojoro

@mojoro mojoro commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a session-only image tile view to Sliding Puzzle, toggled with i, leaving the board, moves, persistence, and reward contract untouched
  • pre-encode one opaque native Kitty/iTerm2/Sixel image per board cell — one per tile value plus one per destination for the solved board — once per source/geometry/protocol, so a move reorders already-encoded cells and only the two changed placements are retransmitted
  • fall back to Chafa fragments cut from the solved-position preview on terminals without a native protocol, and to numbered tiles while loading, on failure, or when the board area is too small
  • generalize the Sixel-only pre-frame wipe to all persistent rasters (pre_frame_persistent_raster_wipe_bytes), covering iTerm2 and non-modal placements, and add stream_modal / history_modal to the raster overlay lists so they match foreground_terminal_overlay_open
  • give the board rect one definition (App::content_area, arcade::ui::SHOW_GAME_BOTTOM_BAR) shared by the draw path, mouse hit-testing, and the pre-frame wipe, replacing the hand-rolled layout in arcade/input.rs

Review

  • reviewed post-rebase by two independent agents scoped to the shared terminal-image infra and to the game code; findings triaged below
  • fixed during review: a clippy::needless_lifetimes error that would have failed CI; an ImageTiles::release leak where a request still in flight when the board closed left its whole encoded set buffered in the channel for the rest of the session; two wipe tests that passed through the modal-close trigger rather than the trigger they named; a duplicated MAX_DECODED_IMAGE_PIXELS; stale Sixel-only doc comments after the rename; tick.rs re-deciding the bottom-bar literal instead of reading SHOW_GAME_BOTTOM_BAR; and two defensive guards (zero tile geometry, saturating_sub in the label centering)
  • the repaired tests were mutation-checked: deleting the non_modal_image_changed / protocol_changed terms now fails all three wipe tests, where two previously stayed green

Known gaps, flagged rather than fixed

  • Partial re-emission assumes the raster covers its cell rect exactly. The in-place fast path re-emits only the placements that changed. Sixel payloads are sized from a hardcoded 8×16 cell guess and iTerm2 letterboxes via preserveAspectRatio=1, so on a terminal whose cell metrics differ, a tile's raster can bleed into a neighbour that the fast path never repaints. Previously any change re-emitted every placement, which masked it; this is also the first multi-placement non-modal scene in the app. Happy to gate the fast path to Kitty, or re-emit the full set for iTerm2/Sixel, if you'd prefer.
  • Kitty image ids are a 24-bit fold of the placement UUID. With 25 cells on screen at once, two can collide (~2e-5 per board, deterministic per build) and one tile would go blank. opaque_replacement_has_same_coverage guards the old-vs-new pair at each index but does not check for collisions between two placements in the same frame.
  • No aggregate cap on an encoded set. Hard is 2 * 5² = 50 images, each with a PNG and, on Sixel, a payload bounded only by the per-image SIXEL_MAX_BYTES. Realistic photos land far below the structural ceiling, but nothing checks the total.
  • The shipped art is hotlinked from a third party. PLACEHOLDER_IMAGE_URLS hardcodes three signed fastly.picsum.photos URLs. Lorem Picsum has no SLA, no published terms, and no rate-limit policy, and the hmac params come from their signing key — a rotation on their side 403s all three at once and everyone drops to numbered tiles. Every other Arcade asset is embedded at build time with include_bytes!; doing the same here would delete the runtime dependency, the per-session download, and the file:// path together, but it needs an artwork/licensing call from you first. Recorded in late-ssh/src/app/arcade/CONTEXT.md under known gaps.
  • artwork/sliding-puzzle/ ships only a README.md and a .gitignore — it is a Compose-bind-mount dev affordance, not a runtime path; no Dockerfile stage copies it.

Verification

  • cargo fmt -p late-cli -p late-core -p late-ssh -p late-web -p late-webview -- --check — clean
  • cargo clippy -p late-ssh -p late-core --all-targets --features otel -- -D warnings — clean
  • cargo nextest run -p late-ssh -p late-core --all-targets — 3395 passed, 8 skipped
  • no test touches the network: the file:// tests use temporary directories, and every poll test runs outside a Tokio runtime so nothing spawns

Environment note

make check could not complete on this host: the workspace clippy build needs atk/gdk-3.0 for late-webview, and only GTK4 is installed here (same gap as #551). The scoped runs above cover every package this branch touches — late-webview is untouched — and CI installs libwebkit2gtk-4.1-dev, so the workspace build is covered there. No system package was installed for this PR.

Signed-off-by: John Moorman <45500240+mojoro@users.noreply.github.com>
@mojoro
mojoro requested a review from mpiorowski as a code owner September 3, 2026 15:46
@mpiorowski
mpiorowski merged commit 1d88d3a into mpiorowski:main Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants