hanabi.rs/src/main.rs

297 lines
8.6 KiB
Rust
Raw Normal View History

2016-03-14 02:11:20 +01:00
extern crate getopts;
2016-03-06 10:35:19 +01:00
#[macro_use]
extern crate log;
2016-03-18 06:44:02 +01:00
extern crate crossbeam;
extern crate float_ord;
2023-01-20 02:31:32 +01:00
extern crate fnv;
extern crate rand;
2016-03-06 01:54:46 +01:00
mod game;
2023-01-20 02:31:32 +01:00
mod helpers;
mod simulator;
2016-04-04 09:26:42 +02:00
mod strategy;
mod strategies {
2016-03-13 10:05:05 +01:00
pub mod cheating;
2023-01-20 02:31:32 +01:00
pub mod examples;
Refactor out a "public information object" One important change is that now, when deciding which questions to ask, they can see the answer to the last question before asking the next one. Some design choices: - Questions now take a BoardState instead of an OwnedGameView. - When deciding which questions to ask (in ask_questions), we get an immutable public information object (representing the public information before any questions were asked), and a mutable HandInfo<CardPossibilityTable> that gets updated as we ask questions. That HandInfo<CardPossibilityTable> was copied instead of taken. - In ask_questions, we also get some &mut u32 representing "info_remaining" that gets updated for us. This will later allow for cases where "info_remaining" depends on the answers to previous questions. - Both get_hint_sum and update_from_hint_sum change the public information object. If you want to compute the hint sum but aren't sure if you actually want to give the hint, you'll have to clone the public information object! - Over time, in the code to decide on a move, we'll be able to build an increasingly complicated tree of "public information object operations" that will have to be matched exactly in the code to update on a move. In order to make this less scary, I moved most of the code into "decide_wrapped" and "update_wrapped". If the call to update_wrapped (for the player who just made the move) changes the public information object in different ways than the previous call to decide_wrapped, we detect this and panic. This commit should be purely refactoring; all changes to win-rates are due to bugs.
2019-03-04 17:24:24 +01:00
mod hat_helpers;
2016-03-27 19:47:58 +02:00
pub mod information;
}
2016-03-06 01:54:46 +01:00
2016-03-14 02:11:20 +01:00
use getopts::Options;
use std::str::FromStr;
2016-03-06 10:35:19 +01:00
struct SimpleLogger;
impl log::Log for SimpleLogger {
fn enabled(&self, metadata: &log::LogMetadata) -> bool {
2016-03-14 02:11:20 +01:00
metadata.level() <= log::LogLevel::Trace
2016-03-06 10:35:19 +01:00
}
fn log(&self, record: &log::LogRecord) {
if self.enabled(record.metadata()) {
println!("{} - {}", record.level(), record.args());
}
}
}
2016-03-14 02:11:20 +01:00
fn print_usage(program: &str, opts: Options) {
print!("{}", opts.usage(&format!("Usage: {} [options]", program)));
}
2016-03-06 01:54:46 +01:00
fn main() {
2016-03-14 02:11:20 +01:00
let args: Vec<String> = std::env::args().collect();
let program = args[0].clone();
let mut opts = Options::new();
2023-01-20 02:31:32 +01:00
opts.optopt(
"l",
"loglevel",
"Log level, one of 'trace', 'debug', 'info', 'warn', and 'error'",
"LOGLEVEL",
);
opts.optopt(
"n",
"ntrials",
"Number of games to simulate (default 1)",
"NTRIALS",
);
opts.optopt(
"o",
"output",
"Number of games after which to print an update",
"OUTPUT_FREQ",
);
opts.optopt(
"t",
"nthreads",
"Number of threads to use for simulation (default 1)",
"NTHREADS",
);
opts.optopt("s", "seed", "Seed for PRNG (default random)", "SEED");
opts.optopt("p", "nplayers", "Number of players", "NPLAYERS");
opts.optopt(
"g",
"strategy",
"Which strategy to use. One of 'random', 'cheat', and 'info'",
"STRATEGY",
);
opts.optflag("h", "help", "Print this help menu");
opts.optflag(
"",
"results-table",
"Print a table of results for each strategy",
);
opts.optflag(
"",
"write-results-table",
"Update the results table in README.md",
);
2016-03-14 02:11:20 +01:00
let matches = match opts.parse(&args[1..]) {
2023-01-20 02:31:32 +01:00
Ok(m) => m,
2016-03-14 02:11:20 +01:00
Err(f) => {
print_usage(&program, opts);
Compile on rust 1.61, resolve warnings, fix some lints (#12) * Fix getopts version The pinned version does not compile anymore because of mutable aliasing: * https://github.com/rust-lang/getopts/pull/61 * https://github.com/rust-lang/getopts/issues/110 This was achieved by temporarily setting the getopts version to "0.2.21" in Cargo.toml, and running `cargo check`. Note that this also converts the Cargo.lock to a new format. * Fix warning: Instead of deprecated macro try!, use question mark operator * Fix warning: Avoid anonymous parameters * Fix warning: Use dyn on trait objects * Fix warning: Avoid unneeded mutability * Fix warning: Avoid redundant format in panic or assert * Fix lint: Avoid redundant field names in initializers * Fix lint: Avoid redundant clone * Fix lint: Avoid literal cast * Fix lint: Collapse if/else where applicable I left some if/else branches in place, if there was a certain symmetry between the branches. * Fix lint: Avoid needless borrow I left some if/else branches in place, if there was a certain symmetry between the branches. * Fix lint: Use cloned instead of custom closure * Fix lint: Avoid unneeded trait bound * Fix lint: Avoid unneeded trait bound (2) avoid redundant clone * Fix lint: Use &[T] instead of &Vec<T> * Fix lint: Avoid & on each pattern * Fix lint: Avoid manual assign * Fix lint: Use implicit return * Fix lint: Merge if/else branches with same value I left one complicated branch in place. * Fix lint: Use is_empty instead of comparing len against 0 * Fix lint: Use sum instead of fold * Fix lint: Avoid clone on Copy types
2022-07-05 20:05:13 +02:00
panic!("{}", f)
2016-03-14 02:11:20 +01:00
}
};
if matches.opt_present("h") {
return print_usage(&program, opts);
}
if !matches.free.is_empty() {
return print_usage(&program, opts);
}
if matches.opt_present("write-results-table") {
return write_results_table();
}
if matches.opt_present("results-table") {
return print!("{}", get_results_table());
}
2016-03-14 02:11:20 +01:00
2023-01-20 02:31:32 +01:00
let log_level_str: &str = &matches.opt_str("l").unwrap_or("info".to_string());
2016-03-14 02:11:20 +01:00
let log_level = match log_level_str {
2023-01-20 02:31:32 +01:00
"trace" => log::LogLevelFilter::Trace,
"debug" => log::LogLevelFilter::Debug,
"info" => log::LogLevelFilter::Info,
"warn" => log::LogLevelFilter::Warn,
"error" => log::LogLevelFilter::Error,
_ => {
2016-03-19 07:34:07 +01:00
print_usage(&program, opts);
panic!("Unexpected log level argument {}", log_level_str);
}
2016-03-14 02:11:20 +01:00
};
2016-03-13 21:50:38 +01:00
2016-03-06 10:35:19 +01:00
log::set_logger(|max_log_level| {
2016-03-14 02:11:20 +01:00
max_log_level.set(log_level);
2016-03-06 10:35:19 +01:00
Box::new(SimpleLogger)
2023-01-20 02:31:32 +01:00
})
.unwrap();
2016-03-06 10:35:19 +01:00
let n_trials = u32::from_str(&matches.opt_str("n").unwrap_or("1".to_string())).unwrap();
2023-01-20 02:31:32 +01:00
let seed = matches
.opt_str("s")
.map(|seed_str| u32::from_str(&seed_str).unwrap());
let progress_info = matches
.opt_str("o")
.map(|freq_str| u32::from_str(&freq_str).unwrap());
2016-03-18 06:44:02 +01:00
let n_threads = u32::from_str(&matches.opt_str("t").unwrap_or("1".to_string())).unwrap();
2016-03-19 07:34:07 +01:00
let n_players = u32::from_str(&matches.opt_str("p").unwrap_or("4".to_string())).unwrap();
2023-01-20 02:31:32 +01:00
let strategy_str: &str = &matches.opt_str("g").unwrap_or("cheat".to_string());
2023-01-20 02:31:32 +01:00
sim_games(
n_players,
strategy_str,
seed,
n_trials,
n_threads,
progress_info,
)
.info();
}
2023-01-20 02:31:32 +01:00
fn sim_games(
n_players: u32,
strategy_str: &str,
seed: Option<u32>,
n_trials: u32,
n_threads: u32,
progress_info: Option<u32>,
) -> simulator::SimResult {
2016-03-19 07:34:07 +01:00
let hand_size = match n_players {
2 => 5,
3 => 5,
4 => 4,
5 => 4,
2023-01-20 02:31:32 +01:00
_ => {
panic!("There should be 2 to 5 players, not {}", n_players);
}
2016-03-19 07:34:07 +01:00
};
2016-03-20 20:40:27 +01:00
let game_opts = game::GameOptions {
2016-03-19 07:34:07 +01:00
num_players: n_players,
Compile on rust 1.61, resolve warnings, fix some lints (#12) * Fix getopts version The pinned version does not compile anymore because of mutable aliasing: * https://github.com/rust-lang/getopts/pull/61 * https://github.com/rust-lang/getopts/issues/110 This was achieved by temporarily setting the getopts version to "0.2.21" in Cargo.toml, and running `cargo check`. Note that this also converts the Cargo.lock to a new format. * Fix warning: Instead of deprecated macro try!, use question mark operator * Fix warning: Avoid anonymous parameters * Fix warning: Use dyn on trait objects * Fix warning: Avoid unneeded mutability * Fix warning: Avoid redundant format in panic or assert * Fix lint: Avoid redundant field names in initializers * Fix lint: Avoid redundant clone * Fix lint: Avoid literal cast * Fix lint: Collapse if/else where applicable I left some if/else branches in place, if there was a certain symmetry between the branches. * Fix lint: Avoid needless borrow I left some if/else branches in place, if there was a certain symmetry between the branches. * Fix lint: Use cloned instead of custom closure * Fix lint: Avoid unneeded trait bound * Fix lint: Avoid unneeded trait bound (2) avoid redundant clone * Fix lint: Use &[T] instead of &Vec<T> * Fix lint: Avoid & on each pattern * Fix lint: Avoid manual assign * Fix lint: Use implicit return * Fix lint: Merge if/else branches with same value I left one complicated branch in place. * Fix lint: Use is_empty instead of comparing len against 0 * Fix lint: Use sum instead of fold * Fix lint: Avoid clone on Copy types
2022-07-05 20:05:13 +02:00
hand_size,
2016-03-06 07:49:40 +01:00
num_hints: 8,
num_lives: 3,
2016-03-19 07:34:07 +01:00
// hanabi rules are a bit ambiguous about whether you can give hints that match 0 cards
allow_empty_hints: false,
2016-03-06 07:49:40 +01:00
};
2016-03-14 02:11:20 +01:00
2023-01-20 02:31:32 +01:00
let strategy_config: Box<dyn strategy::GameStrategyConfig + Sync> = match strategy_str {
"random" => Box::new(strategies::examples::RandomStrategyConfig {
hint_probability: 0.4,
play_probability: 0.2,
}) as Box<dyn strategy::GameStrategyConfig + Sync>,
"cheat" => Box::new(strategies::cheating::CheatingStrategyConfig::new())
as Box<dyn strategy::GameStrategyConfig + Sync>,
"info" => Box::new(strategies::information::InformationStrategyConfig::new())
as Box<dyn strategy::GameStrategyConfig + Sync>,
2016-03-20 20:40:27 +01:00
_ => {
panic!("Unexpected strategy argument {}", strategy_str);
2023-01-20 02:31:32 +01:00
}
2016-03-20 20:40:27 +01:00
};
2023-01-20 02:31:32 +01:00
simulator::simulate(
&game_opts,
strategy_config,
seed,
n_trials,
n_threads,
progress_info,
)
}
fn get_results_table() -> String {
let strategies = ["cheat", "info"];
let player_nums = (2..=5).collect::<Vec<_>>();
let seed = 0;
let n_trials = 20000;
let n_threads = 8;
2023-01-20 02:31:32 +01:00
let intro = format!(
"On the first {} seeds, we have these scores and win rates (average ± standard error):\n\n",
n_trials
);
let format_name = |x| format!(" {:7} ", x);
let format_players = |x| format!(" {}p ", x);
2019-03-11 06:26:24 +01:00
let format_percent = |x, stderr| format!(" {:05.2} ± {:.2} % ", x, stderr);
2023-01-20 02:31:32 +01:00
let format_score = |x, stderr| format!(" {:07.4} ± {:.4} ", x, stderr);
let space = String::from(" ");
let dashes = String::from("---------");
let dashes_long = String::from("------------------");
type TwoLines = (String, String);
2023-01-20 02:31:32 +01:00
fn make_twolines(
player_nums: &[u32],
head: TwoLines,
make_block: &dyn Fn(u32) -> TwoLines,
) -> TwoLines {
let mut blocks = player_nums
.iter()
.cloned()
.map(make_block)
.collect::<Vec<_>>();
blocks.insert(0, head);
fn combine(items: Vec<String>) -> String {
2023-01-20 02:31:32 +01:00
items
.iter()
.fold(String::from("|"), |init, next| init + next + "|")
}
let (a, b): (Vec<_>, Vec<_>) = blocks.into_iter().unzip();
(combine(a), combine(b))
}
fn concat_twolines(body: Vec<TwoLines>) -> String {
2023-01-20 02:31:32 +01:00
body.into_iter().fold(String::default(), |output, (a, b)| {
(output + &a + "\n" + &b + "\n")
})
}
2023-01-20 02:31:32 +01:00
let header = make_twolines(&player_nums, (space.clone(), dashes), &|n_players| {
(format_players(n_players), dashes_long.clone())
});
let mut body = strategies
.iter()
.map(|strategy| {
make_twolines(
&player_nums,
(format_name(strategy), space.clone()),
&|n_players| {
let simresult =
sim_games(n_players, strategy, Some(seed), n_trials, n_threads, None);
(
format_score(simresult.average_score(), simresult.score_stderr()),
format_percent(
simresult.percent_perfect(),
simresult.percent_perfect_stderr(),
),
)
},
2019-03-09 14:41:18 +01:00
)
})
2023-01-20 02:31:32 +01:00
.collect::<Vec<_>>();
body.insert(0, header);
intro + &concat_twolines(body)
}
fn write_results_table() {
let separator = r#"
## Results (auto-generated)
To reproduce:
```
time cargo run --release -- --results-table
```
To update this file:
```
time cargo run --release -- --write-results-table
```
"#;
let readme = "README.md";
let readme_contents = std::fs::read_to_string(readme).unwrap();
let readme_init = {
let parts = readme_contents.splitn(2, separator).collect::<Vec<_>>();
if parts.len() != 2 {
panic!("{} has been modified in the Results section!", readme);
}
parts[0]
};
let table = get_results_table();
let new_readme_contents = String::from(readme_init) + separator + &table;
std::fs::write(readme, new_readme_contents).unwrap();
2016-03-06 01:54:46 +01:00
}