Skip to content

Flashback, mandatory cost, and 3 card implementations#246

Merged
delebedev merged 15 commits intomainfrom
feat/flashback-and-mandatory-cost
Mar 26, 2026
Merged

Flashback, mandatory cost, and 3 card implementations#246
delebedev merged 15 commits intomainfrom
feat/flashback-and-mandatory-cost

Conversation

@delebedev
Copy link
Owner

Summary

Five cards working end-to-end with puzzles and integration tests:

  • Flashback (Think Twice) — ActionMapper extended to scan GY/Exile for castable spells, Forge handles resolve-to-exile automatically
  • Mandatory discard cost (Mardu Outrider) — PromptClassifier routes discard-cost prompts to client as SelectNReq with promptId=1024
  • Electroduplicate — flashback with abilityGrpId on GY cast actions + targeting during flashback cast
  • Novice Inspector — investigate (Clue token creation) + sac-for-draw lifecycle
  • Immersturm Predator — sacrifice-as-cost + tap trigger → GY exile + counter chain

Also fixed: displayCardUnderCard annotation now only fires for BF→Exile (not GY→Exile).

What's NOT in this PR (follow-up issues)

Test plan

  • FlashbackTest: 2 tests (action mapping + full hand→GY→flashback→exile lifecycle)
  • MandatoryDiscardCostTest: 5 tests (SelectNReq shape, resolves to BF, discard to GY)
  • ElectroduplicateTest: flashback abilityGrpId + targeting + exile
  • NoviceInspectorTest: cast → ETB → Clue token → sac-for-draw + ability registration
  • ImmersturmPredatorTest: sacrifice-as-cost → tap trigger → counter + exile zone verification
  • PromptClassifierTest: discard cost prompt classification
  • Full gate: 649/649 pass, detekt clean
  • Arena playtested: flashback (Think Twice), mandatory cost (Mardu Outrider), Novice Inspector, Immersturm Predator

🤖 Generated with Claude Code

delebedev and others added 12 commits March 26, 2026 21:05
ActionMapper now scans Graveyard and Exile zones for castable spells
(flashback, escape, etc.) via chooseCastAbility/getAlternativeCosts.
Uses the alternative cost's ManaCost for mana requirements instead of
the base card cost. AutoTap skipped for zone casts since the alt cost
may differ from cardData.manaCost.

MatchFlowHarness.castSpellByName extended to search Hand → GY → Exile.

Forge's Flashback keyword handles resolve-to-exile automatically via
AlternativeCost.Flashback replacement effect — no leyline change needed.

Tests:
- Pure: ActionMapper offers Cast for Think Twice in GY with 3 Islands
- Integration: cast resolves, draws card, Think Twice exiled from GY

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… end-to-end

Add PromptSemantic.SelectNDiscard so discard-as-cost prompts are classified
as SelectN (not Targeting). The engine fires a choose_cards prompt via
WebCostDecision.visit(CostDiscard); the new semantic routes it through
SelectNReq with context=Discard, listType=Static, optionContext=Payment.

Changes:
- InteractivePromptBridge: add SelectNDiscard to PromptSemantic enum
- WebCostDecision: pass semantic param to selectCards helper; tag typed
  discard path with SelectNDiscard
- PromptClassifier: add SelectN.Reason.Discard; classify SelectNDiscard
  before the candidateRefs catch-all
- TargetingHandler: handle ClassifiedPrompt.SelectN in handlePostCastPrompt
  (sends SelectNReq to client during cost payment)
- RequestBuilder: set Discard context/listType/optionContext for discard
  SelectNReq (vs Resolution for legend rule)
- Puzzle + test: Mardu Outrider (75493, {1}{B}{B} + discard) end-to-end
  cast → discard prompt → resolve → 5/5 on battlefield

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Puzzle now tests hand cast → GY → flashback cast → exile in one turn.
Library top = Coral Merfolk (drawn by hand cast), verifying both draws
land in hand. 6 Islands for full mana coverage ({1}{U} + {2}{U}).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use promptId=1024 (DISCARD_COST) for mandatory discard SelectNReq
  instead of generic 1243 — client now shows correct "discard" label
- Flashback puzzle: add valid Goal type (Play the Specified Permanent)
- Mardu puzzle: add second hand card so discard prompt is exercised
  (single card was auto-resolved without sending SelectNReq)

Playtested both puzzles in Arena — flashback and mandatory cost work
end-to-end.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r, Immersturm Predator

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r-draw

Integration test covering the full mechanic chain: cast Novice Inspector,
ETB creates Clue artifact token via investigate, activate Clue ({2}, sac)
to draw a card. Includes puzzle fixture and Clue token grpId registration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…g from GY

Add zone cast actions (GY/Exile/Command) to ActionMapper with abilityGrpId
derived from the alternate cost keyword (flashback, escape, etc.). Fix
PlayableActionQuery to not gate GY casts on mayPlay() — keyword-based alt
costs may not register explicit grants but are still castable.

- ActionMapper.addZoneCastActions: iterate GY/Exile/Command zones, offer Cast
  actions with abilityGrpId from keywordAbilityGrpIds map
- PlayableActionQuery: remove mayPlay guard for zone casts (fixes smart phase
  skip incorrectly skipping Main when flashback is available)
- MatchFlowHarness.castFromGraveyard: new helper for flashback/escape tests
- Puzzle: electroduplicate-flashback.pzl (Electroduplicate in GY, 2 creatures,
  4 Mountains for flashback cost)
- Integration test: verifies abilityGrpId on offered action, targeting prompt,
  flashback exile, and copy token creation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r chain

Adds integration test and puzzle for Immersturm Predator's sacrifice-as-cost
activated ability. Validates the full mechanic chain: activate ability -> sacrifice
cost prompt (routed as SelectTargetsReq via candidateRefs) -> creature sacrificed ->
Predator tapped -> tap trigger fires -> GY exile targeting -> +1/+1 counter added.

No production code changes needed — sacrifice-as-cost prompts already route correctly
through the existing PromptClassifier (candidateRefs -> Targeting) and SelectTargetsReq
pipeline. The test uses direct prompt bridge submission for reliability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GY→Exile (e.g. Immersturm Predator trigger) was rendering the exiled
card under the source creature instead of in the exile zone. Added
fromBattlefield flag to CardExiled event; displayCardUnderCard now
only fires for BF→Exile transitions (Fiend Hunter pattern).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ImmersturmPredatorTest: verify exiled card in Exile zone (not under
  source) when tap trigger exercises GY→Exile path
- NoviceInspectorTest: register Clue CardData with abilityIds so
  uniqueAbilities are populated in proto (ability grpId 152 = sac-draw)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Mar 26, 2026

Test Results

  262 files  +  149    262 suites  +149   3m 10s ⏱️ +59s
1 042 tests +  240  1 015 ✅ +841   26 💤  - 602  1 ❌ +1 
1 864 runs  +1 062  1 030 ✅ +856  833 💤 +205  1 ❌ +1 

For more details on these failures, see this check.

Results for commit 4f97f7b. ± Comparison against base commit 0d16c23.

♻️ This comment has been updated with latest results.

@github-actions
Copy link

github-actions bot commented Mar 26, 2026

CI Report — Gate

Tests: 182/813 passed, 1 failed (630 skipped)

Failed tests
  • ConformancePipelineTest.Sacrifice segment: land + Treasure mana produces recording-matched annotations (0.2s)

    java.lang.AssertionError: Expected value to not be null, but was null.

Coverage: 10.8% (642/5923 lines)

Module Coverage Lines
tooling 🔴 11% 642/5923
Slow tests (>3s): 4
  • AiTurnNoAarTest.AI turn sends no ActionsAvailableReq to client (34.5s)
  • ActivatedAbilityPuzzleTest.Goblin Fireslinger tap-to-ping kills opponent (12.1s)
  • PvpBridgeEndToEndTest.two-player game: land, creature, turn rotation, per-seat visibility (7.2s)
  • DeclareBlockersDedupeTest.no duplicate blockers req (6.3s)

@delebedev
Copy link
Owner Author

Code Review

Branch: feat/flashback-and-mandatory-costmain
Size: +1005 / -25 across 27 files (12 commits)

Overview

Five cards wired end-to-end with puzzle fixtures and integration tests: flashback (Think Twice, Electroduplicate), mandatory discard cost (Mardu Outrider), investigate/Clue tokens (Novice Inspector), and sacrifice-as-cost (Immersturm Predator). Also fixes displayCardUnderCard annotation to only fire for BF→Exile.

What's Good

  • Puzzle-driven dev followed faithfully. Every card has a .pzl fixture and a corresponding integration test. Tests verify the full mechanic chain, not just happy-path entry.
  • Clean architectural layering. Flashback/zone-cast logic lives in ActionMapper, discard-cost routing goes through PromptClassifierTargetingHandlerRequestBuilder. No cross-layer shortcuts.
  • PlayableActionQuery fix is smart. Removing the mayPlay() gate for zone casts fixes the root cause (smart phase skip incorrectly skipping Main1 when flashback is available) rather than patching around it.
  • displayCardUnderCard fix is precise. Adding fromBattlefield flag to CardExiled and gating the annotation is the right granularity — doesn't over-complicate the event model.
  • Good scope discipline. Known gaps (CopyPermanent subsystem, PayCostsReq conformance, ETB trigger annotations) are deferred to follow-up issues, not shoe-horned in.

Issues

  1. LOG.md should not be committed. Session scratch file with local paths, manual playtest checklists, and arena automation notes. Not in .gitignore and doesn't belong in the public repo. Remove it from the PR.

  2. tools/overlay/native/window-bounds is a committed binary. No explanation in the PR. Compiled binaries shouldn't be tracked unless there's a good reason. What is this, and should it be gitignored instead?

  3. @Suppress("UNUSED_PARAMETER") on abilityRegistryLookup in addZoneCastActions (ActionMapper.kt:296). The parameter is passed from the call site but never used inside the function. Either use it (the abilityGrpId lookup does a manual cardData?.keywordAbilityGrpIds search instead) or remove the parameter. The suppress annotation is a code smell that suggests the wiring was started but abandoned.

  4. addZoneCastActions only uses castable.first() (ActionMapper.kt:303). If a card has multiple castable abilities (e.g., a card with both flashback and escape), only the first is offered. Should this iterate all castable abilities and produce one action per ability?

  5. ImmersturmPredatorTest hardcodes prompt message text (line 653):

    sacPrompt!!.request.message.lowercase() shouldBe "select another creature to sacrifice (%d left)"

    Brittle — tied to Forge's internal prompt string. If Forge changes wording, the test breaks for the wrong reason. Assert on structural properties (candidateRefs count, semantic) instead.

  6. NoviceInspectorTest.beforeSpec mutates shared TestCardRegistry with Clue token grpId 300002. If another test uses the same registry concurrently and expects different token data, this could conflict. The magic numbers (300002, 152, 980803) deserve named constants or at minimum inline comments explaining where they come from.

  7. castSpellByName now searches Hand → GY → Exile but the original method is also used by tests that expect hand-only casting. If a card exists in both hand and GY, the hand copy wins (correct), but the behavior change is implicit. The new castFromGraveyard method covers the explicit case — consider whether the broadened castSpellByName is needed or if callers should be explicit about zone.

Minor

  • ElectroduplicateTest line 543: auto-pass combat skip logic is a workaround for test setup timing — a comment explaining why would help.
  • NoviceInspectorTest uses repeat(15) { ... passPriority() } as a polling loop. The passUntil helper (used in other tests) is cleaner and has a clear failure mode.
  • WebCostDecisionTest reflection call now passes 6 args including the new semantic parameter — one step more fragile. Not actionable now, worth noting for future refactor.

Verdict

Solid feature PR with good test coverage and clean mechanics wiring. Two blockers: LOG.md (remove) and window-bounds binary (explain or remove). The UNUSED_PARAMETER suppress and single-ability-per-card limitation are worth addressing before merge.

🤖 Generated with Claude Code

delebedev and others added 3 commits March 26, 2026 21:17
…lByName to hand-only

- Remove LOG.md (session scratch, not for public repo)
- Remove tools/overlay/native/window-bounds (stray binary from worktree merge)
- Revert castSpellByName to Hand-only search — GY fallback broke
  TargetingFlowTest (found spent spell in GY, submitted invalid cast)
- FlashbackTest uses castFromGraveyard explicitly for GY casts
- Remove @Suppress("UNUSED_PARAMETER") — give abilityRegistryLookup
  a default value instead
- Replace brittle prompt message assertion in ImmersturmPredatorTest
  with structural check (candidateRefs.size > 0)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Detekt UnusedParameter — the function resolves abilityGrpId from
cardData.keywordAbilityGrpIds directly, doesn't need the registry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Existing test for displayCardUnderCard assumed any CardExiled with
source triggers the annotation. Now requires fromBattlefield=true
to match the production gate (BF→Exile only).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@delebedev delebedev merged commit bc8bf62 into main Mar 26, 2026
2 of 4 checks passed
@delebedev delebedev deleted the feat/flashback-and-mandatory-cost branch March 26, 2026 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant