Skip to content

fix(engine): label flexible mana lands with the color they will produce (#6944) - #6953

Merged
matthewevans merged 2 commits into
mainfrom
fix/6944-mana-label-projection
Aug 3, 2026
Merged

fix(engine): label flexible mana lands with the color they will produce (#6944)#6953
matthewevans merged 2 commits into
mainfrom
fix/6944-mana-label-projection

Conversation

@matthewevans

@matthewevans matthewevans commented Aug 3, 2026

Copy link
Copy Markdown
Member

Fixes #6944.

The bug

City of Brass, Reflecting Pool, Command Tower and friends rendered an unlabelled "Tap for mana" instead of showing the mana each activation would produce.

project_action_payload handled TapLandForMana and ActivateManaSource in a single arm and resolved both through live_mana_source_option_for_selection. But the two actions carry deliberately different selection forms:

Action Minted from Executed by
TapLandForMana ManaSourceOption::semantic_selection — one concrete row per producible color handle_tap_land_for_manalive_land_mana_option_for_selection
ActivateManaSource activatable_mana_source_selectionsmanual_selection_for_option, which intentionally collapses a flexible source to Colorless + DeferredColorChoice so the ordinary mana-choice resolver asks for the color activate_mana_source_selectionlive_mana_source_option_for_selection

That divergence is deliberate and is not the bugmana_sources.rs documents it: "The planner emits one concrete row per color, but a manual activation must retain the source capability and let the normal mana-choice resolver ask for its color."

The bug is that the label path resolved a planner-minted TapLandForMana through the manual authority, which can never match a flexible source — so the lookup failed and the arm returned without pushing a surface. The old code was correct for ActivateManaSource and wrong for TapLandForMana. Execution was already right; only labelling called the wrong one of the two resolvers.

The fix

Split the arm so each action is labelled through the same resolver its own reducer executes, with the resolver passed to a shared push_produced_mana_surfaces. A future mana action variant now has to name an authority in order to compile — which is what the function's doc comment already claimed.

The fix is variant-agnostic: production_override_for_option maps all eight flexible_output variants to ProductionOverride::SingleColor(option.mana_type) and live_mana_output_units repeats it count times. Nothing is special-cased per card.

Coverage — all 8 flexible variants enumerated

Variant Tested Fixture
AnyOneColor yes City of Brass; plus a Resonating Lute grant (count: 2)
AnyTypeProduceableBy yes Reflecting Pool
OpponentLandColors yes Exotic Orchard
AnyInCommandersColorIdentity yes Command Tower
AnyOneColorAmongPermanents yes Plaza of Heroes
ChoiceAmongExiledColors yes Pit of Offerings
AnyCombination no — stated gap No bare-{T} land printing exists; every printing (Cascading Cataracts, Calciform Pools, Baxter Building, Chicago Loop) gates it behind a Composite/PaySpeed cost, so reaching it needs a funded pool. Covered only by the shared code path.
AnyCombinationOfObjectColors no — unreachable No land printing; only Omnath, Locus of All, a creature. Already documented as "unreachable for the only current printing" at casting_costs.rs:11369-11373.

Six of eight tested, one stated gap, one unreachable with code evidence. The shared-code-path argument covers the last two, but that is an argument rather than a measurement and is not counted as coverage.

Discrimination — observed, not asserted

The old resolver was restored and the full suite re-run: exactly 7 failures, all of them the new tests, zero collateral (22,800 passed / 7 failed at base; 22,807 passed with the fix). Base output reproduces the report verbatim:

City of Brass:      got [[], [], [], [], []]        <- five unlabelled "Tap for mana"
Reflecting Pool:    got [[]]
Exotic Orchard:     got [[]]
Command Tower:      got [[]]
Pit of Offerings:   got [["C"], []]
Plaza of Heroes:    got [["C"], [], [], [], [], [], []]
Resonating Lute:    got [["G"], [], [], [], []]

The mixed rows are the sharpest evidence — on the same land the non-flexible sibling ability keeps its label while the flexible one goes blank, which is exactly the reported symptom. All seven drive the real projection pipeline (derive_viewer_interaction over a viewer-filtered state); none is an AST-shape test.

Notes for review

  • No CR annotations. project_action_payload projects a label; it implements no rule, and CLAUDE.md exempts plumbing. An earlier draft cited CR 605.3a — that rule is about when a mana ability may be activated, which does not support a claim about selection representation, so it was removed rather than replaced with another plausible number.
  • The two silent returns were kept deliberately. Enumeration runs on the unfiltered state while labelling re-enumerates on the viewer-filtered one, so a legitimate filtering-induced divergence must not panic in a shared multiplayer path. Failing closed mirrors the reducer's own refusal to activate a stale selection. The enforcement added instead is structural (the split arm) plus the seven tests. A tracing::debug! on the Err is available if wanted.
  • ActivateManaSource is the arm that did not change behaviourally, and it has no new test — reaching it needs an AutoExceptSacrificialMana payment window. Its correctness rests on the call-graph trace above, not on observation. Flagging this as the main residual risk.

Out of scope, worth follow-ups

  1. Nonland mana sources get no label at all — Llanowar Elves, Mox Amber, Birds of Paradise surface as ActivateAbility, whose arm pushes only ability_index and no ProducedMana. Wider than Flexible mana lands render as unlabeled "Tap for mana" — label projection uses the wrong selection authority #6944 and needs its own design decision.
  2. ActivateManaSource labelling of a flexible source takes matches.next(), arbitrarily the first color. Pre-existing and untouched here, but arguably wrong for a deferred-choice surface.
  3. GameScenario::build() runs no layer pass (the grant test needed an explicit layers::evaluate_layers), and a grant suppresses a basic land's subtype-inferred mana row because land_mana_options skips that fallback once any explicit Effect::Mana exists. The second may be a real bug.

Summary by CodeRabbit

  • Bug Fixes

    • Improved mana-source selection for flexible mana abilities.
    • Corrected displayed mana labels when selections are stale, invalid, or resolved through different sources.
    • Ensured flexible land candidates consistently show the mana they can produce.
  • Tests

    • Added coverage for flexible lands, granted abilities, land types, opponent lands, commander identity, permanent colors, and exiled-card colors.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@matthewevans, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d2c3bdaf-ca3d-42d7-9d1a-1947cf552584

📥 Commits

Reviewing files that changed from the base of the PR and between ebe129e and 8ee25c7.

📒 Files selected for processing (2)
  • crates/engine/src/game/interaction.rs
  • crates/engine/tests/integration/interaction_contract.rs
📝 Walkthrough

Walkthrough

The engine now resolves projected mana labels through the resolver that matches each action’s selection authority. Integration tests cover flexible mana sources and verify that every projected candidate has a non-empty ProducedMana label.

Changes

Flexible mana projection

Layer / File(s) Summary
Selection-aware mana surface projection
crates/engine/src/game/interaction.rs
Adds a shared produced-mana projection helper. TapLandForMana uses live_land_mana_option_for_selection, while ActivateManaSource uses live_mana_source_option_for_selection.
Flexible mana label regression coverage
crates/engine/tests/integration/interaction_contract.rs
Adds helpers and integration tests for flexible mana from lands, granted abilities, opponent-controlled lands, commander identity, legendary permanents, and exiled cards.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: bug

Suggested reviewers: lgray, invalidcards

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary fix: labeling flexible mana lands with their produced color.
Linked Issues check ✅ Passed The implementation uses the correct resolver for each action type and adds coverage for flexible mana-source label projection as required by [#6944].
Out of Scope Changes check ✅ Passed The changes are limited to engine label projection and regression tests for flexible mana-source interactions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/6944-mana-label-projection

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/engine/tests/integration/interaction_contract.rs (1)

3205-3323: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a regression test for the ActivateManaSource flexible-mana path.

All seven new tests exercise TapLandForMana only. The refactor in interaction.rs extracted the surface-emission logic into a shared push_produced_mana_surfaces helper used by both TapLandForMana and ActivateManaSource. ActivateManaSource keeps its original resolver, but its surface-emission code now runs through the new shared path. Add a test for a flexible mana source activated as a non-land permanent ability (ActivateManaSource, e.g., a creature or artifact with "{T}: Add one mana of any color") to confirm the shared-helper extraction did not change its produced-mana label.

Do you want me to draft this test, following the same pattern as tap_land_for_mana_labels_each_color_of_an_any_one_color_land?

As per path instructions for crates/engine/tests/**: "Test adequacy is the highest-frequency contributor finding — scrutinize it."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/tests/integration/interaction_contract.rs` around lines 3205 -
3323, Add a regression test in the interaction contract tests for a non-land
permanent with an activated mana ability such as "{T}: Add one mana of any
color", exercising the ActivateManaSource interaction rather than
TapLandForMana. Follow the existing projected-label test pattern and assert that
the produced mana labels include the expected flexible color choices, verifying
the shared push_produced_mana_surfaces path preserves ActivateManaSource
behavior.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/engine/tests/integration/interaction_contract.rs`:
- Around line 3205-3323: Add a regression test in the interaction contract tests
for a non-land permanent with an activated mana ability such as "{T}: Add one
mana of any color", exercising the ActivateManaSource interaction rather than
TapLandForMana. Follow the existing projected-label test pattern and assert that
the produced mana labels include the expected flexible color choices, verifying
the shared push_produced_mana_surfaces path preserves ActivateManaSource
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e3a3fe6-1ebb-4dd6-ba62-dbb4a87f609a

📥 Commits

Reviewing files that changed from the base of the PR and between 16c5b96 and ebe129e.

📒 Files selected for processing (2)
  • crates/engine/src/game/interaction.rs
  • crates/engine/tests/integration/interaction_contract.rs

@matthewevans

Copy link
Copy Markdown
Member Author

Agreed on the gap — it's the residual risk called out in the PR body, and it's fair to push on it since the extraction does now carry ActivateManaSource's surface emission.

One correction on the suggested fixture, though: a creature or artifact with {T}: Add one mana of any color activated at priority will not reach this path.

  • engine.rs:5555 does show the reducer accepting GameAction::ActivateManaSource under WaitingFor::Priority, so the action is dispatchable there.
  • But the projection that emits the labels — direct_choice_projection (crates/engine/src/game/interaction.rs:1999) — only constructs ActivateManaSource actions in its WaitingFor::ManaSourceSelection arm (:2015-2023), mapping over that variant's options. There is no Priority arm that produces them.

So a label test has to drive the game into a ManaSourceSelection window rather than activating at priority.

Worth noting for anyone reading this later: nonland mana sources (Llanowar Elves, Mox Amber, Birds of Paradise) surface as GameAction::ActivateAbility, whose arm pushes only ability_index and no produced-mana surface at all — so those permanents get no mana label by any path. That's a wider pre-existing gap than #6944 and is listed as follow-up 1 in the PR description.

I'm attempting the ManaSourceSelection fixture now. If it turns out to need a materially more complex setup than the seven TapLandForMana tests, I'll leave the gap explicitly documented rather than contort the fixture — the arm's resolver and body are unchanged by this PR, and the honest statement of what is untested is worth more than a test that only appears to cover it.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Generated for head 8ee25c75b606dda93499df2a80bcc04916bb2f6f.

Parse changes introduced by this PR

✓ No card-parse changes detected.

…ce (#6944)

City of Brass, Reflecting Pool, Command Tower and friends rendered an
unlabelled "Tap for mana" instead of showing the mana each activation
would produce.

`project_action_payload` handled `TapLandForMana` and `ActivateManaSource`
in one arm and resolved both through `live_mana_source_option_for_selection`.
But the two actions carry deliberately different selection forms:

- `TapLandForMana` is minted from `ManaSourceOption::semantic_selection` --
  one concrete row per producible color -- and is executed by
  `handle_tap_land_for_mana` via `live_land_mana_option_for_selection`.
- `ActivateManaSource` is minted from `activatable_mana_source_selections`,
  whose `manual_selection_for_option` intentionally collapses a flexible
  source to `Colorless` + `DeferredColorChoice` so the ordinary mana-choice
  resolver asks for the color, and is executed by
  `activate_mana_source_selection` via `live_mana_source_option_for_selection`.

That divergence is deliberate and is not the bug. The bug is that the label
path resolved a planner-minted `TapLandForMana` through the *manual*
authority, which can never match a flexible source -- so the lookup failed
and the arm returned without pushing a surface. The old code was correct
for `ActivateManaSource` and wrong for `TapLandForMana`.

Split the arm so each action is labelled through the same resolver its own
reducer executes, with the resolver passed to a shared
`push_produced_mana_surfaces`. A future mana action variant now has to name
an authority to compile, which is what the function's doc comment already
claimed.

The fix is variant-agnostic: `production_override_for_option` maps all eight
`flexible_output` variants to `ProductionOverride::SingleColor`, so nothing
is special-cased per card.

Seven tests drive the real projection pipeline (`derive_viewer_interaction`
over a viewer-filtered state), covering six of the eight flexible variants:
City of Brass, Reflecting Pool, Exotic Orchard, Command Tower, Plaza of
Heroes, Pit of Offerings, and a Resonating Lute grant. All seven were
confirmed red at base by restoring the old resolver and re-running -- 7
failures, no collateral -- reproducing the report verbatim, including mixed
rows where a non-flexible sibling ability keeps its label while the flexible
one goes blank.

Not covered: `AnyCombination` has no bare-{T} land printing (every printing
gates it behind a Composite/PaySpeed cost, so it needs a funded pool) and
`AnyCombinationOfObjectColors` is unreachable for its only current printing,
as already documented at casting_costs.rs. Both share the fixed code path.
The seven tests added with the fix all drive `TapLandForMana`. The
`ActivateManaSource` arm keeps its original resolver and is behaviourally
unchanged, but its surface emission now runs through the extracted
`push_produced_mana_surfaces`, and nothing covered that.

Reaching it is not obvious: the reducer accepts `ActivateManaSource` under
`WaitingFor::Priority` (`engine.rs`), but `direct_choice_projection` only
constructs those actions in its `WaitingFor::ManaSourceSelection` arm, so a
label test has to drive the game into a mana-source-selection window rather
than activate at priority. A sacrificial mana source gets there.

Covers a fixed and a flexible source through the same window, so the shared
helper is pinned for both arms rather than only the one the fix changed.

Raised by CodeRabbit on #6949's sibling PR.
@matthewevans
matthewevans force-pushed the fix/6944-mana-label-projection branch from ebe129e to 8ee25c7 Compare August 3, 2026 10:01
@matthewevans
matthewevans added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit 37fa488 Aug 3, 2026
14 checks passed
@matthewevans
matthewevans deleted the fix/6944-mana-label-projection branch August 3, 2026 10:35
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.

Flexible mana lands render as unlabeled "Tap for mana" — label projection uses the wrong selection authority

1 participant