extern crate getopts; #[macro_use] extern crate log; extern crate crossbeam; extern crate float_ord; extern crate fnv; extern crate rand; mod game; mod helpers; mod simulator; mod strategy; mod strategies { pub mod cheating; pub mod examples; mod hat_helpers; pub mod information; } use getopts::Options; use std::str::FromStr; struct SimpleLogger; impl log::Log for SimpleLogger { fn enabled(&self, metadata: &log::LogMetadata) -> bool { metadata.level() <= log::LogLevel::Trace } fn log(&self, record: &log::LogRecord) { if self.enabled(record.metadata()) { println!("{} - {}", record.level(), record.args()); } } } fn print_usage(program: &str, opts: Options) { print!("{}", opts.usage(&format!("Usage: {} [options]", program))); } fn main() { let args: Vec = std::env::args().collect(); let program = args[0].clone(); let mut opts = Options::new(); 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", ); let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => { print_usage(&program, opts); panic!("{}", f) } }; 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()); } let l_opt = matches.opt_str("l"); let log_level_str = l_opt.as_deref().unwrap_or("info"); 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, _ => { print_usage(&program, opts); panic!("Unexpected log level argument {}", log_level_str); } }; log::set_logger(|max_log_level| { max_log_level.set(log_level); Box::new(SimpleLogger) }) .unwrap(); let n_trials = u32::from_str(matches.opt_str("n").as_deref().unwrap_or("1")).unwrap(); 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()); let n_threads = u32::from_str(matches.opt_str("t").as_deref().unwrap_or("1")).unwrap(); let n_players = u32::from_str(matches.opt_str("p").as_deref().unwrap_or("4")).unwrap(); let g_opt = matches.opt_str("g"); let strategy_str: &str = g_opt.as_deref().unwrap_or("cheat"); 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, n_trials: u32, n_threads: u32, progress_info: Option, ) -> simulator::SimResult { 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); } }; let game_opts = game::GameOptions { num_players: n_players, hand_size, num_hints: 8, num_lives: 3, // hanabi rules are a bit ambiguous about whether you can give hints that match 0 cards allow_empty_hints: false, }; let strategy_config: Box = match strategy_str { "random" => Box::new(strategies::examples::RandomStrategyConfig { hint_probability: 0.4, play_probability: 0.2, }) as Box, "cheat" => Box::new(strategies::cheating::CheatingStrategyConfig::new()) as Box, "info" => Box::new(strategies::information::InformationStrategyConfig::new()) as Box, _ => { 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::>(); let seed = 0; let n_trials = 20000; let n_threads = 8; 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); let format_percent = |x, stderr| format!(" {:05.2} ± {:.2} % ", x, stderr); 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); 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::>(); blocks.insert(0, head); fn combine(items: Vec) -> 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) -> String { body.into_iter().fold(String::default(), |output, (a, b)| { output + &a + "\n" + &b + "\n" }) } 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(), ), ) }, ) }) .collect::>(); 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::>(); 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(); }