use indicatif::{ProgressBar, ProgressStyle};
use pinch_points::sim::{
DEFAULT_NODE_BUDGET, Effort, Level, PuzzleOutcome, SolveOutcome, solve_with,
};
fn unposted_outcome(level: &Level) -> (PuzzleOutcome, u64) {
let mut board = level.board();
loop {
board.tick_idle();
match level.outcome(&board) {
PuzzleOutcome::Running => {}
done @ (PuzzleOutcome::Won | PuzzleOutcome::Lost) => return (done, board.ticks()),
}
}
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let effort = match args.iter().any(|a| a == "--exhaustive") {
true => Effort::Exhaustive,
false => Effort::Budget(DEFAULT_NODE_BUDGET),
};
let mut asked: Option<usize> = None;
let mut paths: Vec<&String> = Vec::new();
let mut rest = args.iter();
while let Some(arg) = rest.next() {
if let Some(n) = arg.strip_prefix("--lanes=") {
asked = n.parse().ok();
} else if arg == "--lanes" {
asked = rest.next().and_then(|n| n.parse().ok());
} else if !arg.starts_with("--") {
paths.push(arg);
}
}
let cores = std::thread::available_parallelism().map_or(1, |n| n.get());
let lanes = match asked {
Some(0) => cores,
Some(n) => n.min(cores),
None => (cores / 2).max(1),
};
eprintln!("{} boards on {lanes} of {cores} cores", paths.len());
let bar = ProgressBar::new(paths.len() as u64);
bar.set_style(
ProgressStyle::with_template(
"{spinner} [{elapsed_precise}] {bar:40} {pos}/{len} boards ETA {eta}",
)
.expect("progress template")
.progress_chars("=> "),
);
std::thread::scope(|scope| {
for lane in 0..lanes {
let mine: Vec<&String> = paths.iter().copied().skip(lane).step_by(lanes).collect();
let bar = &bar;
scope.spawn(move || {
for path in mine {
let line = report(path, effort);
bar.suspend(|| {
use std::io::Write;
print!("{line}");
let _ = std::io::stdout().flush();
});
bar.inc(1);
}
});
}
});
bar.finish_and_clear();
}
fn report(path: &str, effort: Effort) -> String {
use std::fmt::Write;
let mut out = String::new();
let text = std::fs::read_to_string(path).expect("readable level file");
let level = match Level::parse(&text) {
Ok(level) => level,
Err(e) => {
let _ = writeln!(out, "{path}: PARSE ERROR: {e}");
return out;
}
};
match Level::parse(&level.to_text()) {
Ok(back) if back.board().state_hash() == level.board().state_hash() => {}
Ok(_) => {
let _ = writeln!(out, "{path}: ROUND TRIP: rebuilds a different board");
}
Err(e) => {
let _ = writeln!(out, "{path}: ROUND TRIP: will not parse back: {e}");
}
}
let (outcome, ticks) = unposted_outcome(&level);
let _ = writeln!(out, "{path}: unposted -> {outcome:?} after {ticks} ticks");
match solve_with(&level, effort) {
SolveOutcome::Found(placements) => {
let text: Vec<String> = placements
.iter()
.map(|(x, y, d)| format!("{x},{y} {d:?}"))
.collect();
let _ = writeln!(
out,
" solver ({} posts): {}",
placements.len(),
text.join(" | ")
);
}
SolveOutcome::Unsolvable => {
let _ = writeln!(out, " solver: NO SOLUTION within {} posts", level.posts);
}
SolveOutcome::GaveUp => {
let _ = writeln!(
out,
" solver: gave up at {DEFAULT_NODE_BUDGET} simulations, too big to ship"
);
}
}
out
}