diff --git a/Cargo.lock b/Cargo.lock index 3f860e5..fb1192b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,91 +1,183 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "crossbeam" -version = "0.2.8" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd66663db5a988098a89599d4857919b3acf7f61402e61365acfd3919857b9be" [[package]] name = "float-ord" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e" [[package]] name = "fnv" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "getopts" -version = "0.2.14" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] [[package]] name = "itoa" -version = "0.4.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[package]] name = "libc" -version = "0.2.7" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" [[package]] name = "log" -version = "0.3.5" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" dependencies = [ - "libc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.17", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", ] [[package]] name = "rand" -version = "0.3.14" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" dependencies = [ - "libc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", ] [[package]] name = "rust_hanabi" version = "0.1.0" dependencies = [ - "crossbeam 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "float-ord 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "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)", + "crossbeam", + "float-ord", + "fnv", + "getopts", + "log 0.3.9", + "rand 0.3.23", + "serde_json", ] [[package]] name = "ryu" -version = "0.2.7" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" [[package]] name = "serde" -version = "1.0.88" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" [[package]] name = "serde_json" -version = "1.0.38" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" 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)", + "itoa", + "ryu", + "serde", ] -[metadata] -"checksum crossbeam 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "348228ce9f93d20ffc30c18e575f82fa41b9c8bf064806c65d41eba4771595a0" -"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" +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 5f22458..dc3c7eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,12 +2,13 @@ name = "rust_hanabi" version = "0.1.0" authors = ["Jeff Wu "] +edition = "2021" [dependencies] -rand = "*" -log = "*" -getopts = "*" -fnv = "*" -float-ord = "*" +rand = "0.3.0" +log = "0.3.0" +getopts = "0.2.14" +fnv = "1.0.0" +float-ord = "0.2.0" crossbeam = "0.2.5" serde_json = "*" diff --git a/README.md b/README.md index 1802d63..0ee7365 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,14 @@ Hanabi is a cooperative card game of incomplete information. Despite relatively [simple rules](https://boardgamegeek.com/article/10670613#10670613), the space of Hanabi strategies is quite interesting. -This project provides a framework for implementing Hanabi strategies in Rust. -It also explores some implementations, based on ideas from -[this paper](https://d0474d97-a-62cb3a1a-s-sites.googlegroups.com/site/rmgpgrwc/research-papers/Hanabi_final.pdf). -In particular, it contains an improved version of their "information strategy", -which achieves the best results I'm aware of for games with more than 2 players ([see below](#results)). +This project provides a framework for implementing Hanabi strategies in Rust, and also implements extremely strong strategies. + +The best strategy is based on the "information strategy" from +[this paper](https://d0474d97-a-62cb3a1a-s-sites.googlegroups.com/site/rmgpgrwc/research-papers/Hanabi_final.pdf). See results ([below](#results)). +It held state-of-the-art results (from March 2016) until December 2019, when [researchers at Facebook](https://arxiv.org/abs/1912.02318) surpassed it by extending the idea further with explicit search. Please feel free to contact me about Hanabi strategies, or this framework. -Most similar projects I am aware of: -- https://github.com/rjtobin/HanSim (written for the paper mentioned above) -- https://github.com/Quuxplusone/Hanabi - ## Setup Install rust (rustc and cargo), and clone this git repo. @@ -75,3 +71,13 @@ On the first 20000 seeds, we have these scores and win rates (average ± standar | | 90.59 ± 0.21 % | 98.17 ± 0.09 % | 97.76 ± 0.10 % | 96.42 ± 0.13 % | | info | 22.5194 ± 0.0125 | 24.7942 ± 0.0039 | 24.9354 ± 0.0022 | 24.9220 ± 0.0024 | | | 12.58 ± 0.23 % | 84.46 ± 0.26 % | 95.03 ± 0.15 % | 94.01 ± 0.17 % | + +## Other work + +Most similar projects I am aware of: +- https://github.com/rjtobin/HanSim (written for the paper mentioned above which introduces the information strategy) +- https://github.com/Quuxplusone/Hanabi + +Some researchers are trying to solve Hanabi using machine learning techniques: +- [Initial paper](https://arxiv.org/abs/1902.00506) from DeepMind and Google Brain researchers. See [this Wall Street Journal coverage](https://www.wsj.com/articles/why-the-card-game-hanabi-is-the-next-big-hurdle-for-artificial-intelligence-11553875351) +- [This paper](https://arxiv.org/abs/1912.02318) from Facebook, code at https://github.com/facebookresearch/Hanabi_SPARTA which includes their machine-learned agent diff --git a/src/game.rs b/src/game.rs index 43f1359..d06ce4d 100644 --- a/src/game.rs +++ b/src/game.rs @@ -20,7 +20,7 @@ pub fn get_count_for_value(value: Value) -> u32 { 2 | 3 | 4 => 2, 5 => 1, _ => { - panic!(format!("Unexpected value: {}", value)); + panic!("Unexpected value: {value}"); } } } @@ -32,10 +32,7 @@ pub struct Card { } impl Card { pub fn new(color: Color, value: Value) -> Card { - Card { - color: color, - value: value, - } + Card { color, value } } } impl fmt::Display for Card { @@ -61,7 +58,7 @@ impl CardCounts { counts.insert(Card::new(color, value), 0); } } - CardCounts { counts: counts } + CardCounts { counts } } pub fn get_count(&self, card: &Card) -> u32 { @@ -81,16 +78,16 @@ 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,))); + write!(f, "{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))); + write!(f, "{count}/{total} {value}s")?; if value != FINAL_VALUE { - try!(f.write_str(", ")); + f.write_str(", ")?; } } - try!(f.write_str("\n")); + f.write_str("\n")?; } Ok(()) } @@ -126,9 +123,7 @@ impl Discard { } impl fmt::Display for Discard { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // try!(f.write_str(&format!( - // "{}", self.cards, - // ))); + // write!(f, "{}", self.cards)?; write!(f, "{}", self.counts) } } @@ -143,10 +138,7 @@ pub struct Firework { } impl Firework { pub fn new(color: Color) -> Firework { - Firework { - color: color, - top: 0, - } + Firework { color, top: 0 } } pub fn needed_value(&self) -> Option { @@ -194,12 +186,12 @@ 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) + match *self { + Hinted::Color(color) => { + write!(f, "{color}") } - &Hinted::Value(value) => { - write!(f, "{}", value) + Hinted::Value(value) => { + write!(f, "{value}") } } } @@ -282,9 +274,9 @@ impl BoardState { .collect::>(); BoardState { - deck_size: deck_size, + deck_size, total_cards: deck_size, - fireworks: fireworks, + fireworks, discard: Discard::new(), num_players: opts.num_players, hand_size: opts.hand_size, @@ -340,52 +332,28 @@ impl BoardState { return value - 1; } } - return FINAL_VALUE; + FINAL_VALUE } // is never going to play, based on discard + fireworks pub fn is_dead(&self, card: &Card) -> bool { let firework = self.fireworks.get(&card.color).unwrap(); - if firework.complete() { - true - } else { - let needed = firework.needed_value().unwrap(); - if card.value < needed { - true - } else { - card.value > self.highest_attainable(card.color) - } - } + firework.complete() + || card.value < firework.needed_value().unwrap() + || card.value > self.highest_attainable(card.color) } // can be discarded without necessarily sacrificing score, based on discard + fireworks pub fn is_dispensable(&self, card: &Card) -> bool { - let firework = self.fireworks.get(&card.color).unwrap(); - if firework.complete() { - true - } else { - let needed = firework.needed_value().unwrap(); - if card.value < needed { - true - } else { - if card.value > self.highest_attainable(card.color) { - true - } else { - self.discard.remaining(&card) != 1 - } - } - } + self.is_dead(card) || self.discard.remaining(card) != 1 } pub fn get_players(&self) -> Range { - (0..self.num_players) + 0..self.num_players } pub fn score(&self) -> Score { - self.fireworks - .iter() - .map(|(_, firework)| firework.score()) - .fold(0, |a, b| a + b) + self.fireworks.values().map(Firework::score).sum() } pub fn discard_size(&self) -> u32 { @@ -408,35 +376,35 @@ impl BoardState { 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))); + writeln!(f, "Turn {} (GAME ENDED):", self.turn)?; } else { - try!(f.write_str(&format!( - "Turn {} (Player {}'s turn):\n", - self.turn, self.player - ))); + writeln!(f, "Turn {} (Player {}'s turn):", self.turn, self.player)?; } - try!(f.write_str(&format!("{} cards remaining in deck\n", self.deck_size))); + writeln!(f, "{} cards remaining in deck", self.deck_size)?; if self.deck_size == 0 { - try!(f.write_str(&format!( - "Deck is empty. {} turns remaining in game\n", + writeln!( + f, + "Deck is empty. {} turns remaining in game", self.deckless_turns_remaining - ))); + )?; } - try!(f.write_str(&format!( - "{}/{} hints remaining\n", + writeln!( + f, + "{}/{} hints remaining", self.hints_remaining, self.hints_total - ))); - try!(f.write_str(&format!( - "{}/{} lives remaining\n", + )?; + writeln!( + f, + "{}/{} lives remaining", self.lives_remaining, self.lives_total - ))); - try!(f.write_str("Fireworks:\n")); + )?; + f.write_str("Fireworks:\n")?; for &color in COLORS.iter() { - try!(f.write_str(&format!(" {}\n", self.get_firework(color)))); + writeln!(f, " {}", self.get_firework(color))?; } - try!(f.write_str("Discard:\n")); - try!(f.write_str(&format!("{}\n", self.discard))); + f.write_str("Discard:\n")?; + writeln!(f, "{}\n", self.discard)?; Ok(()) } @@ -445,7 +413,7 @@ impl fmt::Display for BoardState { // complete game view of a given player pub trait GameView { fn me(&self) -> Player; - fn get_hand(&self, &Player) -> &Cards; + fn get_hand(&self, player: &Player) -> &Cards; fn get_board(&self) -> &BoardState; fn my_hand_size(&self) -> usize; @@ -461,8 +429,7 @@ 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() + .any(|other_card| card == other_card) } fn get_other_players(&self) -> Vec { @@ -475,12 +442,12 @@ pub trait GameView { fn can_see(&self, card: &Card) -> bool { self.get_other_players() .iter() - .any(|player| self.has_card(&player, card)) + .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) + self.get_hand(player) .iter() .any(|card| self.get_board().is_playable(card)) }) @@ -534,9 +501,9 @@ impl OwnedGameView { .collect::>(); OwnedGameView { - player: borrowed_view.player.clone(), + player: borrowed_view.player, hand_size: borrowed_view.hand_size, - other_hands: other_hands, + other_hands, board: (*borrowed_view.board).clone(), } } @@ -582,22 +549,22 @@ pub struct GameState { } impl fmt::Display for GameState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - try!(f.write_str("\n")); - try!(f.write_str("======\n")); - try!(f.write_str("Hands:\n")); - try!(f.write_str("======\n")); + f.write_str("\n")?; + f.write_str("======\n")?; + f.write_str("Hands:\n")?; + f.write_str("======\n")?; for player in self.board.get_players() { let hand = &self.hands.get(&player).unwrap(); - try!(f.write_str(&format!("player {}:", player))); + write!(f, "player {player}:")?; for (_i, card) in hand.iter() { - try!(f.write_str(&format!(" {}", card))); + write!(f, " {card}")?; } - try!(f.write_str(&"\n")); + f.write_str("\n")?; } - try!(f.write_str("======\n")); - try!(f.write_str("Board:\n")); - try!(f.write_str("======\n")); - try!(f.write_str(&format!("{}", self.board))); + f.write_str("======\n")?; + f.write_str("Board:\n")?; + f.write_str("======\n")?; + write!(f, "{}", self.board)?; Ok(()) } } @@ -654,9 +621,9 @@ impl GameState { } } BorrowedGameView { - player: player, + player, hand_size: self.hands.get(&player).unwrap().len(), - other_hands: other_hands, + other_hands, board: &self.board, } } @@ -669,27 +636,19 @@ impl GameState { // 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 ref mut hand = self.hands.get_mut(&self.board.player).unwrap(); - hand.remove(index).1 - }; + let hand = &mut self.hands.get_mut(&self.board.player).unwrap(); + let card = hand.remove(index).1; self.update_player_hand(); - result + card } fn replenish_hand(&mut self) { - // 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); - } + let hand = &mut 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(); @@ -706,9 +665,10 @@ impl GameState { 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_ne!( + self.board.player, hint.player, + "Player {} gave a hint to himself", + hint.player ); let hand = self.hands.get(&hint.player).unwrap(); @@ -767,9 +727,9 @@ impl GameState { } }; let turn_record = TurnRecord { - player: self.board.player.clone(), + player: self.board.player, result: turn_result, - choice: choice, + choice, }; self.board.turn_history.push(turn_record.clone()); diff --git a/src/helpers.rs b/src/helpers.rs index 71306c6..ac9fb99 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,12 +1,12 @@ use std::cmp::Eq; use std::collections::{HashMap, HashSet}; use std::convert::From; -use std::fmt; +use std::fmt::{self, Write}; use std::hash::Hash; use std::ops::{Index, IndexMut}; use std::slice; -use game::*; +use crate::game::*; // trait representing information about a card pub trait CardInfo { @@ -33,7 +33,7 @@ pub trait CardInfo { // get probability weight for the card #[allow(unused_variables)] fn get_weight(&self, card: &Card) -> f32 { - 1 as f32 + 1.0 } fn get_weighted_possibilities(&self) -> Vec<(Card, f32)> { @@ -49,11 +49,11 @@ pub trait CardInfo { fn total_weight(&self) -> f32 { self.get_possibilities() .iter() - .map(|card| self.get_weight(&card)) + .map(|card| self.get_weight(card)) .fold(0.0, |a, b| a + b) } - fn weighted_score(&self, score_fn: &Fn(&Card) -> T) -> f32 + fn weighted_score(&self, score_fn: &dyn Fn(&Card) -> T) -> f32 where f32: From, { @@ -72,7 +72,7 @@ pub trait CardInfo { self.weighted_score(&|card| card.value as f32) } - fn probability_of_predicate(&self, predicate: &Fn(&Card) -> bool) -> f32 { + fn probability_of_predicate(&self, predicate: &dyn Fn(&Card) -> bool) -> f32 { let f = |card: &Card| { if predicate(card) { 1.0 @@ -149,7 +149,7 @@ where fn get_possibilities(&self) -> Vec { self.get_possibility_set() .iter() - .map(|t| t.clone()) + .copied() .collect::>() } @@ -160,14 +160,14 @@ where fn initialize() -> HashSet { Self::get_all_possibilities() .iter() - .map(|val| val.clone()) + .copied() .collect::>() } fn mark_true(&mut self, value: T) { let possible = self.get_mut_possibility_set(); possible.clear(); - possible.insert(value.clone()); + possible.insert(value); } fn mark_false(&mut self, value: T) { @@ -268,7 +268,7 @@ impl fmt::Display for SimpleCardInfo { //} for &value in &VALUES { if self.value_info.is_possible(value) { - string.push_str(&format!("{}", value)); + write!(string, "{value}").unwrap(); } } f.pad(&string) @@ -301,10 +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) + .unwrap_or_else(|| panic!("Decrementing weight for impossible card: {card}")); *weight -= 1; *weight == 0 }; @@ -368,7 +368,7 @@ impl<'a> From<&'a CardCounts> for CardPossibilityTable { } } } - CardPossibilityTable { possible: possible } + CardPossibilityTable { possible } } } impl CardInfo for CardPossibilityTable { @@ -380,11 +380,7 @@ 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().cloned().collect::>(); cards.sort(); cards } @@ -405,7 +401,7 @@ impl CardInfo for CardPossibilityTable { impl fmt::Display for CardPossibilityTable { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for (card, weight) in &self.possible { - try!(f.write_str(&format!("{} {}, ", weight, card))); + write!(f, "{weight} {card}, ")?; } Ok(()) } @@ -424,20 +420,18 @@ where { pub fn new(hand_size: u32) -> Self { let hand_info = (0..hand_size).map(|_| T::new()).collect::>(); - HandInfo { - hand_info: hand_info, - } + HandInfo { hand_info } } // update for hint to me - pub fn update_for_hint(&mut self, hinted: &Hinted, matches: &Vec) { - match hinted { - &Hinted::Color(color) => { + pub fn update_for_hint(&mut self, hinted: &Hinted, matches: &[bool]) { + match *hinted { + Hinted::Color(color) => { for (card_info, &matched) in self.hand_info.iter_mut().zip(matches.iter()) { card_info.mark_color(color, matched); } } - &Hinted::Value(value) => { + Hinted::Value(value) => { for (card_info, &matched) in self.hand_info.iter_mut().zip(matches.iter()) { card_info.mark_value(value, matched); } diff --git a/src/json_output.rs b/src/json_output.rs index 745c8ee..9f2551b 100644 --- a/src/json_output.rs +++ b/src/json_output.rs @@ -1,4 +1,4 @@ -use game::*; +use crate::game::*; use serde_json::*; fn color_value(color: &Color) -> usize { diff --git a/src/main.rs b/src/main.rs index a0e2014..8966662 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,7 @@ impl log::Log for SimpleLogger { } fn print_usage(program: &str, opts: Options) { - print!("{}", opts.usage(&format!("Usage: {} [options]", program))); + print!("{}", opts.usage(&format!("Usage: {program} [options]"))); } fn main() { @@ -102,7 +102,7 @@ fn main() { Ok(m) => m, Err(f) => { print_usage(&program, opts); - panic!(f.to_string()) + panic!("{}", f) } }; if matches.opt_present("h") { @@ -118,7 +118,8 @@ fn main() { return print!("{}", get_results_table()); } - let log_level_str: &str = &matches.opt_str("l").unwrap_or("info".to_string()); + let l_opt = matches.opt_str("l"); + let log_level_str = l_opt.as_deref().unwrap_or("info"); let log_level = match log_level_str { "trace" => log::LogLevelFilter::Trace, "debug" => log::LogLevelFilter::Debug, @@ -127,7 +128,7 @@ fn main() { "error" => log::LogLevelFilter::Error, _ => { print_usage(&program, opts); - panic!("Unexpected log level argument {}", log_level_str); + panic!("Unexpected log level argument {log_level_str}"); } }; @@ -137,21 +138,20 @@ fn main() { }) .unwrap(); - let n_trials = u32::from_str(&matches.opt_str("n").unwrap_or("1".to_string())).unwrap(); + let n_trials = u32::from_str(matches.opt_str("n").as_deref().unwrap_or("1")).unwrap(); let seed = matches .opt_str("s") .map(|seed_str| u32::from_str(&seed_str).unwrap()); let progress_info = matches .opt_str("o") .map(|freq_str| u32::from_str(&freq_str).unwrap()); - let n_threads = u32::from_str(&matches.opt_str("t").unwrap_or("1".to_string())).unwrap(); - + let n_threads = u32::from_str(matches.opt_str("t").as_deref().unwrap_or("1")).unwrap(); + let n_players = u32::from_str(matches.opt_str("p").as_deref().unwrap_or("4")).unwrap(); + let g_opt = matches.opt_str("g"); + let strategy_str: &str = g_opt.as_deref().unwrap_or("cheat"); 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, @@ -181,30 +181,30 @@ fn sim_games( 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 { num_players: n_players, - hand_size: hand_size, + hand_size, num_hints: 8, num_lives: 3, // hanabi rules are a bit ambiguous about whether you can give hints that match 0 cards allow_empty_hints: false, }; - let strategy_config: Box = match strategy_str { + let strategy_config: Box = match strategy_str { "random" => Box::new(strategies::examples::RandomStrategyConfig { hint_probability: 0.4, play_probability: 0.2, - }) as Box, + }) as Box, "cheat" => Box::new(strategies::cheating::CheatingStrategyConfig::new()) - as Box, + as Box, "info" => Box::new(strategies::information::InformationStrategyConfig::new()) - as Box, + as Box, _ => { - panic!("Unexpected strategy argument {}", strategy_str); + panic!("Unexpected strategy argument {strategy_str}"); } }; simulator::simulate( @@ -227,19 +227,18 @@ fn get_results_table() -> String { 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 + "On the first {n_trials} seeds, we have these scores and win rates (average ± standard error):\n\n" ); - 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 format_name = |x| format!(" {x:7} "); + let format_players = |x| format!(" {x}p "); + let format_percent = |x, stderr| format!(" {x:05.2} ± {stderr:.2} % "); + let format_score = |x, stderr| format!(" {x:07.4} ± {stderr:.4} "); let space = String::from(" "); let dashes = String::from("---------"); let dashes_long = String::from("------------------"); type TwoLines = (String, String); fn make_twolines( - player_nums: &Vec, + player_nums: &[u32], head: TwoLines, make_block: &dyn Fn(u32) -> TwoLines, ) -> TwoLines { @@ -259,14 +258,12 @@ fn get_results_table() -> String { } fn concat_twolines(body: Vec) -> String { body.into_iter().fold(String::default(), |output, (a, b)| { - (output + &a + "\n" + &b + "\n") + 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 header = make_twolines(&player_nums, (space.clone(), dashes), &|n_players| { + (format_players(n_players), dashes_long.clone()) + }); let mut body = strategies .iter() .map(|strategy| { @@ -319,7 +316,7 @@ time cargo run --release -- --write-results-table let readme_init = { let parts = readme_contents.splitn(2, separator).collect::>(); if parts.len() != 2 { - panic!("{} has been modified in the Results section!", readme); + panic!("{readme} has been modified in the Results section!"); } parts[0] }; diff --git a/src/simulator.rs b/src/simulator.rs index d8ad866..84727a1 100644 --- a/src/simulator.rs +++ b/src/simulator.rs @@ -1,11 +1,10 @@ -use crossbeam; use fnv::FnvHashMap; use rand::{self, Rng, SeedableRng}; use std::fmt; -use game::*; -use json_output::*; -use strategy::*; +use crate::game::*; +use crate::json_output::*; +use crate::strategy::*; fn new_deck(seed: u32) -> Cards { let mut deck: Cards = Cards::new(); @@ -25,7 +24,7 @@ fn new_deck(seed: u32) -> Cards { pub fn simulate_once( opts: &GameOptions, - game_strategy: Box, + game_strategy: Box, seed: u32, output_json: bool, ) -> (GameState, Option) { @@ -41,7 +40,7 @@ pub fn simulate_once( game_strategy.initialize(player, &game.get_view(player)), ) }) - .collect::>>(); + .collect::>>(); let mut actions = Vec::new(); @@ -55,7 +54,7 @@ pub fn simulate_once( debug!("{}", game); let choice = { - let mut strategy = strategies.get_mut(&player).unwrap(); + let strategy = strategies.get_mut(&player).unwrap(); strategy.decide(&game.get_view(player)) }; if output_json { @@ -75,7 +74,7 @@ pub fn simulate_once( let turn = game.process_choice(choice); for player in game.get_players() { - let mut strategy = strategies.get_mut(&player).unwrap(); + let strategy = strategies.get_mut(&player).unwrap(); strategy.update(&turn, &game.get_view(player)); } } @@ -112,14 +111,14 @@ impl Histogram { fn insert_many(&mut self, val: Score, count: u32) { let new_count = self.get_count(&val) + count; self.hist.insert(val, new_count); - self.sum += val * (count as u32); + self.sum += val * count; self.total_count += count; } pub fn insert(&mut self, val: Score) { self.insert_many(val, 1); } pub fn get_count(&self, val: &Score) -> u32 { - *self.hist.get(&val).unwrap_or(&0) + *self.hist.get(val).unwrap_or(&0) } pub fn percentage_with(&self, val: &Score) -> f32 { self.get_count(val) as f32 / self.total_count as f32 @@ -149,7 +148,7 @@ 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),))); + write!(f, "\n{}: {}", val, self.get_count(val))?; } Ok(()) } @@ -239,11 +238,11 @@ where lives_histogram.merge(thread_lives_histogram); } - non_perfect_seeds.sort(); + non_perfect_seeds.sort_unstable(); SimResult { scores: score_histogram, lives: lives_histogram, - non_perfect_seed: non_perfect_seeds.get(0).cloned(), + non_perfect_seed: non_perfect_seeds.first().cloned(), } }) } diff --git a/src/strategies/cheating.rs b/src/strategies/cheating.rs index 0614301..54a90c5 100644 --- a/src/strategies/cheating.rs +++ b/src/strategies/cheating.rs @@ -2,8 +2,8 @@ use fnv::{FnvHashMap, FnvHashSet}; use std::cell::RefCell; use std::rc::Rc; -use game::*; -use strategy::*; +use crate::game::*; +use crate::strategy::*; // strategy that explicitly cheats by using Rc/RefCell // serves as a reference point for other strategies @@ -25,7 +25,7 @@ impl CheatingStrategyConfig { } } impl GameStrategyConfig for CheatingStrategyConfig { - fn initialize(&self, _: &GameOptions) -> Box { + fn initialize(&self, _: &GameOptions) -> Box { Box::new(CheatingStrategy::new()) } } @@ -42,7 +42,7 @@ impl CheatingStrategy { } } impl GameStrategy for CheatingStrategy { - fn initialize(&self, player: Player, view: &BorrowedGameView) -> Box { + fn initialize(&self, player: Player, view: &BorrowedGameView) -> Box { for (&player, &hand) in &view.other_hands { self.player_hands_cheat .borrow_mut() @@ -95,7 +95,7 @@ impl CheatingPlayerStrategy { 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) + .sum() } // how badly do we need to play a particular card @@ -106,13 +106,11 @@ impl CheatingPlayerStrategy { let my_hand_value = self.hand_play_value(view, my_hand); for player in view.board.get_players() { - if player != self.me { - if view.has_card(&player, card) { - 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); - } + if player != self.me && view.has_card(&player, card) { + 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); } } } @@ -134,7 +132,7 @@ impl CheatingPlayerStrategy { } set.insert(card.clone()); } - return None; + None } } impl PlayerStrategy for CheatingPlayerStrategy { @@ -152,7 +150,7 @@ impl PlayerStrategy for CheatingPlayerStrategy { .filter(|&(_, card)| view.board.is_playable(card)) .collect::>(); - if playable_cards.len() > 0 { + if !playable_cards.is_empty() { // play the best playable card // the higher the play_score, the better to play let mut index = 0; @@ -184,10 +182,8 @@ impl PlayerStrategy for CheatingPlayerStrategy { // hinting is better than discarding dead cards // (probably because it stalls the deck-drawing). - if view.board.hints_remaining > 0 { - if view.someone_else_can_play() { - return self.throwaway_hint(view); - } + if view.board.hints_remaining > 0 && view.someone_else_can_play() { + return self.throwaway_hint(view); } // if anything is totally useless, discard it diff --git a/src/strategies/examples.rs b/src/strategies/examples.rs index b818961..a996251 100644 --- a/src/strategies/examples.rs +++ b/src/strategies/examples.rs @@ -1,6 +1,6 @@ -use game::*; +use crate::game::*; +use crate::strategy::*; use rand::{self, Rng}; -use strategy::*; // dummy, terrible strategy, as an example #[derive(Clone)] @@ -10,7 +10,7 @@ pub struct RandomStrategyConfig { } impl GameStrategyConfig for RandomStrategyConfig { - fn initialize(&self, _: &GameOptions) -> Box { + fn initialize(&self, _: &GameOptions) -> Box { Box::new(RandomStrategy { hint_probability: self.hint_probability, play_probability: self.play_probability, @@ -23,7 +23,7 @@ pub struct RandomStrategy { play_probability: f64, } impl GameStrategy for RandomStrategy { - fn initialize(&self, player: Player, _: &BorrowedGameView) -> Box { + fn initialize(&self, player: Player, _: &BorrowedGameView) -> Box { Box::new(RandomStrategyPlayer { hint_probability: self.hint_probability, play_probability: self.play_probability, @@ -51,7 +51,7 @@ impl PlayerStrategy for RandomStrategyPlayer { 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)) + .choose(view.get_hand(&hint_player)) .unwrap(); let hinted = { if rand::random() { @@ -63,7 +63,7 @@ impl PlayerStrategy for RandomStrategyPlayer { }; TurnChoice::Hint(Hint { player: hint_player, - hinted: hinted, + hinted, }) } else { TurnChoice::Discard(0) diff --git a/src/strategies/hat_helpers.rs b/src/strategies/hat_helpers.rs index 44c5697..cf82d49 100644 --- a/src/strategies/hat_helpers.rs +++ b/src/strategies/hat_helpers.rs @@ -1,5 +1,5 @@ -use game::*; -use helpers::*; +use crate::game::*; +use crate::helpers::*; #[derive(Debug, Clone)] pub struct ModulusInformation { @@ -9,10 +9,7 @@ pub struct ModulusInformation { impl ModulusInformation { pub fn new(modulus: u32, value: u32) -> Self { assert!(value < modulus); - ModulusInformation { - modulus: modulus, - value: value, - } + ModulusInformation { modulus, value } } pub fn none() -> Self { @@ -21,7 +18,7 @@ impl ModulusInformation { pub fn combine(&mut self, other: Self, max_modulus: u32) { assert!(other.modulus <= self.info_remaining(max_modulus)); - self.value = self.value + self.modulus * other.value; + self.value += self.modulus * other.value; self.modulus = std::cmp::min(max_modulus, self.modulus * other.modulus); assert!(self.value < self.modulus); } @@ -45,7 +42,7 @@ impl ModulusInformation { let original_modulus = self.modulus; let original_value = self.value; let value = self.value % modulus; - self.value = self.value / modulus; + self.value /= modulus; // `self.modulus` is the largest number such that // `value + (self.modulus - 1) * modulus < original_modulus`. // TODO: find an explanation of why this makes everything work out @@ -80,9 +77,14 @@ pub trait Question { // how much info does this question ask for? fn info_amount(&self) -> u32; // get the answer to this question, given cards - fn answer(&self, &Cards, &BoardState) -> u32; + fn answer(&self, hand: &Cards, board: &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, + hand_info: &mut HandInfo, + board: &BoardState, + ); fn answer_info(&self, hand: &Cards, board: &BoardState) -> ModulusInformation { ModulusInformation::new(self.info_amount(), self.answer(hand, board)) @@ -100,11 +102,11 @@ pub trait Question { } pub trait PublicInformation: Clone { - fn get_player_info(&self, &Player) -> HandInfo; - fn set_player_info(&mut self, &Player, HandInfo); + fn get_player_info(&self, player: &Player) -> HandInfo; + fn set_player_info(&mut self, player: &Player, hand_info: HandInfo); - fn new(&BoardState) -> Self; - fn set_board(&mut self, &BoardState); + fn new(board: &BoardState) -> Self; + fn set_board(&mut self, board: &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) {} @@ -122,17 +124,17 @@ pub trait PublicInformation: Clone { /// before the entire "hat value" calculation. fn ask_question( &self, - &Player, - &HandInfo, + player: &Player, + hand_info: &HandInfo, total_info: u32, - ) -> Option>; + ) -> Option>; fn ask_question_wrapper( &self, player: &Player, hand_info: &HandInfo, total_info: u32, - ) -> Option> { + ) -> Option> { assert!(total_info > 0); if total_info == 1 { None @@ -205,7 +207,7 @@ pub trait PublicInformation: Clone { .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)) + (info, (*player, hand_info)) }) .unzip(); self.set_player_infos(new_player_hands); @@ -231,7 +233,7 @@ pub trait PublicInformation: Clone { 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)) + (player_info, (player, hand_info)) }) .unzip(); for other_info in other_infos { @@ -251,7 +253,7 @@ pub trait PublicInformation: Clone { fn get_private_info(&self, view: &OwnedGameView) -> HandInfo { let mut info = self.get_player_info(&view.player); for card_table in info.iter_mut() { - for (_, hand) in &view.other_hands { + for hand in view.other_hands.values() { for card in hand { card_table.decrement_weight_if_possible(card); } diff --git a/src/strategies/information.rs b/src/strategies/information.rs index ff5b57a..a6f281a 100644 --- a/src/strategies/information.rs +++ b/src/strategies/information.rs @@ -2,10 +2,10 @@ use float_ord::*; use fnv::{FnvHashMap, FnvHashSet}; use std::cmp::Ordering; -use game::*; -use helpers::*; -use strategies::hat_helpers::*; -use strategy::*; +use crate::game::*; +use crate::helpers::*; +use crate::strategies::hat_helpers::*; +use crate::strategy::*; // TODO: use random extra information - i.e. when casting up and down, // we sometimes have 2 choices of value to choose @@ -23,7 +23,7 @@ impl Question for CardHasProperty { 2 } fn answer(&self, hand: &Cards, board: &BoardState) -> u32 { - let ref card = hand[self.index]; + let card = &hand[self.index]; if (self.property)(board, card) { 1 } else { @@ -36,17 +36,15 @@ impl Question for CardHasProperty { hand_info: &mut HandInfo, board: &BoardState, ) { - let ref mut card_table = hand_info[self.index]; + let card_table = &mut hand_info[self.index]; let possible = card_table.get_possibilities(); for card in &possible { if (self.property)(board, card) { if answer == 0 { card_table.mark_false(card); } - } else { - if answer == 1 { - card_table.mark_false(card); - } + } else if answer == 1 { + card_table.mark_false(card); } } } @@ -72,7 +70,7 @@ fn q_is_dead(index: usize) -> CardHasProperty { /// It's named that way because the `info_amount` grows additively with the `info_amount`s of /// the questions in `l`. struct AdditiveComboQuestion { - questions: Vec>, + questions: Vec>, } impl Question for AdditiveComboQuestion { fn info_amount(&self) -> u32 { @@ -134,7 +132,7 @@ impl CardPossibilityPartition { let mut partition = FnvHashMap::default(); let mut n_partitions = 0; - let has_dead = card_table.probability_is_dead(&board) != 0.0; + let has_dead = card_table.probability_is_dead(board) != 0.0; // TODO: group things of different colors and values? let mut effective_max = max_n_partitions; @@ -161,21 +159,21 @@ impl CardPossibilityPartition { n_partitions += 1; } - // let mut s : String = "Partition: |".to_string(); + // let mut s: String = "Partition: |".to_string(); // for i in 0..n_partitions { // for (card, block) in partition.iter() { // if *block == i { // s = s + &format!(" {}", card); // } // } - // s = s + &format!(" |"); + // s.push_str(" |"); // } // debug!("{}", s); CardPossibilityPartition { - index: index, - n_partitions: n_partitions, - partition: partition, + index, + n_partitions, + partition, } } } @@ -184,8 +182,8 @@ impl Question for CardPossibilityPartition { self.n_partitions } fn answer(&self, hand: &Cards, _: &BoardState) -> u32 { - let ref card = hand[self.index]; - *self.partition.get(&card).unwrap() + let card = &hand[self.index]; + *self.partition.get(card).unwrap() } fn acknowledge_answer( &self, @@ -193,7 +191,7 @@ impl Question for CardPossibilityPartition { hand_info: &mut HandInfo, _: &BoardState, ) { - let ref mut card_table = hand_info[self.index]; + let card_table = &mut hand_info[self.index]; let possible = card_table.get_possibilities(); for card in &possible { if *self.partition.get(card).unwrap() != answer { @@ -220,10 +218,7 @@ 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).map(|i| (player + 1 + i) % n).collect() } // Returns the number of ways to hint the player. @@ -232,7 +227,7 @@ impl MyPublicInformation { // - it is public that there are at least two colors // - it is public that there are at least two numbers - let ref info = self.hand_info[&player]; + let info = &self.hand_info[&player]; let may_be_all_one_color = COLORS .iter() @@ -242,11 +237,11 @@ impl MyPublicInformation { .iter() .any(|value| info.iter().all(|card| card.can_be_value(*value))); - return if !may_be_all_one_color && !may_be_all_one_number { + if !may_be_all_one_color && !may_be_all_one_number { 4 } else { 3 - }; + } } fn get_hint_index_score(&self, card_table: &CardPossibilityTable) -> i32 { @@ -264,7 +259,7 @@ impl MyPublicInformation { if !card_table.value_determined() { score += 1; } - return score; + score } fn get_index_for_hint(&self, player: &Player) -> usize { @@ -276,7 +271,7 @@ impl MyPublicInformation { (-score, i) }) .collect::>(); - scores.sort(); + scores.sort_unstable(); scores[0].1 } @@ -392,12 +387,12 @@ impl MyPublicInformation { .into_iter() .map(|hinted| Hint { player: hint_player, - hinted: hinted, + hinted, }) .collect() } - fn decode_hint_choice(&self, hint: &Hint, result: &Vec) -> ModulusInformation { + fn decode_hint_choice(&self, hint: &Hint, result: &[bool]) -> ModulusInformation { let hinter = self.board.player; let info_per_player: Vec<_> = self @@ -411,10 +406,7 @@ impl MyPublicInformation { 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: u32 = info_per_player.iter().take(player_amt as usize).sum(); 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); @@ -427,17 +419,15 @@ impl MyPublicInformation { } else { 2 } + } else 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, - } + match hint.hinted { + Hinted::Value(_) => 2, + Hinted::Color(_) => 3, } }; @@ -446,12 +436,12 @@ impl MyPublicInformation { ModulusInformation::new(total_info, hint_value) } - fn update_from_hint_choice(&mut self, hint: &Hint, matches: &Vec, view: &OwnedGameView) { + fn update_from_hint_choice(&mut self, hint: &Hint, matches: &[bool], view: &OwnedGameView) { let info = self.decode_hint_choice(hint, matches); self.update_from_hat_sum(info, view); } - fn update_from_hint_matches(&mut self, hint: &Hint, matches: &Vec) { + fn update_from_hint_matches(&mut self, hint: &Hint, matches: &[bool]) { let info = self.get_player_info_mut(&hint.player); info.update_for_hint(&hint.hinted, matches); } @@ -466,10 +456,10 @@ impl MyPublicInformation { // 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) + .get_hand(player) .iter() .any(|card| view.get_board().is_playable(card)); - has_playable_card && !self.knows_playable_card(&player) + has_playable_card && !self.knows_playable_card(player) }) } @@ -508,7 +498,7 @@ impl MyPublicInformation { info.remove(index); // push *before* incrementing public counts - if info.len() < new_view.hand_size(&player) { + if info.len() < new_view.hand_size(player) { info.push(new_card_table); } } @@ -536,7 +526,7 @@ impl PublicInformation for MyPublicInformation { }) .collect::>(); MyPublicInformation { - hand_info: hand_info, + hand_info, card_counts: CardCounts::new(), board: board.clone(), } @@ -563,7 +553,7 @@ impl PublicInformation for MyPublicInformation { _me: &Player, hand_info: &HandInfo, total_info: u32, - ) -> Option> { + ) -> Option> { // Changing anything inside this function will not break the information transfer // mechanisms! @@ -571,10 +561,10 @@ impl PublicInformation for MyPublicInformation { .iter() .cloned() .enumerate() - .filter_map(|(i, card_table)| { + .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)) + (i, p_play, p_dead) }) .collect::>(); let know_playable_card = augmented_hand_info_raw @@ -587,15 +577,7 @@ impl PublicInformation for MyPublicInformation { // 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 - } - }) + .filter(|&(i, _, p_dead)| p_dead != 1.0 && !hand_info[i].is_determined()) .collect::>(); if !know_playable_card { @@ -637,7 +619,7 @@ impl PublicInformation for MyPublicInformation { 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 { + .map(|(ask_dead, i, _)| -> Box { if ask_dead { Box::new(q_is_dead(i)) } else { @@ -645,7 +627,7 @@ impl PublicInformation for MyPublicInformation { } }) .collect::>(); - if questions.len() > 0 { + if !questions.is_empty() { return Some(Box::new(AdditiveComboQuestion { questions })); } } @@ -657,14 +639,14 @@ impl PublicInformation for MyPublicInformation { .cloned() .collect::>(); ask_play.sort_by_key(|&(i, p_play, _)| (ask_play_score(p_play), i)); - if let Some(&(i, _, _)) = ask_play.get(0) { + if let Some(&(i, _, _)) = ask_play.first() { return Some(Box::new(q_is_playable(i))); } 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)); - if let Some(&(i, _, _)) = ask_partition.get(0) { + if let Some(&(i, _, _)) = ask_partition.first() { let question = CardPossibilityPartition::new(i, total_info, &hand_info[i], &self.board); Some(Box::new(question)) } else { @@ -681,7 +663,7 @@ impl InformationStrategyConfig { } } impl GameStrategyConfig for InformationStrategyConfig { - fn initialize(&self, _: &GameOptions) -> Box { + fn initialize(&self, _: &GameOptions) -> Box { Box::new(InformationStrategy::new()) } } @@ -694,7 +676,7 @@ impl InformationStrategy { } } impl GameStrategy for InformationStrategy { - fn initialize(&self, player: Player, view: &BorrowedGameView) -> Box { + fn initialize(&self, player: Player, view: &BorrowedGameView) -> Box { Box::new(InformationPlayerStrategy { me: player, public_info: MyPublicInformation::new(view.board), @@ -728,10 +710,8 @@ impl InformationPlayerStrategy { let mut num_with = 1; if view.board.deck_size > 0 { for player in view.board.get_players() { - if player != self.me { - if view.has_card(&player, card) { - num_with += 1; - } + if player != self.me && view.has_card(&player, card) { + num_with += 1; } } } @@ -743,27 +723,30 @@ impl InformationPlayerStrategy { board: &BoardState, hand: &HandInfo, ) -> Vec { + use std::collections::hash_map::Entry::{Occupied, Vacant}; + let mut useless: FnvHashSet = FnvHashSet::default(); let mut seen: FnvHashMap = FnvHashMap::default(); for (i, card_table) in hand.iter().enumerate() { if card_table.probability_is_dead(board) == 1.0 { useless.insert(i); - } else { - if let Some(card) = card_table.get_card() { - if seen.contains_key(&card) { + } else if let Some(card) = card_table.get_card() { + match seen.entry(card) { + Occupied(e) => { // found a duplicate card useless.insert(i); - useless.insert(*seen.get(&card).unwrap()); - } else { - seen.insert(card, i); + useless.insert(*e.get()); + } + Vacant(e) => { + e.insert(i); } } } } let mut useless_vec: Vec = useless.into_iter().collect(); - useless_vec.sort(); - return useless_vec; + useless_vec.sort_unstable(); + useless_vec } // how good is it to give this hint to this player? @@ -774,8 +757,8 @@ impl InformationPlayerStrategy { let hint_player = &hint.player; let hinted = &hint.hinted; - let hand = view.get_hand(&hint_player); - let mut hand_info = self.public_info.get_player_info(&hint_player); + let hand = view.get_hand(hint_player); + let mut hand_info = self.public_info.get_player_info(hint_player); let mut goodness = 1.0; for (i, card_table) in hand_info.iter_mut().enumerate() { @@ -794,9 +777,8 @@ impl InformationPlayerStrategy { let new_weight = card_table.total_weight(); assert!(new_weight <= old_weight); let bonus = { - if card_table.is_determined() { - 2 - } else if card_table.probability_is_dead(&view.board) == 1.0 { + if card_table.is_determined() || card_table.probability_is_dead(&view.board) == 1.0 + { 2 } else { 1 @@ -821,12 +803,10 @@ impl InformationPlayerStrategy { hint_options.sort_by(|h1, h2| h2.0.partial_cmp(&h1.0).unwrap_or(Ordering::Equal)); - if hint_options.len() == 0 { + if hint_options.is_empty() { // NOTE: Technically possible, but never happens - } else { - if hint_options.len() > 1 { - debug!("Choosing amongst hint options: {:?}", hint_options); - } + } else if hint_options.len() > 1 { + debug!("Choosing amongst hint options: {:?}", hint_options); } hint_options.remove(0).1 } @@ -868,7 +848,7 @@ impl InformationPlayerStrategy { }) .collect::>(); playable_cards.sort_by_key(|&(i, play_score)| (FloatOrd(-play_score), i)); - if let Some(&(play_index, _)) = playable_cards.get(0) { + if let Some(&(play_index, _)) = playable_cards.first() { return TurnChoice::Play(play_index); } @@ -894,7 +874,7 @@ impl InformationPlayerStrategy { }) .collect::>(); - if risky_playable_cards.len() > 0 { + if !risky_playable_cards.is_empty() { risky_playable_cards .sort_by(|c1, c2| c2.2.partial_cmp(&c1.2).unwrap_or(Ordering::Equal)); @@ -910,23 +890,20 @@ impl InformationPlayerStrategy { 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 - } - // 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 - } - // this is the only case in which we discard a potentially useful card. - else { - false - }; + 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.is_empty() { + 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 { + // this being false is the only case in which we discard a potentially useful card. + view.board.hints_remaining > 4 + }; if will_hint { let hint_set = public_info.get_hint(view); @@ -942,7 +919,7 @@ impl InformationPlayerStrategy { if public_useless_indices.len() > 1 { let info = public_info.get_hat_sum(public_useless_indices.len() as u32, view); return TurnChoice::Discard(public_useless_indices[info.value as usize]); - } else if useless_indices.len() > 0 { + } else if !useless_indices.is_empty() { // TODO: have opponents infer that i knew a card was useless // TODO: after that, potentially prefer useless indices that arent public return TurnChoice::Discard(useless_indices[0]); @@ -1026,7 +1003,7 @@ 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 { + let hint_matches = if let TurnResult::Hint(matches) = &turn_record.result { Some(matches) } else { None @@ -1042,7 +1019,7 @@ impl PlayerStrategy for InformationPlayerStrategy { } match turn_record.choice { TurnChoice::Hint(ref hint) => { - if let &TurnResult::Hint(ref matches) = &turn_record.result { + if let TurnResult::Hint(matches) = &turn_record.result { self.public_info.update_from_hint_matches(hint, matches); } else { panic!( @@ -1052,7 +1029,7 @@ impl PlayerStrategy for InformationPlayerStrategy { } } TurnChoice::Discard(index) => { - if let &TurnResult::Discard(ref card) = &turn_record.result { + if let TurnResult::Discard(card) = &turn_record.result { self.public_info.update_from_discard_or_play_result( view, &turn_record.player, @@ -1067,7 +1044,7 @@ impl PlayerStrategy for InformationPlayerStrategy { } } TurnChoice::Play(index) => { - if let &TurnResult::Play(ref card, _) = &turn_record.result { + if let TurnResult::Play(card, _) = &turn_record.result { self.public_info.update_from_discard_or_play_result( view, &turn_record.player, diff --git a/src/strategy.rs b/src/strategy.rs index f9d8620..2e370f3 100644 --- a/src/strategy.rs +++ b/src/strategy.rs @@ -1,4 +1,4 @@ -use game::*; +use crate::game::*; // Traits to implement for any valid Hanabi strategy @@ -11,20 +11,20 @@ pub trait PlayerStrategy { 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; + fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice; // A function to update internal state after other players' turns. // Given what happened last turn, and the new state. - fn update(&mut self, &TurnRecord, &BorrowedGameView); + fn update(&mut self, turn_record: &TurnRecord, view: &BorrowedGameView); } // Represents the overall strategy for a game // Shouldn't do much, except store configuration parameters and // possibility initialize some shared randomness between players pub trait GameStrategy { - fn initialize(&self, Player, &BorrowedGameView) -> Box; + fn initialize(&self, me: Player, view: &BorrowedGameView) -> Box; } // Represents configuration for a strategy. // Acts as a factory for game strategies, so we can play many rounds pub trait GameStrategyConfig { - fn initialize(&self, &GameOptions) -> Box; + fn initialize(&self, opts: &GameOptions) -> Box; }