Skip to main content

cardinal_kernel/engine/
core.rs

1use crate::{
2    error::{EngineError, LegalityError},
3    ids::PlayerId,
4    model::action::Action,
5    model::event::Event,
6    rules::schema::Ruleset,
7    state::gamestate::GameState,
8    engine::scripting::RhaiEngine,
9};
10
11pub struct GameEngine {
12    pub rules: Ruleset,
13    pub state: GameState,
14    pub cards: crate::engine::cards::CardRegistry,
15    pub scripting: RhaiEngine,
16    seed: u64,
17    next_choice_id: u32,
18    next_stack_id: u32,
19}
20
21pub struct StepResult {
22    pub events: Vec<Event>,
23}
24
25impl GameEngine {
26    pub fn new(rules: Ruleset, seed: u64, initial_state: GameState) -> Self {
27        let cards = crate::engine::cards::build_registry(&rules.cards);
28        let scripting = RhaiEngine::new();
29        Self { rules, state: initial_state, cards, scripting, seed, next_choice_id: 1, next_stack_id: 1 }
30    }
31
32    /// Build a GameEngine directly from a `Ruleset`. This will create a minimal GameState
33    /// via `GameState::from_ruleset`.
34    pub fn from_ruleset(rules: Ruleset, seed: u64) -> Self {
35        let initial = GameState::from_ruleset(&rules);
36        let cards = crate::engine::cards::build_registry(&rules.cards);
37        let scripting = RhaiEngine::new();
38        
39        // Note: Script loading from files is intentionally NOT done here to maintain
40        // determinism in the core engine. Scripts should be loaded via a separate
41        // initialization step at a higher level (e.g., in cardinal-cli or a web frontend).
42        // This keeps file I/O out of the engine core.
43        
44        Self { rules, state: initial, cards, scripting, seed, next_choice_id: 1, next_stack_id: 1 }
45    }
46
47    pub fn legal_actions(&self, _player: PlayerId) -> Vec<Action> {
48        // Start simple: implement legality later in engine/legality.rs
49        // Return only actions that make sense (PassPriority, PlayCard if allowed, etc).
50        vec![Action::PassPriority]
51    }
52
53    /// Generate the next unique stack item ID
54    pub fn next_stack_id(&mut self) -> u32 {
55        let id = self.next_stack_id;
56        self.next_stack_id += 1;
57        id
58    }
59
60    /// Generate the next unique choice ID
61    pub fn next_choice_id(&mut self) -> u32 {
62        let id = self.next_choice_id;
63        self.next_choice_id += 1;
64        id
65    }
66
67    pub fn apply_action(&mut self, player: PlayerId, action: Action) -> Result<StepResult, EngineError> {
68        // 1) validate
69        self.validate_action(player, &action)?;
70
71        // 2) apply (reducer)
72        let mut events = crate::engine::reducer::apply(self, player, action)?;
73
74        // 3) post-step checks (win/loss, auto-resolve stack, advance phase)
75        
76        // Check for win/loss conditions
77        self.check_game_end(&mut events);
78        
79        // Auto-resolve stack if it has items and no pending choice
80        self.auto_resolve_stack(&mut events);
81        
82        // Advance to next phase/step if appropriate
83        self.advance_phase_if_ready(&mut events);
84
85        Ok(StepResult { events })
86    }
87
88    fn check_game_end(&mut self, events: &mut Vec<Event>) {
89        // Check if any player has <= 0 life (loses)
90        let losers: Vec<PlayerId> = self.state.players.iter()
91            .filter(|p| p.life <= 0)
92            .map(|p| p.id)
93            .collect();
94
95        if !losers.is_empty() {
96            // Determine winner: last player with > 0 life
97            let winner = self.state.players.iter()
98                .find(|p| p.life > 0)
99                .map(|p| p.id);
100            
101            self.state.ended = Some(crate::state::gamestate::GameEnd {
102                winner,
103                reason: "Life total reached 0".to_string(),
104            });
105            events.push(Event::GameEnded { winner, reason: "Life total reached 0".to_string() });
106        }
107    }
108
109    fn auto_resolve_stack(&mut self, events: &mut Vec<Event>) {
110        // If the stack has items and there's no pending choice, resolve the top item
111        while !self.state.stack.is_empty() && self.state.pending_choice.is_none() {
112            if let Some(item) = self.state.stack.pop() {
113                let item_id = item.id;
114                
115                // Execute the effect and apply resulting commands
116                match crate::engine::effect_executor::execute_effect(
117                    &item.effect,
118                    item.source,
119                    item.controller,
120                    &self.state,
121                    Some(&self.scripting),
122                ) {
123                    Ok(commands) => {
124                        // Apply the commands and collect their events
125                        let effect_events = crate::engine::events::commit_commands(&mut self.state, &commands);
126                        events.extend(effect_events);
127                    }
128                    Err(_err) => {
129                        // Effect execution failed; silently continue resolving the stack.
130                        // Future: emit a dedicated Event to report the failure to callers
131                    }
132                }
133                
134                // Emit StackResolved event after executing effect
135                events.push(Event::StackResolved { item_id });
136            }
137        }
138    }
139
140    fn advance_phase_if_ready(&mut self, events: &mut Vec<Event>) {
141        // Phase advancement logic with priority system:
142        // 1. Only advance if stack is empty and no pending choices
143        // 2. Check if all players have passed priority
144        // 3. If all passed: resolve stack items, reset passes, advance phase
145        // 4. If not all passed: don't advance yet, wait for more passes
146        
147        if !self.state.stack.is_empty() || self.state.pending_choice.is_some() {
148            // Can't advance while stack has items or choice is pending
149            return;
150        }
151
152        let num_players = self.state.players.len() as u32;
153        
154        // Check if all players have passed priority
155        if self.state.turn.priority_passes < num_players {
156            // Not all players have passed yet, don't advance
157            return;
158        }
159        
160        // All players have passed! Reset priority counter and give priority to active player
161        self.state.turn.priority_passes = 0;
162        self.state.turn.priority_player = self.state.turn.active_player;
163
164        // Find current phase index
165        let current_phase_idx = self.rules.turn.phases.iter()
166            .position(|p| p.id.as_str() == self.state.turn.phase.0)
167            .unwrap_or(0);
168        let current_phase = &self.rules.turn.phases[current_phase_idx];
169
170        // Find current step index within current phase
171        let current_step_idx = current_phase.steps.iter()
172            .position(|s| s.id.as_str() == self.state.turn.step.0)
173            .unwrap_or(0);
174
175        // Try to advance to next step in current phase
176        if current_step_idx + 1 < current_phase.steps.len() {
177            let next_step = &current_phase.steps[current_step_idx + 1];
178            let step_box: Box<str> = next_step.id.clone().into_boxed_str();
179            let step_static: &'static str = Box::leak(step_box);
180            self.state.turn.step = crate::ids::StepId(step_static);
181            events.push(Event::PhaseAdvanced {
182                phase: self.state.turn.phase.clone(),
183                step: self.state.turn.step.clone(),
184            });
185            return;
186        }
187
188        // No more steps in current phase; advance to next phase
189        if current_phase_idx + 1 < self.rules.turn.phases.len() {
190            let next_phase = &self.rules.turn.phases[current_phase_idx + 1];
191            let phase_box: Box<str> = next_phase.id.clone().into_boxed_str();
192            let phase_static: &'static str = Box::leak(phase_box);
193            self.state.turn.phase = crate::ids::PhaseId(phase_static);
194
195            // Start at first step of new phase
196            if let Some(first_step) = next_phase.steps.first() {
197                let step_box: Box<str> = first_step.id.clone().into_boxed_str();
198                let step_static: &'static str = Box::leak(step_box);
199                self.state.turn.step = crate::ids::StepId(step_static);
200            } else {
201                self.state.turn.step = crate::ids::StepId("start");
202            }
203
204            events.push(Event::PhaseAdvanced {
205                phase: self.state.turn.phase.clone(),
206                step: self.state.turn.step.clone(),
207            });
208            return;
209        }
210
211        // End of turn: cycle back to first phase and advance turn number
212        if let Some(first_phase) = self.rules.turn.phases.first() {
213            let phase_box: Box<str> = first_phase.id.clone().into_boxed_str();
214            let phase_static: &'static str = Box::leak(phase_box);
215            self.state.turn.phase = crate::ids::PhaseId(phase_static);
216
217            if let Some(first_step) = first_phase.steps.first() {
218                let step_box: Box<str> = first_step.id.clone().into_boxed_str();
219                let step_static: &'static str = Box::leak(step_box);
220                self.state.turn.step = crate::ids::StepId(step_static);
221            } else {
222                self.state.turn.step = crate::ids::StepId("start");
223            }
224
225            self.state.turn.number += 1;
226
227            // Rotate active player and give them priority
228            let next_player_idx = (self.state.turn.active_player.0 + 1) % num_players as u8;
229            self.state.turn.active_player = crate::ids::PlayerId(next_player_idx);
230            self.state.turn.priority_player = self.state.turn.active_player;
231
232            events.push(Event::PhaseAdvanced {
233                phase: self.state.turn.phase.clone(),
234                step: self.state.turn.step.clone(),
235            });
236        }
237    }
238
239    fn validate_action(&self, player: PlayerId, action: &Action) -> Result<(), LegalityError> {
240        crate::engine::legality::validate(self, player, action)
241    }
242}