diff --git a/tidepool/src/cli.rs b/tidepool/src/cli.rs index 63f5a6a..cb50901 100644 --- a/tidepool/src/cli.rs +++ b/tidepool/src/cli.rs @@ -107,6 +107,6 @@ impl From for Mode { } } -pub fn parse() -> Mode { +pub fn parse_cli() -> Mode { CliArgs::parse().into() } diff --git a/tidepool/src/main.rs b/tidepool/src/main.rs index 91bfca4..30e320a 100644 --- a/tidepool/src/main.rs +++ b/tidepool/src/main.rs @@ -1,4 +1,6 @@ use anyhow::{Context as _, Result}; +use fish::bot::Bot; +use mino::queue::Queue; use rand::RngCore as _; use std::collections::HashMap; use std::io::{Read, Write}; @@ -6,9 +8,10 @@ use std::path::Path; use std::sync::{atomic, mpsc, Arc}; use std::time::{Duration, Instant}; -use fish::{bot, eval}; -use tidepool::output::SummaryStats; -use tidepool::{cli, config, output, sim}; +use tidepool::cli::{parse_cli, IoArgs, Mode, MultiRun, OutputDataArgs, SingleRun}; +use tidepool::config::Config; +use tidepool::output::{Move, Output, SummaryStats}; +use tidepool::sim::{Simul, SimulOptions}; fn main() -> Result<()> { tracing_subscriber::fmt::fmt() @@ -16,19 +19,21 @@ fn main() -> Result<()> { .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) .init(); - match cli::parse() { - cli::Mode::Single(args) => single(args), - cli::Mode::Multi(args) => multi(args), + match parse_cli() { + Mode::Single(args) => single(args), + Mode::Multi(args) => multi(args), } } +// syncronize multiple threads by having them all send messages of this type to a common +// channel, which receives and process those messages sequentially on the main thread #[derive(Debug)] enum Msg { Interrupt, Heartbeat(usize), Shutdown(usize), Status(usize, Box), - Output(usize, Box), + Output(usize, Box), } #[derive(Debug)] @@ -55,7 +60,7 @@ fn create_mailbox() -> (mpsc::SyncSender, mpsc::Receiver) { (tx, rx) } -fn single(args: cli::SingleRun) -> Result<()> { +fn single(args: SingleRun) -> Result<()> { let config = parse_config_file(&args.config_file)?; let (tx, rx) = create_mailbox(); @@ -64,7 +69,7 @@ fn single(args: cli::SingleRun) -> Result<()> { std::thread::spawn({ let config = config.clone(); let exit_early = exit_early.clone(); - move || run_simulation(&args.data, config, args.seed, 0, &tx, &exit_early) + move || run_simulation(&args.data, &config, args.seed, 0, &tx, &exit_early) }); while let Ok(msg) = rx.recv() { @@ -84,13 +89,7 @@ fn single(args: cli::SingleRun) -> Result<()> { } Msg::Status(_id, status) => { if !exit_early.load(atomic::Ordering::Relaxed) { - print_single_progress( - &args.io, - status.pieces, - status.lines_left, - config.game.goal, - status.time, - )?; + print_single_progress(&args.io, &status, config.game.goal)?; } } Msg::Output(_id, output) => { @@ -99,10 +98,8 @@ fn single(args: cli::SingleRun) -> Result<()> { // be sent so the ctrl-c handler will abort the program std::mem::drop(rx); - if !output.did_complete() { - if !prompt_yn(&args.io, "run did not finish, keep it?") { - break; - } + if !output.did_complete() && !prompt_yn(&args.io, "run did not finish, keep it?") { + break; } write_output(&args.io, args.output_file.as_deref(), &output)?; @@ -114,7 +111,7 @@ fn single(args: cli::SingleRun) -> Result<()> { Ok(()) } -fn multi(args: cli::MultiRun) -> Result<()> { +fn multi(args: MultiRun) -> Result<()> { let config = parse_config_file(&args.config_file)?; let (tx, rx) = create_mailbox(); @@ -135,7 +132,7 @@ fn multi(args: cli::MultiRun) -> Result<()> { let tx = tx.clone(); let tasks = tasks.clone(); let exit_early = exit_early.clone(); - move || run_simulations(&data_args, config, id, &tx, &tasks, &exit_early) + move || run_simulations(&data_args, &config, id, &tx, &tasks, &exit_early) }); } @@ -185,7 +182,7 @@ fn multi(args: cli::MultiRun) -> Result<()> { Ok(()) } -fn parse_config_file(path: &Path) -> Result { +fn parse_config_file(path: &Path) -> Result { let mut contents = String::new(); std::fs::File::open(path) .with_context(|| format!("error opening config file '{}'", path.display()))? @@ -194,7 +191,7 @@ fn parse_config_file(path: &Path) -> Result { toml::from_str(&contents).context("error parsing config file") } -fn write_output(io_args: &cli::IoArgs, path: Option<&Path>, output: &output::Output) -> Result<()> { +fn write_output(io_args: &IoArgs, path: Option<&Path>, output: &Output) -> Result<()> { let mut writer: Box = match &path { Some(path) => { if path.is_file() { @@ -224,7 +221,7 @@ fn write_output(io_args: &cli::IoArgs, path: Option<&Path>, output: &output::Out } } -fn write_output_in_dir(dir_path: &Path, output: &output::Output) -> Result<()> { +fn write_output_in_dir(dir_path: &Path, output: &Output) -> Result<()> { let date: chrono::DateTime = std::time::SystemTime::now().into(); std::fs::create_dir_all(dir_path).context("error creating directory to store output")?; @@ -232,31 +229,28 @@ fn write_output_in_dir(dir_path: &Path, output: &output::Output) -> Result<()> { let goal = output.config.goal; let pieces = output.pieces; let incomplete = if !output.did_complete() { "I-" } else { "" }; - let date = date.format("%Y-%m-%d_%H-%M-%S"); // "YYYYmmdd-HHMMSS"; + let date = date.format("%Y-%m-%d_%H-%M-%S"); let file_name = format!("{goal}L-{incomplete}{pieces}p-{date}.json"); - let io_args = cli::IoArgs { + let io_args = IoArgs { + // overwrite existing file noninteractive: true, + // don't prompt about incomplete run quiet: true, }; let path = dir_path.join(file_name); write_output(&io_args, Some(&path), output) } -fn print_single_progress( - io_args: &cli::IoArgs, - pieces: usize, - lines_left: usize, - goal: usize, - time: Duration, -) -> std::io::Result<()> { +fn print_single_progress(io_args: &IoArgs, status: &Status, goal: usize) -> std::io::Result<()> { if io_args.quiet { return Ok(()); } let width = 40; // TODO: get terminal size? - let cleared = goal - lines_left; - let pps = pieces as f64 / time.as_secs_f64().max(0.001); + let pieces = status.pieces; + let cleared = goal - status.lines_left; + let pps = pieces as f64 / status.time.as_secs_f64().max(0.001); let pace = pieces * 100 / cleared.max(1); let writer = std::io::stderr().lock(); @@ -283,7 +277,7 @@ fn print_single_progress( } fn print_multi_progress( - io_args: &cli::IoArgs, + io_args: &IoArgs, jobs: &HashMap>>, completed: &SummaryStats, ) -> std::io::Result<()> { @@ -300,7 +294,6 @@ fn print_multi_progress( let pps = status.pieces as f64 / status.time.as_secs_f64().max(0.001); pps_stats.insert_f64(pps); } - if let Some(pps) = pps_stats.avg_f64() { write!(writer, ", pps(avg):{pps:.2}")?; } @@ -320,7 +313,7 @@ fn print_multi_progress( writer.flush() } -fn prompt_yn(io_args: &cli::IoArgs, msg: &str) -> bool { +fn prompt_yn(io_args: &IoArgs, msg: &str) -> bool { if io_args.noninteractive { return true; } @@ -333,24 +326,27 @@ fn prompt_yn(io_args: &cli::IoArgs, msg: &str) -> bool { } fn run_simulation( - data_args: &cli::OutputDataArgs, - config: config::Config, + data_args: &OutputDataArgs, + config: &Config, seed: Option, id: usize, tx: &mpsc::SyncSender, exit_early: &atomic::AtomicBool, ) { + // create lists to collect move/metric data into let mut moves = data_args.list_moves.then(Vec::new); let mut metrics = data_args.metrics.then(Vec::new); - let seed = seed.unwrap_or_else(|| rand::thread_rng().next_u64()); - let weights = eval::Weights::default(); - let sim_opts = sim::Options { - goal: config.game.goal, - garbage: config.game.rules.min..=config.game.rules.max, - previews: config.game.rules.previews, - }; - let mut sim = sim::Simul::new(seed, sim_opts); + // initialize the cheese race simulation + let seed = seed.unwrap_or_else(|| rand::thread_rng().next_u64()); + let mut sim = Simul::new( + seed, + SimulOptions { + goal: config.game.goal, + garbage: config.game.rules.min..=config.game.rules.max, + previews: config.game.rules.previews, + }, + ); let time_start = Instant::now(); @@ -359,35 +355,39 @@ fn run_simulation( break; } - let mut bot = bot::Bot::new( - &weights, + // initialize the bot for the current state + let mut bot = Bot::new( + &config.bot.weights, sim.matrix(), - mino::Queue::new(sim.queue().hold(), sim.queue().next()), + Queue::new(sim.queue().hold(), sim.queue().next()), ); if data_args.metrics { bot.start_instrumenting(); } + // run the bot for a while; this could have been just `bot.think_for(iters)` but + // we limit the gas argument to prevent the thread from hanging too long while + // thinking. while bot.iterations() < config.bot.iters { let gas = std::cmp::min(50_000, config.bot.iters - bot.iterations()); bot.think_for(gas); - if exit_early.load(atomic::Ordering::Relaxed) { - break; - } - if tx.send(Msg::Heartbeat(id)).is_err() { + // send a heartbeat back to the main thread to test if its still alive + if exit_early.load(atomic::Ordering::Relaxed) || tx.send(Msg::Heartbeat(id)).is_err() { break; } } let Some(placement) = bot.suggest() else { + // this should never fail unless something went wrong e.g. not enough iters, + // impossible board state, or buggy bot tracing::warn!("gave up :( pieces={}, id={id}", sim.pieces()); break; }; sim.play(placement); if let Some(moves) = moves.as_mut() { - moves.push(output::Move { placement }) + moves.push(Move { placement }) } if let Some(metrics) = metrics.as_mut() { metrics.push(bot.metrics().unwrap()); @@ -403,11 +403,13 @@ fn run_simulation( } } - let output = output::Output { + let pieces = sim.pieces(); + let cleared = config.game.goal - sim.lines_left(); + let output = Output { seed, - cleared: config.game.goal - sim.lines_left(), - pieces: sim.pieces(), - config: config.game, + config: config.game.clone(), + cleared, + pieces, moves, metrics, }; @@ -417,8 +419,8 @@ fn run_simulation( } fn run_simulations( - data_args: &cli::OutputDataArgs, - config: config::Config, + data_args: &OutputDataArgs, + config: &Config, id: usize, tx: &mpsc::SyncSender, tasks: &atomic::AtomicI64, @@ -429,7 +431,7 @@ fn run_simulations( break; } - run_simulation(data_args, config.clone(), None, id, tx, exit_early); + run_simulation(data_args, config, None, id, tx, exit_early); } if tx.send(Msg::Shutdown(id)).is_err() { diff --git a/tidepool/src/sim.rs b/tidepool/src/sim.rs index 54944c6..8a78a18 100644 --- a/tidepool/src/sim.rs +++ b/tidepool/src/sim.rs @@ -5,7 +5,7 @@ use mino::srs::{Piece, PieceType}; use rand::{Rng as _, SeedableRng as _}; use std::ops::RangeInclusive; -pub struct Options { +pub struct SimulOptions { /// Total number of garbage lines required to clear. pub goal: usize, /// Min/max number of garbage lines on the matrix at a given time. @@ -28,7 +28,7 @@ pub struct Simul { impl Simul { /// Constructs a new simulation with PRNG seeded by the given values, and given game /// configuration. - pub fn new(seed: u64, options: Options) -> Self { + pub fn new(seed: u64, options: SimulOptions) -> Self { let rng1 = Rng::seed_from_u64(seed); let rng2 = rng1.clone(); @@ -443,7 +443,7 @@ mod tests { fn test_deterministic() { let sim = Simul::new( TEST_SEED, - Options { + SimulOptions { goal: 100, garbage: 0..=9, previews: 7,