Skip to main content

cardinal_kernel/engine/
reducer.rs

1use crate::{
2    engine::core::GameEngine,
3    ids::PlayerId,
4    model::action::Action,
5    model::event::Event,
6    model::command::Command,
7    error::CardinalError,
8};
9
10/// Apply an action to the game state, returning events that occurred.
11/// This includes: direct action effects, trigger evaluation, and command application.
12pub fn apply(engine: &mut GameEngine, player: PlayerId, action: Action) -> Result<Vec<Event>, CardinalError> {
13    match action {
14        Action::PassPriority => {
15            // Only the priority player can pass priority
16            if player != engine.state.turn.priority_player {
17                return Err(CardinalError("Only the priority player can pass priority".to_string()));
18            }
19            
20            // Track this player's pass
21            engine.state.turn.priority_passes += 1;
22            
23            // Check if all players have passed (priority_passes == num_players means full round)
24            let num_players = engine.state.players.len() as u32;
25            let all_passed = engine.state.turn.priority_passes >= num_players;
26            
27            // Rotate priority to next player if not all have passed
28            if !all_passed {
29                let next_priority_idx = (player.0 + 1) % num_players as u8;
30                engine.state.turn.priority_player = crate::ids::PlayerId(next_priority_idx);
31            }
32            
33            Ok(vec![Event::PriorityPassed { by: player }])
34        }
35        Action::Concede => {
36            // Handle concede - determine winner as the other player (or None if no valid winner)
37            let winner = engine.state.players.iter()
38                .find(|p| p.id != player)
39                .map(|p| p.id);
40            
41            // Mark game as ended
42            engine.state.ended = Some(crate::state::gamestate::GameEnd {
43                winner,
44                reason: format!("Player {:?} conceded", player),
45            });
46            
47            Ok(vec![Event::GameEnded { 
48                winner, 
49                reason: format!("Player {:?} conceded", player),
50            }])
51        }
52        Action::PlayCard { card, from } => {
53            // Look up the play_card action definition to find target zone
54            let action_def = engine.rules.actions.iter()
55                .find(|a| a.id == "play_card")
56                .ok_or_else(|| CardinalError("play_card action not defined in rules".to_string()))?;
57            
58            let target_zone_str = action_def.target_zone.as_ref()
59                .ok_or_else(|| CardinalError("play_card action has no target_zone defined".to_string()))?;
60            
61            // Construct the target zone ID (if it's player-owned, append player index)
62            let target_zone_id = if let Some(zone_def) = engine.rules.zones.iter()
63                .find(|z| z.id == *target_zone_str)
64            {
65                match zone_def.owner_scope {
66                    crate::rules::schema::ZoneOwnerScope::Player => {
67                        format!("{}@{}", target_zone_str, player.0)
68                    }
69                    crate::rules::schema::ZoneOwnerScope::Shared => {
70                        target_zone_str.clone()
71                    }
72                }
73            } else {
74                return Err(CardinalError(format!("target zone '{}' not found in rules", target_zone_str)));
75            };
76            
77            let target_zone_box: Box<str> = target_zone_id.into_boxed_str();
78            let target_zone = crate::ids::ZoneId(Box::leak(target_zone_box));
79            
80            // Generate commands to move the card
81            let commands = vec![
82                Command::MoveCard { card, from, to: target_zone },
83            ];
84            
85            // Commit commands to state and collect events
86            let mut events = crate::engine::events::commit_commands(&mut engine.state, &commands);
87            
88            // Add the CardPlayed event
89            let card_played_event = Event::CardPlayed { player, card };
90            events.push(card_played_event.clone());
91            
92            // Evaluate triggers from CardPlayed event
93            let trigger_commands = crate::engine::triggers::evaluate_triggers(engine, &card_played_event);
94            let trigger_events = crate::engine::events::commit_commands(&mut engine.state, &trigger_commands);
95            events.extend(trigger_events);
96            
97            // Evaluate triggers from CardMoved events (extract them first to avoid borrow issues)
98            let card_moved_events: Vec<Event> = events.iter()
99                .filter(|e| matches!(e, Event::CardMoved { .. }))
100                .cloned()
101                .collect();
102            
103            for event in card_moved_events {
104                let trigger_commands = crate::engine::triggers::evaluate_triggers(engine, &event);
105                let trigger_events = crate::engine::events::commit_commands(&mut engine.state, &trigger_commands);
106                events.extend(trigger_events);
107            }
108            
109            Ok(events)
110        }
111        Action::ChooseTarget { choice_id: _, target: _ } => {
112            // Clear the pending choice and emit appropriate event
113            // For now, just remove the choice without applying effects
114            // (effect handling will be part of the trigger system)
115            engine.state.pending_choice = None;
116            
117            // In a full implementation, this would:
118            // 1. Validate the target against the choice's allowed targets
119            // 2. Generate commands based on the effect
120            // 3. Apply those commands
121            
122            Ok(vec![])
123        }
124    }
125}