tidepool: run a simulation and generate JSON output

This commit is contained in:
tali 2023-04-12 15:07:32 -04:00
parent 8461fb8379
commit 501c333048
3 changed files with 179 additions and 18 deletions

View File

@ -1,5 +1,6 @@
pub mod cli;
pub mod config;
pub mod garbage;
pub mod output;
pub mod queue;
pub mod sim;

View File

@ -3,7 +3,8 @@ use rand::RngCore as _;
use std::io::{Read, Write};
use std::path::Path;
use tidepool::{cli, config};
use fish::bot;
use tidepool::{cli, config, output, sim};
fn main() -> Result<()> {
tracing_subscriber::fmt::fmt()
@ -13,50 +14,50 @@ fn main() -> Result<()> {
match cli::parse() {
cli::Mode::Single(args) => single(args),
cli::Mode::Multi(_args) => {
panic!("multi-run mode not implemented yet");
}
cli::Mode::Multi(args) => multi(args),
}
}
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 output = simulation(&args.io, &args.data, args.seed, config);
let writer: Box<dyn std::io::Write> = match args.output_file {
let mut 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)?;
std::fs::remove_file(path)?;
} else {
tracing::warn!("output file already exists, exiting.");
return Ok(());
}
}
Box::new(
std::fs::File::create(&path)
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")?;
let pretty = args.output_file.is_none() && !args.io.quiet;
if pretty {
serde_json::to_writer_pretty(&mut writer, &output).context("error writing output")?;
writeln!(&mut writer).context("error writing output")?;
} else {
serde_json::to_writer(&mut writer, &output).context("error writing output")?;
}
Ok(())
}
fn multi(args: cli::MultiRun) -> Result<()> {
let _config = parse_config_file(&args.config_file)?;
panic!("multi-run mode not implemented yet")
}
fn parse_config_file(path: &Path) -> Result<config::Config> {
let mut contents = String::new();
std::fs::File::open(path)
@ -77,3 +78,68 @@ fn prompt_yn(io: &cli::IoArgs, msg: &str) -> bool {
std::io::stdin().read_line(&mut user_input).unwrap();
user_input.trim().eq_ignore_ascii_case("y")
}
fn simulation(
io: &cli::IoArgs,
data: &cli::OutputDataArgs,
seed: Option<u64>,
config: config::Config,
) -> output::Output {
// TODO: status signals
let mut moves = data.list_moves.then(Vec::new);
let seed = seed.unwrap_or_else(|| rand::thread_rng().next_u64());
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);
let mut pieces = 0;
let time_start = std::time::Instant::now();
while sim.lines_left() > 0 {
// TODO: progress bar
if !io.quiet {
eprintln!("#{}, {} left", pieces, sim.lines_left());
}
let (hold, next) = sim.queue();
let queue = mino::Queue::new(hold, &next);
let mut bot = bot::Bot::new(sim.matrix(), queue);
for i in 0..config.bot.iters {
bot.think();
if i % 1000 == 999 {
tracing::debug!("iteration {i}");
}
}
if let Some(placement) = bot.suggest() {
sim.play(placement);
pieces += 1;
if let Some(moves) = moves.as_mut() {
moves.push(output::Move {
location: placement.into(),
})
}
} else {
break;
}
}
let time_end = std::time::Instant::now();
let profile = data.profile.then(|| output::Profile {
time: time_end - time_start,
});
output::Output {
seed,
cleared: config.game.goal - sim.lines_left(),
pieces,
config: config.game,
moves,
profile,
}
}

94
tidepool/src/output.rs Normal file
View File

@ -0,0 +1,94 @@
use mino::srs::PieceType;
use serde::Serialize;
use std::time::Duration;
use crate::config::GameConfig;
#[derive(Serialize, Debug)]
pub struct Output {
#[serde(serialize_with = "ser::seed")]
pub seed: u64,
pub config: GameConfig,
pub pieces: usize,
pub cleared: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub moves: Option<Vec<Move>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<Profile>,
}
#[derive(Serialize, Debug)]
pub struct Move {
pub location: Location,
// TODO: garbage added
}
#[derive(Serialize, Debug)]
pub struct Location {
#[serde(rename = "type", serialize_with = "ser::piece_type")]
pub ty: PieceType,
pub x: i16,
pub y: i16,
pub orientation: Orientation,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum Orientation {
North,
East,
South,
West,
}
impl From<mino::srs::Piece> for Location {
fn from(pc: mino::srs::Piece) -> Self {
Self {
ty: pc.ty,
x: pc.loc.x,
y: pc.loc.y,
orientation: match pc.loc.r {
mino::Rot::N => Orientation::North,
mino::Rot::E => Orientation::East,
mino::Rot::S => Orientation::South,
mino::Rot::W => Orientation::West,
},
}
}
}
#[derive(Serialize, Debug)]
pub struct Profile {
#[serde(serialize_with = "ser::seconds")]
pub time: Duration,
// TODO: pps mean/variance
// TODO: timing stats for specific functions/routines
// TODO: stats info about top <N> evaluations
// TODO: histograms about selected nodes
}
mod ser {
use super::*;
pub fn seed<S>(seed: &u64, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
// :')
seed.to_string().serialize(serializer)
}
pub fn piece_type<S>(ty: &PieceType, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
ty.name().serialize(serializer)
}
pub fn seconds<S>(dur: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
dur.as_secs_f64().serialize(serializer)
}
}