Something went wrong. Try again.
HEXWORK - A Lantern in the Deep: 3D ASCII terminal roguelike RPG in Rust.
Entirely designed and built by Muse Spark 1.3 to evaluate if I like the model.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240//! Phase 12 keymap integration gates nested under `phase11_gate` so every//! process case reuses the accepted bounded `support::Harness`.
use super::support::Harness;use hexwork_core::action::{CommittedAction, Direction};use hexwork_core::character::{Ancestry, Calling};use hexwork_core::runtime::ActorRole;use hexwork_save::{decode_run, read_file_bounded, SessionEventKind, MAX_COMPRESSED_BYTES};use hexwork_term::remap::{DEFAULT_KEYMAP_TOML, MAX_KEYMAP_BYTES};use std::fs;use std::process::Command;
fn interactive(harness: &Harness) -> Command { let mut command = harness.command(); command .arg("--data-dir") .arg(&harness.data) .arg("--content-root") .arg(&harness.content) .arg("--ancestry") .arg("1") .arg("--calling") .arg("1"); command}
fn assert_preterminal_failure(harness: &Harness, expected_exit: i32, needle: &str) { let output = Harness::run(interactive(harness)); assert_eq!( output.status.code(), Some(expected_exit), "stdout={} stderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains(needle), "stderr={stderr}"); assert!( !output.stdout.contains(&0x1b), "terminal escape reached stdout" ); assert!( !output.stderr.contains(&0x1b), "terminal escape reached stderr" ); assert!(!harness.data.join("lineage-vault.json").exists()); assert!(!harness.data.join("runs").exists());}
#[test]fn binary_bad_keymap_refuses_before_terminal_and_allocation() { for (label, bytes, exit, needle) in [ ( "parse", b"[action]\nwarp = [\"x\"]\n".as_slice(), 2, "keymap", ), ("utf8", b"\xff\xfe".as_slice(), 2, "UTF-8"), ] { let harness = Harness::new(&format!("keymap-bad-{label}")); fs::create_dir_all(&harness.data).unwrap(); fs::write(harness.data.join("keymap.toml"), bytes).unwrap(); assert_preterminal_failure(&harness, exit, needle); }
let directory = Harness::new("keymap-bad-directory"); fs::create_dir_all(directory.data.join("keymap.toml")).unwrap(); assert_preterminal_failure(&directory, 4, "not a regular file");
let oversized = Harness::new("keymap-bad-oversized"); fs::create_dir_all(&oversized.data).unwrap(); let file = fs::File::create(oversized.data.join("keymap.toml")).unwrap(); file.set_len(MAX_KEYMAP_BYTES as u64 + 1).unwrap(); assert_preterminal_failure(&oversized, 5, "65536");}
#[test]fn headless_ignores_invalid_optional_keymap() { let harness = Harness::new("keymap-headless-ignore"); fs::create_dir_all(&harness.data).unwrap(); fs::write(harness.data.join("keymap.toml"), b"\xff\xfe").unwrap(); let script = harness.script("quit.commands", "quit\n"); let output = Harness::run(harness.managed(&script)); assert!( output.status.success(), "headless stderr={}", String::from_utf8_lossy(&output.stderr) );}
#[test]fn read_only_commands_ignore_invalid_optional_keymap() { let harness = Harness::new("keymap-readonly-ignore"); fs::create_dir_all(&harness.data).unwrap(); fs::write(harness.data.join("keymap.toml"), b"\xff\xfe").unwrap();
let help = Harness::run({ let mut command = harness.command(); command.arg("--help"); command }); assert!(help.status.success());
let arena = Harness::run({ let mut command = harness.command(); command.arg("--arena"); command }); assert!(arena.status.success());
let recover = Harness::run({ let mut command = harness.command(); command .arg("--data-dir") .arg(&harness.data) .arg("--content-root") .arg(&harness.content) .arg("--recover"); command }); assert!( recover.status.success(), "recover stderr={}", String::from_utf8_lossy(&recover.stderr) );}
#[cfg(any(target_os = "linux", target_os = "macos"))]fn remapped_fixture() -> String { DEFAULT_KEYMAP_TOML .replace("left = [\"Left\", \"a\", \"h\"]", "left = [\"f\"]") .replace("quit = [\"Esc\", \"q\"]", "quit = [\"v\"]") .replace("chargen_pick_1 = [\"1\"]", "chargen_pick_1 = [\"7\"]") .replace("chargen_pick_2 = [\"2\"]", "chargen_pick_2 = [\"8\"]")}
#[cfg(any(target_os = "linux", target_os = "macos"))]fn run_pty_scenario(harness: &Harness, mode: &str) -> serde_json::Value { let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/pty/keymap_scenario.py"); let output = Harness::run({ let mut command = Command::new("python3"); command .current_dir(&harness.cwd) .arg(script) .arg(env!("CARGO_BIN_EXE_hexwork")) .arg(&harness.data) .arg(&harness.content) .arg(mode); command }); assert!( output.status.success(), "PTY scenario failed: stdout={} stderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); serde_json::from_slice(&output.stdout).expect("small PTY JSON summary")}
#[test]#[cfg(any(target_os = "linux", target_os = "macos"))]fn binary_pty_remapped_chargen_movement_quit_and_load() { let harness = Harness::new("keymap-pty-remapped"); fs::create_dir_all(&harness.data).unwrap(); fs::write(harness.data.join("keymap.toml"), remapped_fixture()).unwrap(); let summary = run_pty_scenario(&harness, "remapped"); assert_eq!(summary["prompt_before_allocation"], true); assert_eq!(summary["load_completed"], true);
let run_id = summary["run_id"].as_str().expect("run id"); let pack = hexwork_content::RuntimePack::load(&harness.content).unwrap(); let decode = |path| { decode_run( &read_file_bounded(path, MAX_COMPRESSED_BYTES).unwrap(), pack.hash(), ) .unwrap() }; let first = decode(harness.data.join("first-run.save")); let saved = decode(harness.data.join("runs").join(run_id).join("run.save")); assert_eq!(saved.identity.ancestry, Ancestry::Mudborn); assert_eq!(saved.identity.calling, Calling::Lampbearer); let player_feet = |run: &hexwork_save::DecodedRun| { let player = run .current .actors .iter() .find(|actor| { run.current .identity(actor.id) .is_some_and(|identity| identity.role == ActorRole::Player) }) .expect("one saved player"); assert!(player.alive && player.hp > 0); player.position.millimetres() }; // One remapped `left` press is a single paced movement sample (3000 mm/s // over one 100 ms tick), so it steps 300 mm west inside the spawn cell // `[21,31,8]` instead of teleporting into `[20,31,8]`; the resumed `right` // press returns the body exactly to the spawn cell centre. let first_position = player_feet(&first); let resumed_position = player_feet(&saved); assert_eq!(first_position, [21_500 - 300, 31_500, 8_000]); assert_eq!(resumed_position, [21_500, 31_500, 8_000]); assert_eq!( first.events.len(), 1, "first save events: {:?}", first.events ); assert!(matches!( &first.events[0].kind, SessionEventKind::Action(CommittedAction::Move(Direction::West)) )); assert_eq!( saved.events.len(), 2, "resumed save events: {:?}", saved.events ); assert_eq!(saved.events[0], first.events[0]); assert!(matches!( &saved.events[1].kind, SessionEventKind::Action(CommittedAction::Move(Direction::East)) ));}
#[test]#[cfg(any(target_os = "linux", target_os = "macos"))]fn binary_pty_default_keymap_fallback() { let harness = Harness::new("keymap-pty-default"); assert!(!harness.data.join("keymap.toml").exists()); let summary = run_pty_scenario(&harness, "default"); assert_eq!(summary["prompt_before_allocation"], true); assert_eq!(summary["fallback_reported"], true); assert!(!harness.data.join("keymap.toml").exists());}