//! Persistenter Game-State Key-Value-Store. //! //! Single Source of Truth für alle Spielvariablen (Quest-Flags, Inventar- //! Counter, Story-Beats). Ink-Skripte haben eigene `variables_state`-Stores; //! die werden bei jedem `cont()` mit diesem KV synchronisiert (siehe //! [`crate::engine::ink`]). Damit der KV autoritativ bleibt: externe Actions //! (`set`, `inc`, `clear`) ändern *immer* den KV, niemals direkt die Story. //! //! [`Value`] ist ein eigener, Ink- und frontend-neutraler Typ. Die Umrechnung //! auf `bladeink::ValueType` passiert allein an der Ink-Grenze in //! [`crate::engine::ink`] — so hängt der Kern-KV nicht an der Story-Crate. use std::collections::HashMap; use std::fmt; pub type Store = HashMap; /// Ein KV-Wert. Deckt die Skalare ab, die signals.toml-Actions und Ink-`VAR`s /// austauschen; Ink-eigene Typen (Listen, Divert-Referenzen) haben hier /// bewusst keinen Platz. #[derive(Debug, Clone, PartialEq)] pub enum Value { Bool(bool), Int(i32), Float(f32), Str(String), } impl Value { /// Als Ganzzahl lesen (für `inc`): `Bool` zählt als 0/1, `Float` schneidet /// ab, `Str` hat keinen Zahlwert. Spiegelt die Coercion, die vorher /// bladeink lieferte. pub fn as_int(&self) -> Option { match self { Value::Bool(b) => Some(*b as i32), Value::Int(i) => Some(*i), Value::Float(f) => Some(*f as i32), Value::Str(_) => None, } } } /// Menschenlesbare Darstellung für REPL-Dumps und Debug-Logs. impl fmt::Display for Value { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Value::Bool(b) => write!(f, "{b}"), Value::Int(i) => write!(f, "{i}"), Value::Float(x) => write!(f, "{x}"), Value::Str(s) => write!(f, "{s}"), } } } /// Parst einen Action-Arg-String zu einem [`Value`]. Reihenfolge: /// `true`/`false` → Bool, dann i32, dann f32, sonst String. pub fn parse_value(s: &str) -> Value { let s = s.trim(); match s { "true" => return Value::Bool(true), "false" => return Value::Bool(false), _ => {} } if let Ok(i) = s.parse::() { return Value::Int(i); } if let Ok(x) = s.parse::() { return Value::Float(x); } Value::Str(s.to_string()) } pub fn apply_set(args: &str, store: &mut Store) { let Some((name, value)) = args.split_once(char::is_whitespace) else { eprintln!("[kv] set: expected 'name value', got {:?}", args); return; }; store.insert(name.trim().to_string(), parse_value(value)); } pub fn apply_inc(args: &str, store: &mut Store) { let (name, delta) = args.split_once(char::is_whitespace).unwrap_or((args, "1")); let name = name.trim(); let delta = delta.trim().parse::().unwrap_or(1); let cur = store.get(name).and_then(Value::as_int).unwrap_or(0); store.insert(name.to_string(), Value::Int(cur + delta)); } pub fn apply_clear(args: &str, store: &mut Store) { store.remove(args.trim()); } #[cfg(test)] mod tests { use super::*; #[test] fn parse_value_types() { assert!(matches!(parse_value("true"), Value::Bool(true))); assert!(matches!(parse_value("false"), Value::Bool(false))); assert!(matches!(parse_value("42"), Value::Int(42))); assert!(matches!(parse_value("-7"), Value::Int(-7))); assert!(matches!(parse_value("1.5"), Value::Float(f) if f == 1.5)); assert!(matches!(parse_value("hallo"), Value::Str(_))); } #[test] fn as_int_coerces_like_ink_did() { assert_eq!(Value::Bool(true).as_int(), Some(1)); assert_eq!(Value::Int(-7).as_int(), Some(-7)); assert_eq!(Value::Float(2.9).as_int(), Some(2)); assert_eq!(Value::Str("x".into()).as_int(), None); } #[test] fn set_inc_clear_roundtrip() { let mut s = Store::new(); apply_set("clicks 3", &mut s); assert_eq!(s["clicks"].as_int().unwrap(), 3); apply_inc("clicks", &mut s); assert_eq!(s["clicks"].as_int().unwrap(), 4); apply_inc("clicks -2", &mut s); assert_eq!(s["clicks"].as_int().unwrap(), 2); // inc auf unbekannten Key startet bei 0 apply_inc("fresh 5", &mut s); assert_eq!(s["fresh"].as_int().unwrap(), 5); apply_clear("clicks", &mut s); assert!(!s.contains_key("clicks")); } #[test] fn set_without_value_is_noop() { let mut s = Store::new(); apply_set("only_name", &mut s); assert!(s.is_empty()); } #[test] fn display_is_plain_scalar() { assert_eq!(Value::Bool(true).to_string(), "true"); assert_eq!(Value::Int(-3).to_string(), "-3"); assert_eq!(Value::Str("hi".into()).to_string(), "hi"); } }