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.
10 kB · 289 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290//! AC9/10: current binary, completed PTY grids, and real durable run evidence.//! Headless route inputs are separate commands, not physical-batch equivalence.#![cfg(any(target_os = "linux", target_os = "macos"))]
pub mod support;
use hexwork_core::action::{CommittedAction, Direction};use hexwork_core::character::{Ancestry, Calling};use hexwork_core::modes::Mode;use hexwork_core::runtime::{ActorRole, RunLocation};use hexwork_save::{DecodedRun, InitialLineage, SessionEventKind};use hexwork_term::Input;use std::fs;use std::path::Path;use std::process::{Command, Output};use support::Harness;
const SEED: u64 = 223;const NAME: &str = "Tactical orientation";
fn success(output: &Output) { assert!( output.status.success(), "stdout={} stderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) );}
fn pty(harness: &Harness, scenario: &str, run_id: &str) -> serde_json::Value { let output = Harness::run({ let mut command = Command::new("python3"); command .current_dir(&harness.cwd) .env("HOME", &harness.home) .env("XDG_DATA_HOME", harness.home.join("xdg")) .env("PYTHONDONTWRITEBYTECODE", "1") .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/pty/tactical_orientation.py")) .arg(env!("CARGO_BIN_EXE_hexwork")) .arg(&harness.data) .arg(&harness.content) .arg(scenario) .arg(run_id); command }); success(&output); let evidence: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(evidence["size"], serde_json::json!([80, 24])); assert_eq!(evidence["exit"], 0); assert_eq!(evidence["scenario"], scenario); assert_eq!( evidence["screens"].as_object().unwrap().len(), if scenario == "deep" { 3 } else { 5 } ); evidence}
fn saved(harness: &Harness, mode: Mode) -> DecodedRun { let runs: Vec<_> = fs::read_dir(harness.data.join("runs")) .unwrap() .map(Result::unwrap) .filter(|entry| entry.file_type().unwrap().is_dir()) .collect(); assert_eq!(runs.len(), 1, "exactly one ordinary allocated run"); let pack = hexwork_content::RuntimePack::load(&harness.content).unwrap(); let bytes = hexwork_save::read_file_bounded( runs[0].path().join("run.save"), hexwork_save::MAX_COMPRESSED_BYTES, ) .unwrap(); let run = hexwork_save::decode_run(&bytes, pack.hash()).unwrap(); // Every identity field is checked against explicit inputs or the actual vault // allocation. Neither wall-clock state nor immutable fixture bytes are used. assert_eq!(run.identity.seed, SEED); assert_eq!(run.identity.mode, mode); assert_eq!(run.identity.ancestry, Ancestry::Mudborn); assert_eq!(run.identity.calling, Calling::Ratcatcher); assert_eq!(run.identity.display_name, NAME); assert_eq!(run.identity.content_hash, pack.hash()); assert_eq!( run.identity.run_id.to_string(), runs[0].file_name().to_string_lossy() ); let vault = hexwork_save::decode_vault( &hexwork_save::read_file_bounded( harness.data.join("lineage-vault.json"), hexwork_save::MAX_VAULT_BYTES, ) .unwrap(), ) .unwrap(); assert_eq!(run.identity.lineage_id, vault.lineage_id); assert_eq!(run.identity.initial_lineage, InitialLineage::default()); run}
fn player_position(run: &DecodedRun) -> [f32; 3] { let players: Vec<_> = run .current .actors .iter() .filter(|actor| { run.current .identity(actor.id) .is_some_and(|identity| identity.role == ActorRole::Player) }) .collect(); assert_eq!(players.len(), 1); assert!(players[0].alive && players[0].hp > 0); players[0].position.presentation_metres()}
fn starter(scenario: &str, mode: Mode) { let harness = Harness::new(&format!("tactical-{scenario}")); let evidence = pty(&harness, scenario, "-"); let run = saved(&harness, mode); assert_eq!( run.current.run_location(), &RunLocation::Town("milthollow".into()) ); assert_eq!( player_position(&run), hexwork_worldgen::milthollow(SEED).start.map(|v| v as f32) ); assert!( run.events.is_empty(), "presentation navigation must not record actions" ); let mut ticks = Vec::new(); for screen in ["play", "map", "up", "down", "return"] { let row = evidence["screens"][screen][19].as_str().unwrap(); let tick = row .split(" · t") .nth(1) .unwrap() .split(' ') .next() .unwrap() .parse::<u64>() .unwrap(); assert!(tick <= run.current.tick); ticks.push(tick); } assert!(ticks.windows(2).all(|pair| pair[0] <= pair[1])); if mode == Mode::Story { assert!(ticks.iter().all(|tick| *tick == 0)); assert_eq!(run.current.tick, 0); } // Classic may accrue unattended time: never compare entire saves from // different wall-clock moments or require an exact live tick count.}
#[test]fn binary_pty_tactical_story_orientation() { starter("story", Mode::Story);}
#[test]fn binary_pty_tactical_no_color_classic_orientation() { starter("classic", Mode::Classic);}
#[test]fn binary_pty_loaded_deep_two_opens_player_slice() { let harness = Harness::new("tactical-loaded-deep-two"); let mut confirmations = 0; let mut route = Vec::new(); // Reuse the exported real-action script; its route module remains read-only // and is loaded only once by support. Stop before every lethal command. for line in support::grate_mother_death_script(SEED).lines() { let input = match line { "observe state" => continue, "input north" => Input::Up, "input south" => Input::Down, "input west" => Input::Left, "input east" => Input::Right, "input confirm" => Input::Confirm, _ => panic!("unexpected ordinary route line before Deep2: {line}"), }; route.push(input); if input == Input::Confirm { confirmations += 1; if confirmations == 2 { break; } } } assert_eq!(confirmations, 2); assert_eq!(route.last(), Some(&Input::Confirm)); let mut body = String::new(); for input in &route { let command = match input { Input::Up => "north", Input::Down => "south", Input::Left => "west", Input::Right => "east", Input::Confirm => "confirm", _ => panic!("unexpected ordinary route input: {input:?}"), }; body.push_str(&format!("input {command}\n")); } body.push_str("observe state\nsave\nquit\n"); let script = harness.script("deep-two.commands", &body); let output = Harness::run({ let mut command = harness.managed(&script); command.args([ "--story", "--seed", "223", "--name", NAME, "--ancestry", "1", "--calling", "1", ]); command }); success(&output); let run = saved(&harness, Mode::Story); let allocation = Harness::records(&output) .into_iter() .find(|row| row["code"] == "allocation-durable") .unwrap(); assert_eq!(allocation["run_id"], run.identity.run_id.to_string()); assert_eq!(run.current.run_location(), &RunLocation::Deep(2)); let arrival = support::first_two_deep_arrivals(SEED)[1]; assert_eq!(arrival[2], -19); assert_eq!(player_position(&run), arrival.map(|v| v as f32));
// Derive exact connector IDs from the same generated catalogs as the // existing ordinary route, rather than accepting any two transitions. let town = hexwork_worldgen::milthollow(SEED); let floors = hexwork_worldgen::playable_deep(SEED); let transition_ids = [ town.connectors .iter() .find(|c| c.destination == hexwork_worldgen::PlanDestination::Deep { depth: 1 }) .unwrap() .transition_id, floors[0] .connectors .iter() .find(|c| c.destination == hexwork_worldgen::PlanDestination::Deep { depth: 2 }) .unwrap() .transition_id, ]; let mut connector = 0; let expected: Vec<_> = route .iter() .map(|input| match input { Input::Up => CommittedAction::Move(Direction::North), Input::Down => CommittedAction::Move(Direction::South), Input::Left => CommittedAction::Move(Direction::West), Input::Right => CommittedAction::Move(Direction::East), Input::Confirm => { let transition_id = transition_ids[connector]; connector += 1; CommittedAction::UseTransition { transition_id } } _ => unreachable!(), }) .collect(); assert_eq!(connector, 2); let actual: Vec<_> = run .events .iter() .map(|event| match &event.kind { SessionEventKind::Action(action) => action.clone(), _ => panic!("no resurrection belongs in a nonlethal prefix"), }) .collect(); assert_eq!( actual, expected, "exact recorded ordinary route, no lethal suffix" );
pty(&harness, "deep", &run.identity.run_id.to_string()); let after = saved(&harness, Mode::Story); assert_eq!(after.identity, run.identity); assert_eq!(after.events, run.events); assert_eq!(after.current.run_location(), &RunLocation::Deep(2)); assert_eq!(player_position(&after), arrival.map(|v| v as f32)); assert_eq!(after.current.tick, run.current.tick); assert_eq!( after.current.try_hash_state().unwrap(), run.current.try_hash_state().unwrap() );}