fix: enable invading the enemy hive (closes #14)#29
Merged
Conversation
) Phase 09.1 already shipped the cross-grid invasion sim path: a Fighting ant standing on an open enemy entrance descends into the enemy underground grid, targets the nearest hostile via pickNearestHostileUnderground, and resolving combat against the enemy queen triggers Victory (REQ-C3a, REQ-C4a). Issue #14 was the missing input + HUD seams that left this mechanic invisible to the player. Four touches: 1. Surface input — left-click on an enemy colony's entrance tile sets the player's rally point there. New `isForeignColonyEntrance` helper + a new branch in `handleSurfaceLeftClick` between food-pile mark and empty-tile rally. Own-colony entrance left-click stays a no-op so the right-click → left-click entrance designation flow is undisturbed. The rally-on-entrance carve-out in `updateFightAntTargets` (already in place) walks fighters onto the exact entrance tile, and the descent-intent gate in `tickAntMovement` (already in place) crosses them into the enemy grid. 2. Underground input — `handleUndergroundLeftClick`, `handleUndergroundDrag`, `handleUndergroundRightClick` now no-op when `viewState.activeUndergroundColonyId !== PLAYER_COLONY_ID`. Without this, clicks on the enemy view at screen coords (sx, sy) would silently dispatch dig-marks against the player's grid at the matching world coords — invisible scribbling. The enemy view is spectator-only. Defensive `state.isDragging = false` reset on the guard path covers focus-loss aborted gestures. 3. HUD — the X-keybind was undiscoverable; added a clickable "Your Colony (X)" / "Enemy Colony (X)" toggle button in a new HUD.UNDERGROUND_COLONY_TOGGLE zone, stacked just above VIEW_TOGGLE. Clicking dispatches the same `toggleUndergroundColony` reducer the X key calls. The new zone is included in the HUD pointer-block mask ONLY while activeView === 'underground' (the button is invisible on the surface view; masking unconditionally would create a 112×22 dead zone above the minimap that swallows rally / food-mark / entrance-designation clicks). isPointerOverHUD gains an optional viewState param to gate the conditional mask. Ant-activity panel dismiss allowlist updated so a click on the new button doesn't simultaneously dismiss the panel and drop the toggle. 4. Render — enemy entrances on the surface view get a 1-pixel COLOR_ENEMY_COLONY perimeter ring overlaid on the existing dirt rim, so the player reads them as "rally here to invade" targets. Player entrances are unchanged. Render-only — no sim impact. REQUIREMENTS, sim, and Phase 09.1 invasion + cross-grid combat tests unchanged. New input/HUD/render regression tests cover the four seams. 1533/1533 tests green; lint, typecheck, sim-boundary, and asset-path checks all clean. Closes #14 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LightAxe
added a commit
that referenced
this pull request
Apr 30, 2026
Codex flagged that the new tileY === UNDERGROUND_CEILING_ROW_Y guard in handleUndergroundLeftClick returned without clearing state.isDragging. If a prior gesture left isDragging=true (focus-loss / missed pointerup), the next pointermove would resume the stale stroke from the stale lastMarkedTile and emit hidden marks — exactly the "invisible marking" behavior issue #30 is fixing. Reset the flag before returning, symmetric with the enemy-view guard's defensive reset (issue #14 PR #29). New regression test pre-seeds isDragging=true and asserts the ceiling- row guard clears it. 1541/1541 tests green; lint, typecheck, sim-boundary, asset-path clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LightAxe
added a commit
that referenced
this pull request
Apr 30, 2026
…ths (closes #30 #31) (#32) * fix: reject ceiling-strip clicks + dig demand checks reachability Two related bugs surfaced from a debug snapshot where the player saw an ant "sitting at a food chamber doing nothing" while four orphan Marked tiles sat in the topmost underground row. **Issue #30** — In the underground view, the topmost row (`tileY === 0`) is rendered as a grass-textured ceiling strip — the renderer's "this is the surface boundary, not a diggable wall" cue. But the underground click handler accepted clicks anywhere inside grid bounds, so a click on the visible grass dispatched MarkDigTile against the tile beneath. The mark was real but the renderer kept painting grass on top, so the player got zero feedback that anything happened. Fix: new `UNDERGROUND_CEILING_ROW_Y` constant; both `handleUndergroundLeftClick` and `handleUndergroundDrag` reject `tileY === UNDERGROUND_CEILING_ROW_Y`. The drag's emit path skips row-0 tiles per-iteration (Bresenham continues), and the sentinel-fallback path rebases the cursor without emitting. **Issue #31** — `computeDigDemand` returned 1 the moment any Marked tile existed, regardless of whether a digger could actually reach it. Result: an Idle worker got assigned to dig an unreachable Marked island each tick, the dig flow-field reported -2 (unreachable), the worker bounced back to Idle, and the cycle locked one worker out of forage/nurse. Fix: a Marked tile counts toward dig demand only if it has at least one 4-connected `Open` or `BeingDug` neighbor — the same reachability predicate `computeDigFlowField`'s BFS uses to expand. Cheap extra check inside the existing single linear scan. Tests: 4 new for issue #30 (ceiling click rejection, drag-cross-ceiling continuation, sentinel rebase) + 2 for issue #31 (isolated Marked island suppresses dig demand, mixed reachable+isolated marks still fire). Added a `beforeEach(resetFlowFieldCaches)` to the auto-dig describe block to prevent the module-level dig flow-field cache from leaking across tests that mutate the underground grid via `ugSet` (which bypasses the `digFlowFieldDirty` flag). 1540/1540 tests green; lint, typecheck, sim-boundary, asset-path checks all clean. Closes #30 Closes #31 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: clear stale isDragging when ceiling-row guard rejects (codex P2) Codex flagged that the new tileY === UNDERGROUND_CEILING_ROW_Y guard in handleUndergroundLeftClick returned without clearing state.isDragging. If a prior gesture left isDragging=true (focus-loss / missed pointerup), the next pointermove would resume the stale stroke from the stale lastMarkedTile and emit hidden marks — exactly the "invisible marking" behavior issue #30 is fixing. Reset the flag before returning, symmetric with the enemy-view guard's defensive reset (issue #14 PR #29). New regression test pre-seeds isDragging=true and asserts the ceiling- row guard clears it. 1541/1541 tests green; lint, typecheck, sim-boundary, asset-path clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: reject ceiling-row dig + chamber placement at sim layer UAT screenshot from PR #32 showed enemy AI building chambers straddling the grass strip even after the player-input ceiling gate landed. Root cause: the input-layer gate (#30 first commit) only covers player clicks. The AI controller pushes MarkDigTileCommand directly to the queue, every chamber's top-border perimeter mark hits ty=0 when chTileY=1 (the typical near-surface case), and the sim's MarkDigTile handler accepted ceiling-row tiles. Same gap for PlaceChamber. Three layered fixes (defense in depth): 1. Sim-layer MarkDigTile reject — handler rejects `cmd.tileY === UNDERGROUND_CEILING_ROW_Y` before the Solid-state read. Authoritative for any current or future MarkDigTile dispatch source. CLNY-08-compliant; no isPlayer branching. 2. Sim-layer PlaceChamber reject — handler rejects `cmd.anchorTileY === UNDERGROUND_CEILING_ROW_Y`. CHAMBER_DIMENSIONS extend down from the anchor, so the equality check covers all footprint-overlap-with-ceiling cases as long as the ceiling stays a single row. 3. AI controller pre-filter — `isDirtTileUnderground` returns false for `ty === UNDERGROUND_CEILING_ROW_Y` so the AI never queues dead-on- arrival ceiling commands every AI_DIG_INTERVAL ticks. Performance / queue-cleanliness; the sim gate above is the source of truth. Carve-out (intentional, regression-pinned): DesignateEntrance writes Marked at the entrance column from sy=0 down via direct ugSet, bypassing the MarkDigTile gate. That's correct — entrance columns are exempt because the renderer paints them as the gold-tinted "way in" hole, not as the grass ceiling. New test pins entrance shaft excavation still works. 3 new regression tests: MarkDigTile rejects tileY=0; PlaceChamber rejects anchorTileY=0; AI controller chamber at chTileY=1 doesn't push row-0 marks. Plus the entrance-shaft carve-out test. 1545/1545 tests green; lint, typecheck, sim-boundary, asset-path clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 30, 2026
LightAxe
added a commit
that referenced
this pull request
May 1, 2026
…ntier dig (closes #33) (#37) * feat(ai): spread enemy colony layout — depth gate + spread bias + frontier dig (closes #33) Pre-fix the enemy AI placed every chamber at the entrance shaft floor (F9 snapshot: 5 chambers wedged into a 12×6 area at Y=1..6). Three compounding causes: 1. AI_QUEEN_CHAMBER_DEPTH=10 was shallow, but the BFS had no depth gate so the Queen anchored at Y≈1 on tick 0 because that's the first reachable Open tile. 2. The FoodStorage gate (food >= threshold && no FS) fired on tick 0 too — starting foodStored=1280 ≫ threshold 8 — so a shallow FS landed before the Queen ever got to depth. 3. The bootstrap dig was gated on chambers.length === 0, so once that first shallow chamber landed the deep-dig path turned off permanently. Fix - Bump AI_QUEEN_CHAMBER_DEPTH from 10 to 18 (Queen footprint extends to Y=20, well past the >15 acceptance criterion). - Add AI_PLACEMENT_DEPTH_TOLERANCE=4 depth gate to findOpenChamberSpot: refuse to issue a placement until at least one candidate is within ±tolerance of preferredDepth. Holds the Queen until bootstrap reaches Y≈14. - Re-gate the bootstrap on "no Queen chamber AND no Queen pending" (helper hasChamberOrPending), so deep digging continues until the Queen actually lands. - Re-gate FS placement on Queen-pending-or-completed. Mirrors a human player's natural build sequence (Queen first); blocks the pre-fix shallow-FS race. - Tighten FS uniqueness check from completed-only to chamber-or-pending, closing a duplicate-FS-pending window that opens between the Queen pending and FS pending transitions. - Add findOpenChamberSpot anchor scoring: among same-depth candidates, prefer anchors with the LARGEST Manhattan distance to existing chambers. Wins horizontal spread once the dug area expands laterally. - Add aiDigHeuristic frontier-extension pass: on each cycle, before the legacy full-perimeter pass, mark up to AI_DIG_OUTWARD_BUDGET=3 tiles whose 4-neighborhood touches no other chamber's footprint — preferentially extending dirt outward from the colony centroid instead of re-marking tiles between existing chambers. Activates only once chambers.length > 1 (single-chamber perimeter is uniformly outward). Acceptance (issue #33) - After 18000 ticks (15 minutes @ 20Hz, default scenario seed 42): Queen at (98, 14), max chamber Y = 17 (was 6) — meets criterion "max chamber Y > 15". - Layout is byte-identical across seeded runs (verified by integration test). - All AI behavior remains deterministic: pure integer arithmetic, no PRNG, no Math.random, Set instances accessed only via has/add. Trade-off - Slower bootstrap. Pre-fix Queen placed by tick ≈100; post-fix Queen places by tick ≈3000-5000 because the dig must reach Y=14+ first. Integration test budget extended from 3000 to 6000 ticks. The player-facing impact is "enemy colony visible later"; in exchange, the colony is significantly more interesting to invade once it appears (per the Phase 09.1 invasion mechanic in PR #29). Tests - 5 new ai-controller.test.ts cases: depth-gate accept/reject, spread bias picks farther anchor, Queen-first FS gate, Queen-pending gate for FS. - 2 new ai-controller.integration.test.ts cases: 18000-tick acceptance (max chamber Y > 15 OR 30% width spread), determinism (two seeded runs produce byte-identical chamber positions). - Existing REQ-C1 integration test budget extended from 3000 to 6000 ticks. - AI_QUEEN_CHAMBER_DEPTH constant test updated from 10 to 18. 1576/1576 tests green; tsc and full vitest pass; 2 internal review rounds — final pass clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ai): bounds-check footprint probes in collectFrontierTiles (codex P2) `collectFrontierTiles` built footprint membership keys as `ty * grid.width + tx` and probed neighbors via `isFootprint(tx + 1, ty)` and friends without bounds-checking. For candidates on the right edge (`tx === grid.width - 1`), `tx + 1 === grid.width` aliased the key onto `(0, ty + 1)` — a chamber footprint hugging the LEFT edge one row below would falsely "block" right-edge frontier tiles. The same hazard applied to (-1, ty) wrapping into (width-1, ty-1). Add the bounds guard inside `isFootprint`, mirroring `canEnterUndergroundTile`'s out-of-bounds rejection (off-grid cells are treated as not-in-any-footprint, so they never block frontier emission). Regression test: two 1×1 chambers at opposite ends of the grid (X=0 and X=63) in the same row. Without the fix the right-edge chamber's top tile (63, 4) is wrongly classified as adjacent to the left chamber and never makes the frontier dig pass; the legacy perimeter pass also exhausts the remaining budget before reaching it. With the fix (63, 4) appears in the dig commands as expected. 1577/1577 tests green; 1 internal review round on the fix — clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ai): bootstrap-while-Queen-pending + Queen-only depth gate (codex P1+P2) Two follow-ups from the codex round-2 review: P1 — bootstrap deadlock when Queen anchor is unreachable ======================================================== The previous gate stopped bootstrap as soon as Queen was PENDING. If the Queen anchor sat behind unreachable Solid tiles, workers couldn't reach it, and with bootstrap halted no further dig marks were issued — the Queen never completed and the colony was stuck. Restrict the gate to "Queen ChamberRecord exists" so bootstrap continues marking neighbors of the deepest Open tile until the Queen actually lands. The extra dig marks around the Queen anchor's vicinity guarantee workers can punch their way to the anchor as needed; once Queen completes, bootstrap stops and steady-state perimeter dig takes over. P2 — depth gate silently rejected FS/Nursery placements ======================================================= The depth gate in findOpenChamberSpot rejected ALL chamber types when no candidate was within ±tolerance of preferredDepth. For Queen this is the intended behavior (issue #33's whole point — wait for the bootstrap dig). For FoodStorage (preferredDepth=5) and Nursery (preferredDepth=7), restricting valid anchors to ±4 rows could silently never place the chamber if the dug area extended deeper than ±tolerance before the gate fired. Restrict the gate to Queen placement only; FS and Nursery now fall through to the closest-available-Y sort and land at whatever depth the dug area provides. Also tightens the Queen placement gate to use hasChamberOrPending (matches FS / Nursery uniqueness pattern) — pre-fix the sim layer rejected duplicate Queen PlaceChamber commands during the pending→completed window, but no point spamming the queue with rejected commands. Tests - New test: bootstrap dig continues while Queen is pending (regression for the P1 deadlock scenario). - New test: depth gate does NOT apply to FoodStorage when only deep Open tiles exist (regression for the P2 silent-stall scenario). 1579/1579 tests green; 1 internal review round on the fix — clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 09.1 already shipped the cross-grid invasion sim path (REQ-C3a / REQ-C4a) — but the player had no UI to actually use it. This PR adds the four input + HUD seams that surface the mechanic:
Closes #14.
What changed
src/input/surface-input.tsisForeignColonyEntrancehelper; new branch inhandleSurfaceLeftClickbetween food-pile mark and empty-tile rally.src/input/underground-input.tsviewState.activeUndergroundColonyId !== PLAYER_COLONY_ID. Defensivestate.isDragging = falseon the new guard.src/render/sprites.tsHUD.UNDERGROUND_COLONY_TOGGLEzone (112×22 above VIEW_TOGGLE).src/input/camera-input.tsisPointerOverHUDgains optionalviewState; the toggle zone is masked only whenactiveView === 'underground'.src/render/ui-scene.tstoggleUndergroundColony). Added to ant-activity panel dismiss allowlist.src/render/draw-surface.tsVerification
npm run verify— exit 0 (lint + typecheck + sim-boundary + asset-path + 1533/1533 tests).surface-input.test.ts,underground-input.test.ts,camera-input.test.ts,draw-surface.test.ts,sprites.test.ts.Test plan
npm run devand verify the new "Your Colony (X)" button is clickable from the underground view; it flips to "Enemy Colony (X)" and renders the enemy hive.🤖 Generated with Claude Code