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