//! Golden tests for the parser and a full headless play-through of the //! sample game — the safety net under every future refactor. use mrpgi_core::ai::AiConfig; use mrpgi_core::parser::{build_verbs, default_verbs, run_command}; use mrpgi_core::state::{Command, Event}; use mrpgi_core::world::ObjDef; use mrpgi_core::{GameState, World}; use std::collections::HashMap; fn obj(name: &str, takeable: bool, needs: &str, use_text: &str, wins: bool) -> ObjDef { ObjDef { name: name.into(), sprite: "sign".into(), x: 80, y: 100, look: format!("It's a {}.", name), takeable, synonyms: String::new(), use_text: use_text.into(), needs: needs.into(), wins, requires_flag: String::new(), sets_flag: String::new(), dialogue: Vec::new(), } } /// Run one line against a tiny fixture room; return (first line, understood). fn run(input: &str, objects: &[ObjDef], inventory: &mut Vec) -> (String, bool) { let verbs = default_verbs(); let mut taken = Vec::new(); let mut won = false; let mut flags = HashMap::new(); let mut talk = None; let (lines, understood) = run_command(input, &verbs, objects, 0, inventory, &mut taken, &mut won, &mut flags, &mut talk); (lines.first().cloned().unwrap_or_default(), understood) } #[test] fn parser_goldens() { let objects = vec![obj("battery", true, "", "", false), obj("hatch", false, "battery", "It opens!", true)]; let mut inv = Vec::new(); assert_eq!(run("look", &objects, &mut inv), ("Room 0. You see: battery, hatch.".into(), true)); assert_eq!(run("examine the battery", &objects, &mut inv), ("It's a battery.".into(), true)); assert_eq!(run("grab battery", &objects, &mut inv), ("You take the battery.".into(), true)); assert_eq!(inv, vec!["battery".to_string()]); assert_eq!(run("take hatch", &objects, &mut inv), ("You can't take the hatch.".into(), true)); assert_eq!(run("i", &objects, &mut inv), ("Carrying: battery.".into(), true)); assert_eq!(run("drop battery", &objects, &mut inv), ("You drop the battery.".into(), true)); assert_eq!(run("drop battery", &objects, &mut inv), ("You aren't carrying a 'battery'.".into(), true)); assert_eq!(run("dance wildly", &objects, &mut inv).1, false); assert_eq!(run("talk hatch", &objects, &mut inv), ("hatch has nothing to say.".into(), true)); } #[test] fn use_respects_needs_lock() { let objects = vec![obj("battery", true, "", "", false), obj("hatch", false, "battery", "It opens!", true)]; let verbs = default_verbs(); let mut inv = Vec::new(); let mut taken = Vec::new(); let mut won = false; let mut flags = HashMap::new(); let mut talk = None; let (lines, _) = run_command("open hatch", &verbs, &objects, 0, &mut inv, &mut taken, &mut won, &mut flags, &mut talk); assert_eq!(lines[0], "It's locked. You need: battery."); assert!(!won); run_command("take battery", &verbs, &objects, 0, &mut inv, &mut taken, &mut won, &mut flags, &mut talk); let (lines, _) = run_command("open hatch", &verbs, &objects, 0, &mut inv, &mut taken, &mut won, &mut flags, &mut talk); assert_eq!(lines[0], "It opens!"); assert!(won); } #[test] fn use_respects_flag_gate_and_sets_flags() { let mut lever = obj("lever", false, "", "The lever clunks over.", false); lever.sets_flag = "power_on".into(); let mut door = obj("door", false, "", "The door slides open.", true); door.requires_flag = "power_on".into(); let objects = vec![lever, door]; let verbs = default_verbs(); let (mut inv, mut taken, mut won, mut flags, mut talk) = (Vec::new(), Vec::new(), false, HashMap::new(), None); let (lines, _) = run_command("use door", &verbs, &objects, 0, &mut inv, &mut taken, &mut won, &mut flags, &mut talk); assert_eq!(lines[0], "Nothing happens. Something else must come first."); assert!(!won); run_command("use lever", &verbs, &objects, 0, &mut inv, &mut taken, &mut won, &mut flags, &mut talk); assert_eq!(flags.get("power_on"), Some(&true)); let (lines, _) = run_command("use door", &verbs, &objects, 0, &mut inv, &mut taken, &mut won, &mut flags, &mut talk); assert_eq!(lines[0], "The door slides open."); assert!(won); } #[test] fn manifest_verbs_extend_the_table() { let extra = vec![mrpgi_core::parser::VerbDef { name: "take".into(), words: vec!["yoink".into()] }]; let verbs = build_verbs(&extra); let objects = vec![obj("battery", true, "", "", false)]; let (mut inv, mut taken, mut won, mut flags, mut talk) = (Vec::new(), Vec::new(), false, HashMap::new(), None); let (lines, understood) = run_command("yoink battery", &verbs, &objects, 0, &mut inv, &mut taken, &mut won, &mut flags, &mut talk); assert!(understood); assert_eq!(lines[0], "You take the battery."); } /// Play the shipped sample game start → win, entirely over the bus, twice — /// and require the two transcripts to be byte-identical (replay determinism). #[test] fn sample_game_win_path_is_deterministic() { let dir = std::env::temp_dir().join(format!("mrpgi-test-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); mrpgi_core::kit::write_sample_world(&dir); let play = || -> (Vec, bool) { let mut gs = GameState::new(World::load_game(&dir), AiConfig::off()); let script = [ Command::NewGame { room: None }, Command::Parse { text: "take battery".into() }, Command::WalkTo { x: 156, y: 85 }, // through the east door of room 0 Command::Tick { n: 10 }, Command::WalkTo { x: 80, y: 18 }, // through the north door of room 1 Command::Tick { n: 10 }, Command::Parse { text: "use hatch".into() }, Command::Query, ]; let mut won = false; for cmd in script { for e in gs.apply(cmd) { if matches!(e, Event::Won) { won = true; } } } (gs.transcript.clone(), won) }; let (t1, won1) = play(); let (t2, won2) = play(); assert!(won1, "expected to win the sample game; transcript: {:?}", t1); assert!(won2); assert_eq!(t1, t2, "replay must be bit-identical"); std::fs::remove_dir_all(&dir).ok(); } /// Headless render smoke test: the frame is a real PNG with the right size. #[test] fn renders_a_png_with_no_window() { let dir = std::env::temp_dir().join(format!("mrpgi-render-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); mrpgi_core::kit::write_sample_world(&dir); let mut gs = GameState::new(World::load_game(&dir), AiConfig::off()); gs.apply(Command::NewGame { room: None }); let png = gs.render_png(true); assert_eq!(&png[..8], &[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]); let img = image::load_from_memory(&png).unwrap(); assert_eq!((img.width(), img.height()), (160, 168)); std::fs::remove_dir_all(&dir).ok(); } /// Lore ingestion: upsert a new room over the bus, walk into it. #[test] fn upsert_room_extends_a_running_game() { let dir = std::env::temp_dir().join(format!("mrpgi-lore-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); mrpgi_core::kit::write_sample_world(&dir); let mut gs = GameState::new(World::load_game(&dir), AiConfig::off()); gs.apply(Command::NewGame { room: None }); // A bad room (exit into the void) must be rejected... let mut bad = mrpgi_core::RoomDoc::default(); bad.exits[0] = Some(99); let ev = gs.apply(Command::UpsertRoom { n: 7, doc: bad, persist: false }); assert!(matches!(ev[0], Event::Error { .. })); // ...and a good one accepted and playable. let mut doc = mrpgi_core::RoomDoc::default(); doc.objects.push(obj("gift", true, "", "", false)); let ev = gs.apply(Command::UpsertRoom { n: 7, doc, persist: false }); assert!(matches!(ev[0], Event::RoomUpserted { room: 7, .. }), "unexpected: {}", serde_json::to_string(&ev).unwrap()); gs.apply(Command::GotoRoom { n: 7 }); let ev = gs.apply(Command::Parse { text: "take gift".into() }); assert!(ev.iter().any(|e| matches!(e, Event::InventoryChanged { .. }))); std::fs::remove_dir_all(&dir).ok(); }