Skip to main content

cardinal_kernel/engine/
scripting.rs

1use rhai::{Engine, AST, Scope, Dynamic};
2use std::collections::HashMap;
3use crate::error::CardinalError;
4
5/// Wrapper around Rhai engine for executing card scripts
6/// Configured for deterministic, safe execution
7pub struct RhaiEngine {
8    engine: Engine,
9    /// Compiled scripts indexed by card ID
10    scripts: HashMap<String, AST>,
11}
12
13impl RhaiEngine {
14    /// Create a new RhaiEngine configured for Cardinal
15    pub fn new() -> Self {
16        let mut engine = Engine::new();
17        
18        // Configure for determinism and safety
19        engine.set_max_operations(10_000); // Prevent infinite loops
20        engine.set_max_expr_depths(32, 32); // Limit recursion
21        
22        // Register safe helper functions that scripts can call
23        Self::register_helpers(&mut engine);
24        
25        RhaiEngine {
26            engine,
27            scripts: HashMap::new(),
28        }
29    }
30    
31    /// Register helper functions available to card scripts
32    fn register_helpers(engine: &mut Engine) {
33        // Helper: deal_damage(target: i32, amount: i32) -> Dynamic
34        engine.register_fn("deal_damage", |target: i32, amount: i32| {
35            let mut map = rhai::Map::new();
36            map.insert("type".into(), Dynamic::from("damage"));
37            map.insert("target".into(), Dynamic::from(target));
38            map.insert("amount".into(), Dynamic::from(amount));
39            Dynamic::from(map)
40        });
41        
42        // Helper: draw_cards(player: i32, count: i32) -> Dynamic
43        engine.register_fn("draw_cards", |player: i32, count: i32| {
44            let mut map = rhai::Map::new();
45            map.insert("type".into(), Dynamic::from("draw"));
46            map.insert("player".into(), Dynamic::from(player));
47            map.insert("count".into(), Dynamic::from(count));
48            Dynamic::from(map)
49        });
50        
51        // Helper: gain_life(player: i32, amount: i32) -> Dynamic
52        engine.register_fn("gain_life", |player: i32, amount: i32| {
53            let mut map = rhai::Map::new();
54            map.insert("type".into(), Dynamic::from("gain_life"));
55            map.insert("player".into(), Dynamic::from(player));
56            map.insert("amount".into(), Dynamic::from(amount));
57            Dynamic::from(map)
58        });
59        
60        // Helper: pump_creature(card: i32, power: i32, toughness: i32) -> Dynamic
61        engine.register_fn("pump_creature", |card: i32, power: i32, toughness: i32| {
62            let mut map = rhai::Map::new();
63            map.insert("type".into(), Dynamic::from("pump"));
64            map.insert("card".into(), Dynamic::from(card));
65            map.insert("power".into(), Dynamic::from(power));
66            map.insert("toughness".into(), Dynamic::from(toughness));
67            Dynamic::from(map)
68        });
69    }
70    
71    /// Register a card script from source code
72    pub fn register_script(&mut self, card_id: String, script: &str) -> Result<(), CardinalError> {
73        match self.engine.compile(script) {
74            Ok(ast) => {
75                self.scripts.insert(card_id, ast);
76                Ok(())
77            }
78            Err(err) => {
79                Err(CardinalError(format!("Failed to compile script for card {}: {}", card_id, err)))
80            }
81        }
82    }
83    
84    /// Execute a card script's ability
85    /// Returns a list of effect descriptions as Dynamic values
86    pub fn execute_ability(&self, card_id: &str, context: ScriptContext) -> Result<Vec<Dynamic>, CardinalError> {
87        let ast = self.scripts.get(card_id)
88            .ok_or_else(|| CardinalError(format!("No script registered for card {}", card_id)))?;
89        
90        let mut scope = Scope::new();
91        
92        // Pass context to script
93        scope.push("controller", context.controller as i32);
94        scope.push("source_card", context.source_card as i32);
95        
96        // Call the execute_ability function in the script
97        match self.engine.call_fn::<Dynamic>(&mut scope, ast, "execute_ability", ()) {
98            Ok(result) => {
99                // Convert result to Vec<Dynamic>
100                // Script should return an array of command maps
101                if let Some(arr) = result.clone().try_cast::<rhai::Array>() {
102                    Ok(arr)
103                } else {
104                    // Single command, wrap in array
105                    Ok(vec![result])
106                }
107            }
108            Err(err) => {
109                Err(CardinalError(format!("Script execution failed for card {}: {}", card_id, err)))
110            }
111        }
112    }
113}
114
115/// Context passed to script execution
116#[derive(Debug, Clone)]
117pub struct ScriptContext {
118    pub controller: u8,
119    pub source_card: u32,
120}
121
122impl Default for RhaiEngine {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    
132    #[test]
133    fn test_rhai_engine_creation() {
134        let engine = RhaiEngine::new();
135        assert_eq!(engine.scripts.len(), 0);
136    }
137    
138    #[test]
139    fn test_register_simple_script() {
140        let mut engine = RhaiEngine::new();
141        let script = r#"
142            fn execute_ability() {
143                deal_damage(0, 2)
144            }
145        "#;
146        
147        let result = engine.register_script("test_card".to_string(), script);
148        assert!(result.is_ok());
149        assert_eq!(engine.scripts.len(), 1);
150    }
151    
152    #[test]
153    fn test_execute_simple_script() {
154        let mut engine = RhaiEngine::new();
155        let script = r#"
156            fn execute_ability() {
157                deal_damage(0, 2)
158            }
159        "#;
160        
161        engine.register_script("test_card".to_string(), script).unwrap();
162        
163        let context = ScriptContext {
164            controller: 0,
165            source_card: 1,
166        };
167        
168        let result = engine.execute_ability("test_card", context);
169        assert!(result.is_ok());
170        
171        let commands = result.unwrap();
172        assert_eq!(commands.len(), 1);
173    }
174    
175    #[test]
176    fn test_execute_multi_command_script() {
177        let mut engine = RhaiEngine::new();
178        let script = r#"
179            fn execute_ability() {
180                [
181                    deal_damage(0, 2),
182                    draw_cards(0, 1)
183                ]
184            }
185        "#;
186        
187        engine.register_script("test_card".to_string(), script).unwrap();
188        
189        let context = ScriptContext {
190            controller: 0,
191            source_card: 1,
192        };
193        
194        let result = engine.execute_ability("test_card", context);
195        assert!(result.is_ok());
196        
197        let commands = result.unwrap();
198        assert_eq!(commands.len(), 2);
199    }
200}