Skip to content

Add Dazzling Beauty#5074

Merged
matthewevans merged 7 commits into
phase-rs:mainfrom
Spaceint:card/dazzling-beauty
Jul 5, 2026
Merged

Add Dazzling Beauty#5074
matthewevans merged 7 commits into
phase-rs:mainfrom
Spaceint:card/dazzling-beauty

Conversation

@Spaceint

@Spaceint Spaceint commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds engine support for Dazzling Beauty by building the general
Effect::BecomeBlocked { target: TargetFilter } primitive — an effect that makes
a target unblocked attacking creature become blocked with no blockers assigned
(CR 509.1h; works even on creatures that can't be blocked). This is built for
the class, not the card: ~21 unsupported cards share the "target … becomes
blocked" effect (Curtain of Light, Trap Runner, Dazzling Beauty, …). Dazzling
Beauty then composes it with the already-supported delayed-draw and
declare-blockers-step casting restriction (both left untouched).

Key design points:

  • New typed GameEvent::AttackerBecameBlockedByEffect { attacker } so an
    effect-driven block fires the bare "Whenever ~ becomes blocked" trigger
    only if the attacker was unblocked at that instant (CR 509.3c), while
    blocker-side "whenever ~ blocks" and "becomes blocked by a creature"
    triggers do not fire (CR 509.3d). A dedicated event — rather than a
    synthetic BlockersDeclared with a fake blocker — is what keeps the 509.3d
    exclusion correct.
  • combat::mark_attacker_blocked sets the AttackerInfo.blocked flag with no
    blocker_assignments entry (contrast place_blocking, which needs a legal
    blocker and is deliberately not reused). The existing combat-damage seam
    already assigns no damage for a blocked-with-zero-blockers creature
    (CR 510.1c), so no change there.
  • Rules-read correctness: is_source_blocked and activate_ninjutsu now read the
    authoritative blocked flag (CR 509.1h, CR 702.49a) instead of
    blocker_assignments non-emptiness, matching unblocked_attackers and
    FilterProp::Unblocked. Without this, an effect-blocked creature would read as
    "not blocked" for "if ~ is blocked" conditions and would remain ninjutsu-eligible.

Files changed

  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/events.rs
  • crates/engine/src/game/combat.rs
  • crates/engine/src/game/effects/become_blocked.rs (new)
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/trigger_matchers.rs
  • crates/engine/src/game/trigger_index.rs
  • crates/engine/src/game/public_state.rs
  • crates/engine/src/game/log.rs
  • crates/engine/src/game/targeting.rs
  • crates/engine/src/game/restrictions.rs
  • crates/engine/src/game/keywords.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/game/printed_cards.rs
  • crates/engine/src/game/ability_scan.rs
  • crates/engine/src/parser/oracle_effect/subject.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/analysis/ability_graph.rs
  • crates/phase-ai/src/policies/effect_classify.rs
  • crates/phase-ai/src/policies/redundancy_avoidance.rs
  • crates/engine/tests/dazzling_beauty_become_blocked.rs (new)

CR references

  • CR 509.1h — an effect can make an attacking creature become blocked; it remains blocked even if all blockers are removed (authorizing rule).
  • CR 510.1c — a blocked creature with no creatures blocking it assigns no combat damage.
  • CR 509.3c — the "becomes blocked" trigger also fires when a creature becomes blocked by an effect, but only if it was unblocked at that time.
  • CR 509.3d — it won't fire "becomes blocked by a creature" / blocker-side triggers when the block is caused by an effect.
  • CR 509.1b, CR 509.1g — declare-blockers turn-based action / a blocking creature is declared (contrast: the place-a-blocker case).
  • CR 702.49a — Ninjutsu returns an unblocked attacking creature.
  • CR 608.2b — illegal-target fizzle (a non-attacking target no-ops).
  • CR 613.1f — Layer 6 re-evaluation on a combat-state change.

Gate A

./scripts/check-parser-combinators.sh — exit 0, no violations (the script emits
no output on success). No .contains/.starts_with/.split_once/match-arm string
literals introduced; parser dispatch is a nom all_consuming(tag("blocked")) arm.

Anchored on

  • crates/engine/src/parser/oracle_effect/subject.rs:3416 — the existing
    all_consuming(tag("saddled")) BecomeSaddled arm in build_become_clause;
    the new "blocked" arm (subject.rs:3432) mirrors the same nom-combinator idiom
    and the same target = application.target … unwrap_or_else(affected) threading.
  • crates/engine/src/game/effects/saddle.rs:78resolve_object_targets
    (SelfRef → source / event-context ref / announced targets); become_blocked.rs
    is a structural clone of this resolver shape.
  • crates/engine/src/game/combat.rs:391place_blocking, the sibling
    combat-state mutator; mark_attacker_blocked (combat.rs:438) follows the same
    shape (deliberately not reused — place_blocking requires a legal blocker,
    which an unblockable creature has none of).
  • crates/engine/src/types/ability.rs:11143RemoveFromCombat, the sibling
    combat-state Effect variant BecomeBlocked is placed alongside and mirrors
    for every registration site (target_filter, EffectKind, From<&Effect>).

Track

Non-developer — this environment cannot build client/public/card-data.json
(the per-set MTGJSON fetch fails under Git Bash on Windows), so cargo coverage
and cargo semantic-audit could not run locally. CI runs them on this PR.
Substantial local verification did run (below).

LLM

Model: claude-opus-4-8
Thinking: high

Verification

Local (Non-developer — coverage/semantic-audit deferred to CI):

  • cargo fmt --all — clean
  • ./scripts/check-parser-combinators.sh — clean (exit 0)
  • cargo clippy --workspace --exclude phase-tauri --all-targets --features engine/proptest -- -D warnings — clean (engine, phase-ai, engine-wasm, server-core, …)
  • cargo test -p engine --lib — 14941 passed / 0 failed (incl. the new resolver, is_source_blocked, ninjutsu, and CR 509.3d block-side-guard unit tests)
  • cargo test -p engine --test dazzling_beauty_become_blocked — 5 passed / 0 failed
  • cargo test -p phase-ai — all green
  • cargo check --package engine-wasm --target wasm32-unknown-unknown — clean
  • cargo engine-inventory — regenerated; BecomeBlocked present, no collision
  • Re-verified after merging current upstream/main.

Discriminating runtime tests (drive the real cast pipeline; fail on revert):

  • becomes_blocked_deals_no_damage (+ control_without_spell_takes_full_damage reach-guard) — target deals 0 to the defending player once made blocked.
  • becomes_blocked_trigger_fires_and_block_side_trigger_does_not — CR 509.3c positive paired with the CR 509.3d negative in one scenario.
  • works_on_unblockable — a "can't be blocked" attacker still becomes blocked (proves the effect does not route through place_blocking).
  • is_source_blocked_reads_blocked_flag_not_assignments, ninjutsu_fails_if_attacker_blocked_by_effect_without_assignments — each with a positive reach-guard.

Scope Expansion

This Add ships a general building block (as its size reflects): a new
Effect::BecomeBlocked variant + resolver, a new typed
GameEvent::AttackerBecameBlockedByEffect for CR 509.3c/509.3d trigger fidelity,
the combat::mark_attacker_blocked helper, and rules-read corrections to
is_source_blocked and activate_ninjutsu (both previously keyed off
blocker_assignments non-emptiness, which is wrong for effect-driven blocks). The
primitive covers the whole ~21-card "becomes blocked" effect class, not just
Dazzling Beauty.

Validation Failures

None.

CI Failures

None.

Tier: Frontier

AI Contributor and others added 2 commits July 4, 2026 14:00
Adds a general Effect::BecomeBlocked { target: TargetFilter } primitive so an
effect can make a target unblocked attacking creature become blocked with no
blockers (CR 509.1h), covering the ~21-card 'becomes blocked' effect class.

- New Effect::BecomeBlocked variant + resolver (game/effects/become_blocked.rs)
- New GameEvent::AttackerBecameBlockedByEffect for CR 509.3c triggers (fires the
  bare 'becomes blocked' trigger only if the attacker was unblocked; CR 509.3d
  keeps blocker-side 'whenever ~ blocks' from firing)
- combat::mark_attacker_blocked helper (sets blocked flag, no blocker_assignments)
- Fix is_source_blocked and activate_ninjutsu to read the blocked flag rather
  than blocker_assignments non-emptiness (CR 509.1h / 702.49a)
- Parser: build_become_clause 'blocked' arm (nom all_consuming(tag))

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Spaceint Spaceint requested a review from matthewevans as a code owner July 4, 2026 06:15
@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!

@matthewevans

matthewevans commented Jul 4, 2026

Copy link
Copy Markdown
Member

Parse changes introduced by this PR · 5 card(s), 8 signature(s) (baseline: main 3734fb9f017d)

2 card(s) · ability/BecomeBlocked · added: BecomeBlocked (target=unblocked attacking creature)

Examples: Curtain of Light, Dazzling Beauty

2 card(s) · ability/become · removed: become

Examples: Curtain of Light, Dazzling Beauty

1 card(s) · ability/BecomeBlocked · added: BecomeBlocked (kind=activated, target=unblocked attacking creature)

Examples: Trap Runner

1 card(s) · ability/BecomeBlocked · added: BecomeBlocked (target=attacking creature)

Examples: Fog Patch

1 card(s) · ability/BecomeBlocked · added: BecomeBlocked (target=attacking creature, targets=X-X)

Examples: Choking Vines

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

Examples: Fog Patch

1 card(s) · ability/become · removed: become (kind=activated)

Examples: Trap Runner

1 card(s) · ability/become · removed: become (targets=X-X)

Examples: Choking Vines

2 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Jul 4, 2026
@matthewevans matthewevans self-assigned this Jul 4, 2026

@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.

Reviewed current head db72f0b. This is a parameterized BecomeBlocked engine primitive for the Dazzling Beauty/Curtain of Light/Fog Patch/Trap Runner/Choking Vines class, not a card-specific special case. I traced resolution through combat blocked state, trigger matching, ninjutsu/restriction predicates, parser output, AI classifiers, and the targeted runtime tests; live checks are green.

@matthewevans matthewevans added the enhancement New feature or request label Jul 4, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 4, 2026
@matthewevans matthewevans removed their assignment Jul 4, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 4, 2026
@matthewevans matthewevans self-assigned this Jul 4, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 4, 2026
@matthewevans matthewevans removed their assignment Jul 4, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 4, 2026
@matthewevans matthewevans self-assigned this Jul 4, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 4, 2026
@matthewevans matthewevans removed their assignment Jul 4, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 4, 2026
@matthewevans matthewevans self-assigned this Jul 4, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 4, 2026
@matthewevans matthewevans removed their assignment Jul 4, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 4, 2026
@matthewevans matthewevans self-assigned this Jul 4, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 4, 2026
@matthewevans matthewevans removed their assignment Jul 4, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 4, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 4, 2026
@matthewevans matthewevans self-assigned this Jul 5, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 5, 2026
@matthewevans matthewevans removed their assignment Jul 5, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 5, 2026
@matthewevans matthewevans self-assigned this Jul 5, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 5, 2026
@matthewevans matthewevans removed their assignment Jul 5, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 5, 2026
@matthewevans matthewevans self-assigned this Jul 5, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 5, 2026
@matthewevans matthewevans removed their assignment Jul 5, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 5, 2026
@matthewevans matthewevans self-assigned this Jul 5, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 5, 2026
@matthewevans matthewevans removed their assignment Jul 5, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 5, 2026
@matthewevans matthewevans self-assigned this Jul 5, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 5, 2026
@matthewevans matthewevans removed their assignment Jul 5, 2026
matthewevans and others added 4 commits July 4, 2026 19:40
Merge of upstream/main brought in the CR 603.3b read/write conflict profiler
(ability_rw.rs). Add BecomeBlocked to both exhaustive Effect matches, mirroring
the sibling combat-state mutator RemoveFromCombat (a TurnStructure write,
idempotent on a non-attacker/already-blocked target; CR 509.1h).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Spaceint

Spaceint commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Status: CI red is fixed and pushed.

Cause: merging current main into the branch pulled in the new CR 603.3b read/write conflict profiler (crates/engine/src/game/ability_rw.rs, from #5072), which has two exhaustive Effect matches — so the new Effect::BecomeBlocked variant made them non-exhaustive (E0004). The plain Merge branch 'main' left them unhandled.

Fix (commit in ability_rw.rs): added a BecomeBlocked arm to both matches, mirroring the sibling combat-state mutator RemoveFromCombat — a StateKind::TurnStructure write, idempotent on a non-attacker/already-blocked target (CR 509.1h). Re-verified locally against current main: Gate A clean, cargo clippy --workspace -D warnings clean, engine lib tests green, dazzling_beauty_become_blocked 5/5, wasm check clean.

On the parse-diff: the changes are the intended class generalization — the general Effect::BecomeBlocked primitive now covers the whole "target … becomes blocked" class (Dazzling Beauty, Curtain of Light, Trap Runner, plus Fog Patch and Choking Vines). It stays strictly on the effect axis and does not touch the casting/activation-timing (AfterBlockersDeclared) axis owned by #4997.

🤖 Addressed by Claude Code

@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.

Reviewed current head c0cc33d740de8358786a031eabe83a16e58cea58 after the merge-main fix.

The only PR-local change since the previously approved implementation is the ability_rw.rs classification for Effect::BecomeBlocked. It is threaded through both exhaustive matches: the legacy-tag visitor descends the target filter, and the read/write profiler mirrors RemoveFromCombat as a combat/turn-structure write while preserving target flagging. That is the right seam for the new CR 603.3b profiler interaction and does not change the previously reviewed BecomeBlocked runtime/parser surface.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants