//! Command-line arguments, `orrery names` and `orrery ship`. use crate::dungeon::Pos; use crate::dungeon::crawl::Crawl; use crate::names::{self, Culture}; use crate::scenes::derelict::{item_look, tile_look}; use crate::seed; pub const USAGE: &str = "\ usage: orrery [--seed N] orrery names [--culture NAME] [-n COUNT] [--seed N] orrery ship [-n WHICH] [--seed N] --seed N universe seed; the same seed always makes the same universe names print generated names instead of playing --culture NAME qaari, cant, houses or drift (default: all four) -n COUNT how many of each kind to print (default: 8) ship print a whole derelict, as you'd find it on boarding -n WHICH which derelict: the first you'd board, the second... (default: 1) "; pub enum Command { Play { seed: Option }, Names(NamesOpts), Ship { seed: u64, which: u64 }, Help, } pub struct NamesOpts { pub cultures: Vec, pub count: usize, pub seed: u64, } pub fn parse(args: &[String]) -> Result { let mut args = args.iter().map(String::as_str).peekable(); let sub = args.next_if(|a| matches!(*a, "names" | "ship")); let (mut seed, mut culture, mut count) = (None, None, None); while let Some(arg) = args.next() { match arg { "-h" | "--help" => return Ok(Command::Help), "--seed" => seed = Some(number(args.next(), "--seed")?), "-n" if sub.is_some() => count = Some(number(args.next(), "-n")?), "--culture" if sub == Some("names") => { let name = args.next().ok_or("--culture needs a value")?; culture = Some(Culture::parse(name).ok_or(format!("no culture called {name:?}"))?); } other => return Err(format!("unexpected argument {other:?}")), } } match sub { None => Ok(Command::Play { seed }), Some("ship") => Ok(Command::Ship { seed: seed.unwrap_or_else(seed::from_clock), which: count.unwrap_or(1), }), _ => Ok(Command::Names(NamesOpts { cultures: culture.map_or(Culture::ALL.to_vec(), |c| vec![c]), count: count.unwrap_or(8) as usize, seed: seed.unwrap_or_else(seed::from_clock), })), } } /// Prints the `which`th derelict you'd board in universe `seed`, everything /// on it revealed. pub fn print_ship(seed: u64, which: u64) { let crawl = Crawl::new(seed::derive(seed, "derelict", which)); let map = &crawl.map; println!( "{}, a derelict {} (universe {seed}, derelict {which})\n", crawl.name, crawl.description ); for y in 0..map.height { let row: String = (0..map.width) .map(|x| { let p = Pos::new(x, y); if p == crawl.player { '@' } else if let Some(m) = crawl.monster_at(p) { m.kind.glyph } else if let Some(l) = crawl.loot_at(p) { item_look(&l.item).0 } else { tile_look(map.tile(p)).0 } }) .collect(); println!("{}", row.trim_end()); } let mut rooms: Vec<&str> = map.rooms.iter().map(|r| r.name).collect(); rooms.sort_unstable(); rooms.dedup(); println!("\n@ you Θ airlock + door * artifact % salvage ! medpatch"); println!( "monsters: {}", crawl.monsters.iter().map(|m| m.kind.name).collect::>().join(", ") ); println!("rooms: {}", rooms.join(", ")); } fn number(value: Option<&str>, flag: &str) -> Result { let value = value.ok_or(format!("{flag} needs a value"))?; value.parse().map_err(|_| format!("{flag}: {value:?} isn't a whole number")) } pub fn print_names(opts: &NamesOpts) { println!("universe {} (pass --seed {} to get these again)", opts.seed, opts.seed); for &culture in &opts.cultures { // Each culture draws from its own seed, so `--culture` alone prints // the same names as that culture's section of the full list. let mut rng = seed::rng(seed::derive(opts.seed, culture.name(), 0)); let people: Vec<_> = (0..opts.count).map(|_| culture.person(&mut rng)).collect(); let places: Vec<_> = (0..opts.count).map(|_| culture.place(&mut rng)).collect(); println!("\n{}: {}", culture.name().to_uppercase(), culture.blurb()); println!(" {:<36}places", "people"); for (person, place) in people.iter().zip(&places) { println!(" {person:<36}{place}"); } } if opts.cultures.len() == Culture::ALL.len() { let mut rng = seed::rng(seed::derive(opts.seed, "artifacts", 0)); println!("\nARTIFACTS: as found -> once identified"); for _ in 0..opts.count { let a = names::artifact(&mut rng); println!(" {:<40}{}", a.unidentified, a.true_name); } } } #[cfg(test)] mod tests { use super::*; fn args(s: &str) -> Vec { s.split_whitespace().map(String::from).collect() } #[test] fn parses_play_and_names() { assert!(matches!(parse(&args("")), Ok(Command::Play { seed: None }))); assert!(matches!(parse(&args("--seed 42")), Ok(Command::Play { seed: Some(42) }))); let Ok(Command::Names(opts)) = parse(&args("names --culture drift -n 3 --seed 9")) else { panic!("names didn't parse"); }; assert_eq!((opts.cultures, opts.count, opts.seed), (vec![Culture::Drift], 3, 9)); } #[test] fn rejects_names_flags_when_playing() { assert!(parse(&args("-n 3")).is_err()); assert!(parse(&args("--culture cant")).is_err()); assert!(parse(&args("names --culture nobody")).is_err()); assert!(parse(&args("--seed lots")).is_err()); } }