//! Command-line arguments, `orrery names`, `ship`, `weapons`, `armour`, //! `galaxy`, `system` and `town`. use std::collections::HashMap; use crate::dungeon::Pos; use crate::dungeon::crawl::Crawl; use crate::dungeon::{armour, arms}; use crate::items::Item; use crate::names::{self, Culture}; use crate::scenes::derelict::tile_look; use crate::scenes::inventory::look; use crate::seed; use crate::space::Vec2; use crate::space::galaxy::{self, HALF_HEIGHT, HALF_WIDTH}; use crate::space::system::{Landing, PlaceKind, SystemKind}; use crate::space::{orbit, survey}; use crate::{scenes, town}; pub const USAGE: &str = "\ usage: orrery [--seed N] orrery names [--culture NAME] [-n COUNT] [--seed N] orrery ship [-n WHICH] [--seed N] orrery weapons [-n COUNT] [--seed N] orrery armour [-n COUNT] [--seed N] orrery galaxy [--seed N] orrery system [-n WHICH] [--seed N] orrery town [-n WHICH] [--inside] [--seed N] --seed N universe seed; the same seed always makes the same universe. To play, without it you carry on as whoever you last played; with it, you make a new character in that 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) weapons print weapons as you might find them aboard wrecks -n COUNT how many (default: 12) armour print armour as you might find it aboard wrecks -n COUNT how many (default: 12) galaxy print the star map: every system, its lanes and who holds it system print a star system: orbits, moons, places -n WHICH which system, numbered as `galaxy` lists them (default: the one the universe started in) town print a capital city in the system you start in -n WHICH which capital: the homeworld's is 1, then outwards (default: 1) --inside print the inside of one building of each kind, too "; pub enum Command { Play { seed: Option }, Names(NamesOpts), Ship { seed: u64, which: u64 }, Weapons { seed: u64, count: usize }, Armour { seed: u64, count: usize }, Galaxy { seed: u64 }, System { seed: u64, which: Option }, Town { seed: u64, which: u64, inside: bool }, 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" | "weapons" | "armour" | "galaxy" | "system" | "town") }); let (mut seed, mut culture, mut count, mut inside) = (None, None, None, false); while let Some(arg) = args.next() { match arg { "-h" | "--help" => return Ok(Command::Help), "--seed" => seed = Some(number(args.next(), "--seed")?), "--inside" if sub == Some("town") => inside = true, "-n" if matches!( sub, Some("names" | "ship" | "weapons" | "armour" | "system" | "town") ) => { 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), }), Some("weapons") => Ok(Command::Weapons { seed: seed.unwrap_or_else(seed::from_clock), count: count.unwrap_or(12) as usize, }), Some("armour") => Ok(Command::Armour { seed: seed.unwrap_or_else(seed::from_clock), count: count.unwrap_or(12) as usize, }), Some("galaxy") => Ok(Command::Galaxy { seed: seed.unwrap_or_else(seed::from_clock) }), Some("system") => { Ok(Command::System { seed: seed.unwrap_or_else(seed::from_clock), which: count }) } Some("town") => Ok(Command::Town { seed: seed.unwrap_or_else(seed::from_clock), which: count.unwrap_or(1), inside, }), _ => 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) { look(&l.stack.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 ) weapon }} gun [ armor % food" ); println!( "monsters: {}", crawl.monsters.iter().map(|m| m.kind.name).collect::>().join(", ") ); let weapons: Vec = crawl .loot .iter() .filter_map(|l| match &l.stack.item { Item::Weapon(w) => Some(format!("{} ({})", w.name(), w.damage_text())), _ => None, }) .collect(); println!("weapons: {}", weapons.join(", ")); let armour: Vec = crawl .loot .iter() .filter_map(|l| match &l.stack.item { Item::Armor(a) => Some(format!("{} ({})", a.name(), a.guards_text())), _ => None, }) .collect(); println!("armour: {}", armour.join(", ")); println!("rooms: {}", rooms.join(", ")); } /// Prints `count` weapons as you might find them aboard wrecks in universe /// `seed`, for tuning the tables in `dungeon::arms`. pub fn print_weapons(seed: u64, count: usize) { print!("{}", arms::catalogue(&mut seed::rng(seed::derive(seed, "catalogue", 0)), count)); } /// Prints `count` pieces of armour as you might find them aboard wrecks in /// universe `seed`, for tuning the tables in `dungeon::armour`. pub fn print_armour(seed: u64, count: usize) { let mut rng = seed::rng(seed::derive(seed, "armour catalogue", 0)); print!("{}", armour::catalogue(&mut rng, count)); } /// Prints the galaxy of universe `seed`: a map, then every system by who /// holds it, numbered for `orrery system -n`. pub fn print_galaxy(seed: u64) { let g = galaxy::generate(seed); // Two light-years to a column, four to a row, so it fits a terminal. let (cols, rows) = ((HALF_WIDTH * 1.2) as usize, (HALF_HEIGHT * 0.6) as usize); let cell = |p: Vec2| { let col = ((p.x / HALF_WIDTH * 1.1 + 1.1) / 2.2 * cols as f64) as usize; let row = ((1.1 - p.y / HALF_HEIGHT * 1.1) / 2.2 * rows as f64) as usize; (col.min(cols - 1), row.min(rows - 1)) }; let mut grid = vec![vec![' '; cols]; rows]; for (i, s) in g.sites.iter().enumerate() { for &j in s.lanes.iter().filter(|&&j| j > i) { let to = g.sites[j].pos; let steps = (s.pos.distance(to) * 2.0) as usize; for k in 1..steps { let (c, r) = cell(s.pos + (to - s.pos) * (k as f64 / steps as f64)); grid[r][c] = '·'; } } } for (i, s) in g.sites.iter().enumerate() { let (c, r) = cell(s.pos); let letter = s.culture.adjective().chars().next().unwrap(); grid[r][c] = if g.is_seat(i) { letter } else { letter.to_ascii_lowercase() }; } println!("A galaxy of {} systems (universe {seed})\n", g.sites.len()); for row in grid { println!(" {}", row.into_iter().collect::().trim_end()); } println!("\n Q/q Qaari C/c Cant H/h High Houses D/d Drift (capitals: seats)"); for c in Culture::ALL { let held: Vec = (0..g.sites.len()).filter(|&i| g.sites[i].culture == c).collect(); let seat = &g.sites[g.seat(c)].name; println!("\n{}: {} systems, seat {seat}", c.name().to_uppercase(), held.len()); for i in held { let s = &g.sites[i]; let start = if i == g.home { " (the universe began here)" } else { "" }; let border = s.neighbour.map_or(String::new(), |n| format!(", borders {}", n.name())); let lanes = match s.lanes.len() { 1 => "1 lane".to_string(), n => format!("{n} lanes"), }; println!( " {:>3} {:<16}{:<22}{:<16}{lanes}{border}{start}", i + 1, s.name, s.kind.name(), s.star.name ); } } let count = |k: SystemKind| g.sites.iter().filter(|s| s.kind == k).count(); let kinds: Vec = SystemKind::ALL.into_iter().map(|k| format!("{} {}", count(k), k.name())).collect(); println!("\n{}", kinds.join(", ")); } /// Prints a star system in universe `seed`: the `which`th as `galaxy` /// numbers them, or the one the universe started in. pub fn print_system(seed: u64, which: Option) { let g = galaxy::generate(seed); let site = match which { Some(n) if (1..=g.sites.len() as u64).contains(&n) => n as usize - 1, Some(n) => { println!("There's no system {n}: they're numbered 1 to {}.", g.sites.len()); return; } None => g.home, }; let s = g.system(site); let kind = match s.kind { SystemKind::Seat => format!("the seat of {}", s.culture.name()), k => format!("{} held by {}", k.name(), s.culture.name()), }; println!("The {} system, a {}: {kind} (universe {seed})", s.name, s.star.name); println!("\n {:<26}{:>7}{:>6}{:>9} what", "planet", "AU", "e", "year"); for (i, p) in s.planets.iter().enumerate() { let people = p.people.map_or(String::new(), |c| format!(", {}", c.adjective())); let home = if i == s.home { " (home)" } else { "" }; println!( " {:<26}{:>7.2}{:>6.2}{:>8.0}d {}{people}{home}", p.name, p.orbit.a, p.orbit.e, p.orbit.period, p.kind.name ); for m in &p.moons { let people = m.people.map_or(String::new(), |c| format!(", {}", c.adjective())); let km = m.orbit.a * 149.6e6; println!( " moon {:<21}{:>6.0}k km{:>7.1}d {}{people}", m.name, km / 1000.0, m.orbit.period, m.kind.name ); } } println!("\n {} lies from {:.2} to {:.2} AU", s.belt.name, s.belt.inner, s.belt.outer); for st in &s.stations { let point = if st.leading { "L4" } else { "L5" }; println!(" {} sits at {}'s {point} point", st.name, s.planets[st.planet].name); } let home = s.home_place(); let from = s.position(home, 0.0); println!( "\n places (⌂ capital, ¤ trade only, · neither), and the trip from {} on day 0:", s.places[home].name ); // The wrecks calling on day 0, when a new game starts. let calling = s.wrecks_on(0.0); for (i, place) in s.places.iter().enumerate() { let indent = if matches!(place.kind, PlaceKind::Moon(..) | PlaceKind::Station(_)) { " " } else { " " }; let trip = if i == home { "you are here".to_string() } else { format!("{:.1} days", orbit::plan_trip(from, 0.0, |d| s.position(i, d))) }; let wrecks: Vec = calling .iter() .filter(|w| w.place == i) .map(|w| format!("wreck: {}, {} (until day {:.0})", w.name, w.description, w.leaves)) .collect(); let mark = match s.landing(i) { Landing::Capital(_) => "⌂", Landing::Market(_) => "¤", _ => "·", }; let name = format!("{indent}{}", place.name); println!("{mark} {name:<30}{trip:<14}{}", wrecks.join("; ")); } } /// Prints the `which`th capital in the system you start in, in universe /// `seed`: the homeworld's first, then the others from the star outwards. /// With `inside`, prints the inside of one building of each kind too. pub fn print_town(seed: u64, which: u64, inside: bool) { let s = crate::space::home_system(seed); let home = s.home_place(); let mut capitals: Vec<(usize, Culture)> = (0..s.places.len()) .filter_map(|i| match s.landing(i) { Landing::Capital(c) => Some((i, c)), _ => None, }) .collect(); capitals.sort_by_key(|&(i, _)| i != home); let Some(&(place, culture)) = capitals.get(which.saturating_sub(1) as usize) else { println!("There are only {} capitals in this system.", capitals.len()); return; }; let people = survey::survey(&s, place, 0.0).population; let land = town::Land::of(&s, place, 0.0).expect("a capital is on a planet or moon"); let t = town::generate(town::seed_for(s.seed, place), culture, town::size_for(people), land); let theme = scenes::town::theme(&t); println!( "{}, the capital of {}: a city of {} on {:?} ground, {} by {} \ (universe {seed}, capital {which} of {})\n", t.name, s.places[place].name, culture.name(), t.land.terrain, t.width(), t.height(), capitals.len() ); let folk: HashMap = t.site.people.iter().map(|p| (p.pos, ())).collect(); for y in 0..t.height() { let row: String = (0..t.width()) .map(|x| { let p = Pos::new(x, y); if folk.contains_key(&p) { '&' } else { scenes::town::street_look(&t, p, &theme, 0).0 } }) .collect(); println!("{}", row.trim_end()); } println!("\nquarters:"); for q in &t.quarters { println!(" {:<28}{:?}", q.name, q.kind); } println!("\nshops:"); let wide = t.shops.iter().map(|s| s.name.chars().count()).max().unwrap_or(0) + 2; for shop in &t.shops { let b = &t.buildings[shop.building]; let quarter = t.quarter_at(b.door).map_or("", |q| q.name.as_str()); println!( " {: = Vec::new(); for b in &t.buildings { let kind = format!("{:?}", b.kind); match kinds.iter_mut().find(|(k, _)| *k == kind) { Some((_, n)) => *n += 1, None => kinds.push((kind, 1)), } } let kinds: Vec = kinds.iter().map(|(k, n)| format!("{n} {k}")).collect(); println!( "\n{} buildings: {}\n{} people in the streets", t.buildings.len(), kinds.join(", "), t.site.people.len() ); if !inside { return; } let mut shown: Vec = Vec::new(); for (b, building) in t.buildings.iter().enumerate() { let kind = format!("{:?}", building.kind); if building.barred || shown.contains(&kind) { continue; } shown.push(kind); let inside = t.enter(b); println!( "\n{} ({} rooms, {} people)", building.name, inside.room_count(), inside.site.people.len() ); for y in 0..inside.site.height { let row: String = (0..inside.site.width) .map(|x| { let p = Pos::new(x, y); if p == inside.start { '@' } else if inside.site.person_at(p).is_some() { '&' } else { scenes::town::inside_look(&t, &inside, p, &theme, 0).0 } }) .collect(); println!(" {}", row.trim_end()); } } } 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()); } }