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;
|
2019-03-07 13:54:30 +01:00
|
|
|
extern crate fnv;
|
2016-03-06 01:54:46 +01:00
|
|
|
|
2016-04-04 07:29:57 +02:00
|
|
|
mod helpers;
|
2016-03-06 01:54:46 +01:00
|
|
|
mod game;
|
2016-03-11 07:26:32 +01:00
|
|
|
mod simulator;
|
2016-04-04 09:26:42 +02:00
|
|
|
mod strategy;
|
2016-03-11 07:26:32 +01:00
|
|
|
mod strategies {
|
|
|
|
pub mod examples;
|
2016-03-13 10:05:05 +01:00
|
|
|
pub mod cheating;
|
2016-03-27 19:47:58 +02:00
|
|
|
pub mod information;
|
2016-03-11 07:26:32 +01:00
|
|
|
}
|
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",
|
2016-03-31 09:02:09 +02:00
|
|
|
"Number of games to simulate (default 1)",
|
2016-03-20 20:40:27 +01:00
|
|
|
"NTRIALS");
|
2016-04-01 11:08:46 +02:00
|
|
|
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",
|
2016-03-31 09:02:09 +02:00
|
|
|
"Number of threads to use for simulation (default 1)",
|
2016-03-20 20:40:27 +01:00
|
|
|
"NTHREADS");
|
|
|
|
opts.optopt("s", "seed",
|
2016-03-31 09:02:09 +02:00
|
|
|
"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",
|
2016-03-31 09:02:09 +02:00
|
|
|
"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");
|
2019-03-07 19:12:31 +01:00
|
|
|
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);
|
|
|
|
}
|
2019-03-07 19:12:31 +01:00
|
|
|
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
|
|
|
|
2019-03-07 19:12:31 +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() });
|
2016-04-01 11:08:46 +02:00
|
|
|
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();
|
2019-03-07 19:12:31 +01:00
|
|
|
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);
|
|
|
|
},
|
|
|
|
};
|
2019-03-07 19:12:31 +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;
|
2019-03-07 21:38:55 +01:00
|
|
|
let n_trials = 20000;
|
2019-03-07 19:12:31 +01:00
|
|
|
let n_threads = 8;
|
|
|
|
|
2019-03-11 06:26:24 +01:00
|
|
|
let intro = format!("On the first {} seeds, we have these scores and win rates (average ± standard error):\n\n", n_trials);
|
2019-03-09 14:41:18 +01:00
|
|
|
let format_name = |x| format!(" {:7} ", x);
|
2019-03-11 06:26:24 +01:00
|
|
|
let format_players = |x| format!(" {}p ", x);
|
|
|
|
let format_percent = |x, stderr| format!(" {:05.2} ± {:.2} % ", x, stderr);
|
|
|
|
let format_score = |x, stderr| format!(" {:07.4} ± {:.4} ", x, stderr);
|
2019-03-09 14:41:18 +01:00
|
|
|
let space = String::from(" ");
|
|
|
|
let dashes = String::from("---------");
|
|
|
|
let dashes_long = String::from("------------------");
|
2019-03-07 19:12:31 +01:00
|
|
|
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"))
|
|
|
|
}
|
2019-03-09 14:41:18 +01:00
|
|
|
let header = make_twolines(&player_nums,
|
|
|
|
(space.clone(), dashes.clone()),
|
|
|
|
&|n_players| (format_players(n_players), dashes_long.clone()));
|
2019-03-07 19:12:31 +01:00
|
|
|
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);
|
2019-03-09 14:41:18 +01:00
|
|
|
(
|
|
|
|
format_score(simresult.average_score(), simresult.score_stderr()),
|
|
|
|
format_percent(simresult.percent_perfect(), simresult.percent_perfect_stderr())
|
|
|
|
)
|
2019-03-07 19:12:31 +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
|
|
|
}
|