From 180d8c5f4e50106a442d3e00cdc5e0e83f7c254e Mon Sep 17 00:00:00 2001 From: Felix Bauckholt Date: Sun, 24 Feb 2019 21:52:03 +0100 Subject: [PATCH 1/6] Second attempt at outputting JSON for hanabi.live We don't support any notes yet. Inside our GameState, we annotate each card with its index in the original (reverse) deck. In order to support per-card notes, we should probably share those annotations with the players, and refactor the player code around them. --- Cargo.lock | 30 ++++++++++++++ Cargo.toml | 1 + src/game.rs | 77 ++++++++++++++++++++++++++--------- src/json_output.rs | 56 +++++++++++++++++++++++++ src/main.rs | 25 +++++++++--- src/simulator.rs | 53 +++++++++++++++++++++--- src/strategies/cheating.rs | 3 ++ src/strategies/examples.rs | 3 ++ src/strategies/information.rs | 4 ++ src/strategy.rs | 5 +++ 10 files changed, 228 insertions(+), 29 deletions(-) create mode 100644 src/json_output.rs diff --git a/Cargo.lock b/Cargo.lock index e063b68..3f860e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,11 @@ name = "getopts" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "itoa" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "libc" version = "0.2.7" @@ -49,6 +54,27 @@ dependencies = [ "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ryu" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_json" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", ] [metadata] @@ -56,6 +82,10 @@ dependencies = [ "checksum float-ord 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9047cfbd08a437050b363d35ef160452c5fe8ea5187ae0a624708c91581d685" +"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" "checksum libc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "4870ef6725dde13394134e587e4ab4eca13cb92e916209a31c851b49131d3c75" "checksum log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "038b5d13189a14e5b6ac384fdb7c691a45ef0885f6d2dddbf422e6c3506b8234" "checksum rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2791d88c6defac799c3f20d74f094ca33b9332612d9aef9078519c82e4fe04a5" +"checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7" +"checksum serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)" = "9f301d728f2b94c9a7691c90f07b0b4e8a4517181d9461be94c04bddeb4bd850" +"checksum serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "27dce848e7467aa0e2fcaf0a413641499c0b745452aaca1194d24dedde9e13c9" diff --git a/Cargo.toml b/Cargo.toml index 79c0936..5f22458 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ getopts = "*" fnv = "*" float-ord = "*" crossbeam = "0.2.5" +serde_json = "*" diff --git a/src/game.rs b/src/game.rs index e973a3c..3e64fc6 100644 --- a/src/game.rs +++ b/src/game.rs @@ -389,7 +389,7 @@ impl BoardState { } pub fn is_over(&self) -> bool { - (self.lives_remaining == 0) || (self.deckless_turns_remaining == 0) + (self.lives_remaining == 0) || (self.deckless_turns_remaining == 0) || (self.score() == PERFECT_SCORE) } } impl fmt::Display for BoardState { @@ -541,12 +541,28 @@ impl GameView for OwnedGameView { } } +// Internally, every card is annotated with its index in the deck in order to +// generate easy-to-interpret JSON output. These annotations are stripped off +// when passing GameViews to strategies. +// +// TODO: Maybe we should give strategies access to the annotations as well? +// This could simplify code like in InformationPlayerStrategy::update_public_info_for_discard_or_play. +// Also, this would let a strategy publish "notes" on cards more easily. +pub type AnnotatedCard = (usize, Card); +pub type AnnotatedCards = Vec; + +fn strip_annotations(cards: &AnnotatedCards) -> Cards { + cards.iter().map(|(_i, card)| { card.clone() }).collect() +} + // complete game state (known to nobody!) #[derive(Debug)] pub struct GameState { - pub hands: FnvHashMap, + pub hands: FnvHashMap, + // used to construct BorrowedGameViews + pub unannotated_hands: FnvHashMap, pub board: BoardState, - pub deck: Cards, + pub deck: AnnotatedCards, } impl fmt::Display for GameState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -557,7 +573,7 @@ impl fmt::Display for GameState { for player in self.board.get_players() { let hand = &self.hands.get(&player).unwrap(); try!(f.write_str(&format!("player {}:", player))); - for card in hand.iter() { + for (_i, card) in hand.iter() { try!(f.write_str(&format!(" {}", card))); } try!(f.write_str(&"\n")); @@ -571,7 +587,9 @@ impl fmt::Display for GameState { } impl GameState { - pub fn new(opts: &GameOptions, mut deck: Cards) -> GameState { + pub fn new(opts: &GameOptions, deck: Cards) -> GameState { + // We enumerate the cards in reverse order since they'll be drawn from the back of the deck. + let mut deck: AnnotatedCards = deck.into_iter().rev().enumerate().rev().collect(); let mut board = BoardState::new(opts, deck.len() as u32); let hands = @@ -583,11 +601,15 @@ impl GameState { }).collect::>(); (player, hand) }).collect::>(); + let unannotated_hands = hands.iter().map(|(player, hand)| { + (player.clone(), strip_annotations(hand)) + }).collect::>(); GameState { - hands: hands, - board: board, - deck: deck, + hands, + unannotated_hands, + board, + deck, } } @@ -606,7 +628,7 @@ impl GameState { // get the game state view of a particular player pub fn get_view(&self, player: Player) -> BorrowedGameView { let mut other_hands = FnvHashMap::default(); - for (&other_player, hand) in &self.hands { + for (&other_player, hand) in &self.unannotated_hands { if player != other_player { other_hands.insert(other_player, hand); } @@ -619,21 +641,38 @@ impl GameState { } } + fn update_player_hand(&mut self) { + let player = self.board.player.clone(); + self.unannotated_hands.insert(player, strip_annotations(self.hands.get(&player).unwrap())); + } + // takes a card from the player's hand, and replaces it if possible fn take_from_hand(&mut self, index: usize) -> Card { - let ref mut hand = self.hands.get_mut(&self.board.player).unwrap(); - hand.remove(index) + // FIXME this code looks like it's awfully contorted in order to please the borrow checker. + // Can we have this look nicer? + let result = + { + let ref mut hand = self.hands.get_mut(&self.board.player).unwrap(); + hand.remove(index).1 + }; + self.update_player_hand(); + result } fn replenish_hand(&mut self) { - let ref mut hand = self.hands.get_mut(&self.board.player).unwrap(); - if (hand.len() as u32) < self.board.hand_size { - if let Some(new_card) = self.deck.pop() { - self.board.deck_size -= 1; - debug!("Drew new card, {}", new_card); - hand.push(new_card); + // FIXME this code looks like it's awfully contorted in order to please the borrow checker. + // Can we have this look nicer? + { + let ref mut hand = self.hands.get_mut(&self.board.player).unwrap(); + if (hand.len() as u32) < self.board.hand_size { + if let Some(new_card) = self.deck.pop() { + self.board.deck_size -= 1; + debug!("Drew new card, {}", new_card.1); + hand.push(new_card); + } } } + self.update_player_hand(); } pub fn process_choice(&mut self, choice: TurnChoice) -> TurnRecord { @@ -651,10 +690,10 @@ impl GameState { let hand = self.hands.get(&hint.player).unwrap(); let results = match hint.hinted { Hinted::Color(color) => { - hand.iter().map(|card| { card.color == color }).collect::>() + hand.iter().map(|(_i, card)| { card.color == color }).collect::>() } Hinted::Value(value) => { - hand.iter().map(|card| { card.value == value }).collect::>() + hand.iter().map(|(_i, card)| { card.value == value }).collect::>() } }; if !self.board.allow_empty_hints { diff --git a/src/json_output.rs b/src/json_output.rs new file mode 100644 index 0000000..ab8d216 --- /dev/null +++ b/src/json_output.rs @@ -0,0 +1,56 @@ +use game::*; +use serde_json::*; + +fn color_value(color: &Color) -> usize { + COLORS.iter().position(|&card_color| &card_color == color).unwrap() +} + +fn card_to_json(card: &Card) -> serde_json::Value { + json!({ + "rank": card.value, + "suit": color_value(&card.color), + }) +} + +pub fn action_clue(hint: &Hint) -> serde_json::Value { + json!({ + "type": 0, + "target": hint.player, + "clue": match hint.hinted { + Hinted::Value(value) => { json!({ + "type": 0, + "value": value, + }) } + Hinted::Color(color) => { json!({ + "type": 1, + "value": color_value(&color), + }) } + } + }) +} + +pub fn action_play((i, _card): &AnnotatedCard) -> serde_json::Value { + json!({ + "type": 1, + "target": i, + }) +} + +pub fn action_discard((i, _card): &AnnotatedCard) -> serde_json::Value { + json!({ + "type": 2, + "target": i, + }) +} + +pub fn json_format(deck: &Cards, actions: &Vec, players: &Vec) -> serde_json::Value { + json!({ + "variant": "No Variant", + "players": players, + "first_player": 0, + "notes": players.iter().map(|_player| {json!([])}).collect::>(), // TODO add notes + // The deck is reversed since in our implementation we draw from the end of the deck. + "deck": deck.iter().rev().map(card_to_json).collect::>(), + "actions": actions, + }) +} diff --git a/src/main.rs b/src/main.rs index 1568470..beeea2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,9 +5,11 @@ extern crate rand; extern crate crossbeam; extern crate fnv; extern crate float_ord; +extern crate serde_json; mod helpers; mod game; +mod json_output; mod simulator; mod strategy; mod strategies { @@ -53,6 +55,9 @@ fn main() { opts.optopt("o", "output", "Number of games after which to print an update", "OUTPUT_FREQ"); + opts.optopt("j", "json-output", + "Pattern for the JSON output file. '%s' will be replaced by the seed.", + "FILE_PATTERN"); opts.optopt("t", "nthreads", "Number of threads to use for simulation (default 1)", "NTHREADS"); @@ -113,14 +118,24 @@ fn main() { 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").unwrap_or("1".to_string())).unwrap(); + + let json_output_pattern = matches.opt_str("j"); + 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(); + sim_games(n_players, strategy_str, seed, n_trials, n_threads, progress_info, json_output_pattern).info(); } -fn sim_games(n_players: u32, strategy_str: &str, seed: Option, n_trials: u32, n_threads: u32, progress_info: Option) - -> simulator::SimResult { +fn sim_games( + n_players: u32, + strategy_str: &str, + seed: Option, + n_trials: u32, + n_threads: u32, + progress_info: Option, + json_output_pattern: Option, + ) -> simulator::SimResult { let hand_size = match n_players { 2 => 5, 3 => 5, @@ -157,7 +172,7 @@ fn sim_games(n_players: u32, strategy_str: &str, seed: Option, n_trials: u3 panic!("Unexpected strategy argument {}", strategy_str); }, }; - simulator::simulate(&game_opts, strategy_config, seed, n_trials, n_threads, progress_info) + simulator::simulate(&game_opts, strategy_config, seed, n_trials, n_threads, progress_info, json_output_pattern) } fn get_results_table() -> String { @@ -193,7 +208,7 @@ fn get_results_table() -> String { &|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); + let simresult = sim_games(n_players, strategy, Some(seed), n_trials, n_threads, None, None); ( format_score(simresult.average_score(), simresult.score_stderr()), format_percent(simresult.percent_perfect(), simresult.percent_perfect_stderr()) diff --git a/src/simulator.rs b/src/simulator.rs index cf96183..a5ee3f9 100644 --- a/src/simulator.rs +++ b/src/simulator.rs @@ -5,6 +5,7 @@ use crossbeam; use game::*; use strategy::*; +use json_output::*; fn new_deck(seed: u32) -> Cards { let mut deck: Cards = Cards::new(); @@ -22,19 +23,24 @@ fn new_deck(seed: u32) -> Cards { deck } + pub fn simulate_once( opts: &GameOptions, game_strategy: Box, seed: u32, - ) -> GameState { + output_json: bool, + ) -> (GameState, Option) { + let deck = new_deck(seed); - let mut game = GameState::new(opts, deck); + let mut game = GameState::new(opts, deck.clone()); let mut strategies = game.get_players().map(|player| { (player, game_strategy.initialize(player, &game.get_view(player))) }).collect::>>(); + let mut actions = Vec::new(); + while !game.is_over() { let player = game.board.player; @@ -49,6 +55,22 @@ pub fn simulate_once( let mut strategy = strategies.get_mut(&player).unwrap(); strategy.decide(&game.get_view(player)) }; + if output_json { + actions.push(match choice { + TurnChoice::Hint(ref hint) => { + action_clue(hint) + } + TurnChoice::Play(index) => { + let card = &game.hands[&player][index]; + action_play(card) + } + TurnChoice::Discard(index) => { + let card = &game.hands[&player][index]; + action_discard(card) + } + }); + } + let turn = game.process_choice(choice); @@ -56,13 +78,20 @@ pub fn simulate_once( let mut strategy = strategies.get_mut(&player).unwrap(); strategy.update(&turn, &game.get_view(player)); } - } debug!(""); debug!("======================================================="); debug!("Final state:\n{}", game); debug!("SCORE: {:?}", game.score()); - game + let json_output = if output_json { + let player_names = game.get_players().map(|player| { + strategies[&player].name() + }).collect(); + Some(json_format(&deck, &actions, &player_names)) + } else { + None + }; + (game, json_output) } #[derive(Debug)] @@ -134,12 +163,14 @@ pub fn simulate( n_trials: u32, n_threads: u32, progress_info: Option, + json_output_pattern: Option, ) -> SimResult where T: GameStrategyConfig + Sync { let first_seed = first_seed_opt.unwrap_or_else(|| rand::thread_rng().next_u32()); let strat_config_ref = &strat_config; + let json_output_pattern_ref = &json_output_pattern; crossbeam::scope(|scope| { let mut join_handles = Vec::new(); for i in 0..n_threads { @@ -164,11 +195,23 @@ pub fn simulate( ); } } - let game = simulate_once(&opts, strat_config_ref.initialize(&opts), seed); + let (game, json_output) = simulate_once(&opts, + strat_config_ref.initialize(&opts), + seed, + json_output_pattern_ref.is_some()); let score = game.score(); lives_histogram.insert(game.board.lives_remaining); score_histogram.insert(score); if score != PERFECT_SCORE { non_perfect_seeds.push(seed); } + match json_output_pattern_ref { + Some(file_pattern) => { + let file_pattern = file_pattern.clone().replace("%s", &seed.to_string()); + let path = std::path::Path::new(&file_pattern); + let file = std::fs::File::create(path).unwrap(); + serde_json::to_writer(file, &json_output.unwrap()).unwrap(); + } + None => { } + } } if progress_info.is_some() { info!("Thread {} done", i); diff --git a/src/strategies/cheating.rs b/src/strategies/cheating.rs index c71bfd5..c3dc0ef 100644 --- a/src/strategies/cheating.rs +++ b/src/strategies/cheating.rs @@ -136,6 +136,9 @@ impl CheatingPlayerStrategy { } } impl PlayerStrategy for CheatingPlayerStrategy { + fn name(&self) -> String { + String::from("cheat") + } fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice { self.inform_last_player_cards(view); diff --git a/src/strategies/examples.rs b/src/strategies/examples.rs index 60c7aad..5b3462f 100644 --- a/src/strategies/examples.rs +++ b/src/strategies/examples.rs @@ -39,6 +39,9 @@ pub struct RandomStrategyPlayer { } impl PlayerStrategy for RandomStrategyPlayer { + fn name(&self) -> String { + format!("random(hint={}, play={})", self.hint_probability, self.play_probability) + } fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice { let p = rand::random::(); if p < self.hint_probability { diff --git a/src/strategies/information.rs b/src/strategies/information.rs index e28951d..3ca4bb2 100644 --- a/src/strategies/information.rs +++ b/src/strategies/information.rs @@ -913,6 +913,10 @@ impl InformationPlayerStrategy { } impl PlayerStrategy for InformationPlayerStrategy { + fn name(&self) -> String { + String::from("info") + } + fn decide(&mut self, _: &BorrowedGameView) -> TurnChoice { let mut public_info = self.public_info.clone(); let turn_choice = self.decide_wrapped(&mut public_info); diff --git a/src/strategy.rs b/src/strategy.rs index 8ef667f..3ba501b 100644 --- a/src/strategy.rs +++ b/src/strategy.rs @@ -4,6 +4,11 @@ use game::*; // Represents the strategy of a given player pub trait PlayerStrategy { + // A function returning the name of a strategy. + // This is a method of PlayerStrategy rather than GameStrategyConfig + // so that the name may incorporate useful information that's specific + // to this player instance. + fn name(&self) -> String; // A function to decide what to do on the player's turn. // Given a BorrowedGameView, outputs their choice. fn decide(&mut self, &BorrowedGameView) -> TurnChoice; From ade33395c28d250c821e4124badfed3fe24959aa Mon Sep 17 00:00:00 2001 From: Felix Bauckholt Date: Wed, 27 Feb 2019 12:32:47 +0100 Subject: [PATCH 2/6] Add option to only generate outputs for lost games --- src/main.rs | 10 +++++++--- src/simulator.rs | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index beeea2f..d8f2d0c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,6 +76,8 @@ fn main() { "Print a table of results for each strategy"); opts.optflag("", "write-results-table", "Update the results table in README.md"); + opts.optflag("", "losses-only", + "When saving JSON outputs, save lost games only"); let matches = match opts.parse(&args[1..]) { Ok(m) => { m } Err(f) => { @@ -120,11 +122,12 @@ fn main() { let n_threads = u32::from_str(&matches.opt_str("t").unwrap_or("1".to_string())).unwrap(); let json_output_pattern = matches.opt_str("j"); + let json_losses_only = matches.opt_present("losses-only"); 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, json_output_pattern).info(); + sim_games(n_players, strategy_str, seed, n_trials, n_threads, progress_info, json_output_pattern, json_losses_only).info(); } fn sim_games( @@ -135,6 +138,7 @@ fn sim_games( n_threads: u32, progress_info: Option, json_output_pattern: Option, + json_losses_only: bool, ) -> simulator::SimResult { let hand_size = match n_players { 2 => 5, @@ -172,7 +176,7 @@ fn sim_games( panic!("Unexpected strategy argument {}", strategy_str); }, }; - simulator::simulate(&game_opts, strategy_config, seed, n_trials, n_threads, progress_info, json_output_pattern) + simulator::simulate(&game_opts, strategy_config, seed, n_trials, n_threads, progress_info, json_output_pattern, json_losses_only) } fn get_results_table() -> String { @@ -208,7 +212,7 @@ fn get_results_table() -> String { &|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, None); + let simresult = sim_games(n_players, strategy, Some(seed), n_trials, n_threads, None, None, false); ( format_score(simresult.average_score(), simresult.score_stderr()), format_percent(simresult.percent_perfect(), simresult.percent_perfect_stderr()) diff --git a/src/simulator.rs b/src/simulator.rs index a5ee3f9..56b4d2e 100644 --- a/src/simulator.rs +++ b/src/simulator.rs @@ -164,6 +164,7 @@ pub fn simulate( n_threads: u32, progress_info: Option, json_output_pattern: Option, + json_losses_only: bool, ) -> SimResult where T: GameStrategyConfig + Sync { @@ -203,14 +204,13 @@ pub fn simulate( lives_histogram.insert(game.board.lives_remaining); score_histogram.insert(score); if score != PERFECT_SCORE { non_perfect_seeds.push(seed); } - match json_output_pattern_ref { - Some(file_pattern) => { + if let Some(file_pattern) = json_output_pattern_ref { + if !(score == PERFECT_SCORE && json_losses_only) { let file_pattern = file_pattern.clone().replace("%s", &seed.to_string()); let path = std::path::Path::new(&file_pattern); let file = std::fs::File::create(path).unwrap(); serde_json::to_writer(file, &json_output.unwrap()).unwrap(); } - None => { } } } if progress_info.is_some() { From 1a42a4dd21ef6458cdffef1aa89aa70ac4176a9c Mon Sep 17 00:00:00 2001 From: timotree3 Date: Thu, 19 Jan 2023 21:22:16 -0500 Subject: [PATCH 3/6] Run `cargo fmt` --- src/game.rs | 214 ++++++++------- src/helpers.rs | 180 +++++++++---- src/json_output.rs | 11 +- src/main.rs | 274 ++++++++++++------- src/simulator.rs | 109 ++++---- src/strategies/cheating.rs | 46 ++-- src/strategies/examples.rs | 14 +- src/strategies/hat_helpers.rs | 83 +++--- src/strategies/information.rs | 493 +++++++++++++++++++++------------- src/strategy.rs | 1 - 10 files changed, 877 insertions(+), 548 deletions(-) diff --git a/src/game.rs b/src/game.rs index 3e64fc6..43f1359 100644 --- a/src/game.rs +++ b/src/game.rs @@ -11,26 +11,31 @@ pub const COLORS: [Color; NUM_COLORS] = ['r', 'y', 'g', 'b', 'w']; pub type Value = u32; // list of values, assumed to be small to large pub const NUM_VALUES: usize = 5; -pub const VALUES : [Value; NUM_VALUES] = [1, 2, 3, 4, 5]; -pub const FINAL_VALUE : Value = 5; +pub const VALUES: [Value; NUM_VALUES] = [1, 2, 3, 4, 5]; +pub const FINAL_VALUE: Value = 5; pub fn get_count_for_value(value: Value) -> u32 { match value { - 1 => 3, + 1 => 3, 2 | 3 | 4 => 2, - 5 => 1, - _ => { panic!(format!("Unexpected value: {}", value)); } + 5 => 1, + _ => { + panic!(format!("Unexpected value: {}", value)); + } } } -#[derive(Clone,PartialEq,Eq,Hash,Ord,PartialOrd)] +#[derive(Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct Card { pub color: Color, pub value: Value, } impl Card { pub fn new(color: Color, value: Value) -> Card { - Card { color: color, value: value } + Card { + color: color, + value: value, + } } } impl fmt::Display for Card { @@ -44,7 +49,7 @@ impl fmt::Debug for Card { } } -#[derive(Debug,Clone,Eq,PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct CardCounts { counts: FnvHashMap, } @@ -56,9 +61,7 @@ impl CardCounts { counts.insert(Card::new(color, value), 0); } } - CardCounts { - counts: counts, - } + CardCounts { counts: counts } } pub fn get_count(&self, card: &Card) -> u32 { @@ -78,15 +81,11 @@ impl CardCounts { impl fmt::Display for CardCounts { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for &color in COLORS.iter() { - try!(f.write_str(&format!( - "{}: ", color, - ))); + try!(f.write_str(&format!("{}: ", color,))); for &value in VALUES.iter() { let count = self.get_count(&Card::new(color, value)); let total = get_count_for_value(value); - try!(f.write_str(&format!( - "{}/{} {}s", count, total, value - ))); + try!(f.write_str(&format!("{}/{} {}s", count, total, value))); if value != FINAL_VALUE { try!(f.write_str(", ")); } @@ -99,7 +98,7 @@ impl fmt::Display for CardCounts { pub type Cards = Vec; -#[derive(Debug,Clone,Eq,PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Discard { pub cards: Cards, counts: CardCounts, @@ -137,7 +136,7 @@ impl fmt::Display for Discard { pub type Score = u32; pub const PERFECT_SCORE: Score = (NUM_COLORS * NUM_VALUES) as u32; -#[derive(Debug,Clone,Eq,PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Firework { pub color: Color, pub top: Value, @@ -151,7 +150,11 @@ impl Firework { } pub fn needed_value(&self) -> Option { - if self.complete() { None } else { Some(self.top + 1) } + if self.complete() { + None + } else { + Some(self.top + 1) + } } pub fn score(&self) -> Score { @@ -184,7 +187,7 @@ impl fmt::Display for Firework { } } -#[derive(Debug,Clone,Hash,PartialEq,Eq)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum Hinted { Color(Color), Value(Value), @@ -192,20 +195,24 @@ pub enum Hinted { impl fmt::Display for Hinted { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - &Hinted::Color(color) => { write!(f, "{}", color) } - &Hinted::Value(value) => { write!(f, "{}", value) } + &Hinted::Color(color) => { + write!(f, "{}", color) + } + &Hinted::Value(value) => { + write!(f, "{}", value) + } } } } -#[derive(Debug,Clone,Eq,PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Hint { pub player: Player, pub hinted: Hinted, } // represents the choice a player made in a given turn -#[derive(Debug,Clone,Eq,PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum TurnChoice { Hint(Hint), Discard(usize), // index of card to discard @@ -213,7 +220,7 @@ pub enum TurnChoice { } // represents what happened in a turn -#[derive(Debug,Clone,Eq,PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum TurnResult { Hint(Vec), // vector of whether each was in the hint Discard(Card), // card discarded @@ -221,7 +228,7 @@ pub enum TurnResult { } // represents a turn taken in the game -#[derive(Debug,Clone,Eq,PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct TurnRecord { pub player: Player, pub choice: TurnChoice, @@ -243,7 +250,7 @@ pub struct GameOptions { // State of everything except the player's hands // Is all completely common knowledge -#[derive(Debug,Clone,Eq,PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct BoardState { pub deck_size: u32, pub total_cards: u32, @@ -269,9 +276,10 @@ pub struct BoardState { } impl BoardState { pub fn new(opts: &GameOptions, deck_size: u32) -> BoardState { - let fireworks = COLORS.iter().map(|&color| { - (color, Firework::new(color)) - }).collect::>(); + let fireworks = COLORS + .iter() + .map(|&color| (color, Firework::new(color))) + .collect::>(); BoardState { deck_size: deck_size, @@ -324,7 +332,7 @@ impl BoardState { for &value in VALUES.iter() { if value < needed { // already have these cards - continue + continue; } let needed_card = Card::new(color, value); if self.discard.has_all(&needed_card) { @@ -374,7 +382,10 @@ impl BoardState { } pub fn score(&self) -> Score { - self.fireworks.iter().map(|(_, firework)| firework.score()).fold(0, |a, b| a + b) + self.fireworks + .iter() + .map(|(_, firework)| firework.score()) + .fold(0, |a, b| a + b) } pub fn discard_size(&self) -> u32 { @@ -389,34 +400,36 @@ impl BoardState { } pub fn is_over(&self) -> bool { - (self.lives_remaining == 0) || (self.deckless_turns_remaining == 0) || (self.score() == PERFECT_SCORE) + (self.lives_remaining == 0) + || (self.deckless_turns_remaining == 0) + || (self.score() == PERFECT_SCORE) } } impl fmt::Display for BoardState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.is_over() { - try!(f.write_str(&format!( - "Turn {} (GAME ENDED):\n", self.turn - ))); + try!(f.write_str(&format!("Turn {} (GAME ENDED):\n", self.turn))); } else { try!(f.write_str(&format!( - "Turn {} (Player {}'s turn):\n", self.turn, self.player + "Turn {} (Player {}'s turn):\n", + self.turn, self.player ))); } - try!(f.write_str(&format!( - "{} cards remaining in deck\n", self.deck_size - ))); + try!(f.write_str(&format!("{} cards remaining in deck\n", self.deck_size))); if self.deck_size == 0 { try!(f.write_str(&format!( - "Deck is empty. {} turns remaining in game\n", self.deckless_turns_remaining + "Deck is empty. {} turns remaining in game\n", + self.deckless_turns_remaining ))); } try!(f.write_str(&format!( - "{}/{} hints remaining\n", self.hints_remaining, self.hints_total + "{}/{} hints remaining\n", + self.hints_remaining, self.hints_total ))); try!(f.write_str(&format!( - "{}/{} lives remaining\n", self.lives_remaining, self.lives_total + "{}/{} lives remaining\n", + self.lives_remaining, self.lives_total ))); try!(f.write_str("Fireworks:\n")); for &color in COLORS.iter() { @@ -446,28 +459,30 @@ pub trait GameView { } fn has_card(&self, player: &Player, card: &Card) -> bool { - self.get_hand(player).iter().position(|other_card| { - card == other_card - }).is_some() + self.get_hand(player) + .iter() + .position(|other_card| card == other_card) + .is_some() } fn get_other_players(&self) -> Vec { - self.get_board().get_players().filter(|&player| { - player != self.me() - }).collect() + self.get_board() + .get_players() + .filter(|&player| player != self.me()) + .collect() } fn can_see(&self, card: &Card) -> bool { - self.get_other_players().iter().any(|player| { - self.has_card(&player, card) - }) + self.get_other_players() + .iter() + .any(|player| self.has_card(&player, card)) } fn someone_else_can_play(&self) -> bool { self.get_other_players().iter().any(|player| { - self.get_hand(&player).iter().any(|card| { - self.get_board().is_playable(card) - }) + self.get_hand(&player) + .iter() + .any(|card| self.get_board().is_playable(card)) }) } } @@ -483,7 +498,7 @@ pub struct BorrowedGameView<'a> { // board state pub board: &'a BoardState, } -impl <'a> GameView for BorrowedGameView<'a> { +impl<'a> GameView for BorrowedGameView<'a> { fn me(&self) -> Player { self.player } @@ -512,10 +527,11 @@ pub struct OwnedGameView { } impl OwnedGameView { pub fn clone_from(borrowed_view: &BorrowedGameView) -> OwnedGameView { - let other_hands = borrowed_view.other_hands.iter() - .map(|(&other_player, &player_state)| { - (other_player, player_state.clone()) - }).collect::>(); + let other_hands = borrowed_view + .other_hands + .iter() + .map(|(&other_player, &player_state)| (other_player, player_state.clone())) + .collect::>(); OwnedGameView { player: borrowed_view.player.clone(), @@ -552,7 +568,7 @@ pub type AnnotatedCard = (usize, Card); pub type AnnotatedCards = Vec; fn strip_annotations(cards: &AnnotatedCards) -> Cards { - cards.iter().map(|(_i, card)| { card.clone() }).collect() + cards.iter().map(|(_i, card)| card.clone()).collect() } // complete game state (known to nobody!) @@ -592,18 +608,22 @@ impl GameState { let mut deck: AnnotatedCards = deck.into_iter().rev().enumerate().rev().collect(); let mut board = BoardState::new(opts, deck.len() as u32); - let hands = - (0..opts.num_players).map(|player| { - let hand = (0..opts.hand_size).map(|_| { - // we can assume the deck is big enough to draw initial hands - board.deck_size -= 1; - deck.pop().unwrap() - }).collect::>(); + let hands = (0..opts.num_players) + .map(|player| { + let hand = (0..opts.hand_size) + .map(|_| { + // we can assume the deck is big enough to draw initial hands + board.deck_size -= 1; + deck.pop().unwrap() + }) + .collect::>(); (player, hand) - }).collect::>(); - let unannotated_hands = hands.iter().map(|(player, hand)| { - (player.clone(), strip_annotations(hand)) - }).collect::>(); + }) + .collect::>(); + let unannotated_hands = hands + .iter() + .map(|(player, hand)| (player.clone(), strip_annotations(hand))) + .collect::>(); GameState { hands, @@ -643,15 +663,15 @@ impl GameState { fn update_player_hand(&mut self) { let player = self.board.player.clone(); - self.unannotated_hands.insert(player, strip_annotations(self.hands.get(&player).unwrap())); + self.unannotated_hands + .insert(player, strip_annotations(self.hands.get(&player).unwrap())); } // takes a card from the player's hand, and replaces it if possible fn take_from_hand(&mut self, index: usize) -> Card { // FIXME this code looks like it's awfully contorted in order to please the borrow checker. // Can we have this look nicer? - let result = - { + let result = { let ref mut hand = self.hands.get_mut(&self.board.player).unwrap(); hand.remove(index).1 }; @@ -679,26 +699,34 @@ impl GameState { let turn_result = { match choice { TurnChoice::Hint(ref hint) => { - assert!(self.board.hints_remaining > 0, - "Tried to hint with no hints remaining"); + assert!( + self.board.hints_remaining > 0, + "Tried to hint with no hints remaining" + ); self.board.hints_remaining -= 1; debug!("Hint to player {}, about {}", hint.player, hint.hinted); - assert!(self.board.player != hint.player, - format!("Player {} gave a hint to himself", hint.player)); + assert!( + self.board.player != hint.player, + format!("Player {} gave a hint to himself", hint.player) + ); let hand = self.hands.get(&hint.player).unwrap(); let results = match hint.hinted { - Hinted::Color(color) => { - hand.iter().map(|(_i, card)| { card.color == color }).collect::>() - } - Hinted::Value(value) => { - hand.iter().map(|(_i, card)| { card.value == value }).collect::>() - } + Hinted::Color(color) => hand + .iter() + .map(|(_i, card)| card.color == color) + .collect::>(), + Hinted::Value(value) => hand + .iter() + .map(|(_i, card)| card.value == value) + .collect::>(), }; if !self.board.allow_empty_hints { - assert!(results.iter().any(|matched| *matched), - "Tried hinting an empty hint"); + assert!( + results.iter().any(|matched| *matched), + "Tried hinting an empty hint" + ); } TurnResult::Hint(results) @@ -714,10 +742,7 @@ impl GameState { TurnChoice::Play(index) => { let card = self.take_from_hand(index); - debug!( - "Playing card at position {}, which is {}", - index, card - ); + debug!("Playing card at position {}, which is {}", index, card); let playable = self.board.is_playable(&card); if playable { { @@ -758,7 +783,10 @@ impl GameState { let cur = self.board.player; self.board.player_to_left(&cur) }; - assert_eq!((self.board.turn - 1) % self.board.num_players, self.board.player); + assert_eq!( + (self.board.turn - 1) % self.board.num_players, + self.board.player + ); turn_record } diff --git a/src/helpers.rs b/src/helpers.rs index 43aa3b7..71306c6 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,9 +1,9 @@ use std::cmp::Eq; use std::collections::{HashMap, HashSet}; -use std::fmt; -use std::ops::{Index,IndexMut}; -use std::hash::Hash; use std::convert::From; +use std::fmt; +use std::hash::Hash; +use std::ops::{Index, IndexMut}; use std::slice; use game::*; @@ -37,21 +37,25 @@ pub trait CardInfo { } fn get_weighted_possibilities(&self) -> Vec<(Card, f32)> { - self.get_possibilities().into_iter() + self.get_possibilities() + .into_iter() .map(|card| { let weight = self.get_weight(&card); (card, weight) - }).collect::>() + }) + .collect::>() } fn total_weight(&self) -> f32 { - self.get_possibilities().iter() + self.get_possibilities() + .iter() .map(|card| self.get_weight(&card)) - .fold(0.0, |a, b| a+b) + .fold(0.0, |a, b| a + b) } fn weighted_score(&self, score_fn: &Fn(&Card) -> T) -> f32 - where f32: From + where + f32: From, { let mut total_score = 0.; let mut total_weight = 0.; @@ -65,12 +69,16 @@ pub trait CardInfo { } fn average_value(&self) -> f32 { - self.weighted_score(&|card| card.value as f32 ) + self.weighted_score(&|card| card.value as f32) } fn probability_of_predicate(&self, predicate: &Fn(&Card) -> bool) -> f32 { let f = |card: &Card| { - if predicate(card) { 1.0 } else { 0.0 } + if predicate(card) { + 1.0 + } else { + 0.0 + } }; self.weighted_score(&f) } @@ -124,9 +132,11 @@ pub trait CardInfo { } } - // Represents hinted information about possible values of type T -pub trait Info where T: Hash + Eq + Clone + Copy { +pub trait Info +where + T: Hash + Eq + Clone + Copy, +{ // get all a-priori possibilities fn get_all_possibilities() -> Vec; @@ -137,7 +147,10 @@ pub trait Info where T: Hash + Eq + Clone + Copy { // get what is now possible fn get_possibilities(&self) -> Vec { - self.get_possibility_set().iter().map(|t| t.clone()).collect::>() + self.get_possibility_set() + .iter() + .map(|t| t.clone()) + .collect::>() } fn is_possible(&self, value: T) -> bool { @@ -145,8 +158,10 @@ pub trait Info where T: Hash + Eq + Clone + Copy { } fn initialize() -> HashSet { - Self::get_all_possibilities().iter() - .map(|val| val.clone()).collect::>() + Self::get_all_possibilities() + .iter() + .map(|val| val.clone()) + .collect::>() } fn mark_true(&mut self, value: T) { @@ -160,35 +175,55 @@ pub trait Info where T: Hash + Eq + Clone + Copy { } fn mark(&mut self, value: T, info: bool) { - if info { self.mark_true(value); } else { self.mark_false(value); } + if info { + self.mark_true(value); + } else { + self.mark_false(value); + } } } -#[derive(Debug,Clone)] +#[derive(Debug, Clone)] pub struct ColorInfo(HashSet); impl ColorInfo { - pub fn new() -> ColorInfo { ColorInfo(ColorInfo::initialize()) } + pub fn new() -> ColorInfo { + ColorInfo(ColorInfo::initialize()) + } } impl Info for ColorInfo { - fn get_all_possibilities() -> Vec { COLORS.to_vec() } - fn get_possibility_set(&self) -> &HashSet { &self.0 } - fn get_mut_possibility_set(&mut self) -> &mut HashSet { &mut self.0 } + fn get_all_possibilities() -> Vec { + COLORS.to_vec() + } + fn get_possibility_set(&self) -> &HashSet { + &self.0 + } + fn get_mut_possibility_set(&mut self) -> &mut HashSet { + &mut self.0 + } } -#[derive(Debug,Clone)] +#[derive(Debug, Clone)] pub struct ValueInfo(HashSet); impl ValueInfo { - pub fn new() -> ValueInfo { ValueInfo(ValueInfo::initialize()) } + pub fn new() -> ValueInfo { + ValueInfo(ValueInfo::initialize()) + } } impl Info for ValueInfo { - fn get_all_possibilities() -> Vec { VALUES.to_vec() } - fn get_possibility_set(&self) -> &HashSet { &self.0 } - fn get_mut_possibility_set(&mut self) -> &mut HashSet { &mut self.0 } + fn get_all_possibilities() -> Vec { + VALUES.to_vec() + } + fn get_possibility_set(&self) -> &HashSet { + &self.0 + } + fn get_mut_possibility_set(&mut self) -> &mut HashSet { + &mut self.0 + } } // represents information only of the form: // this color is/isn't possible, this value is/isn't possible -#[derive(Debug,Clone)] +#[derive(Debug, Clone)] pub struct SimpleCardInfo { pub color_info: ColorInfo, pub value_info: ValueInfo, @@ -211,13 +246,10 @@ impl CardInfo for SimpleCardInfo { v } fn is_possible(&self, card: &Card) -> bool { - self.color_info.is_possible(card.color) && - self.value_info.is_possible(card.value) - + self.color_info.is_possible(card.color) && self.value_info.is_possible(card.value) } fn mark_color_false(&mut self, color: Color) { self.color_info.mark_false(color); - } fn mark_value_false(&mut self, value: Value) { self.value_info.mark_false(value); @@ -246,7 +278,7 @@ impl fmt::Display for SimpleCardInfo { // Can represent information of the form: // this card is/isn't possible // also, maintains integer weights for the cards -#[derive(Clone,Debug,Eq,PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct CardPossibilityTable { possible: HashMap, } @@ -269,9 +301,10 @@ impl CardPossibilityTable { pub fn decrement_weight(&mut self, card: &Card) { let remove = { - let weight = - self.possible.get_mut(card) - .expect(&format!("Decrementing weight for impossible card: {}", card)); + let weight = self.possible.get_mut(card).expect(&format!( + "Decrementing weight for impossible card: {}", + card + )); *weight -= 1; *weight == 0 }; @@ -295,27 +328,35 @@ impl CardPossibilityTable { pub fn color_determined(&self) -> bool { self.get_possibilities() - .iter().map(|card| card.color) + .iter() + .map(|card| card.color) .collect::>() - .len() == 1 + .len() + == 1 } pub fn value_determined(&self) -> bool { self.get_possibilities() - .iter().map(|card| card.value) + .iter() + .map(|card| card.value) .collect::>() - .len() == 1 + .len() + == 1 } pub fn can_be_color(&self, color: Color) -> bool { - self.get_possibilities().into_iter().any(|card| card.color == color) + self.get_possibilities() + .into_iter() + .any(|card| card.color == color) } pub fn can_be_value(&self, value: Value) -> bool { - self.get_possibilities().into_iter().any(|card| card.value == value) + self.get_possibilities() + .into_iter() + .any(|card| card.value == value) } } -impl <'a> From<&'a CardCounts> for CardPossibilityTable { +impl<'a> From<&'a CardCounts> for CardPossibilityTable { fn from(counts: &'a CardCounts) -> CardPossibilityTable { let mut possible = HashMap::new(); for &color in COLORS.iter() { @@ -327,9 +368,7 @@ impl <'a> From<&'a CardCounts> for CardPossibilityTable { } } } - CardPossibilityTable { - possible: possible, - } + CardPossibilityTable { possible: possible } } } impl CardInfo for CardPossibilityTable { @@ -341,7 +380,11 @@ impl CardInfo for CardPossibilityTable { self.possible.contains_key(card) } fn get_possibilities(&self) -> Vec { - let mut cards = self.possible.keys().map(|card| {card.clone() }).collect::>(); + let mut cards = self + .possible + .keys() + .map(|card| card.clone()) + .collect::>(); cards.sort(); cards } @@ -349,7 +392,6 @@ impl CardInfo for CardPossibilityTable { for &value in VALUES.iter() { self.mark_false(&Card::new(color, value)); } - } fn mark_value_false(&mut self, value: Value) { for &color in COLORS.iter() { @@ -369,11 +411,17 @@ impl fmt::Display for CardPossibilityTable { } } -#[derive(Clone,Eq,PartialEq)] -pub struct HandInfo where T: CardInfo { - pub hand_info: Vec +#[derive(Clone, Eq, PartialEq)] +pub struct HandInfo +where + T: CardInfo, +{ + pub hand_info: Vec, } -impl HandInfo where T: CardInfo { +impl HandInfo +where + T: CardInfo, +{ pub fn new(hand_size: u32) -> Self { let hand_info = (0..hand_size).map(|_| T::new()).collect::>(); HandInfo { @@ -397,19 +445,35 @@ impl HandInfo where T: CardInfo { } } - pub fn remove(&mut self, index: usize) -> T { self.hand_info.remove(index) } - pub fn push(&mut self, card_info: T) { self.hand_info.push(card_info) } - pub fn iter_mut(&mut self) -> slice::IterMut { self.hand_info.iter_mut() } - pub fn iter(&self) -> slice::Iter { self.hand_info.iter() } - pub fn len(&self) -> usize { self.hand_info.len() } + pub fn remove(&mut self, index: usize) -> T { + self.hand_info.remove(index) + } + pub fn push(&mut self, card_info: T) { + self.hand_info.push(card_info) + } + pub fn iter_mut(&mut self) -> slice::IterMut { + self.hand_info.iter_mut() + } + pub fn iter(&self) -> slice::Iter { + self.hand_info.iter() + } + pub fn len(&self) -> usize { + self.hand_info.len() + } } -impl Index for HandInfo where T: CardInfo { +impl Index for HandInfo +where + T: CardInfo, +{ type Output = T; fn index(&self, index: usize) -> &T { &self.hand_info[index] } } -impl IndexMut for HandInfo where T: CardInfo { +impl IndexMut for HandInfo +where + T: CardInfo, +{ fn index_mut(&mut self, index: usize) -> &mut T { &mut self.hand_info[index] } diff --git a/src/json_output.rs b/src/json_output.rs index ab8d216..745c8ee 100644 --- a/src/json_output.rs +++ b/src/json_output.rs @@ -2,7 +2,10 @@ use game::*; use serde_json::*; fn color_value(color: &Color) -> usize { - COLORS.iter().position(|&card_color| &card_color == color).unwrap() + COLORS + .iter() + .position(|&card_color| &card_color == color) + .unwrap() } fn card_to_json(card: &Card) -> serde_json::Value { @@ -43,7 +46,11 @@ pub fn action_discard((i, _card): &AnnotatedCard) -> serde_json::Value { }) } -pub fn json_format(deck: &Cards, actions: &Vec, players: &Vec) -> serde_json::Value { +pub fn json_format( + deck: &Cards, + actions: &Vec, + players: &Vec, +) -> serde_json::Value { json!({ "variant": "No Variant", "players": players, diff --git a/src/main.rs b/src/main.rs index d8f2d0c..a0e2014 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,20 @@ extern crate getopts; #[macro_use] extern crate log; -extern crate rand; extern crate crossbeam; -extern crate fnv; extern crate float_ord; +extern crate fnv; +extern crate rand; extern crate serde_json; -mod helpers; mod game; +mod helpers; mod json_output; mod simulator; mod strategy; mod strategies { - pub mod examples; pub mod cheating; + pub mod examples; mod hat_helpers; pub mod information; } @@ -35,51 +35,71 @@ impl log::Log for SimpleLogger { } } - 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("j", "json-output", - "Pattern for the JSON output file. '%s' will be replaced by the seed.", - "FILE_PATTERN"); - 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"); - opts.optflag("", "losses-only", - "When saving JSON outputs, save lost games only"); + 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( + "j", + "json-output", + "Pattern for the JSON output file. '%s' will be replaced by the seed.", + "FILE_PATTERN", + ); + 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", + ); + opts.optflag( + "", + "losses-only", + "When saving JSON outputs, save lost games only", + ); let matches = match opts.parse(&args[1..]) { - Ok(m) => { m } + Ok(m) => m, Err(f) => { print_usage(&program, opts); panic!(f.to_string()) @@ -98,14 +118,14 @@ fn main() { return print!("{}", get_results_table()); } - let log_level_str : &str = &matches.opt_str("l").unwrap_or("info".to_string()); + 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 } - _ => { + "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); } @@ -114,38 +134,55 @@ fn main() { log::set_logger(|max_log_level| { max_log_level.set(log_level); Box::new(SimpleLogger) - }).unwrap(); + }) + .unwrap(); let n_trials = u32::from_str(&matches.opt_str("n").unwrap_or("1".to_string())).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 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").unwrap_or("1".to_string())).unwrap(); let json_output_pattern = matches.opt_str("j"); let json_losses_only = matches.opt_present("losses-only"); 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()); + 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, json_output_pattern, json_losses_only).info(); + sim_games( + n_players, + strategy_str, + seed, + n_trials, + n_threads, + progress_info, + json_output_pattern, + json_losses_only, + ) + .info(); } fn sim_games( - n_players: u32, - strategy_str: &str, - seed: Option, - n_trials: u32, - n_threads: u32, - progress_info: Option, - json_output_pattern: Option, - json_losses_only: bool, - ) -> simulator::SimResult { + n_players: u32, + strategy_str: &str, + seed: Option, + n_trials: u32, + n_threads: u32, + progress_info: Option, + json_output_pattern: Option, + json_losses_only: bool, +) -> 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); } + _ => { + panic!("There should be 2 to 5 players, not {}", n_players); + } }; let game_opts = game::GameOptions { @@ -157,26 +194,29 @@ fn sim_games( 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 - }, + 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, json_output_pattern, json_losses_only) + simulator::simulate( + &game_opts, + strategy_config, + seed, + n_trials, + n_threads, + progress_info, + json_output_pattern, + json_losses_only, + ) } fn get_results_table() -> String { @@ -186,39 +226,75 @@ fn get_results_table() -> String { 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 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("------------------"); + 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: &Vec, head: TwoLines, make_block: &dyn Fn(u32) -> TwoLines) -> TwoLines { - let mut blocks = player_nums.iter().cloned().map(make_block).collect::>(); + fn make_twolines( + player_nums: &Vec, + 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 + "|" }) + 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")) + 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_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, None, false); - ( - format_score(simresult.average_score(), simresult.score_stderr()), - format_percent(simresult.percent_perfect(), simresult.percent_perfect_stderr()) + let header = make_twolines( + &player_nums, + (space.clone(), dashes.clone()), + &|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, + None, + false, + ); + ( + format_score(simresult.average_score(), simresult.score_stderr()), + format_percent( + simresult.percent_perfect(), + simresult.percent_perfect_stderr(), + ), + ) + }, ) }) - }).collect::>(); + .collect::>(); body.insert(0, header); intro + &concat_twolines(body) } diff --git a/src/simulator.rs b/src/simulator.rs index 56b4d2e..d8ad866 100644 --- a/src/simulator.rs +++ b/src/simulator.rs @@ -1,11 +1,11 @@ -use rand::{self, Rng, SeedableRng}; -use fnv::FnvHashMap; -use std::fmt; use crossbeam; +use fnv::FnvHashMap; +use rand::{self, Rng, SeedableRng}; +use std::fmt; use game::*; -use strategy::*; use json_output::*; +use strategy::*; fn new_deck(seed: u32) -> Cards { let mut deck: Cards = Cards::new(); @@ -16,28 +16,32 @@ fn new_deck(seed: u32) -> Cards { deck.push(Card::new(color, value)); } } - }; + } rand::ChaChaRng::from_seed(&[seed]).shuffle(&mut deck[..]); debug!("Deck: {:?}", deck); deck } - pub fn simulate_once( - opts: &GameOptions, - game_strategy: Box, - seed: u32, - output_json: bool, - ) -> (GameState, Option) { - + opts: &GameOptions, + game_strategy: Box, + seed: u32, + output_json: bool, +) -> (GameState, Option) { let deck = new_deck(seed); let mut game = GameState::new(opts, deck.clone()); - let mut strategies = game.get_players().map(|player| { - (player, game_strategy.initialize(player, &game.get_view(player))) - }).collect::>>(); + let mut strategies = game + .get_players() + .map(|player| { + ( + player, + game_strategy.initialize(player, &game.get_view(player)), + ) + }) + .collect::>>(); let mut actions = Vec::new(); @@ -50,28 +54,24 @@ pub fn simulate_once( debug!("======================================================="); debug!("{}", game); - let choice = { let mut strategy = strategies.get_mut(&player).unwrap(); strategy.decide(&game.get_view(player)) }; if output_json { actions.push(match choice { - TurnChoice::Hint(ref hint) => { - action_clue(hint) - } + TurnChoice::Hint(ref hint) => action_clue(hint), TurnChoice::Play(index) => { let card = &game.hands[&player][index]; action_play(card) } - TurnChoice::Discard(index) => { + TurnChoice::Discard(index) => { let card = &game.hands[&player][index]; action_discard(card) } }); } - let turn = game.process_choice(choice); for player in game.get_players() { @@ -84,9 +84,10 @@ pub fn simulate_once( debug!("Final state:\n{}", game); debug!("SCORE: {:?}", game.score()); let json_output = if output_json { - let player_names = game.get_players().map(|player| { - strategies[&player].name() - }).collect(); + let player_names = game + .get_players() + .map(|player| strategies[&player].name()) + .collect(); Some(json_format(&deck, &actions, &player_names)) } else { None @@ -148,26 +149,25 @@ impl fmt::Display for Histogram { let mut keys = self.hist.keys().collect::>(); keys.sort(); for val in keys { - try!(f.write_str(&format!( - "\n{}: {}", val, self.get_count(val), - ))); + try!(f.write_str(&format!("\n{}: {}", val, self.get_count(val),))); } Ok(()) } } pub fn simulate( - opts: &GameOptions, - strat_config: Box, - first_seed_opt: Option, - n_trials: u32, - n_threads: u32, - progress_info: Option, - json_output_pattern: Option, - json_losses_only: bool, - ) -> SimResult - where T: GameStrategyConfig + Sync { - + opts: &GameOptions, + strat_config: Box, + first_seed_opt: Option, + n_trials: u32, + n_threads: u32, + progress_info: Option, + json_output_pattern: Option, + json_losses_only: bool, +) -> SimResult +where + T: GameStrategyConfig + Sync, +{ let first_seed = first_seed_opt.unwrap_or_else(|| rand::thread_rng().next_u32()); let strat_config_ref = &strat_config; @@ -176,7 +176,7 @@ pub fn simulate( let mut join_handles = Vec::new(); for i in 0..n_threads { let start = first_seed + ((n_trials * i) / n_threads); - let end = first_seed + ((n_trials * (i+1)) / n_threads); + let end = first_seed + ((n_trials * (i + 1)) / n_threads); join_handles.push(scope.spawn(move || { if progress_info.is_some() { info!("Thread {} spawned: seeds {} to {}", i, start, end); @@ -188,25 +188,33 @@ pub fn simulate( for seed in start..end { if let Some(progress_info_frequency) = progress_info { - if (seed > start) && ((seed-start) % progress_info_frequency == 0) { + if (seed > start) && ((seed - start) % progress_info_frequency == 0) { info!( "Thread {}, Trials: {}, Stats so far: {} score, {} lives, {}% win", - i, seed-start, score_histogram.average(), lives_histogram.average(), + i, + seed - start, + score_histogram.average(), + lives_histogram.average(), score_histogram.percentage_with(&PERFECT_SCORE) * 100.0 ); } } - let (game, json_output) = simulate_once(&opts, - strat_config_ref.initialize(&opts), - seed, - json_output_pattern_ref.is_some()); + let (game, json_output) = simulate_once( + &opts, + strat_config_ref.initialize(&opts), + seed, + json_output_pattern_ref.is_some(), + ); let score = game.score(); lives_histogram.insert(game.board.lives_remaining); score_histogram.insert(score); - if score != PERFECT_SCORE { non_perfect_seeds.push(seed); } + if score != PERFECT_SCORE { + non_perfect_seeds.push(seed); + } if let Some(file_pattern) = json_output_pattern_ref { if !(score == PERFECT_SCORE && json_losses_only) { - let file_pattern = file_pattern.clone().replace("%s", &seed.to_string()); + let file_pattern = + file_pattern.clone().replace("%s", &seed.to_string()); let path = std::path::Path::new(&file_pattern); let file = std::fs::File::create(path).unwrap(); serde_json::to_writer(file, &json_output.unwrap()).unwrap(); @@ -220,11 +228,12 @@ pub fn simulate( })); } - let mut non_perfect_seeds : Vec = Vec::new(); + let mut non_perfect_seeds: Vec = Vec::new(); let mut score_histogram = Histogram::new(); let mut lives_histogram = Histogram::new(); for join_handle in join_handles { - let (thread_non_perfect_seeds, thread_score_histogram, thread_lives_histogram) = join_handle.join(); + let (thread_non_perfect_seeds, thread_score_histogram, thread_lives_histogram) = + join_handle.join(); non_perfect_seeds.extend(thread_non_perfect_seeds.iter()); score_histogram.merge(thread_score_histogram); lives_histogram.merge(thread_lives_histogram); @@ -252,7 +261,7 @@ impl SimResult { pub fn percent_perfect_stderr(&self) -> f32 { let pp = self.percent_perfect() / 100.0; - let stdev = (pp*(1.0 - pp) / ((self.scores.total_count - 1) as f32)).sqrt(); + let stdev = (pp * (1.0 - pp) / ((self.scores.total_count - 1) as f32)).sqrt(); stdev * 100.0 } diff --git a/src/strategies/cheating.rs b/src/strategies/cheating.rs index c3dc0ef..0614301 100644 --- a/src/strategies/cheating.rs +++ b/src/strategies/cheating.rs @@ -1,9 +1,9 @@ -use std::rc::Rc; -use std::cell::{RefCell}; use fnv::{FnvHashMap, FnvHashSet}; +use std::cell::RefCell; +use std::rc::Rc; -use strategy::*; use game::*; +use strategy::*; // strategy that explicitly cheats by using Rc/RefCell // serves as a reference point for other strategies @@ -44,9 +44,9 @@ impl CheatingStrategy { impl GameStrategy for CheatingStrategy { fn initialize(&self, player: Player, view: &BorrowedGameView) -> Box { for (&player, &hand) in &view.other_hands { - self.player_hands_cheat.borrow_mut().insert( - player, hand.clone() - ); + self.player_hands_cheat + .borrow_mut() + .insert(player, hand.clone()); } Box::new(CheatingPlayerStrategy { player_hands_cheat: self.player_hands_cheat.clone(), @@ -64,9 +64,9 @@ impl CheatingPlayerStrategy { fn inform_last_player_cards(&self, view: &BorrowedGameView) { let next = view.board.player_to_right(&self.me); let their_hand = *view.other_hands.get(&next).unwrap(); - self.player_hands_cheat.borrow_mut().insert( - next, their_hand.clone() - ); + self.player_hands_cheat + .borrow_mut() + .insert(next, their_hand.clone()); } // give a throwaway hint - we only do this when we have nothing to do @@ -75,7 +75,7 @@ impl CheatingPlayerStrategy { let hint_card = &view.get_hand(&hint_player).first().unwrap(); TurnChoice::Hint(Hint { player: hint_player, - hinted: Hinted::Value(hint_card.value) + hinted: Hinted::Value(hint_card.value), }) } @@ -93,12 +93,14 @@ impl CheatingPlayerStrategy { // given a hand of cards, represents how badly it will need to play things fn hand_play_value(&self, view: &BorrowedGameView, hand: &Cards) -> u32 { - hand.iter().map(|card| self.card_play_value(view, card)).fold(0, |a,b| a+b) + hand.iter() + .map(|card| self.card_play_value(view, card)) + .fold(0, |a, b| a + b) } // how badly do we need to play a particular card fn get_play_score(&self, view: &BorrowedGameView, card: &Card) -> i32 { - let hands = self.player_hands_cheat.borrow(); + let hands = self.player_hands_cheat.borrow(); let my_hand = hands.get(&self.me).unwrap(); let my_hand_value = self.hand_play_value(view, my_hand); @@ -109,7 +111,7 @@ impl CheatingPlayerStrategy { let their_hand_value = self.hand_play_value(view, hands.get(&player).unwrap()); // they can play this card, and have less urgent plays than i do if their_hand_value < my_hand_value { - return 10 - (card.value as i32) + return 10 - (card.value as i32); } } } @@ -132,7 +134,7 @@ impl CheatingPlayerStrategy { } set.insert(card.clone()); } - return None + return None; } } impl PlayerStrategy for CheatingPlayerStrategy { @@ -144,9 +146,11 @@ impl PlayerStrategy for CheatingPlayerStrategy { let hands = self.player_hands_cheat.borrow(); let my_hand = hands.get(&self.me).unwrap(); - let playable_cards = my_hand.iter().enumerate().filter(|&(_, card)| { - view.board.is_playable(card) - }).collect::>(); + let playable_cards = my_hand + .iter() + .enumerate() + .filter(|&(_, card)| view.board.is_playable(card)) + .collect::>(); if playable_cards.len() > 0 { // play the best playable card @@ -161,15 +165,14 @@ impl PlayerStrategy for CheatingPlayerStrategy { play_score = score; } } - return TurnChoice::Play(index) + return TurnChoice::Play(index); } // discard threshold is how many cards we're willing to discard // such that if we only played, // we would not reach the final countdown round // e.g. 50 total, 25 to play, 20 in hand - let discard_threshold = - view.board.total_cards + let discard_threshold = view.board.total_cards - (COLORS.len() * VALUES.len()) as u32 - (view.board.num_players * view.board.hand_size); if view.board.discard_size() <= discard_threshold { @@ -211,6 +214,5 @@ impl PlayerStrategy for CheatingPlayerStrategy { } TurnChoice::Discard(index) } - fn update(&mut self, _: &TurnRecord, _: &BorrowedGameView) { - } + fn update(&mut self, _: &TurnRecord, _: &BorrowedGameView) {} } diff --git a/src/strategies/examples.rs b/src/strategies/examples.rs index 5b3462f..b818961 100644 --- a/src/strategies/examples.rs +++ b/src/strategies/examples.rs @@ -1,6 +1,6 @@ -use strategy::*; use game::*; use rand::{self, Rng}; +use strategy::*; // dummy, terrible strategy, as an example #[derive(Clone)] @@ -40,14 +40,19 @@ pub struct RandomStrategyPlayer { impl PlayerStrategy for RandomStrategyPlayer { fn name(&self) -> String { - format!("random(hint={}, play={})", self.hint_probability, self.play_probability) + format!( + "random(hint={}, play={})", + self.hint_probability, self.play_probability + ) } fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice { let p = rand::random::(); if p < self.hint_probability { if view.board.hints_remaining > 0 { let hint_player = view.board.player_to_left(&self.me); - let hint_card = rand::thread_rng().choose(&view.get_hand(&hint_player)).unwrap(); + let hint_card = rand::thread_rng() + .choose(&view.get_hand(&hint_player)) + .unwrap(); let hinted = { if rand::random() { // hint a color @@ -69,6 +74,5 @@ impl PlayerStrategy for RandomStrategyPlayer { TurnChoice::Discard(0) } } - fn update(&mut self, _: &TurnRecord, _: &BorrowedGameView) { - } + fn update(&mut self, _: &TurnRecord, _: &BorrowedGameView) {} } diff --git a/src/strategies/hat_helpers.rs b/src/strategies/hat_helpers.rs index 7550a5b..44c5697 100644 --- a/src/strategies/hat_helpers.rs +++ b/src/strategies/hat_helpers.rs @@ -1,7 +1,7 @@ use game::*; use helpers::*; -#[derive(Debug,Clone)] +#[derive(Debug, Clone)] pub struct ModulusInformation { pub modulus: u32, pub value: u32, @@ -82,22 +82,17 @@ pub trait Question { // get the answer to this question, given cards fn answer(&self, &Cards, &BoardState) -> u32; // process the answer to this question, updating card info - fn acknowledge_answer( - &self, value: u32, &mut HandInfo, &BoardState - ); + fn acknowledge_answer(&self, value: u32, &mut HandInfo, &BoardState); fn answer_info(&self, hand: &Cards, board: &BoardState) -> ModulusInformation { - ModulusInformation::new( - self.info_amount(), - self.answer(hand, board) - ) + ModulusInformation::new(self.info_amount(), self.answer(hand, board)) } fn acknowledge_answer_info( &self, answer: ModulusInformation, hand_info: &mut HandInfo, - board: &BoardState + board: &BoardState, ) { assert!(self.info_amount() == answer.modulus); self.acknowledge_answer(answer.value, hand_info, board); @@ -112,8 +107,7 @@ pub trait PublicInformation: Clone { fn set_board(&mut self, &BoardState); /// If we store more state than just `HandInfo`s, update it after `set_player_info` has been called. - fn update_other_info(&mut self) { - } + fn update_other_info(&mut self) {} fn agrees_with(&self, other: Self) -> bool; @@ -126,11 +120,19 @@ pub trait PublicInformation: Clone { /// /// Note that `self` does not reflect the answers to previous questions; it reflects the state /// before the entire "hat value" calculation. - fn ask_question(&self, &Player, &HandInfo, total_info: u32) -> Option>; + fn ask_question( + &self, + &Player, + &HandInfo, + total_info: u32, + ) -> Option>; - fn ask_question_wrapper(&self, player: &Player, hand_info: &HandInfo, total_info: u32) - -> Option> - { + fn ask_question_wrapper( + &self, + player: &Player, + hand_info: &HandInfo, + total_info: u32, + ) -> Option> { assert!(total_info > 0); if total_info == 1 { None @@ -138,8 +140,11 @@ pub trait PublicInformation: Clone { let result = self.ask_question(player, hand_info, total_info); if let Some(ref question) = result { if question.info_amount() > total_info { - panic!("ask_question returned question with info_amount = {} > total_info = {}!", - question.info_amount(), total_info); + panic!( + "ask_question returned question with info_amount = {} > total_info = {}!", + question.info_amount(), + total_info + ); } if question.info_amount() == 1 { panic!("ask_question returned a trivial question!"); @@ -157,11 +162,17 @@ pub trait PublicInformation: Clone { } fn get_hat_info_for_player( - &self, player: &Player, hand_info: &mut HandInfo, total_info: u32, view: &OwnedGameView + &self, + player: &Player, + hand_info: &mut HandInfo, + total_info: u32, + view: &OwnedGameView, ) -> ModulusInformation { assert!(player != &view.player); let mut answer_info = ModulusInformation::none(); - while let Some(question) = self.ask_question_wrapper(player, hand_info, answer_info.info_remaining(total_info)) { + while let Some(question) = + self.ask_question_wrapper(player, hand_info, answer_info.info_remaining(total_info)) + { let new_answer_info = question.answer_info(view.get_hand(player), view.get_board()); question.acknowledge_answer_info(new_answer_info.clone(), hand_info, view.get_board()); answer_info.combine(new_answer_info, total_info); @@ -188,18 +199,22 @@ pub trait PublicInformation: Clone { /// `self.get_hat_sum(total_info, view)` tells us which choice to take, and at the same time /// mutates `self` to simulate the choice becoming common knowledge. fn get_hat_sum(&mut self, total_info: u32, view: &OwnedGameView) -> ModulusInformation { - let (infos, new_player_hands): (Vec<_>, Vec<_>) = view.get_other_players().iter().map(|player| { - let mut hand_info = self.get_player_info(player); - let info = self.get_hat_info_for_player(player, &mut hand_info, total_info, view); - (info, (player.clone(), hand_info)) - }).unzip(); + let (infos, new_player_hands): (Vec<_>, Vec<_>) = view + .get_other_players() + .iter() + .map(|player| { + let mut hand_info = self.get_player_info(player); + let info = self.get_hat_info_for_player(player, &mut hand_info, total_info, view); + (info, (player.clone(), hand_info)) + }) + .unzip(); self.set_player_infos(new_player_hands); infos.into_iter().fold( ModulusInformation::new(total_info, 0), |mut sum_info, info| { sum_info.add(&info); sum_info - } + }, ) } @@ -208,13 +223,17 @@ pub trait PublicInformation: Clone { /// from that fact. fn update_from_hat_sum(&mut self, mut info: ModulusInformation, view: &OwnedGameView) { let info_source = view.board.player; - let (other_infos, mut new_player_hands): (Vec<_>, Vec<_>) = view.get_other_players().into_iter().filter(|player| { - *player != info_source - }).map(|player| { - let mut hand_info = self.get_player_info(&player); - let player_info = self.get_hat_info_for_player(&player, &mut hand_info, info.modulus, view); - (player_info, (player.clone(), hand_info)) - }).unzip(); + let (other_infos, mut new_player_hands): (Vec<_>, Vec<_>) = view + .get_other_players() + .into_iter() + .filter(|player| *player != info_source) + .map(|player| { + let mut hand_info = self.get_player_info(&player); + let player_info = + self.get_hat_info_for_player(&player, &mut hand_info, info.modulus, view); + (player_info, (player.clone(), hand_info)) + }) + .unzip(); for other_info in other_infos { info.subtract(&other_info); } diff --git a/src/strategies/information.rs b/src/strategies/information.rs index 3ca4bb2..ff5b57a 100644 --- a/src/strategies/information.rs +++ b/src/strategies/information.rs @@ -1,11 +1,11 @@ +use float_ord::*; use fnv::{FnvHashMap, FnvHashSet}; use std::cmp::Ordering; -use float_ord::*; -use strategy::*; use game::*; use helpers::*; use strategies::hat_helpers::*; +use strategy::*; // TODO: use random extra information - i.e. when casting up and down, // we sometimes have 2 choices of value to choose @@ -14,17 +14,21 @@ use strategies::hat_helpers::*; type PropertyPredicate = fn(&BoardState, &Card) -> bool; -struct CardHasProperty -{ +struct CardHasProperty { index: usize, property: PropertyPredicate, } -impl Question for CardHasProperty -{ - fn info_amount(&self) -> u32 { 2 } +impl Question for CardHasProperty { + fn info_amount(&self) -> u32 { + 2 + } fn answer(&self, hand: &Cards, board: &BoardState) -> u32 { let ref card = hand[self.index]; - if (self.property)(board, card) { 1 } else { 0 } + if (self.property)(board, card) { + 1 + } else { + 0 + } } fn acknowledge_answer( &self, @@ -36,18 +40,28 @@ impl Question for CardHasProperty let possible = card_table.get_possibilities(); for card in &possible { if (self.property)(board, card) { - if answer == 0 { card_table.mark_false(card); } + if answer == 0 { + card_table.mark_false(card); + } } else { - if answer == 1 { card_table.mark_false(card); } + if answer == 1 { + card_table.mark_false(card); + } } } } } fn q_is_playable(index: usize) -> CardHasProperty { - CardHasProperty {index, property: |board, card| board.is_playable(card)} + CardHasProperty { + index, + property: |board, card| board.is_playable(card), + } } fn q_is_dead(index: usize) -> CardHasProperty { - CardHasProperty {index, property: |board, card| board.is_dead(card)} + CardHasProperty { + index, + property: |board, card| board.is_dead(card), + } } /// For some list of questions l, the question `AdditiveComboQuestion { questions : l }` asks: @@ -62,7 +76,11 @@ struct AdditiveComboQuestion { } impl Question for AdditiveComboQuestion { fn info_amount(&self) -> u32 { - self.questions.iter().map(|q| { q.info_amount() - 1 }).sum::() + 1 + self.questions + .iter() + .map(|q| q.info_amount() - 1) + .sum::() + + 1 } fn answer(&self, hand: &Cards, board: &BoardState) -> u32 { let mut toadd = 1; @@ -88,7 +106,7 @@ impl Question for AdditiveComboQuestion { answer -= 1; for q in &self.questions { if answer < q.info_amount() - 1 { - q.acknowledge_answer(answer+1, hand_info, board); + q.acknowledge_answer(answer + 1, hand_info, board); return; } else { q.acknowledge_answer(0, hand_info, board); @@ -107,7 +125,10 @@ struct CardPossibilityPartition { } impl CardPossibilityPartition { fn new( - index: usize, max_n_partitions: u32, card_table: &CardPossibilityTable, board: &BoardState + index: usize, + max_n_partitions: u32, + card_table: &CardPossibilityTable, + board: &BoardState, ) -> CardPossibilityPartition { let mut cur_block = 0; let mut partition = FnvHashMap::default(); @@ -159,7 +180,9 @@ impl CardPossibilityPartition { } } impl Question for CardPossibilityPartition { - fn info_amount(&self) -> u32 { self.n_partitions } + fn info_amount(&self) -> u32 { + self.n_partitions + } fn answer(&self, hand: &Cards, _: &BoardState) -> u32 { let ref card = hand[self.index]; *self.partition.get(&card).unwrap() @@ -180,11 +203,11 @@ impl Question for CardPossibilityPartition { } } -#[derive(Eq,PartialEq,Clone)] +#[derive(Eq, PartialEq, Clone)] struct MyPublicInformation { hand_info: FnvHashMap>, card_counts: CardCounts, // what any newly drawn card should be - board: BoardState, // TODO: maybe we should store an appropriately lifetimed reference? + board: BoardState, // TODO: maybe we should store an appropriately lifetimed reference? } impl MyPublicInformation { @@ -197,7 +220,10 @@ impl MyPublicInformation { fn get_other_players_starting_after(&self, player: Player) -> Vec { let n = self.board.num_players; - (0 .. n - 1).into_iter().map(|i| { (player + 1 + i) % n }).collect() + (0..n - 1) + .into_iter() + .map(|i| (player + 1 + i) % n) + .collect() } // Returns the number of ways to hint the player. @@ -208,19 +234,19 @@ impl MyPublicInformation { let ref info = self.hand_info[&player]; - let may_be_all_one_color = COLORS.iter().any(|color| { - info.iter().all(|card| { - card.can_be_color(*color) - }) - }); + let may_be_all_one_color = COLORS + .iter() + .any(|color| info.iter().all(|card| card.can_be_color(*color))); - let may_be_all_one_number = VALUES.iter().any(|value| { - info.iter().all(|card| { - card.can_be_value(*value) - }) - }); + let may_be_all_one_number = VALUES + .iter() + .any(|value| info.iter().all(|card| card.can_be_value(*value))); - return if !may_be_all_one_color && !may_be_all_one_number { 4 } else { 3 } + return if !may_be_all_one_color && !may_be_all_one_number { + 4 + } else { + 3 + }; } fn get_hint_index_score(&self, card_table: &CardPossibilityTable) -> i32 { @@ -242,10 +268,14 @@ impl MyPublicInformation { } fn get_index_for_hint(&self, player: &Player) -> usize { - let mut scores = self.hand_info[player].iter().enumerate().map(|(i, card_table)| { - let score = self.get_hint_index_score(card_table); - (-score, i) - }).collect::>(); + let mut scores = self.hand_info[player] + .iter() + .enumerate() + .map(|(i, card_table)| { + let score = self.get_hint_index_score(card_table); + (-score, i) + }) + .collect::>(); scores.sort(); scores[0].1 } @@ -266,14 +296,18 @@ impl MyPublicInformation { // knowledge about the cards? let hinter = view.player; - let info_per_player: Vec<_> = self.get_other_players_starting_after(hinter).into_iter().map( - |player| { self.get_info_per_player(player) } - ).collect(); + let info_per_player: Vec<_> = self + .get_other_players_starting_after(hinter) + .into_iter() + .map(|player| self.get_info_per_player(player)) + .collect(); let total_info = info_per_player.iter().sum(); // FIXME explain and clean up - let card_indices: Vec<_> = self.get_other_players_starting_after(hinter).into_iter().map( - |player| { self.get_index_for_hint(&player) } - ).collect(); + let card_indices: Vec<_> = self + .get_other_players_starting_after(hinter) + .into_iter() + .map(|player| self.get_index_for_hint(&player)) + .collect(); let hint_info = self.get_hat_sum(total_info, view); @@ -352,53 +386,60 @@ impl MyPublicInformation { } } }; - hint_option_set.into_iter().collect::>().into_iter().map(|hinted| { - Hint { + hint_option_set + .into_iter() + .collect::>() + .into_iter() + .map(|hinted| Hint { player: hint_player, hinted: hinted, - } - }).collect() + }) + .collect() } fn decode_hint_choice(&self, hint: &Hint, result: &Vec) -> ModulusInformation { let hinter = self.board.player; - let info_per_player: Vec<_> = self.get_other_players_starting_after(hinter).into_iter().map( - |player| { self.get_info_per_player(player) } - ).collect(); + let info_per_player: Vec<_> = self + .get_other_players_starting_after(hinter) + .into_iter() + .map(|player| self.get_info_per_player(player)) + .collect(); let total_info = info_per_player.iter().sum(); let n = self.board.num_players; let player_amt = (n + hint.player - hinter - 1) % n; - let amt_from_prev_players = info_per_player.iter().take(player_amt as usize).fold(0, |a, b| a + b); + let amt_from_prev_players = info_per_player + .iter() + .take(player_amt as usize) + .fold(0, |a, b| a + b); let hint_info_we_can_give_to_this_player = info_per_player[player_amt as usize]; let card_index = self.get_index_for_hint(&hint.player); - let hint_type = - if hint_info_we_can_give_to_this_player == 3 { - if result[card_index] { - match hint.hinted { - Hinted::Value(_) => 0, - Hinted::Color(_) => 1, - } - } else { - 2 + let hint_type = if hint_info_we_can_give_to_this_player == 3 { + if result[card_index] { + match hint.hinted { + Hinted::Value(_) => 0, + Hinted::Color(_) => 1, } } else { - if result[card_index] { - match hint.hinted { - Hinted::Value(_) => 0, - Hinted::Color(_) => 1, - } - } else { - match hint.hinted { - Hinted::Value(_) => 2, - Hinted::Color(_) => 3, - } + 2 + } + } else { + if result[card_index] { + match hint.hinted { + Hinted::Value(_) => 0, + Hinted::Color(_) => 1, } - }; + } else { + match hint.hinted { + Hinted::Value(_) => 2, + Hinted::Color(_) => 3, + } + } + }; let hint_value = amt_from_prev_players + hint_type; @@ -416,17 +457,18 @@ impl MyPublicInformation { } fn knows_playable_card(&self, player: &Player) -> bool { - self.hand_info[player].iter().any(|table| { - table.probability_is_playable(&self.board) == 1.0 - }) + self.hand_info[player] + .iter() + .any(|table| table.probability_is_playable(&self.board) == 1.0) } fn someone_else_needs_hint(&self, view: &OwnedGameView) -> bool { // Does another player have a playable card, but doesn't know it? view.get_other_players().iter().any(|player| { - let has_playable_card = view.get_hand(&player).iter().any(|card| { - view.get_board().is_playable(card) - }); + let has_playable_card = view + .get_hand(&player) + .iter() + .any(|card| view.get_board().is_playable(card)); has_playable_card && !self.knows_playable_card(&player) }) } @@ -457,7 +499,7 @@ impl MyPublicInformation { new_view: &BorrowedGameView, player: &Player, index: usize, - card: &Card + card: &Card, ) { let new_card_table = CardPossibilityTable::from(&self.card_counts); { @@ -486,10 +528,13 @@ impl MyPublicInformation { impl PublicInformation for MyPublicInformation { fn new(board: &BoardState) -> Self { - let hand_info = board.get_players().map(|player| { - let hand_info = HandInfo::new(board.hand_size); - (player, hand_info) - }).collect::>(); + let hand_info = board + .get_players() + .map(|player| { + let hand_info = HandInfo::new(board.hand_size); + (player, hand_info) + }) + .collect::>(); MyPublicInformation { hand_info: hand_info, card_counts: CardCounts::new(), @@ -522,33 +567,58 @@ impl PublicInformation for MyPublicInformation { // Changing anything inside this function will not break the information transfer // mechanisms! - let augmented_hand_info_raw = hand_info.iter().cloned().enumerate().filter_map(|(i, card_table)| { - let p_play = card_table.probability_is_playable(&self.board); - let p_dead = card_table.probability_is_dead(&self.board); - Some((i, p_play, p_dead)) - }).collect::>(); - let know_playable_card = augmented_hand_info_raw.iter().any(|&(_, p_play, _)| p_play == 1.0); - let know_dead_card = augmented_hand_info_raw.iter().any(|&(_, _, p_dead)| p_dead == 1.0); + let augmented_hand_info_raw = hand_info + .iter() + .cloned() + .enumerate() + .filter_map(|(i, card_table)| { + let p_play = card_table.probability_is_playable(&self.board); + let p_dead = card_table.probability_is_dead(&self.board); + Some((i, p_play, p_dead)) + }) + .collect::>(); + let know_playable_card = augmented_hand_info_raw + .iter() + .any(|&(_, p_play, _)| p_play == 1.0); + let know_dead_card = augmented_hand_info_raw + .iter() + .any(|&(_, _, p_dead)| p_dead == 1.0); // We don't need to find out anything about cards that are determined or dead. - let augmented_hand_info = augmented_hand_info_raw.into_iter().filter(|&(i, _, p_dead)| { - if p_dead == 1.0 { false } - else if hand_info[i].is_determined() { false } - else { true } - }).collect::>(); + let augmented_hand_info = augmented_hand_info_raw + .into_iter() + .filter(|&(i, _, p_dead)| { + if p_dead == 1.0 { + false + } else if hand_info[i].is_determined() { + false + } else { + true + } + }) + .collect::>(); if !know_playable_card { // Vector of tuples (ask_dead, i, p_yes), where ask_dead=false means we'll // ask if the card at i is playable, and ask_dead=true means we ask if the card at i is // dead. p_yes is the probability the answer is nonzero. - let mut to_ask: Vec<(bool, usize, f32)> = augmented_hand_info.iter().filter_map(|&(i, p_play, _)| { - if p_play == 0.0 { None } - else { Some((false, i, p_play)) } - }).collect(); + let mut to_ask: Vec<(bool, usize, f32)> = augmented_hand_info + .iter() + .filter_map(|&(i, p_play, _)| { + if p_play == 0.0 { + None + } else { + Some((false, i, p_play)) + } + }) + .collect(); if !know_dead_card { to_ask.extend(augmented_hand_info.iter().filter_map(|&(i, _, p_dead)| { - if p_dead == 0.0 { None } - else { Some((true, i, p_dead)) } + if p_dead == 0.0 { + None + } else { + Some((true, i, p_dead)) + } })); } @@ -556,7 +626,7 @@ impl PublicInformation for MyPublicInformation { if to_ask.len() > combo_question_capacity { // The questions don't fit into an AdditiveComboQuestion. // Sort by type (ask_dead=false first), then by p_yes (bigger first) - to_ask.sort_by_key(|&(ask_dead, _, p_yes)| {(ask_dead, FloatOrd(-p_yes))}); + to_ask.sort_by_key(|&(ask_dead, _, p_yes)| (ask_dead, FloatOrd(-p_yes))); to_ask.truncate(combo_question_capacity); } @@ -564,20 +634,28 @@ impl PublicInformation for MyPublicInformation { // better to put lower-probability-of-playability/death cards first: The difference // only matters if we find a playable/dead card, and conditional on that, it's better // to find out about as many non-playable/non-dead cards as possible. - to_ask.sort_by_key(|&(ask_dead, _, p_yes)| {(ask_dead, FloatOrd(p_yes))}); - let questions = to_ask.into_iter().map(|(ask_dead, i, _)| -> Box { - if ask_dead { Box::new(q_is_dead(i)) } - else { Box::new(q_is_playable(i)) } - }).collect::>(); + to_ask.sort_by_key(|&(ask_dead, _, p_yes)| (ask_dead, FloatOrd(p_yes))); + let questions = to_ask + .into_iter() + .map(|(ask_dead, i, _)| -> Box { + if ask_dead { + Box::new(q_is_dead(i)) + } else { + Box::new(q_is_playable(i)) + } + }) + .collect::>(); if questions.len() > 0 { - return Some(Box::new(AdditiveComboQuestion { questions })) + return Some(Box::new(AdditiveComboQuestion { questions })); } } - let ask_play_score = |p_play: f32| FloatOrd((p_play-0.7).abs()); - let mut ask_play = augmented_hand_info.iter().filter(|&&(_, p_play, _)| { - ask_play_score(p_play) < FloatOrd(0.2) - }).cloned().collect::>(); + let ask_play_score = |p_play: f32| FloatOrd((p_play - 0.7).abs()); + let mut ask_play = augmented_hand_info + .iter() + .filter(|&&(_, p_play, _)| ask_play_score(p_play) < FloatOrd(0.2)) + .cloned() + .collect::>(); ask_play.sort_by_key(|&(i, p_play, _)| (ask_play_score(p_play), i)); if let Some(&(i, _, _)) = ask_play.get(0) { return Some(Box::new(q_is_playable(i))); @@ -585,9 +663,7 @@ impl PublicInformation for MyPublicInformation { let mut ask_partition = augmented_hand_info; // sort by probability of death (lowest first), then by index - ask_partition.sort_by_key(|&(i, _, p_death)| { - (FloatOrd(p_death), i) - }); + ask_partition.sort_by_key(|&(i, _, p_death)| (FloatOrd(p_death), i)); if let Some(&(i, _, _)) = ask_partition.get(0) { let question = CardPossibilityPartition::new(i, total_info, &hand_info[i], &self.board); Some(Box::new(question)) @@ -597,8 +673,6 @@ impl PublicInformation for MyPublicInformation { } } - - pub struct InformationStrategyConfig; impl InformationStrategyConfig { @@ -641,8 +715,12 @@ pub struct InformationPlayerStrategy { impl InformationPlayerStrategy { // how badly do we need to play a particular card - fn get_average_play_score(&self, view: &OwnedGameView, card_table: &CardPossibilityTable) -> f32 { - let f = |card: &Card| { self.get_play_score(view, card) }; + fn get_average_play_score( + &self, + view: &OwnedGameView, + card_table: &CardPossibilityTable, + ) -> f32 { + let f = |card: &Card| self.get_play_score(view, card); card_table.weighted_score(&f) } @@ -660,7 +738,11 @@ impl InformationPlayerStrategy { (10.0 - card.value as f32) / (num_with as f32) } - fn find_useless_cards(&self, board: &BoardState, hand: &HandInfo) -> Vec { + fn find_useless_cards( + &self, + board: &BoardState, + hand: &HandInfo, + ) -> Vec { let mut useless: FnvHashSet = FnvHashSet::default(); let mut seen: FnvHashMap = FnvHashMap::default(); @@ -679,7 +761,7 @@ impl InformationPlayerStrategy { } } } - let mut useless_vec : Vec = useless.into_iter().collect(); + let mut useless_vec: Vec = useless.into_iter().collect(); useless_vec.sort(); return useless_vec; } @@ -706,12 +788,8 @@ impl InformationPlayerStrategy { } let old_weight = card_table.total_weight(); match *hinted { - Hinted::Color(color) => { - card_table.mark_color(color, color == card.color) - } - Hinted::Value(value) => { - card_table.mark_value(value, value == card.value) - } + Hinted::Color(color) => card_table.mark_color(color, color == card.color), + Hinted::Value(value) => card_table.mark_value(value, value == card.value), }; let new_weight = card_table.total_weight(); assert!(new_weight <= old_weight); @@ -736,13 +814,12 @@ impl InformationPlayerStrategy { let view = &self.last_view; // using hint goodness barely helps - let mut hint_options = hints.into_iter().map(|hint| { - (self.hint_goodness(&hint, view), hint) - }).collect::>(); + let mut hint_options = hints + .into_iter() + .map(|hint| (self.hint_goodness(&hint, view), hint)) + .collect::>(); - hint_options.sort_by(|h1, h2| { - h2.0.partial_cmp(&h1.0).unwrap_or(Ordering::Equal) - }); + hint_options.sort_by(|h1, h2| h2.0.partial_cmp(&h1.0).unwrap_or(Ordering::Equal)); if hint_options.len() == 0 { // NOTE: Technically possible, but never happens @@ -765,7 +842,7 @@ impl InformationPlayerStrategy { let me = &view.player; for player in view.board.get_players() { - let hand_info = public_info.get_player_info(&player); + let hand_info = public_info.get_player_info(&player); debug!("Current state of hand_info for {}:", player); for (i, card_table) in hand_info.iter().enumerate() { debug!(" Card {}: {}", i, card_table); @@ -780,39 +857,46 @@ impl InformationPlayerStrategy { // If possible, play the best playable card // the higher the play_score, the better to play - let mut playable_cards = private_info.iter().enumerate().filter_map(|(i, card_table)| { - if card_table.probability_is_playable(&view.board) != 1.0 { return None; } - Some((i, self.get_average_play_score(view, card_table))) - }).collect::>(); + let mut playable_cards = private_info + .iter() + .enumerate() + .filter_map(|(i, card_table)| { + if card_table.probability_is_playable(&view.board) != 1.0 { + return None; + } + Some((i, self.get_average_play_score(view, card_table))) + }) + .collect::>(); playable_cards.sort_by_key(|&(i, play_score)| (FloatOrd(-play_score), i)); if let Some(&(play_index, _)) = playable_cards.get(0) { - return TurnChoice::Play(play_index) + return TurnChoice::Play(play_index); } - let discard_threshold = - view.board.total_cards + let discard_threshold = view.board.total_cards - (COLORS.len() * VALUES.len()) as u32 - (view.board.num_players * view.board.hand_size); // make a possibly risky play // TODO: consider removing this, if we improve information transfer - if view.board.lives_remaining > 1 && - view.board.discard_size() <= discard_threshold - { - let mut risky_playable_cards = private_info.iter().enumerate().filter(|&(_, card_table)| { - // card is either playable or dead - card_table.probability_of_predicate(&|card| { - view.board.is_playable(card) || view.board.is_dead(card) - }) == 1.0 - }).map(|(i, card_table)| { - let p = card_table.probability_is_playable(&view.board); - (i, card_table, p) - }).collect::>(); + if view.board.lives_remaining > 1 && view.board.discard_size() <= discard_threshold { + let mut risky_playable_cards = private_info + .iter() + .enumerate() + .filter(|&(_, card_table)| { + // card is either playable or dead + card_table.probability_of_predicate(&|card| { + view.board.is_playable(card) || view.board.is_dead(card) + }) == 1.0 + }) + .map(|(i, card_table)| { + let p = card_table.probability_is_playable(&view.board); + (i, card_table, p) + }) + .collect::>(); if risky_playable_cards.len() > 0 { - risky_playable_cards.sort_by(|c1, c2| { - c2.2.partial_cmp(&c1.2).unwrap_or(Ordering::Equal) - }); + risky_playable_cards + .sort_by(|c1, c2| c2.2.partial_cmp(&c1.2).unwrap_or(Ordering::Equal)); let maybe_play = risky_playable_cards[0]; if maybe_play.2 > 0.75 { @@ -821,19 +905,28 @@ impl InformationPlayerStrategy { } } - let public_useless_indices = self.find_useless_cards(&view.board, &public_info.get_player_info(me)); + let public_useless_indices = + self.find_useless_cards(&view.board, &public_info.get_player_info(me)); let useless_indices = self.find_useless_cards(&view.board, &private_info); // NOTE When changing this, make sure to keep the "discard" branch of update() up to date! let will_hint = - if view.board.hints_remaining > 0 && public_info.someone_else_needs_hint(view) { true } - else if view.board.discard_size() <= discard_threshold && useless_indices.len() > 0 { false } + if view.board.hints_remaining > 0 && public_info.someone_else_needs_hint(view) { + true + } else if view.board.discard_size() <= discard_threshold && useless_indices.len() > 0 { + false + } // hinting is better than discarding dead cards // (probably because it stalls the deck-drawing). - else if view.board.hints_remaining > 0 && view.someone_else_can_play() { true } - else if view.board.hints_remaining > 4 { true } + else if view.board.hints_remaining > 0 && view.someone_else_can_play() { + true + } else if view.board.hints_remaining > 4 { + true + } // this is the only case in which we discard a potentially useful card. - else { false }; + else { + false + }; if will_hint { let hint_set = public_info.get_hint(view); @@ -856,16 +949,18 @@ impl InformationPlayerStrategy { } // Make the least risky discard. - let mut cards_by_discard_value = private_info.iter().enumerate().map(|(i, card_table)| { - let probability_is_seen = card_table.probability_of_predicate(&|card| { - view.can_see(card) - }); - let compval = - 20.0 * probability_is_seen - + 10.0 * card_table.probability_is_dispensable(&view.board) - + card_table.average_value(); - (i, compval) - }).collect::>(); + let mut cards_by_discard_value = private_info + .iter() + .enumerate() + .map(|(i, card_table)| { + let probability_is_seen = + card_table.probability_of_predicate(&|card| view.can_see(card)); + let compval = 20.0 * probability_is_seen + + 10.0 * card_table.probability_is_dispensable(&view.board) + + card_table.average_value(); + (i, compval) + }) + .collect::>(); cards_by_discard_value.sort_by_key(|&(i, compval)| (FloatOrd(-compval), i)); let (index, _) = cards_by_discard_value[0]; TurnChoice::Discard(index) @@ -885,13 +980,15 @@ impl InformationPlayerStrategy { hint_matches: Option<&Vec>, ) { match turn_choice { - TurnChoice::Hint(ref hint) => { + TurnChoice::Hint(ref hint) => { let matches = hint_matches.unwrap(); - self.public_info.update_from_hint_choice(hint, matches, &self.last_view); + self.public_info + .update_from_hint_choice(hint, matches, &self.last_view); } TurnChoice::Discard(index) => { let known_useless_indices = self.find_useless_cards( - &self.last_view.board, &self.public_info.get_player_info(turn_player) + &self.last_view.board, + &self.public_info.get_player_info(turn_player), ); if self.last_view.board.hints_remaining > 0 { @@ -900,8 +997,12 @@ impl InformationPlayerStrategy { if known_useless_indices.len() > 1 { // unwrap is safe because *if* a discard happened, and there were known // dead cards, it must be a dead card - let value = known_useless_indices.iter().position(|&i| i == *index).unwrap(); - let info = ModulusInformation::new(known_useless_indices.len() as u32, value as u32); + let value = known_useless_indices + .iter() + .position(|&i| i == *index) + .unwrap(); + let info = + ModulusInformation::new(known_useless_indices.len() as u32, value as u32); self.public_info.update_from_hat_sum(info, &self.last_view); } } @@ -927,37 +1028,57 @@ impl PlayerStrategy for InformationPlayerStrategy { fn update(&mut self, turn_record: &TurnRecord, view: &BorrowedGameView) { let hint_matches = if let &TurnResult::Hint(ref matches) = &turn_record.result { Some(matches) - } else { None }; + } else { + None + }; self.update_wrapped(&turn_record.player, &turn_record.choice, hint_matches); if let Some(new_public_info) = self.new_public_info.take() { if !self.public_info.agrees_with(new_public_info) { - panic!("The change made to public_info in self.decide_wrapped differs from \ - the corresponding change in self.update_wrapped!"); + panic!( + "The change made to public_info in self.decide_wrapped differs from \ + the corresponding change in self.update_wrapped!" + ); } } match turn_record.choice { - TurnChoice::Hint(ref hint) => { + TurnChoice::Hint(ref hint) => { if let &TurnResult::Hint(ref matches) = &turn_record.result { self.public_info.update_from_hint_matches(hint, matches); } else { - panic!("Got turn choice {:?}, but turn result {:?}", - turn_record.choice, turn_record.result); + panic!( + "Got turn choice {:?}, but turn result {:?}", + turn_record.choice, turn_record.result + ); } } TurnChoice::Discard(index) => { if let &TurnResult::Discard(ref card) = &turn_record.result { - self.public_info.update_from_discard_or_play_result(view, &turn_record.player, index, card); + self.public_info.update_from_discard_or_play_result( + view, + &turn_record.player, + index, + card, + ); } else { - panic!("Got turn choice {:?}, but turn result {:?}", - turn_record.choice, turn_record.result); + panic!( + "Got turn choice {:?}, but turn result {:?}", + turn_record.choice, turn_record.result + ); } } - TurnChoice::Play(index) => { + TurnChoice::Play(index) => { if let &TurnResult::Play(ref card, _) = &turn_record.result { - self.public_info.update_from_discard_or_play_result(view, &turn_record.player, index, card); + self.public_info.update_from_discard_or_play_result( + view, + &turn_record.player, + index, + card, + ); } else { - panic!("Got turn choice {:?}, but turn result {:?}", - turn_record.choice, turn_record.result); + panic!( + "Got turn choice {:?}, but turn result {:?}", + turn_record.choice, turn_record.result + ); } } } diff --git a/src/strategy.rs b/src/strategy.rs index 3ba501b..f9d8620 100644 --- a/src/strategy.rs +++ b/src/strategy.rs @@ -28,4 +28,3 @@ pub trait GameStrategy { pub trait GameStrategyConfig { fn initialize(&self, &GameOptions) -> Box; } - From e040cdedc6288988f419df520e53949176fba4d1 Mon Sep 17 00:00:00 2001 From: timotree3 Date: Mon, 13 Jun 2022 09:56:31 -0400 Subject: [PATCH 4/6] Update json_ouput for latest site --- .gitignore | 1 + src/json_output.rs | 36 ++++++++++++++++++++---------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 7d0d0d0..79e9249 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target *.sw* +/replays/ \ No newline at end of file diff --git a/src/json_output.rs b/src/json_output.rs index 9f2551b..4beb72f 100644 --- a/src/json_output.rs +++ b/src/json_output.rs @@ -11,37 +11,39 @@ fn color_value(color: &Color) -> usize { fn card_to_json(card: &Card) -> serde_json::Value { json!({ "rank": card.value, - "suit": color_value(&card.color), + "suitIndex": color_value(&card.color), }) } pub fn action_clue(hint: &Hint) -> serde_json::Value { - json!({ - "type": 0, - "target": hint.player, - "clue": match hint.hinted { - Hinted::Value(value) => { json!({ - "type": 0, - "value": value, - }) } - Hinted::Color(color) => { json!({ - "type": 1, + match hint.hinted { + Hinted::Color(color) => { + json!({ + "type": 2, + "target": hint.player, "value": color_value(&color), - }) } + }) } - }) + Hinted::Value(value) => { + json!({ + "type": 3, + "target": hint.player, + "value": value, + }) + } + } } pub fn action_play((i, _card): &AnnotatedCard) -> serde_json::Value { json!({ - "type": 1, + "type": 0, "target": i, }) } pub fn action_discard((i, _card): &AnnotatedCard) -> serde_json::Value { json!({ - "type": 2, + "type": 1, "target": i, }) } @@ -52,7 +54,9 @@ pub fn json_format( players: &Vec, ) -> serde_json::Value { json!({ - "variant": "No Variant", + "options": { + "variant": "No Variant", + }, "players": players, "first_player": 0, "notes": players.iter().map(|_player| {json!([])}).collect::>(), // TODO add notes From eb623454d70c18786014b3593e887b993364f311 Mon Sep 17 00:00:00 2001 From: timotree3 Date: Thu, 19 Jan 2023 23:30:38 -0500 Subject: [PATCH 5/6] Add /replays/ to gitignore for local use --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 79e9249..f4812e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ target *.sw* -/replays/ \ No newline at end of file + +# Developers may wish to generate JSON replays of bot games and store them in /replays/ +/replays/ From 3eb08934749e4758a04f7fc8f2cccd83a0b1fda8 Mon Sep 17 00:00:00 2001 From: timotree3 Date: Thu, 19 Jan 2023 23:30:55 -0500 Subject: [PATCH 6/6] Fix warnings --- src/game.rs | 4 ++-- src/simulator.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/game.rs b/src/game.rs index d06ce4d..bbe80a7 100644 --- a/src/game.rs +++ b/src/game.rs @@ -589,7 +589,7 @@ impl GameState { .collect::>(); let unannotated_hands = hands .iter() - .map(|(player, hand)| (player.clone(), strip_annotations(hand))) + .map(|(player, hand)| (*player, strip_annotations(hand))) .collect::>(); GameState { @@ -629,7 +629,7 @@ impl GameState { } fn update_player_hand(&mut self) { - let player = self.board.player.clone(); + let player = self.board.player; self.unannotated_hands .insert(player, strip_annotations(self.hands.get(&player).unwrap())); } diff --git a/src/simulator.rs b/src/simulator.rs index 84727a1..3467ef5 100644 --- a/src/simulator.rs +++ b/src/simulator.rs @@ -199,8 +199,8 @@ where } } let (game, json_output) = simulate_once( - &opts, - strat_config_ref.initialize(&opts), + opts, + strat_config_ref.initialize(opts), seed, json_output_pattern_ref.is_some(), );