refactor(engine): replace raw bool fields with typed enums in engine … - #2713
Conversation
There was a problem hiding this comment.
Code Review
This pull request executes a major type-safety refactoring across the MTG game engine, replacing legacy boolean fields with strongly-typed enums in accordance with the repository's architectural guidelines (R2). Key changes include migrating enter_tapped to EtbTapState, snow on ManaUnit to Option<ManaSupertype>, discard cost parameters (random and self_ref) to CardSelectionMode and DiscardSelfScope, additional cost repeatability to AdditionalCostRepeatability, and attachment exclusion to SourceExclusion. All associated parsers, game state logic, serialization adapters, and unit tests have been updated to maintain compatibility. No review comments were provided, and the implementation is highly idiomatic, so there is no additional feedback to address.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
mike-theDude
left a comment
There was a problem hiding this comment.
Architecture Review
The bool→enum direction is correct and the new types (EtbTapState, CardSelectionMode, DiscardSelfScope, AdditionalCostRepeatability, SourceExclusion, ManaSupertype) are modeled idiomatically and mirror the existing TargetSelectionMode pattern. Two blockers and some round-trip notes.
[HIGH] The engine does not compile — duplicate Zone import. Evidence: crates/engine/src/types/ability.rs:25-26:
use super::zones::{EtbTapState, Zone};
use super::zones::Zone;This is error[E0252]: the name 'Zone' is defined multiple times. Verified against the PR head (refs/pull/2713/head), so the "cargo check -p engine — clean" box does not hold for the current tip (likely the merge-from-main re-introduced the old line after EtbTapState was folded into the brace import). Why it matters: a non-compiling crate blocks the entire suite, and the Gemini pass ("highly idiomatic, no feedback") missed it. Suggested fix: drop the standalone use super::zones::Zone; line — the brace import already brings Zone in.
[HIGH] Stray gittensory submodule gitlink committed again. Evidence: gittensory in the PR file list (mode 160000). Why it matters: recurring across this contributor's branches (#2706, #2710) — an unrelated gitlink breaks clone/submodule tooling. Suggested fix: git rm --cached gittensory and amend; add a guard so it stops reappearing on every branch.
[LOW] SearchDestinationSplit.primary_enter_tapped changes on-disk JSON shape. Evidence: types/ability.rs — the field went from a bare pub primary_enter_tapped: bool (always serialized) to #[serde(default, with = "...etb_tap_bool_compat", skip_serializing_if = "EtbTapState::is_unspecified")], so it is now omitted when Unspecified (the former false). Round-trips correctly via default on read, but regenerated card-data.json will differ byte-wise from the committed copy and any exact-JSON snapshot of this struct will shift. Why it matters: the PR claims "on-disk field names unchanged" — true, but the presence of this field changes. Suggested fix: regenerate card-data.json in the same PR and confirm tilt logs card-data shows no coverage/round-trip regression (already on the checklist — keep it).
Verified safe (no action needed), but one latent footgun: EtbTapState::Untapped is constructed only in the runtime replacement pipeline (game/replacement.rs:3929,7687,7743), never in parser/AST emission, so the lossy serialize (Untapped → is_tapped() → false → Unspecified on re-read) never touches a persisted field today. is_untapped() has zero non-test callers, so the classic three-state migration trap (an old !enter_tapped rewritten to is_untapped() instead of !is_tapped()) was avoided, and resolve(fallback) preserves the legacy true/false semantics (Unspecified → fallback, matching old false → untapped). The footgun: if any future parser ever emits EtbTapState::Untapped into a field carrying etb_tap_bool_compat, it will silently persist as false and reload as Unspecified. Consider making etb_tap_bool_compat::serialize debug-assert !state.is_untapped() (or document the invariant on the adapter) so that contract is enforced rather than implicit.
Net: fix the duplicate import (blocking) and remove the gitlink; the type design and serde-compat wiring are otherwise sound. I could not exercise runtime behavior since it does not build — re-review the resolver/replacement paths once it compiles and test-engine is green.
|
Pushed maintainer follow-up
Verification:
Broad Rust/card-data validation is left to GitHub CI for this worktree push; the main workspace currently has unrelated tracked edits, so I did not switch it onto this PR for Tilt. |
|
Pushed maintainer follow-up in What changed:
Architecture/value gate note:
Pushed with |
|
Pushed maintainer follow-up in What changed:
Verification:
I did not run local cargo build/test/clippy; relying on GitHub CI for the broad gate. |
|
Pushed a follow-up maintainer fix in What changed:
Validation:
I did not run local cargo builds/tests; relying on GitHub CI per maintainer workflow to avoid redundant local build contention. |
# Conflicts: # crates/engine/src/database/synthesis.rs
|
Pushed maintainer follow-up Conflict resolution:
Validation:
I did not run local cargo build/test/clippy; relying on GitHub CI for the broad gate. |
|
Maintainer update pushed. CI was failing from incomplete typed-field migration after the branch was brought current with
The patch keeps those existing type boundaries instead of widening this rescue change into a larger migration. Locally I ran |
|
Second maintainer update pushed. GitHub CI exposed the remaining typed-field migration gaps outside the engine crate:
Patched those downstream call sites only. Locally reran |
|
Pushed a follow-up compile-alignment fix for the remaining Rust CI failures in Scope:
Local verification run, avoiding cargo/pnpm builds:
I’m leaving this in CI and will still treat the large-refactor value/architecture review as a separate gate before any enqueue decision. |
|
Pushed one more small maintainer follow-up for the remaining low-risk serialization invariant from review. Scope:
Local verification, still avoiding cargo/pnpm builds:
|
|
Pushed a focused fix for the latest Rust CI failure. Scope:
Local verification, avoiding cargo/pnpm builds:
|
|
Final maintainer handling summary before enqueue. Value / architecture verdict:
Fixes applied during maintainer handling:
Review evidence:
This clears the value, architectural seam, code quality, and CI gates, so I am enqueueing it with a head-SHA guard. |
Summary
Closes #2712
Replaces prohibited raw
boolfields across engine AST and runtime carriers with typed enums, establishing a single canonical type per semantic axis and eliminating multi-bool constructor hazards.EtbTapStatetotypes/zones.rsas the canonical enter-tapped type (Unspecified/Tapped/Untapped); re-exported fromproposed_eventfor the replacement pipelineCardSelectionMode,DiscardSelfScope,AdditionalCostRepeatability, andSourceExclusionintypes/ability.rs(patterned after existingTargetSelectionMode)ManaSupertype+ManaUnit::is_snow()intypes/mana.rs, replacingsnow: boolcard-data.jsonround-trip via serde bool-compat adapters (etb_tap_bool_compat,card_selection_bool_compat, etc.) — on-disk field names unchanged (enter_tapped,random,self_ref,repeatable,exclude_source,snow)Intentionally deferred:
oracle_ir/ast.rsstill carryenter_tapped: boolat the lowering boundary (converted toEtbTapStateat AST emission)boolfields inability.rsoutside this issue's named clusters (separate sweep)Files changed
crates/engine/src/types/zones.rs—EtbTapStatecanonical home +etb_tap_bool_compatserde adaptercrates/engine/src/types/ability.rs—CardSelectionMode,DiscardSelfScope,AdditionalCostRepeatability,SourceExclusion; field renames onEffect,AbilityCost,AdditionalCost,FilterProp::HasAttachment,SearchDestinationSplitcrates/engine/src/types/game_state.rs—PendingChangeZoneIteration,EffectZoneChoice,SearchPartitionChoice,RevealUntilKeptChoicecarrierscrates/engine/src/types/mana.rs—ManaSupertype,ManaUnit.supertype+snow_compatserde adaptercrates/engine/src/types/proposed_event.rs— re-exportsEtbTapStatefromzonescrates/engine/src/game/effects/change_zone.rs— largest resolver call-site surfacecrates/engine/src/parser/oracle_effect/,oracle_casting.rs,oracle_cost.rs,oracle_trigger.rs— parser emission + test pattern updatescrates/engine/src/database/synthesis.rs— keyword synthesis construction sitescrates/engine/src/game/casting_costs.rs,triggers.rs,mana_payment.rs,ai_support/— runtime + AI readsCR references
No new CR annotations. Existing annotations on enter-tapped (CR 614.1 / CR 110.5b), random discard (CR 701.9a), and snow mana (CR 205.4g) are unchanged.
Anchored on
crates/engine/src/types/ability.rs:2614—TargetSelectionModeenum pattern (Chosen/Random selection-mode axis;CardSelectionModemirrors it for hand-zone selection)crates/engine/src/types/proposed_event.rs:79— pre-existingEtbTapStatethree-way distinction; promoted tozones.rsas single authoritycrates/engine/src/types/ability.rs:12757—deserialize_enters_under_compatlegacy bool serde pattern (same compat approach for all new adapters)crates/engine/src/types/mana.rs:493—ManaCostShard::Snowalready models snow at the cost-shard layer;ManaSupertypeextends the same concept to producedManaUnitTrack
Developer
Tier: Standard
LLM
Model: claude-4.6-sonnet-medium-thinking
Thinking: high
Verification
cargo fmt --all— cleancargo check -p engine— cleancargo test -p engine --lib --no-run— compilescargo clippy -p engine -- -D warnings— confirm clean after unused-import fixcargo test -p engine --lib— confirm green./scripts/check-parser-combinators.sh— run before opening PRtilt logs card-data --tail 50— confirm no coverage regressionScope Expansion
Infrastructure refactor only — no card-specific behavior change intended. Serde adapters preserve legacy bool JSON shapes for
card-data.jsonround-trip.Validation Failures
None.
CI Failures
None (pre-push).
Test plan
cargo test -p engine --lib— full engine unit/integration suite greencargo clippy -p engine -- -D warnings— no warningstest-engine+card-datagreen after push