feat(engine): implement Fuse keyword runtime — cast both halves of sp… - #2611
Conversation
…lit cards (CR 702.102)
There was a problem hiding this comment.
Code Review
This pull request implements the 'Fuse' casting variant for split cards, adding support across the frontend UI, localization, and the game engine rules (including merging characteristics of both halves on the stack and tracking right-half targeting). Feedback focuses on adhering to repository style guide Rule R2 by refactoring the boolean and vector fields in PendingCast into a single Option<Vec<TargetSelectionSlot>> (and updating tests accordingly), as well as optimizing performance in casting_costs.rs by avoiding cloning the entire CardFace struct.
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.
I am having trouble creating individual review comments. Click here to see my feedback.
crates/engine/src/types/game_state.rs (1106-1113)
[HIGH] Avoid boolean fields on structs by using Option<T> or a typed enum. Evidence: crates/engine/src/types/game_state.rs:1109.
Why it matters: Violates rule R2 of the repository style guide, which prohibits bool fields in favor of more expressive types like Option<T> or custom enums to prevent state space explosion.
Suggested fix: Combine both fields into a single Option<Vec<TargetSelectionSlot>> where None indicates no right-half targeting is pending, and Some(slots) holds the stashed slots.
/// CR 601.2c + CR 702.102d: Stash for right-half target slots while
/// left-half targeting is in progress during a fused split spell cast.
/// None indicates no right-half targeting is pending.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub right_half_target_slots: Option<Vec<TargetSelectionSlot>>,
References
- Rule R2: No bool fields — parameterize with existing typed enums. A bool field never expresses the design space; the project uses typed enums like ControllerRef, Comparator, PlayerScope, Option, and dedicated discriminated unions instead. (link)
crates/engine/src/types/game_state.rs (1159-1164)
[MEDIUM] Update test initialization to use Option for right_half_target_slots. Evidence: crates/engine/src/types/game_state.rs:1162.
Why it matters: Ensures consistency with the refactored PendingCast struct that replaces the boolean field with Option<Vec<TargetSelectionSlot>>.
Suggested fix: Set right_half_target_slots to None.
convoked_creatures: Vec::new(),
cancel_restore_prepared_source: None,
payment_mode: CastPaymentMode::Auto,
right_half_target_slots: None,
}crates/engine/src/game/casting_costs.rs (4163-4182)
[MEDIUM] Avoid cloning the entire CardFace struct when only specific fields are needed. Evidence: crates/engine/src/game/casting_costs.rs:4165.
Why it matters: CardFace can be a large struct containing Oracle text, parsed abilities, and other heavy fields. Cloning it entirely is inefficient and unnecessary.
Suggested fix: Extract and clone only the core_types and color fields from the reference.
if casting_variant == CastingVariant::Fuse {
if let Some(obj) = state.objects.get_mut(&object_id) {
let extra = obj.back_face.as_ref()
.filter(|bf| bf.layout_kind == Some(crate::types::card::LayoutKind::Split))
.map(|bf| (bf.card_types.core_types.clone(), bf.color.clone()));
if let Some((extra_types, extra_colors)) = extra {
for ct in extra_types {
if !obj.card_types.core_types.contains(&ct) {
obj.card_types.core_types.push(ct);
}
}
for color in extra_colors {
if !obj.color.contains(&color) {
obj.color.push(color);
}
}
}
}
}Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🤖 Architecture Review (automated)Verdict: Seam: PASS — Fuse logic lives entirely in the engine cast pipeline ( CR verification: All cited numbers grep-confirmed in Reconciled with existing reviews:
Findings
|
Resolve maintainer review feedback for PR phase-rs#2611: - remove the stray gittensory gitlink - remove unused PendingCast right-half targeting state - avoid cloning the full right CardFace when merging fused stack characteristics - offer Normal plus Fuse when a hand split card has Fuse so the cast pipeline cannot silently fall through to normal casting - add Breaking // Entering cast-pipeline coverage for combined cost, fused stack characteristics, and left-to-right resolution Verification: - cargo fmt --all - git diff --check - Tilt clippy: ok - Tilt test-engine: new fuse_runtime test passed; existing anaphoric_scope_allowlist_guard failure is unrelated
|
Pushed follow-up maintainer changes in 804dd9b addressing the review feedback:
Verification:
|
matthewevans
left a comment
There was a problem hiding this comment.
Approved after maintainer follow-up. I verified the changes are at the casting variant / fused stack-characteristics seams, removed the unused pending-cast state, and added a discriminating real-card cast-pipeline test for fused cost, stack characteristics, and left-to-right resolution. Broad validation is left to GitHub CI/Tilt; local Tilt clippy is green and test-engine only reports the known unrelated anaphoric allowlist failure.
Summary
Closes #2570
Keyword::Fusewas previously parsed and stored but caused no behaviour change at cast timeCastingVariant::FuseandKeywordKind::Fuse; surfaces the "Cast with Fuse" option incasting_variant_candidates()for split cards in hand whose back face haslayout_kind == Some(LayoutKind::Split)restrictions::add_mana_cost(CR 702.102c) and chains the right half's spell ability onto the left's sub-ability so resolution follows left → right (CR 702.102d)right_half_targeting_pending/right_half_target_slotsfields toPendingCast(reserved for a future split-targeting UX; existingbuild_target_slotsrecursion already covers both halves in a single pass per CR 601.2c)Fuse: "variantFuse"toCastingVariantModal's variant key map and"variantFuse": "Cast with Fuse"i18n string; adds{ type: "Fuse" }to the TypeScriptCastingVariantunionTest plan
tilt logs clippyandtilt logs test-engine— no new errorsCR coverage
Model: claude-sonnet-4-6
🤖 Generated with Claude Code