tidepool::main spring cleaning

This commit is contained in:
tali 2023-04-16 19:43:06 -04:00
parent 4ad8f38341
commit 0ab1cf2200
3 changed files with 70 additions and 68 deletions

View File

@ -107,6 +107,6 @@ impl From<CliArgs> for Mode {
}
}
pub fn parse() -> Mode {
pub fn parse_cli() -> Mode {
CliArgs::parse().into()
}

View File

@ -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<Status>),
Output(usize, Box<output::Output>),
Output(usize, Box<Output>),
}
#[derive(Debug)]
@ -55,7 +60,7 @@ fn create_mailbox() -> (mpsc::SyncSender<Msg>, mpsc::Receiver<Msg>) {
(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<config::Config> {
fn parse_config_file(path: &Path) -> Result<Config> {
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<config::Config> {
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<dyn std::io::Write> = 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<chrono::Local> = 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<usize, Option<Box<Status>>>,
completed: &SummaryStats<usize>,
) -> 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<u64>,
id: usize,
tx: &mpsc::SyncSender<Msg>,
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<Msg>,
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() {

View File

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