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.
19 kB · 488 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489//! Real generated-route Wield evidence; no state, HP, or history fabrication.#[path = "support/mod.rs"]pub mod support;use hexwork_core::{action::CommittedAction, runtime::ActorRole};use hexwork_save::{ decode_run, encode_replay, read_file_bounded, DecodedRun, ProductionReplay, SessionEventKind, MAX_COMPRESSED_BYTES,};use hexwork_worldgen::{Cell, PlanDestination, VoxelWrite};use std::{ collections::BTreeSet, fs, path::Path, process::Command,};use support::Harness;const SEED: u64 = 140;
fn assert_identity(h: &Harness, run: &DecodedRun, mode: hexwork_core::modes::Mode, name: &str) { let content = hexwork_content::RuntimePack::load(&h.content).unwrap(); assert_eq!(run.identity.seed, SEED); assert_eq!(run.identity.mode, mode); assert_eq!( run.identity.ancestry, hexwork_core::character::Ancestry::Mudborn ); assert_eq!( run.identity.calling, hexwork_core::character::Calling::Ratcatcher ); assert_eq!(run.identity.display_name, name); assert_eq!(run.identity.content_hash, content.hash()); assert_eq!(run.identity.run_id.to_string(), run_id(h)); let vault = hexwork_save::decode_vault( &read_file_bounded( h.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, hexwork_save::InitialLineage::default() );}
fn assert_attack(before: &DecodedRun, after: &DecodedRun, target: u64) { assert_eq!(after.events.len(), before.events.len() + 1); assert_eq!( &after.events[..before.events.len()], before.events.as_slice() ); let event = after.events.last().unwrap(); assert!( matches!(event.kind, SessionEventKind::Action(CommittedAction::ResolveAttack { target_actor }) if target_actor == target) ); assert_eq!(event.post_tick - event.pre_tick, 10);}
#[test]#[cfg(any(target_os = "linux", target_os = "macos"))]fn binary_pty_wield_confirmation_and_loaded_keys() { for mode in ["story", "classic", "remapped"] { for ending in ["cancel", "commit"] { let h = Harness::new(&format!("wield-pty-{mode}-{ending}")); if mode == "remapped" { fs::create_dir_all(&h.data).unwrap(); let map = hexwork_term::remap::DEFAULT_KEYMAP_TOML .replace("up_left = [\"Q\", \"y\"]", "up_left = [\"y\"]") .replace("quit = [\"Esc\", \"q\"]", "quit = [\"Q\"]") .replace("confirm = [\"Enter\"]", "confirm = [\"BackTab\"]"); fs::write(h.data.join("keymap.toml"), map).unwrap(); } let mut command = Command::new("python3"); command .current_dir(&h.cwd) .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/pty/wield_live.py")) .arg(env!("CARGO_BIN_EXE_hexwork")) .arg(&h.data) .arg(&h.content) .arg(mode) .arg("1") .arg(ending); let output = ok(command); let summary: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(summary["exit"], 0); assert_eq!(summary["size"], serde_json::json!([80, 24])); let run = decode(&h); assert_identity( &h, &run, if mode == "story" { hexwork_core::modes::Mode::Story } else { hexwork_core::modes::Mode::Classic }, "Wield PTY", ); assert_eq!(run.events.len(), usize::from(ending == "commit")); assert_eq!(summary["color_sgr"], mode == "story"); let p = player(&run); let pack = &run.current.packs[&p]; assert_eq!(pack.items.len(), 3); let pike = pack .items .values() .find(|i| i.template == "ratcatcher_pike") .unwrap() .id; assert_eq!(pack.items.keys().next().copied(), Some(pike)); let wield_events: Vec<_> = run .events .iter() .filter(|e| { matches!( e.kind, SessionEventKind::Action(CommittedAction::Wield { .. }) ) }) .collect(); if ending == "cancel" { assert!(pack.wielded().is_none()); assert!(wield_events.is_empty()); } else { assert_eq!(pack.wielded().unwrap().id, pike); assert_eq!(wield_events.len(), 1); assert!( matches!(wield_events[0].kind, SessionEventKind::Action(CommittedAction::Wield{item_id}) if item_id == pike) ); assert_eq!(wield_events[0].post_tick - wield_events[0].pre_tick, 10); } let restored = loaded(&h, "pty-load.commands", ""); assert_eq!(restored.identity, run.identity); assert_eq!(restored.current.packs[&p], *pack); assert_eq!(restored.events, run.events); assert_eq!( restored.current.try_hash_state().unwrap(), run.current.try_hash_state().unwrap() ); } }}
fn ok(command: Command) -> std::process::Output { let out = Harness::run(command); assert!( out.status.success(), "stdout={} stderr={}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr) ); out}fn run_id(h: &Harness) -> String { let ids: Vec<_> = fs::read_dir(h.data.join("runs")) .unwrap() .map(|e| e.unwrap()) .filter(|e| e.file_type().unwrap().is_dir()) .collect(); assert_eq!(ids.len(), 1); ids[0].file_name().into_string().unwrap()}fn decode(h: &Harness) -> DecodedRun { let content = hexwork_content::RuntimePack::load(&h.content).unwrap(); decode_run( &read_file_bounded( h.data.join("runs").join(run_id(h)).join("run.save"), MAX_COMPRESSED_BYTES, ) .unwrap(), content.hash(), ) .unwrap()}fn loaded(h: &Harness, name: &str, body: &str) -> DecodedRun { let script = h.script(name, &format!("{body}observe state\nsave\nquit\n")); let mut command = h.managed(&script); command.arg("--load").arg(run_id(h)); ok(command); decode(h)}fn player(r: &DecodedRun) -> u64 { r.current .actors .iter() .find(|a| r.current.identity(a.id).unwrap().role == ActorRole::Player) .unwrap() .id}fn cell(position: hexwork_core::spatial::FixedVec3) -> Cell { position.cell()}fn hp(r: &DecodedRun, id: u64) -> i32 { r.current.actors.iter().find(|a| a.id == id).unwrap().hp}fn clear(w: &[VoxelWrite], c: Cell) -> bool { let at = |c: Cell| { w.binary_search_by_key(&(c[2], c[1], c[0]), |v| (v.cell[2], v.cell[1], v.cell[0])) .ok() .map(|i| w[i].voxel) .unwrap_or_default() }; at(c).occ == 0 && at([c[0], c[1], c[2] + 1]).occ == 0 && at([c[0], c[1], c[2] - 1]).occ != 0 && at(c).fluid_amt <= 2}const DIRS: [(i32, i32, &str); 4] = [ (0, 1, "north"), (-1, 0, "west"), (1, 0, "east"), (0, -1, "south"),];fn route(w: &[VoxelWrite], start: Cell, goal: Cell, blockers: &BTreeSet<Cell>) -> Option<String> { support::sampled_route::route_words_opt(w, start, goal, blockers).map(|words| { words .into_iter() .map(|input| format!("input {}\n", support::sampled_route::input_word(input))) .collect() })}
/// Body-aware variant used to stand in a melee goal cell: the last approach is/// re-centred through a one-cell mouth so the player reaches the goal cell/// rather than stopping on its edge.fn route_centered( w: &[VoxelWrite], start: Cell, goal: Cell, blockers: &BTreeSet<Cell>,) -> Option<String> { support::sampled_route::route_words_opt(w, start, goal, blockers)?; Some( support::sampled_route::route_words_centered(w, start, goal, blockers) .into_iter() .map(|input| format!("input {}\n", support::sampled_route::input_word(input))) .collect(), )}/// Squared body-surface gap in mm between two actors, assuming 600 mm wide bodies/// (`BodyExtents::PLAYER`). This is the quantity the melee reach contract compares/// against: ordinary reach 1000 mm, polearm reach 2000 mm.fn reach_gap_squared(r: &DecodedRun, a: u64, b: u64) -> i128 { let pa = r.current.actors.iter().find(|x| x.id == a).unwrap().position.millimetres(); let pb = r.current.actors.iter().find(|x| x.id == b).unwrap().position.millimetres(); (0..2) .map(|i| (i128::from(pa[i]) - i128::from(pb[i])).abs().saturating_sub(600)) .map(|gap| gap * gap) .sum()}
fn approach_candidates( r: &DecodedRun, distance: i32, exclude: Option<u64>,) -> Vec<(String, u64, Cell)> { let floors = hexwork_worldgen::playable_deep(SEED); let floor = &floors[0]; let p = player(r); let start = cell(r.current.actors.iter().find(|a| a.id == p).unwrap().position); let mut blockers: BTreeSet<_> = r .current .actors .iter() .filter(|a| a.alive && a.id != p) .map(|a| cell(a.position)) .collect(); blockers.extend(support::sampled_route::floor_blockers(floor)); let mut candidates = Vec::new(); for enemy in r.current.actors.iter().filter(|a| { a.alive && a.hp > 3 && Some(a.id) != exclude && r.current.identity(a.id).unwrap().role == ActorRole::Monster }) { let target = cell(enemy.position); for (dx, dy, _) in DIRS { let goal = [ target[0] + dx * distance, target[1] + dy * distance, target[2], ]; let middle = [target[0] + dx, target[1] + dy, target[2]]; if !clear(&floor.writes, target) || !clear(&floor.writes, goal) || (distance == 2 && (!clear(&floor.writes, middle) || blockers.contains(&middle))) { continue; } // Ensure this is the only target within either legal distance; target-priority cannot hide the proof. if r.current.actors.iter().any(|a| { a.alive && a.id != p && a.id != enemy.id && { let c = cell(a.position); c[2] == goal[2] && (c[0] - goal[0]).abs() <= 2 && (c[1] - goal[1]).abs() <= 2 } }) { continue; } if let Some(path) = route_centered(&floor.writes, start, goal, &blockers) { candidates.push((path, enemy.id, goal)); } } } assert!( !candidates.is_empty(), "no reachable ordinary defender at distance {distance}" ); candidates}
/// Walk toward each candidate goal (re-planning from the durable position) until/// the player lands inside `(min_reach_mm, max_reach_mm]` body-surface reach of a/// candidate's defender. Returns the run, the chosen defender and its goal.fn reach_stage( h: &Harness, p: u64, candidates: &[(String, u64, Cell)], min_reach_mm: i128, max_reach_mm: i128, name: &str,) -> (DecodedRun, u64, Cell) { let floors = hexwork_worldgen::playable_deep(SEED); let floor = &floors[0]; let mut run = decode(h); for (_, target, goal) in candidates { for _ in 0..6 { let gap = reach_gap_squared(&run, p, *target); if gap <= max_reach_mm * max_reach_mm && gap > min_reach_mm * min_reach_mm { return (run, *target, *goal); } let current = cell(run.current.actors.iter().find(|a| a.id == p).unwrap().position); let mut blockers: BTreeSet<Cell> = run .current .actors .iter() .filter(|a| a.alive && a.id != p) .map(|a| cell(a.position)) .collect(); blockers.extend(support::sampled_route::floor_blockers(floor)); let path = route_centered(&floor.writes, current, *goal, &blockers) .unwrap_or_else(|| panic!("no route {current:?} -> {goal:?}")); run = loaded(h, name, &path); } } panic!("no candidate defender reached the required reach band");}#[test]fn binary_wield_pike_range_and_replay() { let h = Harness::new("wield-range"); let script = h.script("new.commands", "save\nquit\n"); let mut command = h.managed(&script); command.args([ "--story", "--seed", "140", "--name", "Wield evidence", "--ancestry", "1", "--calling", "1", ]); ok(command); let baseline = decode(&h); let p = player(&baseline); assert_identity( &h, &baseline, hexwork_core::modes::Mode::Story, "Wield evidence", ); assert!(baseline.events.is_empty()); let pack = &baseline.current.packs[&p]; let pike = pack .items .values() .find(|i| i.template == "ratcatcher_pike") .expect("ordinary Ratcatcher starter pike") .id; let slot = pack.items.keys().position(|id| *id == pike).unwrap() + 1; // Separate headless input commands, NOT evidence of physical terminal batch boundaries. let wielded=loaded(&h,"wield.commands",&format!("input inventory\ninput pick{slot}\ninput confirm\ninput confirm\ninput confirm\ninput escape\n")); assert_eq!(wielded.current.packs[&p].items, pack.items); assert_eq!(wielded.current.packs[&p].wielded().unwrap().id, pike); assert_eq!(wielded.current.tick, baseline.current.tick + 10); assert_eq!(wielded.events.len(), 1); assert!( matches!(wielded.events[0].kind,SessionEventKind::Action(CommittedAction::Wield{item_id}) if item_id==pike) ); assert_eq!(wielded.events[0].post_tick - wielded.events[0].pre_tick, 10); let town = hexwork_worldgen::milthollow(SEED); let down = town .connectors .iter() .find(|c| c.destination == PlanDestination::Deep { depth: 1 }) .unwrap(); let blockers = support::sampled_route::world_blockers(&town); let path = route(&town.writes, town.start, down.access, &blockers).unwrap(); let arrival = loaded(&h, "deep.commands", &format!("{path}input confirm\n")); let floors = hexwork_worldgen::playable_deep(SEED); let up = floors[0] .connectors .iter() .find(|c| c.transition_id == down.paired_transition_id) .unwrap(); assert_eq!(arrival.current.actors.iter().find(|a| a.id == p).unwrap().position.cell(), up.access); let candidates = approach_candidates(&arrival, 2, None); let (before, first, position) = reach_stage(&h, p, &candidates, 1000, 2000, "range-position.commands"); // Reach contract, not exact-cell adjacency: the polearm lands at 2000 mm // body-surface reach while the ordinary defender's own reach is 1000 mm, so // the player must stand beyond 1000 mm and within 2000 mm of the target. // A live walker can block the exact goal cell, so the walk aims at it while // the assertion is the reach band (`position` remains the planned goal). let gap = reach_gap_squared(&before, p, first); assert!( gap <= 2000 * 2000, "player must be within polearm reach of {first}: goal={position:?} player={:?}", cell(before.current.actors.iter().find(|a| a.id == p).unwrap().position) ); assert!( gap > 1000 * 1000, "polearm proof requires standing beyond the defender's ordinary reach: goal={position:?}" ); assert!(hp(&before, first) > 3); let hit = loaded(&h, "range-hit.commands", "input confirm\n"); assert_eq!(hp(&hit, first), hp(&before, first) - 3); assert_eq!(hp(&hit, p), hp(&before, p)); assert_eq!( hit.current.causes.links().len(), before.current.causes.links().len() + 1 ); assert_attack(&before, &hit, first); let candidates = approach_candidates(&hit, 1, Some(first)); let (before, second, position) = reach_stage(&h, p, &candidates, 0, 1000, "adjacent-position.commands"); assert_ne!(first, second); // Adjacent stage: stand inside the defender's own 1000 mm reach so it // retaliates (again a reach band, not an exact cell). assert!( reach_gap_squared(&before, p, second) <= 1000 * 1000, "player must be inside the defender's ordinary reach: goal={position:?} player={:?}", cell(before.current.actors.iter().find(|a| a.id == p).unwrap().position) ); assert!( hp(&before, second) > 3, "fresh defender must survive outgoing damage" ); let final_run = loaded(&h, "adjacent-hit.commands", "input confirm\n"); assert_eq!(hp(&final_run, second), hp(&before, second) - 3); assert_eq!(hp(&final_run, p), hp(&before, p) - 3); assert_eq!( final_run.current.causes.links().len(), before.current.causes.links().len() + 2 ); assert_attack(&before, &final_run, second); assert_eq!(final_run.identity, baseline.identity); assert_eq!(final_run.current.packs[&p].items, pack.items); assert_eq!(final_run.current.packs[&p].wielded().unwrap().id, pike); let hash = final_run.current.try_hash_state().unwrap(); let restored = loaded(&h, "restore.commands", ""); assert_eq!(restored.current.try_hash_state().unwrap(), hash); assert_eq!(restored.events, final_run.events); let replay = ProductionReplay { identity: final_run.identity, events: final_run.events, expected_final_tick: final_run.current.tick, expected_final_hash: hash, }; let root = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../..") .join("target/wield-evidence"); fs::create_dir_all(&root).unwrap(); let replay_path = root.join("reach.hexdemo"); fs::write(&replay_path, encode_replay(&replay).unwrap()).unwrap(); let mut command = h.command(); command .arg("--content-root") .arg(&h.content) .arg("--replay") .arg(replay_path); let output = ok(command); let text = String::from_utf8(output.stdout).unwrap(); assert!(text.contains("verified event-text replay")); assert!(text.contains(&format!("hash {hash:#x}")));}