wip: tidepool cli app

This commit is contained in:
tali 2023-04-11 22:39:30 -04:00
parent 14024f63ff
commit 2910e405be
5 changed files with 184 additions and 81 deletions

3
Cargo.lock generated
View File

@ -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",

View File

@ -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" }

112
tidepool/src/cli.rs Normal file
View File

@ -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<u64>,
/// Output the run to the specified file (single-run mode only).
#[arg(short = 'o', long = "outfile", group = "single")]
pub output_file: Option<PathBuf>,
// 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<PathBuf>,
/// Stop after this many runs (multi-run mode only).
#[arg(short = 'n', long, group = "multi")]
pub count: Option<usize>,
/// Run this many jobs in parallel (multi-run mode only).
#[arg(short = 'j', long, group = "multi")]
pub jobs: Option<usize>,
// 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<PathBuf>,
pub seed: Option<u64>,
pub data: OutputDataArgs,
pub io: IoArgs,
}
#[derive(Debug)]
pub struct MultiRun {
pub config_file: PathBuf,
pub output_dir: PathBuf,
pub count: Option<usize>,
pub jobs: Option<usize>,
pub data: OutputDataArgs,
pub io: IoArgs,
}
impl From<CliArgs> 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()
}

View File

@ -1,3 +1,4 @@
pub mod cli;
pub mod config;
pub mod garbage;
pub mod queue;

View File

@ -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 <CONFIG_FILE>");
return Ok(());
}
};
let mut config = String::new();
config_file.read_to_string(&mut config)?;
let config = toml::from_str::<config::Config>(&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::<String>();
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<dyn std::io::Write> = 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<config::Config> {
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")
}