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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229//! Phase 12 visible-feedback production process gates.
pub mod route_fixture { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/routes.rs"));}
use self::route_fixture::{blocked_move_route, BlockedMoveRoute};use super::support::Harness;use hexwork_core::action::{CommittedAction, Direction};use hexwork_core::runtime::ActorRole;use hexwork_save::{decode_run, read_file_bounded, SessionEventKind, MAX_COMPRESSED_BYTES};use hexwork_term::Input;use std::fs;use std::process::Command;
const SEED: u64 = 223;
fn input_name(input: Input) -> &'static str { match input { Input::Up => "north", Input::Down => "south", Input::Left => "west", Input::Right => "east", _ => panic!("blocked route must contain only ordinary cardinal input: {input:?}"), }}
// The route is canonical/world-relative: North (`Input::Up`) is world -Y and// East is +X. The shipped keymap binds `w` to Up/North and `s` to Down/South,// so the physical keys must be derived from that same mapping; a swapped pair// drives the mirrored route and journals a Y-flipped action list.fn physical_key(input: Input) -> &'static str { match input { Input::Up => "w", Input::Down => "s", Input::Left => "a", Input::Right => "d", _ => panic!("blocked route must contain only default WASD input: {input:?}"), }}
fn committed(input: Input) -> CommittedAction { CommittedAction::Move(match input { Input::Up => Direction::North, Input::Down => Direction::South, Input::Left => Direction::West, Input::Right => Direction::East, _ => panic!("blocked route must contain only cardinal movement: {input:?}"), })}
fn route_json(route: &BlockedMoveRoute) -> serde_json::Value { serde_json::json!({ "prefix_keys": route.prefix_inputs.iter().copied().map(physical_key).collect::<String>(), "blocked_key": physical_key(route.rejected_input), "legal_keys": route.legal_followup.iter().copied().map(physical_key).collect::<String>(), "prefix_len": route.prefix_inputs.len(), })}
fn decode_managed_run(harness: &Harness, run_id: &str) -> hexwork_save::DecodedRun { let pack = hexwork_content::RuntimePack::load(&harness.content).unwrap(); decode_run( &read_file_bounded( harness.data.join("runs").join(run_id).join("run.save"), MAX_COMPRESSED_BYTES, ) .unwrap(), pack.hash(), ) .unwrap()}
fn only_run_id(harness: &Harness) -> String { let mut runs = fs::read_dir(harness.data.join("runs")) .unwrap() .map(|entry| entry.unwrap()) .filter(|entry| entry.file_type().unwrap().is_dir()) .map(|entry| entry.file_name().into_string().unwrap()) .collect::<Vec<_>>(); runs.sort(); assert_eq!(runs.len(), 1, "expected one managed run: {runs:?}"); runs.pop().unwrap()}
fn saved_player(run: &hexwork_save::DecodedRun) -> &hexwork_core::actor::Actor { run.current .actors .iter() .find(|actor| { run.current .identity(actor.id) .is_some_and(|identity| identity.role == ActorRole::Player) }) .expect("one saved Player")}
#[test]#[cfg(any(target_os = "linux", target_os = "macos"))]fn binary_pty_blocked_move_is_recorded_and_play_continues() { let harness = Harness::new("feedback-pty"); let route = blocked_move_route(SEED); assert!(route.prefix_inputs.len() < 64, "route should stay short"); let fixture = harness.root.join("blocked-route.json"); fs::write(&fixture, serde_json::to_vec(&route_json(&route)).unwrap()).unwrap(); let scenario = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/pty/feedback_scenario.py"); let output = Harness::run({ let mut command = Command::new("python3"); command .current_dir(&harness.cwd) .arg(scenario) .arg(env!("CARGO_BIN_EXE_hexwork")) .arg(&harness.data) .arg(&harness.content) .arg(&fixture) .arg(SEED.to_string()); command }); assert!( output.status.success(), "feedback PTY failed: stdout={} stderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); let summary: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(summary["exit"], 0); assert_eq!(summary["session_over"], true);
let run_id = summary["run_id"].as_str().unwrap(); let saved = decode_managed_run(&harness, run_id); let expected_actions = route .prefix_inputs .iter() .copied() .chain([route.rejected_input]) .chain(route.legal_followup.iter().copied()) .map(committed) .collect::<Vec<_>>(); let saved_actions = saved .events .iter() .map(|event| match &event.kind { SessionEventKind::Action(action) => action.clone(), other => panic!("unexpected lifecycle event on movement route: {other:?}"), }) .collect::<Vec<_>>(); assert_eq!(saved_actions, expected_actions); let player = saved_player(&saved); assert!(player.alive && player.hp > 0); // Every sample is journaled: the blocked sample is a recorded, tick-spending // cell no-op (core advances the delver only up to the wall), so the short // fixed follow-up stays inside the blocked sample's origin cell. assert_eq!(player.position.cell(), route.expected_cells[0]);}
#[test]fn binary_headless_blocked_move_records_noop_and_play_continues() { let harness = Harness::new("feedback-headless"); let route = blocked_move_route(SEED); let mut body = String::new(); for input in &route.prefix_inputs { body.push_str(&format!("input {}\n", input_name(*input))); } let blocked_index = route.prefix_inputs.len(); body.push_str(&format!("input {}\n", input_name(route.rejected_input))); for input in &route.legal_followup { body.push_str(&format!("input {}\n", input_name(*input))); } body.push_str("observe state\nsave\n"); let script = harness.script("feedback.commands", &body); let output = Harness::run({ let mut command = harness.managed(&script); command.arg("--seed").arg(SEED.to_string()); command }); assert_eq!( output.status.code(), Some(0), "a blocked sample must not stop later commands: stdout={} stderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); assert!( !String::from_utf8_lossy(&output.stderr).contains("passage-blocked"), "the blocked sample is a recorded no-op, not a rejection" ); let records = Harness::records(&output); assert!(records .iter() .any(|record| record["code"] == "allocation-durable")); // The blocked sample is acknowledged and the later observation still runs. assert!(records.iter().any(|record| { record["record_kind"] == "command_ack" && record["command_index"] .as_u64() .is_some_and(|index| index >= blocked_index as u64) })); assert!(records .iter() .any(|record| record["record_kind"] == "observation"));
let run_id = only_run_id(&harness); let saved = decode_managed_run(&harness, &run_id); let expected_actions = route .prefix_inputs .iter() .copied() .chain([route.rejected_input]) .chain(route.legal_followup.iter().copied()) .map(committed) .collect::<Vec<_>>(); let saved_actions = saved .events .iter() .map(|event| match &event.kind { SessionEventKind::Action(action) => action.clone(), other => panic!("unexpected lifecycle event on movement route: {other:?}"), }) .collect::<Vec<_>>(); assert_eq!(saved_actions, expected_actions); let player = saved_player(&saved); assert!(player.alive && player.hp > 0); // Every sample is journaled: the blocked sample is a recorded, tick-spending // cell no-op (core advances the delver only up to the wall), so the short // fixed follow-up stays inside the blocked sample's origin cell. assert_eq!(player.position.cell(), route.expected_cells[0]);}