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_allynever ran. Static reading offx_arm_beats/items_emit('march_beat')/march_pawnsall checked out, so the code was reproduced in an isolatedgit worktreeat HEAD with a headless probe appended tomain.lua(--headless --fxprobe=egg), printingmarch_t,beat_count,#pawnsand each march gate per frame. - HEAD worked; the working tree did not. Probe output:
march_tpinned at 1.000,paused=false ready_gate=false room_card=true fxlab_frozen=false. - Root cause:
SESSION_MODEnow defaults ON for desktop, so every boot opens on the session room card;room_cardis only ever lowered by BEGIN or the F8 toggle, so it survivesfx_enter'sreset(). The march gate read the bare flag, anddraw_room_cardis itselfsession_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 thenblink_out. - Body is a generated polygon via
layer_polygon— the first polygon drawn anywhere in KVP — 3-5 jittered vertices, tumbling throughlayer_pushrotation. - Drawn in the piece pass: new
draw_board_pieces()mergespawnsandshardsinto one list sorted back-to-front by ground y, refilled into a reused table each frame, with a_sortkfill-index tiebreak becausetable.sortis not stable and a row of pawns shares one y. SHARD_LIFT(stands a shard off its own shadow) and a*1.8shadow radius, because a shadow scaled honestly to a 2-4px polygon is invisible.- Off-board handling:
over_board()in pixel space; past the rimzruns 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
imgsupport — a sprite shard behaves identically but draws an emoji, used for helmet/heart debris and for the skull/helmet corpse at full size (replacingspawn_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 oldk%5gave 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_markwith 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)accumulatesp.committed;pawn_spoken_for(p)iscommitted >= 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 ofmarch_pawnsand each pending effect re-asserts in its ownmarch_beathandler — 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 andlowest_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()readcloud.x/y, whichupdate_cloudintegrates 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 toCLOUD_STEP = 2squares 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 originalsdt*2.2. - Lab fix:
FX_T.nth_beatgained anaturalflag (Cloud sets it) that skipsfx_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_glowwas deliberately unwired (tint is reserved for identity, not status), and Snow's deletedfrost.fragis 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 — atSQUAREthe shells in adjacent rows touch, silhouettes merge and the lower rock loses its outline entirely (the exact hazard documented forICE_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 toshake_traumaat the owner's request. - Sim-at-commit / show-at-landing:
grant_itemruns at commit, but the player reads the pickup at the hop landing (wherepickup_vfxfires its own shake), so the shells started a whole hop early. The freeze still applies at commit; only its show waits onon_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— theand/oridiom silently returns the third term when the middle is nil, andstone_breakwas an empty slot. Replaced withif/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 ongame_layer(outlined) at equal scale — the olds, s*0.65was a ground-plane squash that read as a stretched emoji off the board.spawn_sinking_piecegained a destination so the pawn falls into it.HOLE_PX30 → 22. - Coffee: its "effect" moment never fired because the endless/tray branch doubled
incwithout pulsing (only the session/gold branch pulsed). Pulse + a coin burst now fire on the capture that actually pays. Thenitem_pulsewas split intoitem_pulse_quiet(visual) anditem_pulse(visual +sound_item_fx_play), withgrant_itemusing the quiet one — picking an item up was playing its effect sound. - Per-binding gain:
item_fxentries can be{ moment, gain }, so Coffee borrowscoin_collectat 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 againslider was added to the sound tool's item scope. - Coin: new
coin_throwpending slot at the fling, plussfx_tracked/sfx_stop(built on the engine'ssound_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 declaredborrows = 'shield_block1',fx_item_momentscopied 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 viasound_play_momentso both clips layer without touching Shield's ownshield_blockmoment. - 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_destroyalready flaggeddeadimmediately 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 withback_outovershoot as the bounce → owner hand-tuned the chain. - Restructured at the owner's request from a phase function into chained
timer_tween/timer_aftercalls, matchinghole_fx/sinking_piece, so each beat of the motion is independently retimable.BROOM_STRIKE_Tdeleted 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: optionalstart_seconsound_play/sound_play_handle, seeking withma_sound_seek_to_pcm_framebefore 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 andanchor.execopied into KVP.effects_2_layer: a second board-space outlined layer aboveeffects, camera-attached alongside it, so a draw can state its order instead of relying onfxsinsertion order.
Sound loading made lazy:
- Measured first:
sound_loadruns 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__indexonsoundsthat loads and caches on first touch. Enumeration sites moved tosound_keys();sound_family_nprobesSOUND_FILESor 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_layerfrom inside thefire_layercamera bracket, so they drew without the camera transform (camera_attachis 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_validinmain.lua; caught and cleaned. spring_pull(..., frequency = 26)made the broom fill the screen:spring_updateis 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 safeos.executeliveness probe runs first. Thenexecute("qall!")broke on cmd quoting (E121: Undefined variable: qall), and--remote-exprfor a quit reportedInvalid channelbecause 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--listeninstance), 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 itsbitsrow. 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, everyBROOM_*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