Skip to main content

cardinal_kernel/engine/
legality.rs

1use crate::{
2    engine::core::GameEngine,
3    ids::PlayerId,
4    model::action::Action,
5    error::CardinalError,
6};
7
8/// Validate that an action is legal in the current game state.
9/// Checks:
10/// - Only the priority player can pass priority
11/// - Only the active player can take other actions
12/// - The current phase allows actions
13/// - Stack requirements are met (if action requires empty stack)
14/// - Zone ownership and card ownership are valid
15pub fn validate(engine: &GameEngine, player: PlayerId, action: &Action) -> Result<(), CardinalError> {
16    // If game has ended, no more actions allowed
17    if engine.state.ended.is_some() {
18        return Err(CardinalError("Game has ended".to_string()));
19    }
20
21    // Check action-specific permissions
22    match action {
23        Action::PassPriority => {
24            // Only the priority player can pass priority
25            if player != engine.state.turn.priority_player {
26                return Err(CardinalError(format!(
27                    "Only priority player ({:?}) can pass priority",
28                    engine.state.turn.priority_player
29                )));
30            }
31            Ok(())
32        }
33        Action::Concede => {
34            // Concede is always allowed
35            Ok(())
36        }
37        Action::PlayCard { card, from } => {
38            // Active player only
39            if player != engine.state.turn.active_player {
40                return Err(CardinalError(format!(
41                    "Only active player ({:?}) can take this action",
42                    engine.state.turn.active_player
43                )));
44            }
45
46            // Check phase permissions
47            let current_phase = engine.rules.turn.phases.iter()
48                .find(|p| p.id.as_str() == engine.state.turn.phase.0)
49                .ok_or_else(|| CardinalError("Invalid phase".to_string()))?;
50
51            // PlayCard requires the phase to allow actions
52            if !current_phase.allow_actions {
53                return Err(CardinalError(format!(
54                    "Current phase '{}' does not allow card plays",
55                    current_phase.name
56                )));
57            }
58
59            // Verify the source zone exists and is owned by the player
60            let zone = engine.state.zones.iter()
61                .find(|z| z.id == *from)
62                .ok_or_else(|| CardinalError("Source zone does not exist".to_string()))?;
63
64            if let Some(owner) = zone.owner {
65                if owner != player {
66                    return Err(CardinalError("Cannot play cards from opponent's zones".to_string()));
67                }
68            }
69
70            // Verify the card exists in the source zone
71            if !zone.cards.contains(card) {
72                return Err(CardinalError("Card is not in the specified source zone".to_string()));
73            }
74
75            // If action requires empty stack, check that stack is empty
76            if let Some(action_def) = engine.rules.actions.iter()
77                .find(|a| a.id == "play_card")
78            {
79                if action_def.requires_empty_stack && !engine.state.stack.is_empty() {
80                    return Err(CardinalError(
81                        "Cannot play card: stack is not empty and action requires empty stack"
82                            .to_string(),
83                    ));
84                }
85            }
86
87            Ok(())
88        }
89        Action::ChooseTarget { choice_id, target: _ } => {
90            // ChooseTarget is only valid if there's a pending choice with matching ID
91            match &engine.state.pending_choice {
92                Some(choice) if choice.id == *choice_id => Ok(()),
93                Some(choice) => Err(CardinalError(format!(
94                    "Choice ID mismatch: expected {}, got {}",
95                    choice.id, choice_id
96                ))),
97                None => Err(CardinalError("No pending choice to respond to".to_string())),
98            }
99        }
100    }
101}