use crate::{
ids::{CardId, PlayerId},
model::command::{Command, EffectRef},
state::gamestate::GameState,
engine::scripting::{RhaiEngine, ScriptContext},
error::CardinalError,
};
pub fn execute_effect(
effect: &EffectRef,
source: Option<CardId>,
controller: PlayerId,
_state: &GameState,
scripting: Option<&RhaiEngine>,
) -> Result<Vec<Command>, CardinalError> {
match effect {
EffectRef::Builtin(effect_str) => execute_builtin_effect(effect_str, controller),
EffectRef::Scripted(script_name) => {
if let Some(engine) = scripting {
execute_scripted_effect(script_name, source, controller, engine)
} else {
Err(CardinalError(format!("Cannot execute scripted effect '{}': RhaiEngine not available", script_name)))
}
}
}
}
fn execute_scripted_effect(
script_name: &str,
source: Option<CardId>,
controller: PlayerId,
engine: &RhaiEngine,
) -> Result<Vec<Command>, CardinalError> {
let context = ScriptContext {
controller: controller.0,
source_card: source.map(|c| c.0).unwrap_or(0),
};
let results = engine.execute_ability(script_name, context)?;
let mut commands = Vec::new();
for (index, result) in results.into_iter().enumerate() {
let map = result.try_cast::<rhai::Map>()
.ok_or_else(|| CardinalError(format!(
"Script '{}' returned non-map value at index {}",
script_name, index
)))?;
let effect_type = map.get("type")
.ok_or_else(|| CardinalError(format!(
"Script '{}' result at index {} missing 'type' field",
script_name, index
)))?
.clone()
.try_cast::<String>()
.ok_or_else(|| CardinalError(format!(
"Script '{}' result at index {} has non-string 'type' field",
script_name, index
)))?;
match effect_type.as_str() {
"damage" => {
let target = map.get("target")
.ok_or_else(|| CardinalError(format!(
"Script '{}' damage effect missing 'target' field",
script_name
)))?
.clone()
.try_cast::<i32>()
.ok_or_else(|| CardinalError(format!(
"Script '{}' damage effect has non-integer 'target'",
script_name
)))?;
let amount = map.get("amount")
.ok_or_else(|| CardinalError(format!(
"Script '{}' damage effect missing 'amount' field",
script_name
)))?
.clone()
.try_cast::<i32>()
.ok_or_else(|| CardinalError(format!(
"Script '{}' damage effect has non-integer 'amount'",
script_name
)))?;
if target < 0 {
return Err(CardinalError(format!(
"Script '{}' damage effect has negative target: {}",
script_name, target
)));
}
if amount < 0 {
return Err(CardinalError(format!(
"Script '{}' damage effect has negative amount: {}",
script_name, amount
)));
}
if target > u8::MAX as i32 {
return Err(CardinalError(format!(
"Script '{}' damage effect has target out of range: {}",
script_name, target
)));
}
commands.push(Command::ChangeLife {
player: PlayerId(target as u8),
delta: -amount,
});
}
"draw" => {
let _player = map.get("player")
.ok_or_else(|| CardinalError(format!(
"Script '{}' draw effect missing 'player' field",
script_name
)))?;
let _count = map.get("count")
.ok_or_else(|| CardinalError(format!(
"Script '{}' draw effect missing 'count' field",
script_name
)))?;
}
"gain_life" => {
let player = map.get("player")
.ok_or_else(|| CardinalError(format!(
"Script '{}' gain_life effect missing 'player' field",
script_name
)))?
.clone()
.try_cast::<i32>()
.ok_or_else(|| CardinalError(format!(
"Script '{}' gain_life effect has non-integer 'player'",
script_name
)))?;
let amount = map.get("amount")
.ok_or_else(|| CardinalError(format!(
"Script '{}' gain_life effect missing 'amount' field",
script_name
)))?
.clone()
.try_cast::<i32>()
.ok_or_else(|| CardinalError(format!(
"Script '{}' gain_life effect has non-integer 'amount'",
script_name
)))?;
if player < 0 {
return Err(CardinalError(format!(
"Script '{}' gain_life effect has negative player: {}",
script_name, player
)));
}
if amount < 0 {
return Err(CardinalError(format!(
"Script '{}' gain_life effect has negative amount: {}",
script_name, amount
)));
}
if player > u8::MAX as i32 {
return Err(CardinalError(format!(
"Script '{}' gain_life effect has player out of range: {}",
script_name, player
)));
}
commands.push(Command::ChangeLife {
player: PlayerId(player as u8),
delta: amount,
});
}
"pump" => {
let _card = map.get("card")
.ok_or_else(|| CardinalError(format!(
"Script '{}' pump effect missing 'card' field",
script_name
)))?;
let _power = map.get("power")
.ok_or_else(|| CardinalError(format!(
"Script '{}' pump effect missing 'power' field",
script_name
)))?;
let _toughness = map.get("toughness")
.ok_or_else(|| CardinalError(format!(
"Script '{}' pump effect missing 'toughness' field",
script_name
)))?;
}
_ => {
return Err(CardinalError(format!(
"Script '{}' has unknown effect type: '{}'",
script_name, effect_type
)));
}
}
}
Ok(commands)
}
fn execute_builtin_effect(effect_str: &str, controller: PlayerId) -> Result<Vec<Command>, CardinalError> {
if effect_str.starts_with("damage_") {
let amount = effect_str.strip_prefix("damage_")
.and_then(|s| s.parse::<i32>().ok())
.ok_or_else(|| CardinalError(format!("Invalid damage amount in: {}", effect_str)))?;
if amount < 0 {
return Err(CardinalError(format!(
"Builtin damage effect has negative amount: {} (effect: {})",
amount, effect_str
)));
}
Ok(vec![Command::ChangeLife {
player: controller,
delta: -amount,
}])
} else if effect_str.starts_with("draw_") {
let count = effect_str.strip_prefix("draw_")
.and_then(|s| s.parse::<u32>().ok())
.ok_or_else(|| CardinalError(format!("Invalid draw count in: {}", effect_str)))?;
if count == 0 {
return Err(CardinalError(format!(
"Builtin draw effect has zero count (effect: {})",
effect_str
)));
}
Ok(vec![])
} else if effect_str.starts_with("gain_life_") {
let amount = effect_str.strip_prefix("gain_life_")
.and_then(|s| s.parse::<i32>().ok())
.ok_or_else(|| CardinalError(format!("Invalid life amount in: {}", effect_str)))?;
if amount < 0 {
return Err(CardinalError(format!(
"Builtin gain_life effect has negative amount: {} (effect: {})",
amount, effect_str
)));
}
Ok(vec![Command::ChangeLife {
player: controller,
delta: amount,
}])
} else if effect_str.starts_with("pump_") {
let parts: Vec<&str> = effect_str.strip_prefix("pump_")
.unwrap_or("")
.split('_')
.collect();
let _power = parts.get(0)
.and_then(|s| s.parse::<i32>().ok())
.ok_or_else(|| CardinalError(format!("Invalid power in: {}", effect_str)))?;
let _toughness = parts.get(1)
.and_then(|s| s.parse::<i32>().ok())
.ok_or_else(|| CardinalError(format!("Invalid toughness in: {}", effect_str)))?;
Ok(vec![])
} else {
Err(CardinalError(format!("Unknown builtin effect type: {}", effect_str)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::gamestate::{GameState, TurnState, PlayerState};
use crate::ids::{PhaseId, StepId};
fn minimal_game_state() -> GameState {
GameState {
turn: TurnState {
number: 1,
active_player: PlayerId(0),
priority_player: PlayerId(0),
phase: PhaseId("main"),
step: StepId("main"),
priority_passes: 0,
},
players: vec![
PlayerState { id: PlayerId(0), life: 20 },
PlayerState { id: PlayerId(1), life: 20 },
],
zones: vec![],
stack: vec![],
pending_choice: None,
ended: None,
}
}
#[test]
fn test_execute_damage_effect() {
let effect = EffectRef::Builtin("damage_2");
let controller = PlayerId(0);
let state = minimal_game_state();
let result = execute_effect(&effect, None, controller, &state, None);
assert!(result.is_ok());
let commands = result.unwrap();
assert_eq!(commands.len(), 1);
match &commands[0] {
Command::ChangeLife { player, delta } => {
assert_eq!(*player, controller);
assert_eq!(*delta, -2);
}
_ => panic!("Expected ChangeLife command"),
}
}
#[test]
fn test_execute_gain_life_effect() {
let effect = EffectRef::Builtin("gain_life_5");
let controller = PlayerId(0);
let state = minimal_game_state();
let result = execute_effect(&effect, None, controller, &state, None);
if result.is_err() {
println!("Error: {:?}", result.as_ref().err());
}
assert!(result.is_ok());
let commands = result.unwrap();
assert_eq!(commands.len(), 1);
match &commands[0] {
Command::ChangeLife { player, delta } => {
assert_eq!(*player, controller);
assert_eq!(*delta, 5);
}
_ => panic!("Expected ChangeLife command"),
}
}
#[test]
fn test_execute_draw_effect() {
let effect = EffectRef::Builtin("draw_1");
let controller = PlayerId(0);
let state = minimal_game_state();
let result = execute_effect(&effect, None, controller, &state, None);
assert!(result.is_ok());
let commands = result.unwrap();
assert_eq!(commands.len(), 0);
}
#[test]
fn test_execute_pump_effect() {
let effect = EffectRef::Builtin("pump_1_1");
let controller = PlayerId(0);
let state = minimal_game_state();
let result = execute_effect(&effect, None, controller, &state, None);
assert!(result.is_ok());
let commands = result.unwrap();
assert_eq!(commands.len(), 0);
}
#[test]
fn test_invalid_effect_string() {
let effect = EffectRef::Builtin("invalid");
let controller = PlayerId(0);
let state = minimal_game_state();
let result = execute_effect(&effect, None, controller, &state, None);
assert!(result.is_err());
}
#[test]
fn test_invalid_damage_amount() {
let effect = EffectRef::Builtin("damage_abc");
let controller = PlayerId(0);
let state = minimal_game_state();
let result = execute_effect(&effect, None, controller, &state, None);
assert!(result.is_err());
}
#[test]
fn test_execute_scripted_effect() {
use crate::engine::scripting::RhaiEngine;
let mut engine = RhaiEngine::new();
let script = r#"
fn execute_ability() {
gain_life(0, 3)
}
"#;
engine.register_script("test_card".to_string(), script).unwrap();
let effect = EffectRef::Scripted("test_card".to_string());
let controller = PlayerId(0);
let state = minimal_game_state();
let result = execute_effect(&effect, None, controller, &state, Some(&engine));
assert!(result.is_ok());
let commands = result.unwrap();
assert_eq!(commands.len(), 1);
match &commands[0] {
Command::ChangeLife { player, delta } => {
assert_eq!(*player, PlayerId(0));
assert_eq!(*delta, 3);
}
_ => panic!("Expected ChangeLife command"),
}
}
#[test]
fn test_execute_scripted_damage_effect() {
use crate::engine::scripting::RhaiEngine;
let mut engine = RhaiEngine::new();
let script = r#"
fn execute_ability() {
deal_damage(1, 5)
}
"#;
engine.register_script("bolt_card".to_string(), script).unwrap();
let effect = EffectRef::Scripted("bolt_card".to_string());
let controller = PlayerId(0);
let state = minimal_game_state();
let result = execute_effect(&effect, None, controller, &state, Some(&engine));
assert!(result.is_ok());
let commands = result.unwrap();
assert_eq!(commands.len(), 1);
match &commands[0] {
Command::ChangeLife { player, delta } => {
assert_eq!(*player, PlayerId(1));
assert_eq!(*delta, -5);
}
_ => panic!("Expected ChangeLife command"),
}
}
}