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()
|
CliArgs::parse().into()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue