summaryrefslogtreecommitdiff
path: root/src/cli.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/cli.rs')
-rw-r--r--src/cli.rs156
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());
+ }
+}