Loading…
a327ex.com

Horse Game 14

Summary

A long juice pass through the F7 effect lab covering roughly half the item catalog — Egg, Cloud, Comet, Hole, Coffee, Coin, Helmet, Broom, Hourglass, Ice Cube — plus the two systems that came out of it: a general shard entity (polygon or sprite debris on the fake-z system) and a damage claim system for delayed procs. Also a lazy sound loader, a per-binding sound gain, an engine-side clip start offset, and a cross-session replay-desync fix handed over from Horse Game 13.

The Egg lab never hatched — a stale room_card froze the march (the session's first and biggest diagnosis):

  • Symptom: the Egg F7 scenario armed its beat counter to 11 and never reached 12, so hatch_ally never ran. Static reading of fx_arm_beats / items_emit('march_beat') / march_pawns all checked out, so the code was reproduced in an isolated git worktree at HEAD with a headless probe appended to main.lua (--headless --fxprobe=egg), printing march_t, beat_count, #pawns and each march gate per frame.
  • HEAD worked; the working tree did not. Probe output: march_t pinned at 1.000, paused=false ready_gate=false room_card=true fxlab_frozen=false.
  • Root cause: SESSION_MODE now defaults ON for desktop, so every boot opens on the session room card; room_card is only ever lowered by BEGIN or the F8 toggle, so it survives fx_enter's reset(). The march gate read the bare flag, and draw_room_card is itself session_on()-gated — so the card was invisible while freezing the sim. Pressing F7 straight from a carded boot gave a dead march in every lab scenario; Egg was just the first item whose effect rides the beat.
  • Fix: the march/spawn gate and the aim branch now read session_on() and room_card (session_on() already excludes FXLAB). Flagged that previously "done" items were auditioned with a frozen board.

The shard system (grew across the whole session):

  • Started as the ice shatter: bigger particles → spawn ring around the pawn → more gravity. Then rebuilt as shard, a real entity: fake z, gravity, ground shadow, two bounces (every other z effect in the file dies on floor contact), rest then blink_out.
  • Body is a generated polygon via layer_polygon — the first polygon drawn anywhere in KVP — 3-5 jittered vertices, tumbling through layer_push rotation.
  • Drawn in the piece pass: new draw_board_pieces() merges pawns and shards into one list sorted back-to-front by ground y, refilled into a reused table each frame, with a _sortk fill-index tiebreak because table.sort is not stable and a row of pawns shares one y.
  • SHARD_LIFT (stands a shard off its own shadow) and a *1.8 shadow radius, because a shadow scaled honestly to a 2-4px polygon is invisible.
  • Off-board handling: over_board() in pixel space; past the rim z runs negative, the shard keeps falling past the slab, stops casting a shadow, and is killed clear of the slab face.
  • Directional lean: piece_advance_dir(p) (a new predicate mirroring the walk chain's hold rules) gives the shatter the direction the piece was about to march; push randomized per shard, tuned 30 → 65 → 40-80 → 55-80 → 70-90 by the owner.
  • Later gained img support — a sprite shard behaves identically but draws an emoji, used for helmet/heart debris and for the skull/helmet corpse at full size (replacing spawn_dying_piece, which blinks out mid-air instead of landing). Sprite shards scale their lift up with size and their shadow sub-linearly ((px/ref)^0.6).

Comet debris moved to shards; burst_orb and trail_mark deleted:

  • comet_debris_opts(col, col_2) holds everything the comet does differently — faster/higher launch, in-flight drag, wilder shapes (up to 6 verts, deeper radius jitter, per-shard axis stretch), scattered rebounds.
  • Palette measured off the assets with PIL rather than invented. Rock: 4 tones at 41/23/19/15% of opaque pixels. Final mix is 20 fragments, k%4, five each of blue / white / rock-brown / flame (the old k%5 gave 6/5/4/3 — the owner asked for even shares, which also dropped the deliberate 3:1:1 fire weighting).
  • Trails were removed entirely at the owner's request, taking trail_mark with them.

Damage claims — stopping delayed procs from wasting on doomed pawns:

  • The owner's observation: several items all target "the lowest pawn" (the catalog's most-used phrase, lowest_pawn() plus four inlined pickers). Instant procs cascade harmlessly (each removes its victim before the next runs), but the Cloud commits to a target for several beats while everything else hunts the same pawn.
  • Built as committed damage, not a flag: claim_damage(p, dmg) accumulates p.committed; pawn_spoken_for(p) is committed >= pawn_hp(p). Convergence on a 5-HP tank is focus fire and stays legal; convergence on 1-HP chaff drops out of the pool.
  • claims_clear() at the top of march_pawns and each pending effect re-asserts in its own march_beat handler — re-asserted, never released, so a cancelled effect cannot leave a permanently untargetable pawn.
  • best_target(ok) is the shared picker: lowest-then-leftmost, prefers unclaimed, falls back to a claimed pawn rather than fizzling. Dagger, Magnet, Lightning's auto-capture, Cloud and lowest_pawn() all route through it. Water Gun already used this two-pass shape for its lock rule.
  • Noted that this does not fix build monotony — six items reading "the lowest pawn" still fire at the same corner; that's a catalog-diversity problem, not an engine one.

Cloud: two reworks and a replay desync (reported cross-session by Horse Game 13):

  • First pass paced the drift to the charge and gated the strike on arrival. That introduced a desync: cloud_overhead() read cloud.x/y, which update_cloud integrates per animation frame, so the firing beat depended on frame boundaries. Same invariant class as the strike-arrival ('a') and skull-landing ('l') fixes — a sim decision must resolve inside a recorded event and never read animation state. Fixed with a sim position advanced once per beat; verified against seed 700's Force fixture range and reported back.
  • Second pass (owner: "the movement is unnatural / linear") rebuilt it as a board entity: cloud.gx/gy, up to CLOUD_STEP = 2 squares per beat in Chebyshev, targeting the lowest pawn it can reach in the beats remaining, aiming at the square its target is about to step into (piece_advance_dir), arming when it settles and discharging into that square on the next beat. Visual position eases toward the cell with the original sdt*2.2.
  • Lab fix: FX_T.nth_beat gained a natural flag (Cloud sets it) that skips fx_arm_beats, because slamming the counter to N-1 every 3s showed a different item than the one that ships.

Hourglass reworked into a petrify item (🪨):

  • Design discussion first: time_glow was deliberately unwired (tint is reserved for identity, not status), and Snow's deleted frost.frag is on record warning against per-pixel recolours — "the construction was the bug, not the values". Options laid out (stop the motion / stone casing / sprite swap / desaturate shader); owner picked the casing, opaque, one rock, no per-pawn rotation.
  • Emoji choice: 🗿 Moai turned out to be already designed in the catalog as a Guard item ("While Still, deal your damage to the pawns adjacent to your knight"), so it was left alone; 🪨 Rock chosen instead, petrification fiction (not rockfall).
  • Opaque made it far simpler than the ice cube: no two-layer trick, since that exists only because a translucent draw on an outlined layer composites over outline.frag's black interior fill.
  • ROCK_PX = 24, not 30 — at SQUARE the shells in adjacent rows touch, silhouettes merge and the lower rock loses its outline entirely (the exact hazard documented for ICE_STRETCH; diagnosed from a screenshot).
  • Shells fall in (p.stone_t, 1 - u² accelerating) and the camera kick fires on landing. shake_push (spring impulse) tried first, then reverted to shake_trauma at the owner's request.
  • Sim-at-commit / show-at-landing: grant_item runs at commit, but the player reads the pickup at the hop landing (where pickup_vfx fires its own shake), so the shells started a whole hop early. The freeze still applies at commit; only its show waits on on_hop_land.
  • Sounds: three earth-spell clips brought over from SNKRX-update, later swapped so the crumble rolls two variants and the petrify is one.
  • Bug found by the owner: the release played the ice break. Cause was stone and sounds.stone_break or sounds.ice_break — the and/or idiom silently returns the third term when the middle is nil, and stone_break was an empty slot. Replaced with if/else.

Hole, Coffee, Coin, Ice Cube:

  • Hole: the pit now opens off-board in the Barricade's margin strip (wall_pos) under the leaking column, drawn opaque on game_layer (outlined) at equal scale — the old s, s*0.65 was a ground-plane squash that read as a stretched emoji off the board. spawn_sinking_piece gained a destination so the pawn falls into it. HOLE_PX 30 → 22.
  • Coffee: its "effect" moment never fired because the endless/tray branch doubled inc without pulsing (only the session/gold branch pulsed). Pulse + a coin burst now fire on the capture that actually pays. Then item_pulse was split into item_pulse_quiet (visual) and item_pulse (visual + sound_item_fx_play), with grant_item using the quiet one — picking an item up was playing its effect sound.
  • Per-binding gain: item_fx entries can be { moment, gain }, so Coffee borrows coin_collect at 0.5 while Seedling stays at full. Volume otherwise lives on the key. The lab's save had to round-trip it (fx_binding_src) or the next moment edit would erase it, and a gain slider was added to the sound tool's item scope.
  • Coin: new coin_throw pending slot at the fling, plus sfx_tracked / sfx_stop (built on the engine's sound_play_handle / sound_handle_stop, previously unused from Lua) so the throw clip is cut when the coin lands.
  • Ice Cube: a frozen pawn now shatters when killed, not only when a freeze releases — the item doubles damage against exactly those pawns, so the hit you build for had no payoff.

Helmet:

  • Lab scenario was landing on empty board: skulls march a row per beat and the scenario committed to a fixed cell 0.5s later. Now reads the skull's live cell and restands the knight one L-move away (fx_knight_near). Same class as the Dynamite drop fix Horse Game 13 had already made — both were written against the frozen clock.
  • Sound panel said (silenced) while two clips played: the slot declared borrows = 'shield_block1', fx_item_moments copied it onto the row, and nothing ever read it. Borrowed families now render as real, selectable rows.
  • Then implicit fallbacks were removed entirely at the owner's request (if sounds.X then … else someone else's clip) — for Helmet and Broom both. A slot now names the key that actually plays.
  • Finally Helmet got its own moment: helmet_block = { 'shield_block1', 'shield_block2' }, played by name via sound_play_moment so both clips layer without touching Shield's own shield_block moment.
  • VFX: helmet corpse on a Block, mixed debris (skull burst + helmet shards), sprite shards at emoji sizes.

Broom:

  • Skull debris replaced with a single 🧹 marker over each doomed skull, sweeping before they die. skull_destroy already flagged dead immediately and deferred only the burst, so the wipe plays over corpses already spoken for — no sim decision waits on an animation clock.
  • Owner asked whether to defer the despawn to the next beat instead; argued against it (a doomed skull would stay dangerous for a beat, the broom would have to chase a marching skull, and a beat is ~2× the requested duration) and it was left as-is.
  • Motion iterated: sine → three-phase stroke (shove/hold/return) → pivot at the handle rather than the sprite centre (BROOM_PIVOT) → single stroke with back_out overshoot as the bounce → owner hand-tuned the chain.
  • Restructured at the owner's request from a phase function into chained timer_tween / timer_after calls, matching hole_fx / sinking_piece, so each beat of the motion is independently retimable. BROOM_STRIKE_T deleted because a derived constant goes stale under hot reload.
  • The skull death itself now uses the standard pawn-hit presentation: spawn_hit_effect + 6 stars + only 4 skull pieces (was 12 skulls and nothing else).
  • Added a hit pop and a 💨 dust puff spawned before the arrival, on effects_2_layer.

Two engine/framework changes:

  • anchor.c: optional start_sec on sound_play / sound_play_handle, seeking with ma_sound_seek_to_pcm_frame before start. This is the negative half of a new delay slider (−500…+500ms) in the sound tool — positive schedules the play, negative starts further into the clip, which is the only way to make an impact land earlier since the game can't know an event before it fires. Engine rebuilt and anchor.exe copied into KVP.
  • effects_2_layer: a second board-space outlined layer above effects, camera-attached alongside it, so a draw can state its order instead of relying on fxs insertion order.

Sound loading made lazy:

  • Measured first: sound_load runs a verification decode (ma_decoder_init_memory + uninit) per clip, across 6.1MB of ogg in 60 keys — against only 2.6MB of PNG in 140 images, so the sounds really were the boot cost.
  • SOUND_FILES (declaration, no I/O) + a lazy __index on sounds that loads and caches on first touch. Enumeration sites moved to sound_keys(); sound_family_n probes SOUND_FILES or counting a family would load it.
  • Background warmer, budgeted in milliseconds not files — the first version loaded a fixed count per frame and juddered the opening second, because a load is a file read plus a decoder init and clips range 4KB-450KB.

Bugs I introduced and fixed during the session:

  • Shards queued into effects_layer from inside the fire_layer camera bracket, so they drew without the camera transform (camera_attach is a queue-time push). Same trap hit again with the Hole pit.
  • A python splice with the end marker before the start marker duplicated cloud_beat / cloud_strike / cloud_target_valid in main.lua; caught and cleaned.
  • spring_pull(..., frequency = 26) made the broom fill the screen: spring_update is explicit Euler, k = (2πf)², stable only to about f = 14 at 60fps. Saved to memory as a framework-wide rule (any explicit frequency stays under ~12).
  • The nvim bridge: probed with io.popen, which gives the remote client a live stdout — exactly what its no-server fallback needs to spawn the invisible editor the probe exists to detect. Reordered so the safe os.execute liveness probe runs first. Then execute("qall!") broke on cmd quoting (E121: Undefined variable: qall), and --remote-expr for a quit reported Invalid channel because an rpc request waits for a reply from a process that just exited — replaced with --remote-send.

F7 lab tooling:

  • nvim bridge now asks len(nvim_list_uis()) rather than just "is anyone listening": no server → spawn, server with a UI → reuse (which also protects the owner's own --listen instance), server with zero UIs → evict and respawn. This was the "nvim not launching" report — two windowless instances were holding the pipe.
  • Sound tuner layout: the origin line (from: …) was drawn at a fixed y=50 while the first slider sat at 54, so every imported sound overprinted its bits row. The whole stack now flows off a cursor.
  • Watched code spans for hot reload were extended to what actually gets tuned (broom_fx:new, broom_fx:draw, every BROOM_* constant) — editing an unwatched span silently does nothing.

While the other instance is working, let's fix issues with Horse Game as they appear to me as I work on juice. The egg F7 example never gets to beat 12 and thus never hatches a new pawn.

Opus 5 xhigh

[Think]

I'm checking what the Horse Game project actually is—the memory mentions it's a 1-week trial starting July 26, 2026, and it's associated with KVP, which stands for Knight vs. Pawns, so "horse game" is referring to the knight piece in that context.

The user's example shows an issue where an egg at F7 never reaches the threshold to beat 12 and therefore never hatches a new pawn. I need to locate the project directory, likely at E:/a327ex/KVP or knightvspawns, and explore the codebase to understand the problem.

Bash (ls -d /e/a327ex/*/ 2>/dev/null | head -50)

[REDACTED: private project content]

Bash (grep -ril "egg" /e/a327ex --include=*.lua -l 2>/dev/null | head -30)

/e/a327ex/062026/snkrx/area.lua
/e/a327ex/3D-exploration/Robots/assets/runner/rig.lua
/e/a327ex/3D-exploration/Robots/assets/spider/rig.lua
/e/a327ex/3D-exploration/Robots/main.lua
/e/a327ex/3D-exploration/Robots/runner.lua
/e/a327ex/3D-exploration/Robots/scorpion.lua
/e/a327ex/3D-exploration/Robots/spider.lua
/e/a327ex/a327ex-site/renderer/build-web/.bootdata/data/2026-06-06-202103.lua
/e/a327ex/a327ex-site/renderer/build-web/.bootdata/data/2026-07-16-181804.lua
/e/a327ex/a327ex-site/renderer/data/1_in_4.lua
/e/a327ex/a327ex-site/renderer/data/2026-06-06-202103.lua
/e/a327ex/a327ex-site/renderer/data/2026-07-16-181804.lua
/e/a327ex/a327ex-site/renderer/data/3d-exploration-chibi-character-1.lua
/e/a327ex/a327ex-site/renderer/data/3d-exploration-chibi-character-2.lua
/e/a327ex/a327ex-site/renderer/data/3d-exploration-robots.lua
/e/a327ex/a327ex-site/renderer/data/474000.lua
/e/a327ex/a327ex-site/renderer/data/aba-games-variations-knight-vs-pawns-3.lua
/e/a327ex/a327ex-site/renderer/data/aba-games-variations-knight-vs-pawns-4.lua
/e/a327ex/a327ex-site/renderer/data/aba-games-variations-knight-vs-pawns-5.lua
/e/a327ex/a327ex-site/renderer/data/aba-games-variations-knight-vs-pawns-6.lua
/e/a327ex/a327ex-site/renderer/data/anchor-app-design.lua
/e/a327ex/a327ex-site/renderer/data/attack-system-generalization-3.lua
/e/a327ex/a327ex-site/renderer/data/audible-book-recommendations-2.lua
/e/a327ex/a327ex-site/renderer/data/audible-book-recommendations.lua
/e/a327ex/a327ex-site/renderer/data/ebb-ball-bounce-mechanics.lua
/e/a327ex/a327ex-site/renderer/data/ebb-bow.lua
/e/a327ex/a327ex-site/renderer/data/ebb-gameplay-mechanics-ui-2.lua
/e/a327ex/a327ex-site/renderer/data/ebb-kitchen-knife.lua
/e/a327ex/a327ex-site/renderer/data/ebb-weapon-rotation-mechanics.lua
/e/a327ex/a327ex-site/renderer/data/emoji-aimer-1.lua

[Think]

Looking for a directory called horse-game.

Bash (ls -d /e/a327ex/*/ 2>/dev/null | tail -30)

/e/a327ex/artifact-v2//
/e/a327ex/artifact//
/e/a327ex/be-me//
/e/a327ex/cratebox//
/e/a327ex/data//
/e/a327ex/edition-template//
/e/a327ex/emoji-aimer//
/e/a327ex/emoji-ball-battles-generated//
/e/a327ex/emoji-ball-battles//
/e/a327ex/emoji-ball-bounce//
/e/a327ex/emoji-template//
/e/a327ex/generation-loss//
/e/a327ex/it-follows//
/e/a327ex/lane-battle//
/e/a327ex/ld54-remake//
/e/a327ex/mini-looper-old//
/e/a327ex/mini-looper//
/e/a327ex/music-theory//
/e/a327ex/neural-networms//
/e/a327ex/notes//
/e/a327ex/petri//
/e/a327ex/private//
/e/a327ex/reference//
/e/a327ex/ricochet-template//
/e/a327ex/snkrx-template//
/e/a327ex/steam-market//
/e/a327ex/thalien-lune//
/e/a327ex/tools//
/e/a327ex/ui_kit//
/e/a327ex/word-game//

[Think]

I'm looking for the Knight vs Pawns game directory—it might be in ABAgames or somewhere else—and I remember it shipped in July 2026. The reference to "egg F7" (a chess square) suggests it's related to KVP mechanics, possibly involving pawn eggs. Let me search for where this directory actually is, and I notice another instance is already running.

Bash (ls /e/a327ex/ABAgames/ 2>/dev/null | head -40)

data/
knightvspawns/
kvp-ship/
lists/
one-button-games.md
scratch/
study.md

Bash (grep -ril "egg" /e/a327ex --include=*.lua -l 2>/dev/null | grep -iv "a327ex-site...)

/e/a327ex/ABAgames/knightvspawns/edit.lua
/e/a327ex/ABAgames/knightvspawns/fx_lab.lua
/e/a327ex/ABAgames/knightvspawns/glossary.lua
/e/a327ex/ABAgames/knightvspawns/items.lua
/e/a327ex/ABAgames/knightvspawns/lang_ja.lua
/e/a327ex/ABAgames/knightvspawns/lang_pt.lua
/e/a327ex/ABAgames/knightvspawns/lang_ru.lua
/e/a327ex/ABAgames/knightvspawns/main.lua
/e/a327ex/ABAgames/knightvspawns/verify.lua
/e/a327ex/ABAgames/kvp-ship/edit.lua
/e/a327ex/ABAgames/kvp-ship/items.lua
/e/a327ex/ABAgames/kvp-ship/main.lua
/e/a327ex/ABAgames/kvp-ship/verify.lua
/e/a327ex/BYTEPATH/libraries/draft/draft.lua
/e/a327ex/BYTEPATH/libraries/draft/example_linker.lua
/e/a327ex/BYTEPATH/libraries/draft/main.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/07 - Player Stats and Attacks/libraries/draft/draft.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/07 - Player Stats and Attacks/libraries/draft/example_linker.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/07 - Player Stats and Attacks/libraries/draft/main.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/08 - Enemies/libraries/draft/draft.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/08 - Enemies/libraries/draft/example_linker.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/08 - Enemies/libraries/draft/main.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/09 - Director and Gameplay Loop/libraries/draft/draft.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/09 - Director and Gameplay Loop/libraries/draft/example_linker.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/09 - Director and Gameplay Loop/libraries/draft/main.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/10 - Coding Practices/libraries/draft/draft.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/10 - Coding Practices/libraries/draft/example_linker.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/10 - Coding Practices/libraries/draft/main.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/11 - Passives/libraries/draft/draft.lua
/e/a327ex/BYTEPATH/tutorial/tutorial-code/11 - Passives/libraries/draft/example_linker.lua

[Think]

I'm wondering if "Horse Game" refers to a knight-based game (KVP), but there might be a newer version running in parallel. The mention of "beat 12" and an egg at F7 that hatches into a pawn suggests a rhythm or beat-counter mechanic tied to game progression. Let me look for recent mods that combine these "beat" and "egg" concepts.

Bash (ls -dt / | head -12 && echo "---" && for d in $(ls -dt / | head -8); do echo "...)

3D-exploration//
ABAgames//
tools//
BYTEPATH++//
be-me//
emoji-template//
062026//
BYTEPATH++-old//
private//
Anchor3//
steam-market//
a327ex-site//
---
3D-exploration// 2026-07-31 18:19
ABAgames// 2026-07-27 20:51
tools// 2026-07-27 07:11
BYTEPATH++// 2026-07-26 11:03
be-me// 2026-07-25 14:10
emoji-template// 2026-07-19 10:07
062026// 2026-07-16 08:46
BYTEPATH++-old// 2026-07-14 19:01

Bash (grep -ril "hatch" --include=.lua --include=.md . 2>/dev/null | grep -v "a327ex...)

./a327ex-site/logs/2026-01-03-15-30.md
./a327ex-site/logs/3d-exploration-chibi-character-1.md
./a327ex-site/logs/3d-exploration-skyland.md
./a327ex-site/logs/aba-games-variations-knight-vs-pawns-3.md
./a327ex-site/logs/aba-games-variations-knight-vs-pawns-4.md
./a327ex-site/logs/aba-games-variations-knight-vs-pawns-5.md
./a327ex-site/logs/aba-games-variations-knight-vs-pawns-6.md
./a327ex-site/logs/attack-system-generalization-1.md
./a327ex-site/logs/audible-book-recommendations.md
./a327ex-site/logs/edition-system-generalization.md
./a327ex-site/logs/fable-test-chosen-chains.md
./a327ex-site/logs/gamedev-progress-assessment.md
./a327ex-site/logs/horse-game-10.md
./a327ex-site/logs/horse-game-11.md
./a327ex-site/logs/horse-game-12.md
./a327ex-site/logs/horse-game-8.md
./a327ex-site/logs/horse-game-9.md
./a327ex-site/logs/invoker-hp-and-mana-orbs.md
./a327ex-site/logs/mini-looper-ideaguying.md
./a327ex-site/logs/orblike-map-gen-wall-visuals.md

[Think]

I see that Horse Game logs exist, so it's definitely a project somewhere. The 3D-exploration directory was modified most recently on July 31st, but today is August 2nd, so that's not the most recent activity. I need to search more broadly for the horse-game directory—it might be outside the E:/a327ex location or nested in a subdirectory I haven't checked yet. Let me search for any directories containing "horse" to track it down.

Bash (find /e/a327ex -maxdepth 3 -iname "horse" -not -path "/renderer/" 2>/dev/nul...)

/e/a327ex/a327ex-site/logs/horse-game-10.md
/e/a327ex/a327ex-site/logs/horse-game-11.md
/e/a327ex/a327ex-site/logs/horse-game-12.md
/e/a327ex/a327ex-site/logs/horse-game-8.md
/e/a327ex/a327ex-site/logs/horse-game-9.md
/e/a327ex/a327ex-site/posts/reincarnated-as-the-only-piece-that-can-l-jump-mighty-and-powerful-and-dashingly-handsome-horse-effortlessly-tramples-one-thousand-pawns-for-all-to-witness.md
---logs---
Title: Horse Game 12
Date: 2026-08-01 12:10:22

# Horse Game 12

## Summary

Batch 2 of the KVP4 item catalog (8 damage-ladder items, 48/168 in code), the Glove redesign that founded the Force/knockback archetype (+11 items worded, catalog 168), and the session's centerpiece: the F7 Effect Lab — a full effect-iteration workshop (per-item scenarios, the moment-based sound system with owner-created triggers, drag-drop import, nvim-driven juice-code editing, and the F3+Q merged sound tool).

**Next-10 selection (easiest-first):**
- Picked the 10 easiest remaining items grounded in code reading, not card text: `damage_vs`'s `src` parameter was pre-threaded "for Glove, Drum, Battery" per its own comment, making the damage riders nearly free.
- Selected: Glove, Trident, Thunderbolt, Golden Heart, Banner, Pillar, Collection, Package, Opal, New Moon (+ Compass as near-tie). Flagged the damage-skew honestly: 9 of 10 strict-easiest were damage items; a good run reaches ~6-8 damage before Overkill exists to spend it.

**Glove redesign → the Force archetype:**
- Owner redesigned Glove from "+1 damage on Strikes" to knockback: "Strikes Push the struck pawn in your knight's direction of travel" — opening the knockback/forcer archetype.
- Brainstormed the batch: Push keyword (verb + stat ladder like Chain), 💪 Muscle +1 / 🦾 Iron Arm +2, 🔊 Subwoofer, 🧱 Brick (flat 1), 😵 Dizzy (Stun 2, owner-set), 🍌 Banana Peel (side edges only), 🎱 Eight Ball, 🚂 Locomotive, ⚰️ Coffin (skulls as ammunition via mutual_destroy), 🥅 Goal Net, 🥌 Curling Stone (capstone slide).
- Three laws recorded: the bottom edge NEVER captures (sideways is profit, down is grief); the Push itself never reads health (tank answer = geometry — Banana/Coffin capture tanks outright); a Push ENTERS squares (all entry-triggered terrain becomes aimable — Fire+Glove combos with no new item).
- Catalog updates: Push keyword (29+2), vocabulary rows ("is stopped" never "blocked"; "off the board" vs escape), escape keyword contrast sentence, 5 retrofits (Wave/Chequered Flag/Balloon/Cat/Tornado — fling = the one airborne Push), open rulings 13-15, Force registry entry in endgame_design.md. Catalog 157 → 168.

**Batch 2 implementation (8 items at once, owner-directed):**

[Think]

Horse Game is KVP4, likely stored in a private directory, and the "Effect Lab" is accessed via the F7 key in the game—a tool where per-item scenarios play out. The egg is an item with an F7 scenario that never progresses past beat 12, so it never hatches. I'm trying to locate the source code, which might be in the ABAgames directory rather than the shipped KVP location.

Let me check the ABAgames directories for fx_lab.lua, which should be the Effect Lab code.

Bash (ls -la /e/a327ex/ABAgames/knightvspawns/ | head -40; echo "---"; ls -la /e/a327e...)

total 120056
drwxr-xr-x 1 adn 197121        0 Aug  2 08:15 ./
drwxr-xr-x 1 adn 197121        0 Jul 27 20:51 ../
drwxr-xr-x 1 adn 197121        0 Aug  1 12:15 .git/
-rw-r--r-- 1 adn 197121       55 Jul 24 00:17 .gitignore
drwxr-xr-x 1 adn 197121        0 Jul 18 23:20 anchor/
-rwxr-xr-x 1 adn 197121  5031936 Jul 24 00:11 anchor.exe*
-rw-r--r-- 1 adn 197121     6886 Jul 22 23:09 annotate.lua
drwxr-xr-x 1 adn 197121        0 Aug  2 08:08 assets/
drwxr-xr-x 1 adn 197121        0 Jul 23 11:35 audit/
-rwxr-xr-x 1 adn 197121 65762816 Jul 18 11:25 avcodec-61.dll*
-rwxr-xr-x 1 adn 197121 24924160 Jul 18 11:25 avfilter-10.dll*
-rwxr-xr-x 1 adn 197121 21524480 Jul 18 11:25 avformat-61.dll*
-rwxr-xr-x 1 adn 197121  2836992 Jul 18 11:25 avutil-59.dll*
-rw-r--r-- 1 adn 197121       93 Jul 22 13:17 cuts.txt
-rw-r--r-- 1 adn 197121     1733 Jul 22 13:59 cuts_auto.txt
-rw-r--r-- 1 adn 197121       63 Jul 22 23:18 cuts_intro.txt
-rw-r--r-- 1 adn 197121      359 Jul 22 22:47 cuts_new.txt
drwxr-xr-x 1 adn 197121        0 Jul 24 00:20 dist/
-rw-r--r-- 1 adn 197121    20237 Jul 23 08:56 edit.lua
drwxr-xr-x 1 adn 197121        0 Aug  1 11:50 emoji/
-rw-r--r-- 1 adn 197121    59297 Aug  1 16:33 endgame_design.md
-rw-r--r-- 1 adn 197121      317 Aug  2 08:16 fx_done.lua
-rw-r--r-- 1 adn 197121    79953 Aug  2 08:12 fx_lab.lua
-rw-r--r-- 1 adn 197121    49621 Aug  2 08:11 glossary.lua
-rw-r--r-- 1 adn 197121    12723 Jul 24 21:56 infrastructure.md
-rw-r--r-- 1 adn 197121    48351 Aug  2 08:17 items.lua
-rw-r--r-- 1 adn 197121    71537 Aug  1 05:17 items_catalog.md
-rw-r--r-- 1 adn 197121    41430 Jul 24 00:54 items_plan.md
-rw-r--r-- 1 adn 197121    16799 Jul 29 23:40 lang_ja.lua
... [21 more lines]

[Think]

I see there's another instance editing the same files, so I need to be careful about coordinating changes. Let me look for the egg scenario in fx_lab.lua to understand what needs fixing.

Bash (cd /e/a327ex/ABAgames/knightvspawns && grep -n "egg" fx_lab.lua | head -50)

350:    if FXLAB.item_id == 'egg' then
561:  egg            = { t = 'nth_beat', supply = 2 },
645:  egg            = { { key = 'pawn_spawn', label = 'the hatch' } },
1143:  egg            = { 'fn:hatch_ally', 'fn:ally_clash' },
1202:  hatch_ally      = 'The hatch: ally pawn in a free bottom column, pawn_spawn clip, 9-egg puff, 0.5 spring.',

Read (E:\a327ex\ABAgames\knightvspawns\fx_lab.lua)

300      if sc.spread then fx_supply(sc.spread) end
301   end,
302   trigger = function(sc)
303     FXLAB.cyc = FXLAB.cyc + 1
304     if sc.spread then fx_supply(sc.spread) end
305     if sc.n then fx_arm_count(FXLAB.item_id, sc.n) end
306     if sc.chance then FXLAB.force = FXLAB.force + 1 end
307     if sc.predamage then hp = math.max(1, hp_max - sc.predamage) end
308     if sc.ring then
309       -- Boom/Dynamite-style: victims packed around the target so the splash
310       -- visibly chips more than the epicenter
311       local tx, ty = fx_lcell()
312       if tx then
313         local v = fx_pawn(tx, ty)
314         for _, d in ipairs({ {1,0}, {-1,0}, {0,-1} }) do
315           local ax, ay = tx + d[1], ty + d[2]
316           if in_bounds(ax, ay) and not pawn_at(ax, ay) then fx_pawn(ax, ay) end
317         end
318         fx_after(0.45, function() fx_commit(v.gx, v.gy) end)
319       end
320       return
321     end
322     if sc.fast then
323       -- Coffee: a burst of quick captures so the combo meter climbs
324       for i = 1, sc.fast do
325         fx_after(0.15 + (i - 1)*0.4, function() fx_victim_commit(nil, 0.05) end)
326       end
327       return
328     end
329     fx_victim_commit(nil, 0.45)
... [90 more lines]

[Think]

Looking at how the `fx_arm_beats` function works and understanding the egg's beat counter mechanism.

Bash (grep -n "fx_arm_beats|beat_max|beat_count|fx_arm_count" fx_lab.lua | head -40)

44:      the march/spawn clock freezes while the GRID is open; beat_count is
180:function fx_arm_count(id, n)
184:function fx_arm_beats(id, n)
305:    if sc.n then fx_arm_count(FXLAB.item_id, sc.n) end
349:    fx_arm_beats(FXLAB.item_id, def.beat_max or 12)
1556:    beat_count = 0                     -- pin the director: chunk 0 pace forever

Bash (grep -n "beat_max|beats\b" items.lua | head -40)

48:  -- stun_bonus: extra beats of Stun on a STRIKE only (Web) — read in strike_impact.
100:  if def.beat_max then                         -- "every Nth BEAT" proc: a HUD counter (bottom-right, yellow)
101:    it.beats = 0                               -- ticks on the march beat
155:-- Pop the beat-counter badge (Egg) — the value itself is `it.beats`, ticked on
339:            desc = 'Every 3rd capture, the lowest pawn is Frozen for 3 beats.',
354:            desc = 'The square your knight leaves holds a Flame for 2 beats.',
382:  item_def{ id = 'egg', name = 'Egg', weight = 2, img = egg_img, beat_max = 12, tags = { 'tag_beat', 'tag_summon' },
383:            desc = 'Every 12 beats, hatch an ally pawn on the bottom row.',
386:                it.beats = (it.beats or 0) + 1
387:                if it.beats >= 12 then it.beats = 0; hatch_ally() end
468:  -- holds steady. beat_max drives the yellow beat badge.
469:  item_def{ id = 'snow', name = 'Snow', weight = 2, img = snowflake_img, beat_max = 12, tags = { 'tag_beat', 'tag_tempo' },
470:            desc = 'Every 12 beats, all pawns are Frozen for 2 beats.',
473:                it.beats = (it.beats or 0) + 1
474:                if it.beats >= 12 then it.beats = 0; freeze_pawns(2) end
483:  item_def{ id = 'comet', name = 'Comet', weight = 2, img = comet_img, beat_max = COMET_BEATS, tags = { 'tag_beat', 'tag_projectile' },
484:            desc = 'Every 8 beats, mark a random pawn\'s square. A comet lands there on the next beat, dealing your damage, before the pawns march.',
488:  item_def{ id = 'cloud', name = 'Cloud', weight = 4, img = cloud_img, beat_max = CLOUD_BEATS, tags = { 'tag_beat', 'tag_summon' },
489:            desc = 'Summon a storm cloud. Every 5 beats it hits the lowest pawn for your damage.',
502:            desc = 'Each capture has a 1 in 12 chance to fling a coin onto a random square. Collect it and your captures score triple for 8 beats.',
535:  -- (however you collect it) stops time for 2 beats, in its own grey/hourglass language.
537:            desc = 'Whenever you collect an item, all pawns are Frozen for 2 beats.',
655:  -- Two beats of standstill per hit is what turns the exchange from "damage over
694:  -- a drop's march speed doubles the beats you have to reach one.

Read (E:\a327ex\ABAgames\knightvspawns\fx_lab.lua)

160  end
161 
162 -- An empty in-bounds L-cell from the knight (upward-biased so the action stays
163 -- mid-board). `parity`: 'light'/'dark' filters by target-square color (Opal).
164 function fx_lcell(parity)
165   local best
166   for _, m in ipairs(VALID_MOVES) do
167     local tx, ty = knight.gx + m.x, knight.gy + m.y
168     if in_bounds(tx, ty) and not pawn_at(tx, ty) and not skull_at(tx, ty)
169        and not coin_at(tx, ty) and ty >= 1 and ty <= 6 then
170       local ok = true
171       if parity == 'light' then ok = square_is_light(tx, ty) end
172       if parity == 'dark' then ok = not square_is_light(tx, ty) end
173       if ok and (not best or ty < best.ty) then best = { tx = tx, ty = ty } end
174     end
175   end
176   if best then return best.tx, best.ty end
177 end
178 
179 -- pre-advance an every-Nth-capture counter / an every-N-beats counter
180 function fx_arm_count(id, n)
181   local it = owned_item(id)
182   if it then it.count = n - 1; item_count_show(it, it.count) end
183 end
184 function fx_arm_beats(id, n)
185   local it = owned_item(id)
186   if it then it.beats = n - 1; item_count_beat(it) end
187 end
188 
189 -- the step queue: driver actions sequenced on the lab's own clock
... [30 more lines]

[Think]

I'm tracing through the egg hatching logic — `fx_arm_beats` sets the beat counter to 11, so the next beat tick should increment it to 12 and trigger hatching. The question is whether the lab environment actually drives the beat clock that would trigger this tick, so I need to find where the `on_beat` hook gets called and confirm the lab participates in that march cycle.

Read (E:\a327ex\ABAgames\knightvspawns\fx_lab.lua)

1500   if not sc then print('fx_lab: no scenario for ' .. id) return end
1501      FXLAB.dirty = true
1502      FXLAB.mode, FXLAB.item_id = 'view', id
1503      FXLAB.steps, FXLAB.clock, FXLAB.cyc = {}, 0, 0
1504      FXLAB.force, FXLAB.auto, FXLAB.next_t = 0, true, 0.8
1505      FXLAB.keep = nil
1506      -- arm the sound tool's selection on the first EMPTY slot (a key awaiting
1507      -- its file); the tool itself opens with Q (item scope) or F3 (bank)
1508      FXLAB.choosing, FXLAB.naming, FXLAB.choose_page = nil, nil, 1
1509      for _, s in ipairs(FX_SOUNDS[id] or {}) do
1510        if s.pending and not sounds[s.key] then
1511          if sound_tuner_select then sound_tuner_select(s.key) end
1512          break
1513        end
1514      end
1515      FXLAB.code_open, FXLAB.code_sel_sec, FXLAB.last_perr = false, nil, nil
1516      reset()                        -- clean sandbox: no archive, no dev items
1517      ready_gate = false
1518      spawns_on = false
1519      grant_item(ITEM_DEFS[id])
1520      FX_T[sc.t].setup(sc)
1521      -- sections are built on ENTRY, not on E: the save-watcher hot-reloads this
1522      -- item's spans from disk whether or not the panel is showing
1523      fx_code_build()
1524    end
1525    
1526    function fx_back_to_grid()
1527      FXLAB.mode, FXLAB.steps = 'grid', {}
1528    end
1529    
... [100 more lines]

[Think]

I need to check if the march runs in view mode with those spawn and gate settings disabled, then trace where the on_beat hooks get triggered in the main loop.

Bash (grep -n "on_beat|beat_hook|item_beats|tick_beats" main.lua items.lua | head -...)

(Bash completed with no output)

Read (E:\a327ex\ABAgames\knightvspawns\items.lua)

370                    local n = it.count
371                   on_hop_land(function() item_count_show(it, n) end)   -- tick shows when the hop lands
372                 end
373               end,
374             } }
375   item_def{ id = 'dynamite', name = 'Dynamite', weight = 2, img = dynamite_img, tags = { 'tag_area', 'tag_item' },
376             -- "dealing your damage", NOT "capturing every pawn": this shares
377             -- boom_splash with Boom, so a tank in the blast is chipped.
378             desc = 'When your knight lands on a drop, that square explodes, dealing your damage to the 8 surrounding squares.',
379             on = {
380               item_pickup = function(it, gx, gy) boom_splash(gx, gy, 'dynamite') end,
381             } }
382   item_def{ id = 'egg', name = 'Egg', weight = 2, img = egg_img, beat_max = 12, tags = { 'tag_beat', 'tag_summon' },
383             desc = 'Every 12 beats, hatch an ally pawn on the bottom row.',
384             on = {
385               march_beat = function(it)
386                 it.beats = (it.beats or 0) + 1
387                 if it.beats >= 12 then it.beats = 0; hatch_ally() end
388                 item_count_beat(it)
389               end,
390             } }
391   -- Chick has no handlers — the counter + defect live in resolve_capture (keyed on
392   -- owned_set['chick']), so it counts every capture path. count_max drives the badge.
393   item_def{ id = 'chick', name = 'Chick', weight = 2, img = chick_img, count_max = 12, tags = { 'tag_summon' },
394             desc = 'Every 12th capture, the pawn defects instead: it becomes an ally pawn on its square and is not captured.' }
395   item_def{ id = 'pony', name = 'Pony', weight = 1, img = knight_img, count_max = 24, tags = { 'tag_summon' },
396             desc = 'Summon a pony now, and every 24th capture. It captures 4 pawns it can kill a knight\'s move away, one per beat, then leaves.',
397             on_acquire = function(it) spawn_pony() end,
398             on = {
399               direct_capture = function(it, p)
... [10 more lines]

Bash (grep -n "beat = function|.beat\b