1use crate::{
2 ids::{CardId, PlayerId},
3 model::command::{Command, EffectRef},
4 state::gamestate::GameState,
5 engine::scripting::{RhaiEngine, ScriptContext},
6 error::CardinalError,
7};
8
9pub fn execute_effect(
15 effect: &EffectRef,
16 source: Option<CardId>,
17 controller: PlayerId,
18 _state: &GameState,
19 scripting: Option<&RhaiEngine>,
20) -> Result<Vec<Command>, CardinalError> {
21 match effect {
22 EffectRef::Builtin(effect_str) => execute_builtin_effect(effect_str, controller),
23 EffectRef::Scripted(script_name) => {
24 if let Some(engine) = scripting {
25 execute_scripted_effect(script_name, source, controller, engine)
26 } else {
27 Err(CardinalError(format!("Cannot execute scripted effect '{}': RhaiEngine not available", script_name)))
28 }
29 }
30 }
31}
32
33fn execute_scripted_effect(
35 script_name: &str,
36 source: Option<CardId>,
37 controller: PlayerId,
38 engine: &RhaiEngine,
39) -> Result<Vec<Command>, CardinalError> {
40 let context = ScriptContext {
41 controller: controller.0,
42 source_card: source.map(|c| c.0).unwrap_or(0),
43 };
44
45 let results = engine.execute_ability(script_name, context)?;
46
47 let mut commands = Vec::new();
49
50 for (index, result) in results.into_iter().enumerate() {
51 let map = result.try_cast::<rhai::Map>()
53 .ok_or_else(|| CardinalError(format!(
54 "Script '{}' returned non-map value at index {}",
55 script_name, index
56 )))?;
57
58 let effect_type = map.get("type")
59 .ok_or_else(|| CardinalError(format!(
60 "Script '{}' result at index {} missing 'type' field",
61 script_name, index
62 )))?
63 .clone()
64 .try_cast::<String>()
65 .ok_or_else(|| CardinalError(format!(
66 "Script '{}' result at index {} has non-string 'type' field",
67 script_name, index
68 )))?;
69
70 match effect_type.as_str() {
71 "damage" => {
72 let target = map.get("target")
73 .ok_or_else(|| CardinalError(format!(
74 "Script '{}' damage effect missing 'target' field",
75 script_name
76 )))?
77 .clone()
78 .try_cast::<i32>()
79 .ok_or_else(|| CardinalError(format!(
80 "Script '{}' damage effect has non-integer 'target'",
81 script_name
82 )))?;
83
84 let amount = map.get("amount")
85 .ok_or_else(|| CardinalError(format!(
86 "Script '{}' damage effect missing 'amount' field",
87 script_name
88 )))?
89 .clone()
90 .try_cast::<i32>()
91 .ok_or_else(|| CardinalError(format!(
92 "Script '{}' damage effect has non-integer 'amount'",
93 script_name
94 )))?;
95
96 if target < 0 {
98 return Err(CardinalError(format!(
99 "Script '{}' damage effect has negative target: {}",
100 script_name, target
101 )));
102 }
103
104 if amount < 0 {
106 return Err(CardinalError(format!(
107 "Script '{}' damage effect has negative amount: {}",
108 script_name, amount
109 )));
110 }
111
112 if target > u8::MAX as i32 {
114 return Err(CardinalError(format!(
115 "Script '{}' damage effect has target out of range: {}",
116 script_name, target
117 )));
118 }
119
120 commands.push(Command::ChangeLife {
121 player: PlayerId(target as u8),
122 delta: -amount,
123 });
124 }
125 "draw" => {
126 let _player = map.get("player")
129 .ok_or_else(|| CardinalError(format!(
130 "Script '{}' draw effect missing 'player' field",
131 script_name
132 )))?;
133 let _count = map.get("count")
134 .ok_or_else(|| CardinalError(format!(
135 "Script '{}' draw effect missing 'count' field",
136 script_name
137 )))?;
138 }
139 "gain_life" => {
140 let player = map.get("player")
141 .ok_or_else(|| CardinalError(format!(
142 "Script '{}' gain_life effect missing 'player' field",
143 script_name
144 )))?
145 .clone()
146 .try_cast::<i32>()
147 .ok_or_else(|| CardinalError(format!(
148 "Script '{}' gain_life effect has non-integer 'player'",
149 script_name
150 )))?;
151
152 let amount = map.get("amount")
153 .ok_or_else(|| CardinalError(format!(
154 "Script '{}' gain_life effect missing 'amount' field",
155 script_name
156 )))?
157 .clone()
158 .try_cast::<i32>()
159 .ok_or_else(|| CardinalError(format!(
160 "Script '{}' gain_life effect has non-integer 'amount'",
161 script_name
162 )))?;
163
164 if player < 0 {
166 return Err(CardinalError(format!(
167 "Script '{}' gain_life effect has negative player: {}",
168 script_name, player
169 )));
170 }
171
172 if amount < 0 {
174 return Err(CardinalError(format!(
175 "Script '{}' gain_life effect has negative amount: {}",
176 script_name, amount
177 )));
178 }
179
180 if player > u8::MAX as i32 {
182 return Err(CardinalError(format!(
183 "Script '{}' gain_life effect has player out of range: {}",
184 script_name, player
185 )));
186 }
187
188 commands.push(Command::ChangeLife {
189 player: PlayerId(player as u8),
190 delta: amount,
191 });
192 }
193 "pump" => {
194 let _card = map.get("card")
197 .ok_or_else(|| CardinalError(format!(
198 "Script '{}' pump effect missing 'card' field",
199 script_name
200 )))?;
201 let _power = map.get("power")
202 .ok_or_else(|| CardinalError(format!(
203 "Script '{}' pump effect missing 'power' field",
204 script_name
205 )))?;
206 let _toughness = map.get("toughness")
207 .ok_or_else(|| CardinalError(format!(
208 "Script '{}' pump effect missing 'toughness' field",
209 script_name
210 )))?;
211 }
212 _ => {
213 return Err(CardinalError(format!(
214 "Script '{}' has unknown effect type: '{}'",
215 script_name, effect_type
216 )));
217 }
218 }
219 }
220
221 Ok(commands)
222}
223
224fn execute_builtin_effect(effect_str: &str, controller: PlayerId) -> Result<Vec<Command>, CardinalError> {
228 if effect_str.starts_with("damage_") {
230 let amount = effect_str.strip_prefix("damage_")
231 .and_then(|s| s.parse::<i32>().ok())
232 .ok_or_else(|| CardinalError(format!("Invalid damage amount in: {}", effect_str)))?;
233
234 if amount < 0 {
236 return Err(CardinalError(format!(
237 "Builtin damage effect has negative amount: {} (effect: {})",
238 amount, effect_str
239 )));
240 }
241
242 Ok(vec![Command::ChangeLife {
246 player: controller,
247 delta: -amount,
248 }])
249 } else if effect_str.starts_with("draw_") {
250 let count = effect_str.strip_prefix("draw_")
251 .and_then(|s| s.parse::<u32>().ok())
252 .ok_or_else(|| CardinalError(format!("Invalid draw count in: {}", effect_str)))?;
253
254 if count == 0 {
256 return Err(CardinalError(format!(
257 "Builtin draw effect has zero count (effect: {})",
258 effect_str
259 )));
260 }
261
262 Ok(vec![])
265 } else if effect_str.starts_with("gain_life_") {
266 let amount = effect_str.strip_prefix("gain_life_")
267 .and_then(|s| s.parse::<i32>().ok())
268 .ok_or_else(|| CardinalError(format!("Invalid life amount in: {}", effect_str)))?;
269
270 if amount < 0 {
272 return Err(CardinalError(format!(
273 "Builtin gain_life effect has negative amount: {} (effect: {})",
274 amount, effect_str
275 )));
276 }
277
278 Ok(vec![Command::ChangeLife {
279 player: controller,
280 delta: amount,
281 }])
282 } else if effect_str.starts_with("pump_") {
283 let parts: Vec<&str> = effect_str.strip_prefix("pump_")
284 .unwrap_or("")
285 .split('_')
286 .collect();
287
288 let _power = parts.get(0)
289 .and_then(|s| s.parse::<i32>().ok())
290 .ok_or_else(|| CardinalError(format!("Invalid power in: {}", effect_str)))?;
291 let _toughness = parts.get(1)
292 .and_then(|s| s.parse::<i32>().ok())
293 .ok_or_else(|| CardinalError(format!("Invalid toughness in: {}", effect_str)))?;
294
295 Ok(vec![])
300 } else {
301 Err(CardinalError(format!("Unknown builtin effect type: {}", effect_str)))
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use crate::state::gamestate::{GameState, TurnState, PlayerState};
309 use crate::ids::{PhaseId, StepId};
310
311 fn minimal_game_state() -> GameState {
312 GameState {
313 turn: TurnState {
314 number: 1,
315 active_player: PlayerId(0),
316 priority_player: PlayerId(0),
317 phase: PhaseId("main"),
318 step: StepId("main"),
319 priority_passes: 0,
320 },
321 players: vec![
322 PlayerState { id: PlayerId(0), life: 20 },
323 PlayerState { id: PlayerId(1), life: 20 },
324 ],
325 zones: vec![],
326 stack: vec![],
327 pending_choice: None,
328 ended: None,
329 }
330 }
331
332 #[test]
333 fn test_execute_damage_effect() {
334 let effect = EffectRef::Builtin("damage_2");
335 let controller = PlayerId(0);
336 let state = minimal_game_state();
337
338 let result = execute_effect(&effect, None, controller, &state, None);
339 assert!(result.is_ok());
340
341 let commands = result.unwrap();
342 assert_eq!(commands.len(), 1);
343
344 match &commands[0] {
345 Command::ChangeLife { player, delta } => {
346 assert_eq!(*player, controller);
347 assert_eq!(*delta, -2);
348 }
349 _ => panic!("Expected ChangeLife command"),
350 }
351 }
352
353 #[test]
354 fn test_execute_gain_life_effect() {
355 let effect = EffectRef::Builtin("gain_life_5");
356 let controller = PlayerId(0);
357 let state = minimal_game_state();
358
359 let result = execute_effect(&effect, None, controller, &state, None);
360 if result.is_err() {
361 println!("Error: {:?}", result.as_ref().err());
362 }
363 assert!(result.is_ok());
364
365 let commands = result.unwrap();
366 assert_eq!(commands.len(), 1);
367
368 match &commands[0] {
369 Command::ChangeLife { player, delta } => {
370 assert_eq!(*player, controller);
371 assert_eq!(*delta, 5);
372 }
373 _ => panic!("Expected ChangeLife command"),
374 }
375 }
376
377 #[test]
378 fn test_execute_draw_effect() {
379 let effect = EffectRef::Builtin("draw_1");
380 let controller = PlayerId(0);
381 let state = minimal_game_state();
382
383 let result = execute_effect(&effect, None, controller, &state, None);
384 assert!(result.is_ok());
385
386 let commands = result.unwrap();
388 assert_eq!(commands.len(), 0);
389 }
390
391 #[test]
392 fn test_execute_pump_effect() {
393 let effect = EffectRef::Builtin("pump_1_1");
394 let controller = PlayerId(0);
395 let state = minimal_game_state();
396
397 let result = execute_effect(&effect, None, controller, &state, None);
398 assert!(result.is_ok());
399
400 let commands = result.unwrap();
402 assert_eq!(commands.len(), 0);
403 }
404
405 #[test]
406 fn test_invalid_effect_string() {
407 let effect = EffectRef::Builtin("invalid");
408 let controller = PlayerId(0);
409 let state = minimal_game_state();
410
411 let result = execute_effect(&effect, None, controller, &state, None);
412 assert!(result.is_err());
413 }
414
415 #[test]
416 fn test_invalid_damage_amount() {
417 let effect = EffectRef::Builtin("damage_abc");
418 let controller = PlayerId(0);
419 let state = minimal_game_state();
420
421 let result = execute_effect(&effect, None, controller, &state, None);
422 assert!(result.is_err());
423 }
424
425 #[test]
426 fn test_execute_scripted_effect() {
427 use crate::engine::scripting::RhaiEngine;
428
429 let mut engine = RhaiEngine::new();
430 let script = r#"
431 fn execute_ability() {
432 gain_life(0, 3)
433 }
434 "#;
435
436 engine.register_script("test_card".to_string(), script).unwrap();
437
438 let effect = EffectRef::Scripted("test_card".to_string());
439 let controller = PlayerId(0);
440 let state = minimal_game_state();
441
442 let result = execute_effect(&effect, None, controller, &state, Some(&engine));
443 assert!(result.is_ok());
444
445 let commands = result.unwrap();
446 assert_eq!(commands.len(), 1);
447
448 match &commands[0] {
449 Command::ChangeLife { player, delta } => {
450 assert_eq!(*player, PlayerId(0));
451 assert_eq!(*delta, 3);
452 }
453 _ => panic!("Expected ChangeLife command"),
454 }
455 }
456
457 #[test]
458 fn test_execute_scripted_damage_effect() {
459 use crate::engine::scripting::RhaiEngine;
460
461 let mut engine = RhaiEngine::new();
462 let script = r#"
463 fn execute_ability() {
464 deal_damage(1, 5)
465 }
466 "#;
467
468 engine.register_script("bolt_card".to_string(), script).unwrap();
469
470 let effect = EffectRef::Scripted("bolt_card".to_string());
471 let controller = PlayerId(0);
472 let state = minimal_game_state();
473
474 let result = execute_effect(&effect, None, controller, &state, Some(&engine));
475 assert!(result.is_ok());
476
477 let commands = result.unwrap();
478 assert_eq!(commands.len(), 1);
479
480 match &commands[0] {
481 Command::ChangeLife { player, delta } => {
482 assert_eq!(*player, PlayerId(1));
483 assert_eq!(*delta, -5);
484 }
485 _ => panic!("Expected ChangeLife command"),
486 }
487 }
488}