diff --git a/Cargo.lock b/Cargo.lock index 9ddd539..c539a17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -521,11 +521,14 @@ dependencies = [ name = "tidepool" version = "0.1.0" dependencies = [ + "anyhow", + "clap", "fish", "mino", "rand", "rand_xoshiro", "serde", + "serde_json", "toml", "tracing", "tracing-subscriber", diff --git a/tidepool/Cargo.toml b/tidepool/Cargo.toml index b2075b1..263230c 100644 --- a/tidepool/Cargo.toml +++ b/tidepool/Cargo.toml @@ -17,6 +17,6 @@ tracing = { version = "0.1" } # cli tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"]} toml = { version = "0.7" } - -# anyhow = { version = "1.0" } -# clap = { version = "4.0", features = ["derive"] } +serde_json = { version = "1.0" } +clap = { version = "4.0", features = ["derive"] } +anyhow = { version = "1.0" } diff --git a/tidepool/src/cli.rs b/tidepool/src/cli.rs new file mode 100644 index 0000000..764566b --- /dev/null +++ b/tidepool/src/cli.rs @@ -0,0 +1,112 @@ +use clap::{Args, Parser}; +use std::path::PathBuf; + +/// Tidepool is a program that simulates cheese race games for Blockfish to play and +/// outputs the result in a machine-readable format. Tidepool is useful for benchmarking, +/// analyzing, testing reproducibility, and other tasks for improving Blockfish's +/// algorithms. +#[derive(Parser)] +#[clap(version)] +#[command(group = clap::ArgGroup::new("single").conflicts_with("multi"))] +#[command(group = clap::ArgGroup::new("multi").conflicts_with("single"))] +pub struct CliArgs { + /// Path to file containing configuration for running Blockfish. + pub config_file: PathBuf, + + // single-run + /// Specify the game seed for the run (single-run mode only). + #[arg(short = 's', long, group = "single")] + pub seed: Option, + /// Output the run to the specified file (single-run mode only). + #[arg(short = 'o', long = "outfile", group = "single")] + pub output_file: Option, + + // multi-run + /// Path to directory to generate runs to. If set, activates multi-run mode and will + /// run multiple times in parallel. + #[arg(group = "multi")] + pub output_dir: Option, + /// Stop after this many runs (multi-run mode only). + #[arg(short = 'n', long, group = "multi")] + pub count: Option, + /// Run this many jobs in parallel (multi-run mode only). + #[arg(short = 'j', long, group = "multi")] + pub jobs: Option, + + // global + #[command(flatten)] + pub data: OutputDataArgs, + #[command(flatten)] + pub io: IoArgs, +} + +#[derive(Debug, Args)] +pub struct OutputDataArgs { + /// Generate profiling data in the output. + #[arg(short = 'P', long)] + pub profile: bool, + /// Generate the full list of moves in the output. + #[arg(short = 'L', long)] + pub list_moves: bool, +} + +#[derive(Debug, Args)] +pub struct IoArgs { + /// Disable printing extraneous text like a progress bar. + #[arg(short = 'q', long)] + pub quiet: bool, + /// Never prompt the user for input, and immediately exit on interrupt. + #[arg(short = 'f', long)] + pub noninteractive: bool, +} + +#[derive(Debug)] +pub enum Mode { + Single(SingleRun), + Multi(MultiRun), +} + +#[derive(Debug)] +pub struct SingleRun { + pub config_file: PathBuf, + pub output_file: Option, + pub seed: Option, + pub data: OutputDataArgs, + pub io: IoArgs, +} + +#[derive(Debug)] +pub struct MultiRun { + pub config_file: PathBuf, + pub output_dir: PathBuf, + pub count: Option, + pub jobs: Option, + pub data: OutputDataArgs, + pub io: IoArgs, +} + +impl From for Mode { + fn from(args: CliArgs) -> Self { + match args.output_dir { + Some(output_dir) => Mode::Multi(MultiRun { + config_file: args.config_file, + output_dir, + count: args.count, + jobs: args.jobs, + data: args.data, + io: args.io, + }), + None => Mode::Single(SingleRun { + config_file: args.config_file, + output_file: args.output_file, + seed: args.seed, + data: args.data, + io: args.io, + }), + } + } +} + +pub fn parse() -> Mode { + CliArgs::parse().into() +} diff --git a/tidepool/src/lib.rs b/tidepool/src/lib.rs index a399e35..b15b265 100644 --- a/tidepool/src/lib.rs +++ b/tidepool/src/lib.rs @@ -1,3 +1,4 @@ +pub mod cli; pub mod config; pub mod garbage; pub mod queue; diff --git a/tidepool/src/main.rs b/tidepool/src/main.rs index d1c5c07..fc30621 100644 --- a/tidepool/src/main.rs +++ b/tidepool/src/main.rs @@ -1,92 +1,79 @@ -use fish::bot::Bot; -use mino::srs::Queue; +use anyhow::{Context as _, Result}; use rand::RngCore as _; -use std::io::Read as _; -use tidepool::{config, sim}; +use std::io::{Read, Write}; +use std::path::Path; -pub fn main() -> std::io::Result<()> { +use tidepool::{cli, config}; + +fn main() -> Result<()> { tracing_subscriber::fmt::fmt() .with_writer(std::io::stderr) .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) .init(); - let seed = rand::thread_rng().next_u64(); - println!("using seed: {seed}"); + match cli::parse() { + cli::Mode::Single(args) => single(args), - let mut config_file = match std::env::args().nth(1) { - Some(f) => std::fs::File::open(f)?, - None => { - eprintln!("usage: tidepool "); - return Ok(()); - } - }; - - let mut config = String::new(); - config_file.read_to_string(&mut config)?; - - let config = toml::from_str::(&config).unwrap(); - - let 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, opts); - - let mut ds = 0; - let mut ps = 0; - - loop { - let (hold, next) = sim.queue(); - let hold_str = hold.map_or("", |p| p.name()); - let next_str = next.iter().map(|p| p.as_char()).collect::(); - println!("Q: [{hold_str}]{next_str}"); - - let mat = sim.matrix(); - for y in (0..mat.rows() + 1).rev() { - print!("{y:2} |"); - for x in 0..10 { - print!("{}", if mat.get(x, y) { '#' } else { '.' }); - } - println!(); - } - - let queue = Queue::new(hold, &next); - let mut bot = Bot::new(mat, queue); - - for i in 0..config.bot.iters { - if i > 0 && i % 1000 == 0 { - tracing::debug!("iteration {i}"); - } - bot.think(); - } - - let best = match bot.suggest() { - Some(pc) => pc, - None => { - println!("no suggestion!"); - break; - } - }; - - let ll = sim.lines_left(); - sim.play(best); - - ds += ll - sim.lines_left(); - ps += 1; - print!("#{ps}, ds: {ds}"); - if ds > 0 { - print!(", ppd: {:.2}", ps as f64 / ds as f64); - } - println!(); - - if ll == 0 { - break; + cli::Mode::Multi(_args) => { + panic!("multi-run mode not implemented yet"); } } +} - println!(); - println!("pieces: {ps}"); +fn single(args: cli::SingleRun) -> Result<()> { + let config = parse_config_file(&args.config_file)?; + let seed = args.seed.unwrap_or_else(|| rand::thread_rng().next_u64()); + + // TODO: run a simulation with the bot + let _ = config; + let _ = seed; + let output = serde_json::json!({ + "seed": seed, + }); + + let writer: Box = match args.output_file { + Some(path) => { + if path.is_file() { + if prompt_yn(&args.io, "output file already exists, overwrite?") { + tracing::debug!("removing {path:?}"); + std::fs::remove_file(&path)?; + } else { + tracing::warn!("output file already exists, exiting."); + return Ok(()); + } + } + Box::new( + std::fs::File::create(&path) + .with_context(|| format!("error opening output file '{}'", path.display()))?, + ) + } + None => Box::new(std::io::stdout()), + }; + let mut writer = std::io::BufWriter::new(writer); + + serde_json::to_writer_pretty(&mut writer, &output).context("error writing output")?; + writeln!(&mut writer).context("error writing output")?; Ok(()) } + +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()))? + .read_to_string(&mut contents) + .context("error reading config file")?; + toml::from_str(&contents).context("error parsing config file") +} + +fn prompt_yn(io: &cli::IoArgs, msg: &str) -> bool { + if io.noninteractive { + return false; + } + let mut output = std::io::stdout().lock(); + write!(output, "{msg} [y/N] ").unwrap(); + output.flush().unwrap(); + let mut user_input = String::new(); + std::io::stdin().read_line(&mut user_input).unwrap(); + user_input.trim().eq_ignore_ascii_case("y") +}