//! The edition dataset, vendored and read by hand (ADR-0011). //! //! `Problems.csv` is authoritative (ground-game, GROUND-WP-0002 T01). The //! engine used to invent Problem values and suits; it reads them now. //! //! **Why not the `csv` crate.** It fits — 17,651 marginal lines against //! AM-4b's 19,742 of headroom — and is refused anyway, because that is //! 89% of everything the budget has left to read 20 rows, and the next //! dependency would have 2,091 lines to live in. If this data ever grows //! nested quoting, embedded newlines, or multiple dialects, that decision //! is wrong and `csv` is the answer (ADR-0011 D1). use crate::{SolutionCard, Suit}; /// One Problem as the edition prints it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct EditionProblem { /// `hidden_priority`: 0 is the Surface Problem. pub priority: u8, pub value: u8, pub suit: Suit, /// `visibility == "Surface"` — dealt face up (GR-S01). pub surface: bool, } const CSV: &str = include_str!("../../../editions/ground-darvo-r0/Problems.csv"); /// Split one CSV record, honouring `"…"` quoting. /// /// `problem_text` contains commas, which is the only reason this is not a /// `split(',')`. Doubled quotes inside a quoted field are not handled and /// do not occur; if they ever do, this returns the wrong field count and /// `problems_of` fails loudly rather than mis-parsing. fn fields(line: &str) -> Vec { let mut out = Vec::new(); let mut cur = String::new(); let mut quoted = false; for c in line.chars() { match c { '"' => quoted = !quoted, ',' if !quoted => out.push(std::mem::take(&mut cur)), c => cur.push(c), } } out.push(cur); out } fn suit_of(s: &str) -> Option { match s.trim() { "Clarify" => Some(Suit::Clarify), "Repair" => Some(Suit::Repair), "Boundary" => Some(Suit::Boundary), "Change" => Some(Suit::Change), _ => None, } } /// Every Problem of one scenario, ordered by `hidden_priority`. /// /// Returns `Err` rather than an empty list when the data does not parse: /// a loader that silently reads nothing would hand `setup` a game with no /// Problems and look like a rules bug. pub fn problems_of(scenario_id: &str) -> Result, String> { let mut lines = CSV.lines(); let header = lines.next().ok_or("edition data is empty")?; let cols: Vec = fields(header) .into_iter() .map(|c| c.trim_start_matches('\u{feff}').trim().to_string()) .collect(); let at = |name: &str| -> Result { cols.iter() .position(|c| c == name) .ok_or_else(|| format!("edition data has no column {name:?}")) }; let (c_scn, c_pri, c_val, c_sol, c_vis) = ( at("scenario_id")?, at("hidden_priority")?, at("point_value")?, at("required_solution")?, at("visibility")?, ); let mut out = Vec::new(); for line in lines.filter(|l| !l.trim().is_empty()) { let f = fields(line); if f.len() != cols.len() { return Err(format!( "edition row has {} fields, header has {}: {line}", f.len(), cols.len() )); } if f[c_scn].trim() != scenario_id { continue; } out.push(EditionProblem { priority: f[c_pri] .trim() .parse() .map_err(|_| format!("hidden_priority {:?} is not a number", f[c_pri]))?, value: f[c_val] .trim() .parse() .map_err(|_| format!("point_value {:?} is not a number", f[c_val]))?, suit: suit_of(&f[c_sol]) .ok_or_else(|| format!("required_solution {:?} is not a suit", f[c_sol]))?, surface: f[c_vis].trim() == "Surface", }); } if out.is_empty() { return Err(format!("edition data has no Problems for {scenario_id}")); } out.sort_by_key(|p| p.priority); Ok(out) } /// GR-S01 as ruled by ground-game 2026-08-04: **Surface always, plus /// hidden priorities `1..=k`**, with k by seat band. Surface is never one /// of the hidden slots. /// /// Available points are therefore 6 / 9 / 12 with this edition's values — /// the numbers ground-game ruled the thresholds 5 / 7 / 9 against. pub fn hidden_depth(players: u8) -> Result { match players { 2 => Ok(2), 3..=4 => Ok(3), 5..=6 => Ok(4), other => Err(format!("GR-S01: unsupported player count {other}")), } } /// The Problems dealt at `players` seats, Surface first. pub fn deal(scenario_id: &str, players: u8) -> Result, String> { let k = hidden_depth(players)?; let all = problems_of(scenario_id)?; let dealt: Vec = all .into_iter() .filter(|p| p.surface || (p.priority >= 1 && p.priority <= k)) .collect(); Ok(dealt) } /// The core Solution deck, 6 per suit in canonical order (GR-S04). pub fn solution_deck() -> Vec { [Suit::Clarify, Suit::Repair, Suit::Boundary, Suit::Change] .into_iter() .flat_map(|suit| std::iter::repeat_n(SolutionCard { suit }, 6)) .collect() }