Refactor
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
//! 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 authoritativ bleibt: externe Actions
|
||||
//! (`set`, `inc`, `clear`) ändern *immer* den KV, niemals direkt die Story.
|
||||
//!
|
||||
//! Wir nutzen `bladeink::ValueType` als Value, damit die Sync-Konvertierung
|
||||
//! gratis ist (kein Mapping zwischen Rust- und Ink-Typen nötig).
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bladeink::value_type::ValueType;
|
||||
|
||||
pub type Store = HashMap<String, ValueType>;
|
||||
|
||||
/// Parst einen Action-Arg-String zu einem `ValueType`. Reihenfolge:
|
||||
/// `true`/`false` → Bool, dann i32, dann f32, sonst String.
|
||||
pub fn parse_value(s: &str) -> ValueType {
|
||||
let s = s.trim();
|
||||
match s {
|
||||
"true" => return ValueType::Bool(true),
|
||||
"false" => return ValueType::Bool(false),
|
||||
_ => {}
|
||||
}
|
||||
if let Ok(i) = s.parse::<i32>() { return ValueType::Int(i); }
|
||||
if let Ok(f) = s.parse::<f32>() { return ValueType::Float(f); }
|
||||
ValueType::new(s)
|
||||
}
|
||||
|
||||
/// Menschenlesbare Darstellung für REPL-Dumps und Debug-Logs.
|
||||
pub fn format_value(v: &ValueType) -> String {
|
||||
match v {
|
||||
ValueType::Bool(b) => b.to_string(),
|
||||
ValueType::Int(i) => i.to_string(),
|
||||
ValueType::Float(f) => f.to_string(),
|
||||
other => other.coerce_to_string().unwrap_or_else(|_| "<?>".into()),
|
||||
}
|
||||
}
|
||||
|
||||
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::<i32>().unwrap_or(1);
|
||||
let cur = store.get(name).and_then(|v| v.coerce_to_int().ok()).unwrap_or(0);
|
||||
store.insert(name.to_string(), ValueType::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"), ValueType::Bool(true)));
|
||||
assert!(matches!(parse_value("false"), ValueType::Bool(false)));
|
||||
assert!(matches!(parse_value("42"), ValueType::Int(42)));
|
||||
assert!(matches!(parse_value("-7"), ValueType::Int(-7)));
|
||||
assert!(matches!(parse_value("1.5"), ValueType::Float(f) if f == 1.5));
|
||||
assert!(matches!(parse_value("hallo"), ValueType::String(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_inc_clear_roundtrip() {
|
||||
let mut s = Store::new();
|
||||
apply_set("clicks 3", &mut s);
|
||||
assert_eq!(s["clicks"].coerce_to_int().unwrap(), 3);
|
||||
|
||||
apply_inc("clicks", &mut s);
|
||||
assert_eq!(s["clicks"].coerce_to_int().unwrap(), 4);
|
||||
|
||||
apply_inc("clicks -2", &mut s);
|
||||
assert_eq!(s["clicks"].coerce_to_int().unwrap(), 2);
|
||||
|
||||
// inc auf unbekannten Key startet bei 0
|
||||
apply_inc("fresh 5", &mut s);
|
||||
assert_eq!(s["fresh"].coerce_to_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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user