fix(engine): offer TurnFaceUp/PlayFaceDown actions and reveal own face-down identity (#4381)#4538
fix(engine): offer TurnFaceUp/PlayFaceDown actions and reveal own face-down identity (#4381)#4538carlos4s wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the identity_revealed flag to the game engine and frontend to properly render face-down cards (such as morphs or manifested cards) to authorized viewers (like their controller) while keeping them hidden from others. It also adds AI candidate generation for playing cards face down and turning them face up. Feedback highlights that the AI candidate generation overly restricts face-down casts to the hand and sorcery-speed only, violating CR 702.37a. Additionally, the identity_revealed property in TypeScript should be typed as non-optional to match the backend serialization and avoid redundant defensive checks.
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.
| if is_main_phase && stack_empty && is_active { | ||
| for &obj_id in &p.hand { |
There was a problem hiding this comment.
[MEDIUM] Convenience shortcut restricts face-down casts to hand and sorcery-speed only, violating CR 702.37a. Under the Comprehensive Rules, Morph/Disguise cards can be cast face down from any zone they could be cast face up (e.g., top of library via Future Sight, or exile via Outpost Siege), and at instant speed if they have flash or flash is granted (e.g., via Leyline of Anticipation). Gating strictly on p.hand and is_sorcery_speed_window is a convenience shortcut that gets the rules wrong.
References
- Strict fidelity to the MTG Comprehensive Rules (CR) — every game rule, validation, and computed value matches the CR exactly. Convenience shortcuts that get rules wrong are not simpler; they are wrong. (link)
| * rendering on `!face_down || identity_revealed` — never on `!face_down` | ||
| * alone, which would hide even the controller's own face-down permanents. | ||
| */ | ||
| identity_revealed?: boolean; |
There was a problem hiding this comment.
[MEDIUM] Property identity_revealed is typed as optional but is always serialized by the backend. The Rust GameObject struct defines identity_revealed as a non-optional bool with #[serde(default)] and no skip_serializing_if attribute, meaning it is guaranteed to be present on every serialized object. Typing it as optional in TypeScript forces unnecessary defensive checks or optional chaining.
| identity_revealed?: boolean; | |
| identity_revealed: boolean; |
References
- Do not add redundant defensive guards (such as optional chaining or nullish coalescing) to properties that are typed as non-optional and guaranteed to be initialized by the backend.
Parse changes introduced by this PR✓ No card-parse changes detected. |
matthewevans
left a comment
There was a problem hiding this comment.
Thanks for taking this on. The visibility-side direction is reasonable, but I can't approve the action candidate changes as written.
The new PlayFaceDown and TurnFaceUp candidates expose actions that the runtime still performs without paying the required costs. The PlayFaceDown candidate path explicitly has no affordability/payment gate, and morph::turn_face_up currently restores the card without routing morph/disguise/manifest turn-up costs through payment. Before these actions are surfaced through legal_actions, the candidate must either route through the same cost/payment authority that will actually charge the action, or stay unsurfaced until that payment path exists.
This is not just an AI issue: candidate generation is the engine's advertised legal-action surface. Exposing these as zero-cost actions would make ordinary morph/disguise/manifest play rules-wrong even though the UI symptom is fixed.
|
@matthewevans pls review again |
Summary
Closes #4381
Players could neither look at face-down permanents they controlled nor turn morph/megamorph/manifest/manifest-dread permanents face up. The engine already implemented both actions (
morph::turn_face_up,morph::play_face_down) and revealed face-down identity to the controller (CR 708.5), butpriority_actionsnever offered the actions and the frontend gated face-down display on!face_down, hiding even the controller's own cards. This emits the missing candidates and adds an engine-ownedidentity_revealedflag so the frontend renders the real card to the controller without inferring it.Implementation method (required)
/engine-implementerpipeline (plan → review-plan → implement → review-impl → commit)/engine-implementer— explain why belowIf you did not use
/engine-implementer, state why:CR references
CR 702.37a(Morph) /CR 702.121a(Disguise) — face-down cast from hand, sorcery-speed window for the candidateCR 702.37c/CR 116.2b— turning a face-down permanent face up is a special action available any time its controller has priority (not suppressed by split second, CR 702.61a)CR 708.5— controller may look at their own face-down permanents; drives theidentity_revealedflagCR 708.7/CR 116.2b—CantBeTurnedFaceUpstatic (Karlov Watchdog) gates the candidate viais_blocked_by_cant_be_turned_face_upCR 701.40a— manifested creatures are turn-up eligible (no morph cost)Verification
cargo fmt --all --check— cleancargo clippy -p engine --lib --tests— no warningscargo test -p engine --lib— new tests pass; morph (15), visibility (36), candidates (43) suites green, including CR 708.5 look-permission statics (lumbering_laundry,found_footage) and split-second suppressionpnpm run type-check(protocol:check+tsc -b --noEmit) — cleaneslinton touched files — cleanviteston touched components + boundary guardrail — 35 passedNew tests:
priority_actions_offer_turn_face_up_for_controlled_face_down_morphpriority_actions_skip_turn_face_up_for_opponent_face_down_morphpriority_actions_offer_play_face_down_for_morph_card_in_handpriority_actions_skip_play_face_down_off_sorcery_windowbattlefield_face_down_identity_revealed_flag_split_by_viewer