Skip to content

fix(parser): UNSUPPORTED cluster: Land/permanent STATIC-STRUCTURE parsing — static_structure gap o#4694

Open
ntindle wants to merge 2 commits into
phase-rs:mainfrom
ntindle:fix/who-misparse-81-unsupported-cluster-land-permanent
Open

fix(parser): UNSUPPORTED cluster: Land/permanent STATIC-STRUCTURE parsing — static_structure gap o#4694
ntindle wants to merge 2 commits into
phase-rs:mainfrom
ntindle:fix/who-misparse-81-unsupported-cluster-land-permanent

Conversation

@ntindle

@ntindle ntindle commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes a parser misparse affecting 2 card(s) in the Doctor Who Commander precons.

Root cause: UNSUPPORTED cluster: Land/permanent STATIC-STRUCTURE parsing — static_structure gap on lands/locations with continuous abilities

Cards corrected

  • Singing Towers of Darillium
  • Two Streams Facility

Fix

Implemented the reviewed cluster-81 plan across all three workstreams on the worktree (upstream/main @ 8da56cc, much newer than the plan's base). I first re-verified both cards still misparse on current main (Singing Towers: static_structure Unimplemented; Two Streams: two static_structure Unimplemented + a "who" Unimplemented chaos trigger), then implemented the full plan.

W0 (shared command-zone static-source admission): added game::functioning_abilities::object_sources_static_from_command_zone (single admission authority for emblems, face-up conspiracies, and active planes/phenomena via active_zones.contains(Command)); threaded it through static_source_index.rs and both command-loop gates in layers.rs::for_each_static_effect_source; switched static_abilities::additional_land_drops from battlefield_active_statics to game_active_statics so command-zone plane land-drop statics are seen. synthesize_planechase already stamps [Command] (verify-only).

Workstream A (Two Streams — per-player persistent "last chose "): added Player.chosen_attributes (reuses ChosenAttribute::Label) + game::players::player_last_chose_label single authority; added WaitingFor::NamedChoice.persist_player routing (set from ability.scoped_player during player_scope fan-out), threaded through effects/choose.rs (resolve raise site, bind_named_choice dual-destination that writes to the player NOT the object, resolve_random_in_chain=None) and engine_resolution_choices.rs; added typed variants TargetFilter::PlayerWhoChoseLabel, FilterProp::ControllerChoseLabel, Effect::SwapChosenLabels + new game/effects/swap_chosen_labels.rs resolver; parser: player-scope land-drop subject + rule_static_affected_is_player_scope allow-list, "controlled by players who last chose " anthem subject, strip_each_player_subject "who last chose" bail + parse_swap_chosen_labels registered in parse_effect_clause_inner; static_filter_matches explicit arm before the fail-open catch-all; matches_filter_prop arm + all FilterProp/TargetFilter classifier sites.

Workstream B (Singing Towers — derived-cost off-zone keyword grant): parameterized ContinuousModification::AddKeywordWithDerivedCost { kind: CostBearingKeywordKind, derivation: CostDerivation } (NOT a foretell-only leaf) covering the plain-ManaCost CR-702 off-zone family (Foretell/Madness/Disturb/Mayhem/Dash/Unearth); added CostBearingKeywordKind (mirrors DynamicKeywordKind, with with_cost/from_name/matches_keyword), CostDerivation::ManaCostReducedBy + ManaCost::reduced_generic_by; off-zone applier arm (threads recipient object_id, per-recipient "without " dedup) + supports_off_zone_keyword_query; layer()/apply_continuous_effect_filtered/b_changes_abilities registration; routed foretell_cost through effective_off_zone_keywords across all three call sites (can_foretell_card, handle_foretell, grant_permission compute-before-mut-borrow); parser parse_hand_cards_have_derived_cost_keyword.

Both cards now parse with zero Unimplemented/Unknown (verified via fresh oracle-gen + gen-card-data jq gap check). Added building-block tests (admission helper, mana reduction, keyword family, bind_named_choice routing, swap resolver, parser tests) and TWO runtime card-tests in planechase_tests.rs proving the command-zone statics apply end-to-end (green-anchor land drop, red-waterfall +2/+0 haste anthem, derived-cost foretell {2}{U}{U}).

Verification (worktree not under Tilt, cargo run directly): cargo fmt clean; cargo clippy -p engine -p phase-ai --all-targets 0 warnings; cargo test -p engine 14410 passed 0 failed (all integration binaries green); cargo test -p phase-ai green; cargo check -p engine-wasm clean (tsify boundary); frontend pnpm type-check + lint green (0 errors); parser diff gate PASS (zero string-dispatch in added non-test parser lines).

Files changed

  • crates/engine/src/game/functioning_abilities.rs
  • crates/engine/src/game/static_source_index.rs
  • crates/engine/src/game/layers.rs
  • crates/engine/src/game/static_abilities.rs
  • crates/engine/src/types/player.rs
  • crates/engine/src/game/players.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/keywords.rs
  • crates/engine/src/types/layers.rs
  • crates/engine/src/types/mana.rs
  • crates/engine/src/game/effects/choose.rs
  • crates/engine/src/game/engine_resolution_choices.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/effects/swap_chosen_labels.rs
  • crates/engine/src/game/filter.rs
  • crates/engine/src/game/off_zone_characteristics.rs
  • crates/engine/src/game/casting.rs
  • crates/engine/src/game/effects/grant_permission.rs
  • crates/engine/src/parser/oracle_static/shared.rs
  • crates/engine/src/parser/oracle_static/grammar.rs
  • crates/engine/src/parser/oracle_static/dispatch.rs
  • crates/engine/src/parser/oracle_static/mod.rs
  • crates/engine/src/parser/oracle_static/tests.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/game/printed_cards.rs
  • crates/engine/src/game/quantity.rs
  • crates/engine/src/game/cost_payability.rs
  • crates/engine/src/game/trigger_matchers.rs
  • crates/engine/src/analysis/ability_graph.rs
  • crates/engine/src/ai_support/filter.rs
  • crates/engine/src/ai_support/candidates.rs
  • crates/engine/src/ai_support/mod.rs
  • crates/phase-ai/src/policies/redundancy_avoidance.rs
  • crates/phase-ai/src/policies/x_value.rs
  • crates/engine/src/game/planechase_tests.rs
  • crates/engine/src/game/casting_tests.rs
  • crates/engine/src/game/engine_phase_trigger_regression_tests.rs
  • crates/engine/tests/greymond_avacyns_stalwart.rs
  • crates/engine/tests/integration/frostcliff_siege_anchor_word_modes.rs
  • crates/engine/tests/integration/morophon_chosen_type_1653.rs
  • crates/engine/tests/integration/serras_emissary_chosen_card_type_protection.rs

CR references

  • CR 113.6b
  • CR 114.3
  • CR 114.4
  • CR 311.2
  • CR 312.2
  • CR 305.2
  • CR 305.2a
  • CR 305.2b
  • CR 607
  • CR 614.12c
  • CR 611.2c
  • CR 613.1f
  • CR 613.4c
  • CR 702.10a
  • CR 702.143a
  • CR 702.143b
  • CR 702.143d
  • CR 311.7
  • CR 901.9b
  • CR 102.1
  • CR 905.4

Verification

  • cargo fmt --all — pass (no files changed, nothing to commit)
  • check-parser-combinators.sh (upstream/main merge-base 8e3e928c) — pass (exit 0)
  • cargo clippy -p engine --all-targets -- -D warnings — pass (exit 0, no warnings; all 51 changed files clippy-clean)
  • cargo test -p engine — pass (136 test-result-ok blocks, zero failures)
  • oracle-gen data --filter 'singing towers of darillium|two streams facility' — pass (both cards fully parsed; no Unimplemented effects / Unknown trigger modes)
    Cards confirmed re-parsed correctly: singing towers of darillium, two streams facility

🤖 Generated with Claude Code

@ntindle ntindle requested a review from matthewevans as a code owner July 1, 2026 06:21
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 4 card(s), 7 signature(s) (baseline: main a3e09d5acd58)

4 card(s) · ability/static_structure · removed: static_structure

Examples: Bohn, Beguiling Balladeer, Dream Devourer, Singing Towers of Darillium (+1 more)

3 card(s) · static/Continuous · added: Continuous (affects=in hand you control card non-land, mods=derived-cost keyword Foretell)

Examples: Bohn, Beguiling Balladeer, Dream Devourer, Singing Towers of Darillium

1 card(s) · static/Continuous · added: Continuous (affects=controlled by a player who last chose Red waterfall creature, mods=power +2, toughness +0, grant Haste)

Examples: Two Streams Facility

1 card(s) · static/MayPlayAdditionalLand · added: MayPlayAdditionalLand (affects=player who last chose Green anchor)

Examples: Two Streams Facility

1 card(s) · trigger/Phase · field constraint: once per game

Examples: Two Streams Facility

1 card(s) · ability/SwapChosenLabels · added: SwapChosenLabels (swap=Green anchor <-> Red waterfall)

Examples: Two Streams Facility

1 card(s) · ability/who · removed: who

Examples: Two Streams Facility

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[HIGH] The new NamedChoice field leaves a workspace crate fixture uncompilable. Evidence: crates/manabrew-compat/src/lib.rs:2338 and crates/engine/src/types/game_state.rs:3474. Why it matters: manabrew-compat is included by workspace members = ["crates/*"], so this initializer is now missing required field persist_player and the workspace will not compile. Suggested fix: add persist_player: None to that WaitingFor::NamedChoice test fixture.

Reviewed current head 357eead3076a765996d6ca84555a8838539c491b.

@matthewevans matthewevans added the bug Bug fix label Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants