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;
|
2019-03-08 20:59:14 +01:00
|
|
|
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;
|
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 {
|
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-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();
|
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);
|
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);
|
|
|
|
}
|
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
|
|
|
|
2023-01-20 02:59:20 +01:00
|
|
|
let l_opt = matches.opt_str("l");
|
|
|
|
let log_level_str = l_opt.as_deref().unwrap_or("info");
|
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
|
|
|
|
2023-01-20 02:59:20 +01:00
|
|
|
let n_trials = u32::from_str(matches.opt_str("n").as_deref().unwrap_or("1")).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());
|
2023-01-20 02:59:20 +01:00
|
|
|
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");
|
2019-03-07 19:12:31 +01:00
|
|
|
|
2023-01-20 02:31:32 +01:00
|
|
|
sim_games(
|
|
|
|
n_players,
|
|
|
|
strategy_str,
|
|
|
|
seed,
|
|
|
|
n_trials,
|
|
|
|
n_threads,
|
|
|
|
progress_info,
|
|
|
|
)
|
|
|
|
.info();
|
2019-03-07 19:12:31 +01:00
|
|
|
}
|
|
|
|
|
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,
|
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,
|
|
|
|
)
|
2019-03-07 19:12:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
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("------------------");
|
2019-03-07 19:12:31 +01:00
|
|
|
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<_>>();
|
2019-03-07 19:12:31 +01:00
|
|
|
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 + "|")
|
2019-03-07 19:12:31 +01:00
|
|
|
}
|
|
|
|
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)| {
|
2023-01-20 02:55:47 +01:00
|
|
|
output + &a + "\n" + &b + "\n"
|
2023-01-20 02:31:32 +01:00
|
|
|
})
|
2019-03-07 19:12:31 +01:00
|
|
|
}
|
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
|
|
|
)
|
2019-03-07 19:12:31 +01:00
|
|
|
})
|
2023-01-20 02:31:32 +01:00
|
|
|
.collect::<Vec<_>>();
|
2019-03-07 19:12:31 +01:00
|
|
|
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
|
|
|
}
|