summaryrefslogtreecommitdiff
path: root/src/cli.rs
blob: 70e9156c9d06838cc35c2c24ea12d787bb1ce603 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
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());
    }
}