wip: tidepool cli app
This commit is contained in:
parent
14024f63ff
commit
2910e405be
|
@ -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",
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod garbage;
|
||||
pub mod queue;
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue