diff options
Diffstat (limited to 'src/cli.rs')
| -rw-r--r-- | src/cli.rs | 156 |
1 files changed, 156 insertions, 0 deletions
diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..70e9156 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,156 @@ +//! 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<u64> }, + Names(NamesOpts), + Ship { seed: u64, which: u64 }, + Help, +} + +pub struct NamesOpts { + pub cultures: Vec<Culture>, + pub count: usize, + pub seed: u64, +} + +pub fn parse(args: &[String]) -> Result<Command, String> { + 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::<Vec<_>>().join(", ") + ); + println!("rooms: {}", rooms.join(", ")); +} + +fn number(value: Option<&str>, flag: &str) -> Result<u64, String> { + 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<String> { + 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()); + } +} |