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() CliArgs::parse().into()
} }

View File

@ -1,4 +1,6 @@
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use fish::bot::Bot;
use mino::queue::Queue;
use rand::RngCore as _; use rand::RngCore as _;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::{Read, Write}; use std::io::{Read, Write};
@ -6,9 +8,10 @@ use std::path::Path;
use std::sync::{atomic, mpsc, Arc}; use std::sync::{atomic, mpsc, Arc};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use fish::{bot, eval}; use tidepool::cli::{parse_cli, IoArgs, Mode, MultiRun, OutputDataArgs, SingleRun};
use tidepool::output::SummaryStats; use tidepool::config::Config;
use tidepool::{cli, config, output, sim}; use tidepool::output::{Move, Output, SummaryStats};
use tidepool::sim::{Simul, SimulOptions};
fn main() -> Result<()> { fn main() -> Result<()> {
tracing_subscriber::fmt::fmt() tracing_subscriber::fmt::fmt()
@ -16,19 +19,21 @@ fn main() -> Result<()> {
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
.init(); .init();
match cli::parse() { match parse_cli() {
cli::Mode::Single(args) => single(args), Mode::Single(args) => single(args),
cli::Mode::Multi(args) => multi(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)] #[derive(Debug)]
enum Msg { enum Msg {
Interrupt, Interrupt,
Heartbeat(usize), Heartbeat(usize),
Shutdown(usize), Shutdown(usize),
Status(usize, Box<Status>), Status(usize, Box<Status>),
Output(usize, Box<output::Output>), Output(usize, Box<Output>),
} }
#[derive(Debug)] #[derive(Debug)]
@ -55,7 +60,7 @@ fn create_mailbox() -> (mpsc::SyncSender<Msg>, mpsc::Receiver<Msg>) {
(tx, rx) (tx, rx)
} }
fn single(args: cli::SingleRun) -> Result<()> { fn single(args: SingleRun) -> Result<()> {
let config = parse_config_file(&args.config_file)?; let config = parse_config_file(&args.config_file)?;
let (tx, rx) = create_mailbox(); let (tx, rx) = create_mailbox();
@ -64,7 +69,7 @@ fn single(args: cli::SingleRun) -> Result<()> {
std::thread::spawn({ std::thread::spawn({
let config = config.clone(); let config = config.clone();
let exit_early = exit_early.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() { while let Ok(msg) = rx.recv() {
@ -84,13 +89,7 @@ fn single(args: cli::SingleRun) -> Result<()> {
} }
Msg::Status(_id, status) => { Msg::Status(_id, status) => {
if !exit_early.load(atomic::Ordering::Relaxed) { if !exit_early.load(atomic::Ordering::Relaxed) {
print_single_progress( print_single_progress(&args.io, &status, config.game.goal)?;
&args.io,
status.pieces,
status.lines_left,
config.game.goal,
status.time,
)?;
} }
} }
Msg::Output(_id, output) => { 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 // be sent so the ctrl-c handler will abort the program
std::mem::drop(rx); std::mem::drop(rx);
if !output.did_complete() { if !output.did_complete() && !prompt_yn(&args.io, "run did not finish, keep it?") {
if !prompt_yn(&args.io, "run did not finish, keep it?") { break;
break;
}
} }
write_output(&args.io, args.output_file.as_deref(), &output)?; write_output(&args.io, args.output_file.as_deref(), &output)?;
@ -114,7 +111,7 @@ fn single(args: cli::SingleRun) -> Result<()> {
Ok(()) Ok(())
} }
fn multi(args: cli::MultiRun) -> Result<()> { fn multi(args: MultiRun) -> Result<()> {
let config = parse_config_file(&args.config_file)?; let config = parse_config_file(&args.config_file)?;
let (tx, rx) = create_mailbox(); let (tx, rx) = create_mailbox();
@ -135,7 +132,7 @@ fn multi(args: cli::MultiRun) -> Result<()> {
let tx = tx.clone(); let tx = tx.clone();
let tasks = tasks.clone(); let tasks = tasks.clone();
let exit_early = exit_early.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(()) Ok(())
} }
fn parse_config_file(path: &Path) -> Result<config::Config> { fn parse_config_file(path: &Path) -> Result<Config> {
let mut contents = String::new(); let mut contents = String::new();
std::fs::File::open(path) std::fs::File::open(path)
.with_context(|| format!("error opening config file '{}'", path.display()))? .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") 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 { let mut writer: Box<dyn std::io::Write> = match &path {
Some(path) => { Some(path) => {
if path.is_file() { 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(); 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")?; 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 goal = output.config.goal;
let pieces = output.pieces; let pieces = output.pieces;
let incomplete = if !output.did_complete() { "I-" } else { "" }; 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 file_name = format!("{goal}L-{incomplete}{pieces}p-{date}.json");
let io_args = cli::IoArgs { let io_args = IoArgs {
// overwrite existing file
noninteractive: true, noninteractive: true,
// don't prompt about incomplete run
quiet: true, quiet: true,
}; };
let path = dir_path.join(file_name); let path = dir_path.join(file_name);
write_output(&io_args, Some(&path), output) write_output(&io_args, Some(&path), output)
} }
fn print_single_progress( fn print_single_progress(io_args: &IoArgs, status: &Status, goal: usize) -> std::io::Result<()> {
io_args: &cli::IoArgs,
pieces: usize,
lines_left: usize,
goal: usize,
time: Duration,
) -> std::io::Result<()> {
if io_args.quiet { if io_args.quiet {
return Ok(()); return Ok(());
} }
let width = 40; // TODO: get terminal size? let width = 40; // TODO: get terminal size?
let cleared = goal - lines_left; let pieces = status.pieces;
let pps = pieces as f64 / time.as_secs_f64().max(0.001); 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 pace = pieces * 100 / cleared.max(1);
let writer = std::io::stderr().lock(); let writer = std::io::stderr().lock();
@ -283,7 +277,7 @@ fn print_single_progress(
} }
fn print_multi_progress( fn print_multi_progress(
io_args: &cli::IoArgs, io_args: &IoArgs,
jobs: &HashMap<usize, Option<Box<Status>>>, jobs: &HashMap<usize, Option<Box<Status>>>,
completed: &SummaryStats<usize>, completed: &SummaryStats<usize>,
) -> std::io::Result<()> { ) -> 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); let pps = status.pieces as f64 / status.time.as_secs_f64().max(0.001);
pps_stats.insert_f64(pps); pps_stats.insert_f64(pps);
} }
if let Some(pps) = pps_stats.avg_f64() { if let Some(pps) = pps_stats.avg_f64() {
write!(writer, ", pps(avg):{pps:.2}")?; write!(writer, ", pps(avg):{pps:.2}")?;
} }
@ -320,7 +313,7 @@ fn print_multi_progress(
writer.flush() 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 { if io_args.noninteractive {
return true; return true;
} }
@ -333,24 +326,27 @@ fn prompt_yn(io_args: &cli::IoArgs, msg: &str) -> bool {
} }
fn run_simulation( fn run_simulation(
data_args: &cli::OutputDataArgs, data_args: &OutputDataArgs,
config: config::Config, config: &Config,
seed: Option<u64>, seed: Option<u64>,
id: usize, id: usize,
tx: &mpsc::SyncSender<Msg>, tx: &mpsc::SyncSender<Msg>,
exit_early: &atomic::AtomicBool, exit_early: &atomic::AtomicBool,
) { ) {
// create lists to collect move/metric data into
let mut moves = data_args.list_moves.then(Vec::new); let mut moves = data_args.list_moves.then(Vec::new);
let mut metrics = data_args.metrics.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 { // initialize the cheese race simulation
goal: config.game.goal, let seed = seed.unwrap_or_else(|| rand::thread_rng().next_u64());
garbage: config.game.rules.min..=config.game.rules.max, let mut sim = Simul::new(
previews: config.game.rules.previews, seed,
}; SimulOptions {
let mut sim = sim::Simul::new(seed, sim_opts); goal: config.game.goal,
garbage: config.game.rules.min..=config.game.rules.max,
previews: config.game.rules.previews,
},
);
let time_start = Instant::now(); let time_start = Instant::now();
@ -359,35 +355,39 @@ fn run_simulation(
break; break;
} }
let mut bot = bot::Bot::new( // initialize the bot for the current state
&weights, let mut bot = Bot::new(
&config.bot.weights,
sim.matrix(), sim.matrix(),
mino::Queue::new(sim.queue().hold(), sim.queue().next()), Queue::new(sim.queue().hold(), sim.queue().next()),
); );
if data_args.metrics { if data_args.metrics {
bot.start_instrumenting(); 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 { while bot.iterations() < config.bot.iters {
let gas = std::cmp::min(50_000, config.bot.iters - bot.iterations()); let gas = std::cmp::min(50_000, config.bot.iters - bot.iterations());
bot.think_for(gas); bot.think_for(gas);
if exit_early.load(atomic::Ordering::Relaxed) { // send a heartbeat back to the main thread to test if its still alive
break; if exit_early.load(atomic::Ordering::Relaxed) || tx.send(Msg::Heartbeat(id)).is_err() {
}
if tx.send(Msg::Heartbeat(id)).is_err() {
break; break;
} }
} }
let Some(placement) = bot.suggest() else { 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()); tracing::warn!("gave up :( pieces={}, id={id}", sim.pieces());
break; break;
}; };
sim.play(placement); sim.play(placement);
if let Some(moves) = moves.as_mut() { if let Some(moves) = moves.as_mut() {
moves.push(output::Move { placement }) moves.push(Move { placement })
} }
if let Some(metrics) = metrics.as_mut() { if let Some(metrics) = metrics.as_mut() {
metrics.push(bot.metrics().unwrap()); 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, seed,
cleared: config.game.goal - sim.lines_left(), config: config.game.clone(),
pieces: sim.pieces(), cleared,
config: config.game, pieces,
moves, moves,
metrics, metrics,
}; };
@ -417,8 +419,8 @@ fn run_simulation(
} }
fn run_simulations( fn run_simulations(
data_args: &cli::OutputDataArgs, data_args: &OutputDataArgs,
config: config::Config, config: &Config,
id: usize, id: usize,
tx: &mpsc::SyncSender<Msg>, tx: &mpsc::SyncSender<Msg>,
tasks: &atomic::AtomicI64, tasks: &atomic::AtomicI64,
@ -429,7 +431,7 @@ fn run_simulations(
break; 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() { 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 rand::{Rng as _, SeedableRng as _};
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
pub struct Options { pub struct SimulOptions {
/// Total number of garbage lines required to clear. /// Total number of garbage lines required to clear.
pub goal: usize, pub goal: usize,
/// Min/max number of garbage lines on the matrix at a given time. /// Min/max number of garbage lines on the matrix at a given time.
@ -28,7 +28,7 @@ pub struct Simul {
impl Simul { impl Simul {
/// Constructs a new simulation with PRNG seeded by the given values, and given game /// Constructs a new simulation with PRNG seeded by the given values, and given game
/// configuration. /// 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 rng1 = Rng::seed_from_u64(seed);
let rng2 = rng1.clone(); let rng2 = rng1.clone();
@ -443,7 +443,7 @@ mod tests {
fn test_deterministic() { fn test_deterministic() {
let sim = Simul::new( let sim = Simul::new(
TEST_SEED, TEST_SEED,
Options { SimulOptions {
goal: 100, goal: 100,
garbage: 0..=9, garbage: 0..=9,
previews: 7, previews: 7,