Skip to content

fix(engine): harden crime ledger and unblockable views - #7098

Merged
matthewevans merged 2 commits into
mainfrom
ship/nimble-crime-ledger
Aug 8, 2026
Merged

fix(engine): harden crime ledger and unblockable views#7098
matthewevans merged 2 commits into
mainfrom
ship/nimble-crime-ledger

Conversation

@matthewevans

@matthewevans matthewevans commented Aug 8, 2026

Copy link
Copy Markdown
Member

Follow-up to #7093 addressing post-merge review findings: enforce the one-per-turn crime ledger invariant, make crime zone classification exhaustive, precompute unblockable static restrictions for derived views, restrict that view to creatures, and cover the static-only frontend badge path.

Summary by CodeRabbit

  • New Features

    • Improved crime tracking so crimes are recorded only after successful ability placement.
    • Added support for identifying creatures that cannot be blocked.
    • Automatic targeting now consistently emits targeting events.
  • Bug Fixes

    • Prevented duplicate crime counts during the same turn.
    • Improved crime tracking across turn changes and replayed game events.
    • Permanent and temporary “can’t be blocked” effects now display badges correctly.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ba651997-cd27-40c8-946a-1920ce939522

📥 Commits

Reviewing files that changed from the base of the PR and between 1c9c18b and 5e042db.

📒 Files selected for processing (1)
  • crates/engine/tests/integration/crime_tracking.rs

📝 Walkthrough

Walkthrough

Changes

Crime tracking and blocking-status projections

Layer / File(s) Summary
Crime state and ledger contract
crates/engine/src/types/resolved_commands.rs, crates/engine/src/game/ledger.rs, crates/engine/tests/integration/crime_tracking.rs
Adds the CrimeCommitted ledger edit, replay-invariant validation, per-turn recording, application errors, and coverage for deduplication and turn resets.
Crime classification and post-stack commitment
crates/engine/src/game/casting.rs
Separates targeting events from crime detection, emits events for automatic target selection, and commits crimes only after successful stack placement.
Semantic blocking-status projection and client badge
crates/engine/src/game/derived_views.rs, crates/engine/src/game/combat.rs, client/src/components/board/__tests__/PermanentCard.test.tsx
Adds the sorted cant_be_blocked projection, eagerly collects block restrictions, and tests permanent and temporary badge rendering.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AbilityActivation
  participant StackPlacement
  participant CrimeLedger
  AbilityActivation->>AbilityActivation: evaluate flattened announced targets
  AbilityActivation->>StackPlacement: place ability
  StackPlacement->>CrimeLedger: record CrimeCommitted after placement
  CrimeLedger-->>AbilityActivation: publish crime commitment
Loading

Possibly related PRs

  • phase-rs/phase#6521: Extends the same resolved-ledger and replay-invariant system for crime commitment.
  • phase-rs/phase#6611: Updates the same derived-view and client test paths for cant_be_blocked.
  • phase-rs/phase#7093: Refines overlapping crime-commitment and cant_be_blocked behavior.

Suggested labels: bug

🚥 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 summarizes the main changes to crime ledger handling and unblockable derived views.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 ship/nimble-crime-ledger

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/engine/src/game/engine_stack.rs (1)

27-89: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Persist and commit the trigger crime after distribution selection completes.

finalize_trigger_target_selection computes crime_candidate before storing trigger back into state.pending_trigger, but the DistributeAmong prompt path drops that value. PendingTrigger has no saved crime_candidate field, so when a triggered ability divides damage or counters among multiple targets, the crime decision can be lost instead of being committed once the targets are announced on the stack. Thread the crime flag through PendingTrigger or store/retrieve it from state before returning WaitingFor::DistributeAmong, then call commit_crime_after_stack_placement from the trigger distribution resolution path.

🤖 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/src/game/engine_stack.rs` around lines 27 - 89, Preserve the
crime_candidate computed in finalize_trigger_target_selection when returning
WaitingFor::DistributeAmong by threading it through PendingTrigger or the
pending state. In the trigger distribution-resolution path, retrieve that value
and call commit_crime_after_stack_placement after the ability is placed on the
stack, ensuring the crime is committed exactly once.
crates/engine/src/game/casting_costs.rs (1)

1633-1696: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Emit targeting events before returning from the deferred spell-cast target paths.

In crates/engine/src/game/casting_costs.rs, both the Random and auto-select branches assign targets and set pending.crime_candidate, but they call super::casting::emit_targeting_events(...) only when pending.activation_ability_index.is_some(). The spell path then returns through finish_pending_cost_or_cast, which does not emit BecomesTarget for these targets.

Call super::casting::emit_targeting_events immediately after assigning targets in both branches, before the activation/spell branch, so spells that reach deferred selection still fire target-related events.

🤖 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/src/game/casting_costs.rs` around lines 1633 - 1696, Update
both the Random-target branch and the auto-select branch in the deferred
target-selection flow to call super::casting::emit_targeting_events immediately
after assigning targets and updating pending.ability, before checking
pending.activation_ability_index. Remove the activation-only placement so both
deferred spells and activated abilities emit targeting events exactly once
before their respective completion paths.
🧹 Nitpick comments (1)
crates/engine/src/game/derived_views.rs (1)

29-29: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a regression test for the creature-only restriction.

The PR restricts cant_be_blocked to CoreType::Creature (Line 915). This matches a listed PR objective ("Restricting the derived view to creatures"), which implies a prior review found this projection could leak onto non-creature permanents. The provided integration test (temporary_cant_be_blocked_view.rs) only exercises two creature recipients; it does not cover the negative case of a non-creature permanent carrying a bare CantBeBlocked static.

Add a test that installs a bare CantBeBlocked static on a non-creature battlefield permanent and asserts it does not appear in views.cant_be_blocked. This guards the specific restriction this PR claims to add.

Also applies to: 419-423, 895-923, 945-945

🤖 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/src/game/derived_views.rs` at line 29, Add a regression test
alongside the existing temporary cant-be-blocked view tests that places a bare
CantBeBlocked static on a non-creature battlefield permanent, then assert that
permanent is absent from views.cant_be_blocked. Keep the existing creature
coverage unchanged and target the creature-only projection logic around
cant_be_blocked.
🤖 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.

Inline comments:
In `@crates/engine/src/types/game_state.rs`:
- Around line 5607-5611: Replace the boolean crime_candidate state in the
relevant action/state definitions and all references at the noted locations with
an explicit Unknown, NotCrime, and Crime representation, using
Option<CrimeCandidate> or an equivalent enum. Define serde deserialization for
legacy boolean data so existing false values map consistently to the intended
state, and update classification and stack-placement logic to preserve Unknown
until classification completes. Add a production-pipeline regression covering a
pending action after target declaration through both successful and failed stack
placement.

In `@crates/engine/tests/integration/crime_tracking.rs`:
- Around line 16-24: Extend the test around record_crime_committed and
start_next_turn to record crimes for both players, while preserving the
repeated-crime assertion for PlayerId(0). After advancing the turn, assert that
each player’s crimes_committed_this_turn is zero, covering both per-player
one-crime-per-turn and reset invariants.
- Around line 1-2: Correct the module annotation for CR 700.13 so it only
describes when a player commits a crime. Remove the unsupported claims about
ledger durability and turn-boundary resets, and separately document those engine
invariants with the applicable CR citation whose rule body governs the reset
behavior.
- Around line 30-40: Update the test around record_crime_committed and
resolve_and_apply_ledger_edit to construct and apply one CrimeCommitted edit
successfully, then replay that exact same edit and assert
CrimeCommittedPreconditionMismatch while keeping the expected turn count
unchanged. Ensure the initial application passes the reach guard before
asserting the replay failure.
- Around line 16-17: Add a scenario-runner integration test in the
crime-tracking tests that performs a real crime action through casting,
activation, and trigger/stack placement rather than calling
record_crime_committed or resolve_and_apply_ledger_edit directly. Assert the
ledger has no commitment before successful placement, exactly one after the
successful action, and remains unchanged when the action fails; preserve
existing direct unit coverage while ensuring the production pipeline and failure
path are exercised.

---

Outside diff comments:
In `@crates/engine/src/game/casting_costs.rs`:
- Around line 1633-1696: Update both the Random-target branch and the
auto-select branch in the deferred target-selection flow to call
super::casting::emit_targeting_events immediately after assigning targets and
updating pending.ability, before checking pending.activation_ability_index.
Remove the activation-only placement so both deferred spells and activated
abilities emit targeting events exactly once before their respective completion
paths.

In `@crates/engine/src/game/engine_stack.rs`:
- Around line 27-89: Preserve the crime_candidate computed in
finalize_trigger_target_selection when returning WaitingFor::DistributeAmong by
threading it through PendingTrigger or the pending state. In the trigger
distribution-resolution path, retrieve that value and call
commit_crime_after_stack_placement after the ability is placed on the stack,
ensuring the crime is committed exactly once.

---

Nitpick comments:
In `@crates/engine/src/game/derived_views.rs`:
- Line 29: Add a regression test alongside the existing temporary
cant-be-blocked view tests that places a bare CantBeBlocked static on a
non-creature battlefield permanent, then assert that permanent is absent from
views.cant_be_blocked. Keep the existing creature coverage unchanged and target
the creature-only projection logic around cant_be_blocked.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cbd12db9-aef2-4c8f-b8d8-2e8f470c7323

📥 Commits

Reviewing files that changed from the base of the PR and between b5b8f4e and 1c9c18b.

📒 Files selected for processing (25)
  • client/src/adapter/types.ts
  • client/src/components/board/PermanentCard.tsx
  • client/src/components/board/__tests__/PermanentCard.test.tsx
  • crates/engine/src/game/casting.rs
  • crates/engine/src/game/casting_costs.rs
  • crates/engine/src/game/casting_targets.rs
  • crates/engine/src/game/casting_tests.rs
  • crates/engine/src/game/combat.rs
  • crates/engine/src/game/derived_views.rs
  • crates/engine/src/game/engine_stack.rs
  • crates/engine/src/game/engine_tests.rs
  • crates/engine/src/game/ledger.rs
  • crates/engine/src/game/planeswalker.rs
  • crates/engine/src/game/quantity.rs
  • crates/engine/src/game/triggers.rs
  • crates/engine/src/game/turns.rs
  • crates/engine/src/game/visibility.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/src/types/player.rs
  • crates/engine/src/types/resolved_commands.rs
  • crates/engine/tests/integration/crime_tracking.rs
  • crates/engine/tests/integration/loop_shortcut.rs
  • crates/engine/tests/integration/main.rs
  • crates/engine/tests/integration/temporary_cant_be_blocked_view.rs

Comment thread crates/engine/src/types/game_state.rs
Comment thread crates/engine/tests/integration/crime_tracking.rs Outdated
Comment thread crates/engine/tests/integration/crime_tracking.rs
Comment thread crates/engine/tests/integration/crime_tracking.rs
Comment thread crates/engine/tests/integration/crime_tracking.rs Outdated
@matthewevans
matthewevans force-pushed the ship/nimble-crime-ledger branch from 1c9c18b to 1858865 Compare August 8, 2026 08:17
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Generated for head 5e042db06d382333b40bbcb6faf4a7ba8ce6c4ad.

Parse changes introduced by this PR

✓ No card-parse changes detected.

@matthewevans
matthewevans added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 11cb159 Aug 8, 2026
13 checks passed
@matthewevans
matthewevans deleted the ship/nimble-crime-ledger branch August 8, 2026 09:03
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