hanabi.rs/src/main.rs

228 lines
7.9 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-14 02:11:20 +01:00
extern crate rand;
2016-03-18 06:44:02 +01:00
extern crate crossbeam;
extern crate fnv;
2016-03-06 01:54:46 +01:00
mod helpers;
2016-03-06 01:54:46 +01:00
mod game;
mod simulator;
2016-04-04 09:26:42 +02:00
mod strategy;
mod strategies {
pub mod examples;
2016-03-13 10:05:05 +01:00
pub mod cheating;
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();
2016-03-20 20:40:27 +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)",
2016-03-20 20:40:27 +01:00
"NTRIALS");
opts.optopt("o", "output",
"Number of games after which to print an update",
"OUTPUT_FREQ");
2016-03-20 20:40:27 +01:00
opts.optopt("t", "nthreads",
"Number of threads to use for simulation (default 1)",
2016-03-20 20:40:27 +01:00
"NTHREADS");
opts.optopt("s", "seed",
"Seed for PRNG (default random)",
2016-03-20 20:40:27 +01:00
"SEED");
opts.optopt("p", "nplayers",
"Number of players",
"NPLAYERS");
opts.optopt("g", "strategy",
"Which strategy to use. One of 'random', 'cheat', and 'info'",
2016-03-20 20:40:27 +01:00
"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..]) {
Ok(m) => { m }
Err(f) => {
print_usage(&program, opts);
panic!(f.to_string())
}
};
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
let log_level_str : &str = &matches.opt_str("l").unwrap_or("info".to_string());
let log_level = match log_level_str {
"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)
2016-03-07 06:44:17 +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();
2016-03-14 02:11:20 +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();
let strategy_str : &str = &matches.opt_str("g").unwrap_or("cheat".to_string());
sim_games(n_players, strategy_str, seed, n_trials, n_threads, progress_info).info();
}
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,
_ => { panic!("There should be 2 to 5 players, not {}", n_players); }
};
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,
hand_size: 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
2016-04-04 09:26:42 +02:00
let strategy_config : Box<strategy::GameStrategyConfig + Sync> = match strategy_str {
2016-03-20 20:40:27 +01:00
"random" => {
Box::new(strategies::examples::RandomStrategyConfig {
hint_probability: 0.4,
play_probability: 0.2,
2016-04-04 09:26:42 +02:00
}) as Box<strategy::GameStrategyConfig + Sync>
2016-03-20 20:40:27 +01:00
},
"cheat" => {
Box::new(strategies::cheating::CheatingStrategyConfig::new())
2016-04-04 09:26:42 +02:00
as Box<strategy::GameStrategyConfig + Sync>
2016-03-20 20:40:27 +01:00
},
2016-03-27 19:47:58 +02:00
"info" => {
Box::new(strategies::information::InformationStrategyConfig::new())
2016-04-04 09:26:42 +02:00
as Box<strategy::GameStrategyConfig + Sync>
2016-03-27 19:47:58 +02:00
},
2016-03-20 20:40:27 +01:00
_ => {
panic!("Unexpected strategy argument {}", strategy_str);
},
};
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;
let intro = format!("On the first {} seeds, we have these average scores and win rates:\n\n", n_trials);
let format_name = |x| format!(" {:7} ", x);
let format_players = |x| format!(" {}p ", x);
let format_percent = |x| format!(" {:05.2} % ", x);
let format_score = |x| format!(" {:07.4} ", x);
let space = String::from(" ");
let dashes = String::from("---------");
type TwoLines = (String, String);
fn make_twolines(player_nums: &Vec<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 {
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 {
body.into_iter().fold(String::default(), |output, (a, b)| (output + &a + "\n" + &b + "\n"))
}
let header = make_twolines(&player_nums, (space.clone(), dashes.clone()), &|n_players| (format_players(n_players), dashes.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()), format_percent(simresult.percent_perfect()))
})
}).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
}