Skip to content

fix(engine): offer TurnFaceUp/PlayFaceDown actions and reveal own face-down identity (#4381)#4538

Open
carlos4s wants to merge 4 commits into
phase-rs:mainfrom
carlos4s:fix/4381-facedown-turn-up-and-peek
Open

fix(engine): offer TurnFaceUp/PlayFaceDown actions and reveal own face-down identity (#4381)#4538
carlos4s wants to merge 4 commits into
phase-rs:mainfrom
carlos4s:fix/4381-facedown-turn-up-and-peek

Conversation

@carlos4s

Copy link
Copy Markdown
Contributor

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), but priority_actions never 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-owned identity_revealed flag so the frontend renders the real card to the controller without inferring it.

Implementation method (required)

  • Produced via the /engine-implementer pipeline (plan → review-plan → implement → review-impl → commit)
  • Not /engine-implementer — explain why below

If you did not use /engine-implementer, state why:

This is a candidate-enumeration + visibility-flag wiring fix, not new game/parser logic. The morph turn-up and play-face-down handlers already exist and are CR-annotated; this change only surfaces their existing preconditions as legal actions and propagates an already-computed viewer-permission signal to the display layer.

CR references

  • CR 702.37a (Morph) / CR 702.121a (Disguise) — face-down cast from hand, sorcery-speed window for the candidate
  • CR 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 the identity_revealed flag
  • CR 708.7 / CR 116.2bCantBeTurnedFaceUp static (Karlov Watchdog) gates the candidate via is_blocked_by_cant_be_turned_face_up
  • CR 701.40a — manifested creatures are turn-up eligible (no morph cost)

Verification

  • cargo fmt --all --check — clean
  • cargo clippy -p engine --lib --tests — no warnings
  • cargo 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 suppression
  • pnpm run type-check (protocol:check + tsc -b --noEmit) — clean
  • eslint on touched files — clean
  • vitest on touched components + boundary guardrail — 35 passed

New tests:

  • priority_actions_offer_turn_face_up_for_controlled_face_down_morph
  • priority_actions_skip_turn_face_up_for_opponent_face_down_morph
  • priority_actions_offer_play_face_down_for_morph_card_in_hand
  • priority_actions_skip_play_face_down_off_sorcery_window
  • battlefield_face_down_identity_revealed_flag_split_by_viewer

@carlos4s carlos4s requested a review from matthewevans as a code owner June 28, 2026 13:16

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

Comment on lines +2876 to +2877
if is_main_phase && stack_empty && is_active {
for &obj_id in &p.hand {

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.

medium

[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
  1. 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;

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.

medium

[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.

Suggested change
identity_revealed?: boolean;
identity_revealed: boolean;
References
  1. 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.

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@carlos4s

Copy link
Copy Markdown
Contributor Author

@matthewevans pls review again

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.

Facedown Permanents — I cannot look at facedown permanents I control and I can't turn morph/megamorph/manifest/manifest…

2 participants