use indicatif::{ProgressBar, ProgressStyle};
use pinch_points::sim::{
DEFAULT_NODE_BUDGET, Effort, Level, SolveOutcome, campaign_levels, solve_with,
};
use std::sync::Mutex;
const TUTORIAL: &str = "Welcome Ashore";
fn complaint(index: usize, level: &Level) -> Option<String> {
let n = index + 1;
match solve_with(level, Effort::Budget(DEFAULT_NODE_BUDGET)) {
SolveOutcome::Found(cheapest) if cheapest.len() == level.posts as usize => None,
SolveOutcome::Found(cheapest) => Some(format!(
"level {n} {:?} grants {} posts but falls to {}: {cheapest:?}",
level.name,
level.posts,
cheapest.len(),
)),
SolveOutcome::Unsolvable => Some(format!(
"level {n} {:?}: no signpost set within its inventory beats it",
level.name
)),
SolveOutcome::GaveUp => Some(format!(
"level {n} {:?} no longer fits DEFAULT_NODE_BUDGET \
({DEFAULT_NODE_BUDGET} simulations), so the editor would give up \
on a shipped level",
level.name
)),
}
}
fn lane_count(args: &[String]) -> usize {
let every = std::thread::available_parallelism().map_or(1, |n| n.get());
let mut rest = args.iter();
while let Some(arg) = rest.next() {
let asked = arg
.strip_prefix("--lanes=")
.map(str::to_string)
.or_else(|| (arg == "--lanes").then(|| rest.next().cloned()).flatten());
if let Some(n) = asked {
return match n.parse::<usize>() {
Ok(0) | Err(_) => every,
Ok(n) => n.min(every),
};
}
}
every
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let lanes = lane_count(&args);
let levels = campaign_levels();
let work: Vec<(usize, &Level)> = levels
.iter()
.enumerate()
.filter(|(_, level)| level.name != TUTORIAL && level.posts >= 2)
.collect();
let total = work.len() as u64;
let skipped = levels.len() - work.len();
println!(
"verifying {total} of {} campaign levels across {lanes} lane(s)",
levels.len()
);
println!(
" ({skipped} skipped: the tutorial, and every 0- and 1-post level, \
are covered by the fast tests)"
);
let bar = ProgressBar::new(total);
bar.set_style(
ProgressStyle::with_template(
"{spinner} [{elapsed_precise}] {bar:40} {pos}/{len} levels ETA {eta}",
)
.expect("progress template")
.progress_chars("=> "),
);
let complaints: Mutex<Vec<String>> = Mutex::new(Vec::new());
std::thread::scope(|scope| {
for lane in 0..lanes {
let mine: Vec<_> = work.iter().skip(lane).step_by(lanes).copied().collect();
let complaints = &complaints;
let bar = &bar;
scope.spawn(move || {
for (i, level) in mine {
if let Some(line) = complaint(i, level) {
let mut out = complaints.lock().expect("complaint lock");
bar.suspend(|| eprintln!("FAIL {line}"));
out.push(line);
}
bar.inc(1);
}
});
}
});
bar.finish_and_clear();
let mut complaints = complaints.into_inner().expect("complaint lock");
complaints.sort();
if complaints.is_empty() {
println!("all {total} levels grant exactly the posts they need");
} else {
eprintln!(
"\n{} level(s) grant a post they do not need:",
complaints.len()
);
for line in &complaints {
eprintln!(" {line}");
}
std::process::exit(1);
}
}