tidepool::main spring cleaning
This commit is contained in:
parent
4ad8f38341
commit
0ab1cf2200
|
@ -107,6 +107,6 @@ impl From<CliArgs> for Mode {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn parse() -> Mode {
|
||||
pub fn parse_cli() -> Mode {
|
||||
CliArgs::parse().into()
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue