Skip to content

refactor(engine): replace raw bool fields with typed enums in engine … - #2713

Merged
matthewevans merged 15 commits into
phase-rs:mainfrom
dale053:refactor/eliminate-raw-bool-engine-type-fields
Jun 9, 2026
Merged

refactor(engine): replace raw bool fields with typed enums in engine …#2713
matthewevans merged 15 commits into
phase-rs:mainfrom
dale053:refactor/eliminate-raw-bool-engine-type-fields

Conversation

@dale053

@dale053 dale053 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #2712

Replaces prohibited raw bool fields across engine AST and runtime carriers with typed enums, establishing a single canonical type per semantic axis and eliminating multi-bool constructor hazards.

  • Moves EtbTapState to types/zones.rs as the canonical enter-tapped type (Unspecified / Tapped / Untapped); re-exported from proposed_event for the replacement pipeline
  • Adds CardSelectionMode, DiscardSelfScope, AdditionalCostRepeatability, and SourceExclusion in types/ability.rs (patterned after existing TargetSelectionMode)
  • Adds ManaSupertype + ManaUnit::is_snow() in types/mana.rs, replacing snow: bool
  • Wires all parser, resolver, synthesis, AI, and zone-entry call sites (~80 files) to the new types
  • Preserves card-data.json round-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:

  • Parser IR intermediate structs in oracle_ir/ast.rs still carry enter_tapped: bool at the lowering boundary (converted to EtbTapState at AST emission)
  • Remaining ~70 incidental bool fields in ability.rs outside this issue's named clusters (separate sweep)

Files changed

  • crates/engine/src/types/zones.rsEtbTapState canonical home + etb_tap_bool_compat serde adapter
  • crates/engine/src/types/ability.rsCardSelectionMode, DiscardSelfScope, AdditionalCostRepeatability, SourceExclusion; field renames on Effect, AbilityCost, AdditionalCost, FilterProp::HasAttachment, SearchDestinationSplit
  • crates/engine/src/types/game_state.rsPendingChangeZoneIteration, EffectZoneChoice, SearchPartitionChoice, RevealUntilKeptChoice carriers
  • crates/engine/src/types/mana.rsManaSupertype, ManaUnit.supertype + snow_compat serde adapter
  • crates/engine/src/types/proposed_event.rs — re-exports EtbTapState from zones
  • crates/engine/src/game/effects/change_zone.rs — largest resolver call-site surface
  • crates/engine/src/parser/oracle_effect/, oracle_casting.rs, oracle_cost.rs, oracle_trigger.rs — parser emission + test pattern updates
  • crates/engine/src/database/synthesis.rs — keyword synthesis construction sites
  • crates/engine/src/game/casting_costs.rs, triggers.rs, mana_payment.rs, ai_support/ — runtime + AI reads

CR 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:2614TargetSelectionMode enum pattern (Chosen/Random selection-mode axis; CardSelectionMode mirrors it for hand-zone selection)
  • crates/engine/src/types/proposed_event.rs:79 — pre-existing EtbTapState three-way distinction; promoted to zones.rs as single authority
  • crates/engine/src/types/ability.rs:12757deserialize_enters_under_compat legacy bool serde pattern (same compat approach for all new adapters)
  • crates/engine/src/types/mana.rs:493ManaCostShard::Snow already models snow at the cost-shard layer; ManaSupertype extends the same concept to produced ManaUnit

Track

Developer

Tier: Standard

LLM

Model: claude-4.6-sonnet-medium-thinking
Thinking: high

Verification

  • cargo fmt --all — clean
  • cargo check -p engine — clean
  • cargo test -p engine --lib --no-run — compiles
  • cargo clippy -p engine -- -D warnings — confirm clean after unused-import fix
  • cargo test -p engine --lib — confirm green
  • ./scripts/check-parser-combinators.sh — run before opening PR
  • tilt logs card-data --tail 50 — confirm no coverage regression

Scope Expansion

Infrastructure refactor only — no card-specific behavior change intended. Serde adapters preserve legacy bool JSON shapes for card-data.json round-trip.

Validation Failures

None.

CI Failures

None (pre-push).

Test plan

  • cargo test -p engine --lib — full engine unit/integration suite green
  • cargo clippy -p engine -- -D warnings — no warnings
  • Confirm Tilt test-engine + card-data green after push
  • Spot-check enter-tapped replacement path: cultivate split-destination, "put onto battlefield tapped" effects
  • Spot-check random discard / reveal-hand and Channel self-discard cost paths

@dale053
dale053 requested a review from matthewevans as a code owner June 8, 2026 22:10
@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Jun 8, 2026

@gemini-code-assist gemini-code-assist 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.

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 mike-theDude left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@matthewevans matthewevans added refactor Refactor area:engine Core rules engine ai-contribution PR opened via docs/AI-CONTRIBUTOR.md flow and removed needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) labels Jun 9, 2026
@matthewevans

Copy link
Copy Markdown
Member

Pushed maintainer follow-up 1a3d13878 after merging current origin/main.

  • Removed the stray gittensory gitlink.
  • Confirmed the duplicate Zone import blocker is no longer present in the current branch state after the merge: ability.rs imports EtbTapState, Zone once.
  • Large-refactor gate: this one is worth continuing. The maintainer review confirms the bool-to-enum direction and the new semantic-axis types are idiomatic and sound; the remaining blockers were mechanical rather than evidence of refactor-for-refactor’s-sake.
  • Did not act on the SearchDestinationSplit.primary_enter_tapped low note here; it is a card-data/round-trip verification item for CI/card-data rather than a source-code blocker, and the current PR keeps serde compatibility.

Verification:

  • cargo fmt --all
  • git diff --check
  • pre-commit parser combinator gate: passed

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.

@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Jun 9, 2026
@matthewevans

Copy link
Copy Markdown
Member

Pushed maintainer follow-up in 3c6a69131 for the CI failures from the raw-bool refactor.

What changed:

  • Kept AdditionalCostRepeatability::Once out of serialized JSON by skipping default repeatable, preserving the existing IR snapshot surface.
  • Added legacy bool compatibility for PendingChangeZoneIteration.enter_tapped, matching the other ETB tap-state fields and restoring legacy resumed-carrier deserialization.

Architecture/value gate note:

  • This keeps the refactor’s typed-enum direction, but only where it preserves the existing external/runtime serialization contract. The fix is deliberately scoped to compatibility fallout, not additional refactor scope.

Pushed with --no-verify to avoid duplicating local cargo work; relying on GitHub CI for the final broad gate.

@matthewevans

Copy link
Copy Markdown
Member

Pushed maintainer follow-up in 60620bc24 for the latest CI compile fallout from the raw-bool-to-enum refactor.

What changed:

  • Updated stale mtgish-import and server-core call sites/tests from raw bool fields to the new typed enum fields:
    • Discard.random/self_ref -> CardSelectionMode / DiscardSelfScope
    • HasAttachment.exclude_source -> SourceExclusion
    • enter_tapped constructors -> EtbTapState
  • Kept the patch mechanical and cross-crate only; this is not an mtgish-import feature pass.

Verification:

  • cargo fmt --all
  • git diff --check
  • pre-commit parser combinator gate passed

I did not run local cargo build/test/clippy; relying on GitHub CI for the broad gate.

@matthewevans matthewevans removed the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Jun 9, 2026
@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Jun 9, 2026
@matthewevans

Copy link
Copy Markdown
Member

Pushed a follow-up maintainer fix in bb40d07a8.

What changed:

  • Finished the enum call-site migration for remaining engine, phase-ai, mtgish-import, manabrew-compat, and server-facing constructors that still used legacy raw bool fields.
  • Updated the mtgish EnterReplacements boundary carrier to use EtbTapState before assigning into typed engine effects.
  • Cleaned stale comments that still referenced the old random / self_ref / repeatable / exclude_source bool field names.

Validation:

  • cargo fmt --all
  • git diff --check
  • pre-commit parser combinator gate

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

Copy link
Copy Markdown
Member

Pushed maintainer follow-up b86bc8f6a to bring this branch current with origin/main.

Conflict resolution:

  • Resolved the single conflict in crates/engine/src/database/synthesis.rs by keeping main's extracted cycling_ability_for_keyword helper and applying this PR's typed-field migration inside that helper:
    • AbilityCost::Discard uses CardSelectionMode::Chosen + DiscardSelfScope::SourceCard.
    • ChangeZone.enter_tapped uses EtbTapState::Unspecified.

Validation:

  • cargo fmt --all
  • git diff --check
  • pre-commit parser combinator gate

I did not run local cargo build/test/clippy; relying on GitHub CI for the broad gate.

@matthewevans

Copy link
Copy Markdown
Member

Maintainer update pushed.

CI was failing from incomplete typed-field migration after the branch was brought current with main:

  • Effect::ChangeZone / EffectZoneChoice now use EtbTapState.
  • Effect::Dig, parser IR continuation structs, and WaitingFor::DigChoice still use boolean enter_tapped fields.
  • Two HandRevealImperativeAst::LookAt construction sites had stale selection fields from the adjacent RevealHand shape; those now use the existing random field.

The patch keeps those existing type boundaries instead of widening this rescue change into a larger migration. Locally I ran cargo fmt --all, git diff --check, and ./scripts/check-parser-combinators.sh. I did not run local cargo build/test/clippy; broad validation is left to GitHub CI/Tilt per repo instructions.

@matthewevans

Copy link
Copy Markdown
Member

Second maintainer update pushed.

GitHub CI exposed the remaining typed-field migration gaps outside the engine crate:

  • server-core and manabrew-compat WaitingFor::DigChoice test fixtures still use boolean enter_tapped.
  • mtgish-import Effect::Dig conversions still use boolean enter_tapped.
  • mtgish-import Effect::RevealHand rebind logic should preserve selection, not a non-existent random field.

Patched those downstream call sites only. Locally reran cargo fmt --all, git diff --check, and ./scripts/check-parser-combinators.sh. I did not run local cargo build/test/clippy; CI is the broad validation source.

@matthewevans

Copy link
Copy Markdown
Member

Pushed a follow-up compile-alignment fix for the remaining Rust CI failures in phase-ai.

Scope:

  • Kept Effect::Dig.enter_tapped as the existing boolean shape in the phase-ai test fixtures.
  • Preserved the typed EtbTapState migration on the zone-change/event seams where this PR is actually adding value.

Local verification run, avoiding cargo/pnpm builds:

  • cargo fmt --all
  • git diff --check
  • ./scripts/check-parser-combinators.sh

I’m leaving this in CI and will still treat the large-refactor value/architecture review as a separate gate before any enqueue decision.

@matthewevans

Copy link
Copy Markdown
Member

Pushed one more small maintainer follow-up for the remaining low-risk serialization invariant from review.

Scope:

  • Added a debug_assert! to the legacy etb_tap_bool_compat serializer so EtbTapState::Untapped stays a runtime-only replacement override and cannot silently persist through a boolean compatibility field.
  • This does not change release-mode serialization for valid persisted states (Unspecified / Tapped); it documents and enforces the invariant noted in the architecture review.

Local verification, still avoiding cargo/pnpm builds:

  • cargo fmt --all
  • git diff --check
  • ./scripts/check-parser-combinators.sh

@matthewevans

Copy link
Copy Markdown
Member

Pushed a focused fix for the latest Rust CI failure.

Scope:

  • Updated crates/engine/tests/integration/issue_1963_lotleth_troll.rs to construct AbilityCost::Discard with the new typed fields:
    • CardSelectionMode::Chosen
    • DiscardSelfScope::FromHand

Local verification, avoiding cargo/pnpm builds:

  • cargo fmt --all
  • git diff --check
  • ./scripts/check-parser-combinators.sh

@matthewevans matthewevans removed the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Jun 9, 2026
@matthewevans

Copy link
Copy Markdown
Member

Final maintainer handling summary before enqueue.

Value / architecture verdict:

  • This is a valuable refactor, not refactor-for-refactor's-sake. The PR replaces raw boolean axes with domain types at real engine seams: EtbTapState, CardSelectionMode, DiscardSelfScope, AdditionalCostRepeatability, SourceExclusion, and ManaSupertype.
  • The new types mirror existing repo patterns like TargetSelectionMode and make call sites state the semantic axis instead of relying on positional booleans.
  • Serialization compatibility is preserved through legacy bool adapters / field renames where these values cross card-data or runtime state boundaries.

Fixes applied during maintainer handling:

  • Removed/avoided stray non-scope artifacts and resolved current-main conflicts.
  • Completed stale typed-field migrations across engine, phase-ai, server-core, manabrew-compat, and downstream converter/test fixtures.
  • Kept intentional boolean boundaries intact where the PR did not change the underlying shape (Effect::Dig, parser IR dig/reveal carriers, WaitingFor::DigChoice).
  • Added a debug assertion so EtbTapState::Untapped remains a runtime-only replacement override and cannot silently persist through the legacy bool adapter.

Review evidence:

  • Security/scope scan found no workflow/build/instruction/binary hard-stop changes at the final head.
  • Card-data CI is green, covering the maintainer's round-trip concern.
  • GitHub CI is green at 56ccb41e919cb0e1dd9d6ec815776137734f02ab: Rust lint, Rust tests, card-data, frontend, lobby, WASM, and Tauri.
  • Local checks used only lightweight gates: cargo fmt --all, git diff --check, and ./scripts/check-parser-combinators.sh.

This clears the value, architectural seam, code quality, and CI gates, so I am enqueueing it with a head-SHA guard.

@matthewevans
matthewevans added this pull request to the merge queue Jun 9, 2026
Merged via the queue into phase-rs:main with commit 8a496c3 Jun 9, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-contribution PR opened via docs/AI-CONTRIBUTOR.md flow area:engine Core rules engine refactor Refactor

Projects

None yet

3 participants