Merge pull request #13 from timotree3/rust-2021

Update to Rust 2021 and fix clippy lints
This commit is contained in:
Jeff Wu 2023-01-19 18:58:18 -08:00 committed by GitHub
commit 4abf7d0448
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 968 additions and 634 deletions

109
Cargo.lock generated
View file

@ -3,10 +3,16 @@
version = 3
[[package]]
name = "crossbeam"
version = "0.2.8"
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "348228ce9f93d20ffc30c18e575f82fa41b9c8bf064806c65d41eba4771595a0"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "crossbeam"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd66663db5a988098a89599d4857919b3acf7f61402e61365acfd3919857b9be"
[[package]]
name = "float-ord"
@ -16,9 +22,15 @@ checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e"
[[package]]
name = "fnv"
version = "1.0.6"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
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"
@ -31,26 +43,73 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.7"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4870ef6725dde13394134e587e4ab4eca13cb92e916209a31c851b49131d3c75"
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
[[package]]
name = "log"
version = "0.3.5"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038b5d13189a14e5b6ac384fdb7c691a45ef0885f6d2dddbf422e6c3506b8234"
checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
dependencies = [
"libc",
"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 = "2791d88c6defac799c3f20d74f094ca33b9332612d9aef9078519c82e4fe04a5"
checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
dependencies = [
"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]]
@ -61,8 +120,8 @@ dependencies = [
"float-ord",
"fnv",
"getopts",
"log",
"rand",
"log 0.3.9",
"rand 0.3.23",
]
[[package]]
@ -70,3 +129,25 @@ 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"

View file

@ -2,11 +2,12 @@
name = "rust_hanabi"
version = "0.1.0"
authors = ["Jeff Wu <wuthefwasthat@gmail.com>"]
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"

View file

@ -11,19 +11,21 @@ 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!("Unexpected value: {}", value); }
5 => 1,
_ => {
panic!("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,
@ -44,7 +46,7 @@ impl fmt::Debug for Card {
}
}
#[derive(Debug,Clone,Eq,PartialEq)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct CardCounts {
counts: FnvHashMap<Card, u32>,
}
@ -56,9 +58,7 @@ impl CardCounts {
counts.insert(Card::new(color, value), 0);
}
}
CardCounts {
counts,
}
CardCounts { counts }
}
pub fn get_count(&self, card: &Card) -> u32 {
@ -78,15 +78,11 @@ impl CardCounts {
impl fmt::Display for CardCounts {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for &color in COLORS.iter() {
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);
f.write_str(&format!(
"{}/{} {}s", count, total, value
))?;
write!(f, "{count}/{total} {value}s")?;
if value != FINAL_VALUE {
f.write_str(", ")?;
}
@ -99,7 +95,7 @@ impl fmt::Display for CardCounts {
pub type Cards = Vec<Card>;
#[derive(Debug,Clone,Eq,PartialEq)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Discard {
pub cards: Cards,
counts: CardCounts,
@ -127,9 +123,7 @@ impl Discard {
}
impl fmt::Display for Discard {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// f.write_str(&format!(
// "{}", self.cards,
// ))?;
// write!(f, "{}", self.cards)?;
write!(f, "{}", self.counts)
}
}
@ -137,21 +131,22 @@ 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,
}
impl Firework {
pub fn new(color: Color) -> Firework {
Firework {
color,
top: 0,
}
Firework { color, top: 0 }
}
pub fn needed_value(&self) -> Option<Value> {
if self.complete() { None } else { Some(self.top + 1) }
if self.complete() {
None
} else {
Some(self.top + 1)
}
}
pub fn score(&self) -> Score {
@ -184,28 +179,32 @@ impl fmt::Display for Firework {
}
}
#[derive(Debug,Clone,Hash,PartialEq,Eq)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Hinted {
Color(Color),
Value(Value),
}
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) }
match *self {
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 +212,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<bool>), // vector of whether each was in the hint
Discard(Card), // card discarded
@ -221,7 +220,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 +242,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 +268,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::<FnvHashMap<_, _>>();
let fireworks = COLORS
.iter()
.map(|&color| (color, Firework::new(color)))
.collect::<FnvHashMap<_, _>>();
BoardState {
deck_size,
@ -324,7 +324,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) {
@ -338,31 +338,14 @@ impl BoardState {
// 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 || 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<Player> {
@ -370,7 +353,7 @@ impl BoardState {
}
pub fn score(&self) -> Score {
self.fireworks.iter().map(|(_, firework)| firework.score()).sum()
self.fireworks.values().map(Firework::score).sum()
}
pub fn discard_size(&self) -> u32 {
@ -391,35 +374,35 @@ impl BoardState {
impl fmt::Display for BoardState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.is_over() {
f.write_str(&format!(
"Turn {} (GAME ENDED):\n", self.turn
))?;
writeln!(f, "Turn {} (GAME ENDED):", self.turn)?;
} else {
f.write_str(&format!(
"Turn {} (Player {}'s turn):\n", self.turn, self.player
))?;
writeln!(f, "Turn {} (Player {}'s turn):", self.turn, self.player)?;
}
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 {
f.write_str(&format!(
"Deck is empty. {} turns remaining in game\n", self.deckless_turns_remaining
))?;
writeln!(
f,
"Deck is empty. {} turns remaining in game",
self.deckless_turns_remaining
)?;
}
f.write_str(&format!(
"{}/{} hints remaining\n", self.hints_remaining, self.hints_total
))?;
f.write_str(&format!(
"{}/{} lives remaining\n", self.lives_remaining, self.lives_total
))?;
writeln!(
f,
"{}/{} hints remaining",
self.hints_remaining, self.hints_total
)?;
writeln!(
f,
"{}/{} lives remaining",
self.lives_remaining, self.lives_total
)?;
f.write_str("Fireworks:\n")?;
for &color in COLORS.iter() {
f.write_str(&format!(" {}\n", self.get_firework(color)))?;
writeln!(f, " {}", self.get_firework(color))?;
}
f.write_str("Discard:\n")?;
f.write_str(&format!("{}\n", self.discard))?;
writeln!(f, "{}\n", self.discard)?;
Ok(())
}
@ -442,28 +425,29 @@ 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()
.any(|other_card| card == other_card)
}
fn get_other_players(&self) -> Vec<Player> {
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))
})
}
}
@ -479,7 +463,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
}
@ -508,10 +492,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::<FnvHashMap<_, _>>();
let other_hands = borrowed_view
.other_hands
.iter()
.map(|(&other_player, &player_state)| (other_player, player_state.clone()))
.collect::<FnvHashMap<_, _>>();
OwnedGameView {
player: borrowed_view.player,
@ -552,16 +537,16 @@ impl fmt::Display for GameState {
f.write_str("======\n")?;
for player in self.board.get_players() {
let hand = &self.hands.get(&player).unwrap();
f.write_str(&format!("player {}:", player))?;
write!(f, "player {player}:")?;
for card in hand.iter() {
f.write_str(&format!(" {}", card))?;
write!(f, " {card}")?;
}
f.write_str("\n")?;
}
f.write_str("======\n")?;
f.write_str("Board:\n")?;
f.write_str("======\n")?;
f.write_str(&format!("{}", self.board))?;
write!(f, "{}", self.board)?;
Ok(())
}
}
@ -570,21 +555,20 @@ impl GameState {
pub fn new(opts: &GameOptions, mut deck: Cards) -> GameState {
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::<Vec<_>>();
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::<Vec<_>>();
(player, hand)
}).collect::<FnvHashMap<_, _>>();
})
.collect::<FnvHashMap<_, _>>();
GameState {
hands,
board,
deck,
}
GameState { hands, board, deck }
}
pub fn get_players(&self) -> Range<Player> {
@ -617,12 +601,12 @@ impl GameState {
// 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();
let hand = &mut self.hands.get_mut(&self.board.player).unwrap();
hand.remove(index)
}
fn replenish_hand(&mut self) {
let ref mut hand = self.hands.get_mut(&self.board.player).unwrap();
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;
@ -636,26 +620,35 @@ 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,
"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();
let results = match hint.hinted {
Hinted::Color(color) => {
hand.iter().map(|card| { card.color == color }).collect::<Vec<_>>()
}
Hinted::Value(value) => {
hand.iter().map(|card| { card.value == value }).collect::<Vec<_>>()
}
Hinted::Color(color) => hand
.iter()
.map(|card| card.color == color)
.collect::<Vec<_>>(),
Hinted::Value(value) => hand
.iter()
.map(|card| card.value == value)
.collect::<Vec<_>>(),
};
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)
@ -671,10 +664,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 {
{
@ -715,7 +705,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
}

View file

@ -1,12 +1,12 @@
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::{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,25 +33,29 @@ pub trait CardInfo {
// get probability weight for the card
#[allow(unused_variables)]
fn get_weight(&self, card: &Card) -> f32 {
1.
1.0
}
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::<Vec<_>>()
})
.collect::<Vec<_>>()
}
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<T>(&self, score_fn: &dyn Fn(&Card) -> T) -> f32
where f32: From<T>
where
f32: From<T>,
{
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: &dyn 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<T> where T: Hash + Eq + Clone {
pub trait Info<T>
where
T: Hash + Eq + Clone + Copy,
{
// get all a-priori possibilities
fn get_all_possibilities() -> Vec<T>;
@ -137,7 +147,10 @@ pub trait Info<T> where T: Hash + Eq + Clone {
// get what is now possible
fn get_possibilities(&self) -> Vec<T> {
self.get_possibility_set().iter().cloned().collect::<Vec<T>>()
self.get_possibility_set()
.iter()
.copied()
.collect::<Vec<T>>()
}
fn is_possible(&self, value: T) -> bool {
@ -145,8 +158,10 @@ pub trait Info<T> where T: Hash + Eq + Clone {
}
fn initialize() -> HashSet<T> {
Self::get_all_possibilities().iter()
.cloned().collect::<HashSet<_>>()
Self::get_all_possibilities()
.iter()
.copied()
.collect::<HashSet<_>>()
}
fn mark_true(&mut self, value: T) {
@ -160,35 +175,55 @@ pub trait Info<T> where T: Hash + Eq + Clone {
}
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<Color>);
impl ColorInfo {
pub fn new() -> ColorInfo { ColorInfo(ColorInfo::initialize()) }
pub fn new() -> ColorInfo {
ColorInfo(ColorInfo::initialize())
}
}
impl Info<Color> for ColorInfo {
fn get_all_possibilities() -> Vec<Color> { COLORS.to_vec() }
fn get_possibility_set(&self) -> &HashSet<Color> { &self.0 }
fn get_mut_possibility_set(&mut self) -> &mut HashSet<Color> { &mut self.0 }
fn get_all_possibilities() -> Vec<Color> {
COLORS.to_vec()
}
fn get_possibility_set(&self) -> &HashSet<Color> {
&self.0
}
fn get_mut_possibility_set(&mut self) -> &mut HashSet<Color> {
&mut self.0
}
}
#[derive(Debug,Clone)]
#[derive(Debug, Clone)]
pub struct ValueInfo(HashSet<Value>);
impl ValueInfo {
pub fn new() -> ValueInfo { ValueInfo(ValueInfo::initialize()) }
pub fn new() -> ValueInfo {
ValueInfo(ValueInfo::initialize())
}
}
impl Info<Value> for ValueInfo {
fn get_all_possibilities() -> Vec<Value> { VALUES.to_vec() }
fn get_possibility_set(&self) -> &HashSet<Value> { &self.0 }
fn get_mut_possibility_set(&mut self) -> &mut HashSet<Value> { &mut self.0 }
fn get_all_possibilities() -> Vec<Value> {
VALUES.to_vec()
}
fn get_possibility_set(&self) -> &HashSet<Value> {
&self.0
}
fn get_mut_possibility_set(&mut self) -> &mut HashSet<Value> {
&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);
@ -236,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)
@ -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<Card, u32>,
}
@ -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)
.unwrap_or_else(|| panic!("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::<HashSet<_>>()
.len() == 1
.len()
== 1
}
pub fn value_determined(&self) -> bool {
self.get_possibilities()
.iter().map(|card| card.value)
.iter()
.map(|card| card.value)
.collect::<HashSet<_>>()
.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,
}
CardPossibilityTable { possible }
}
}
impl CardInfo for CardPossibilityTable {
@ -349,7 +388,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() {
@ -363,53 +401,73 @@ impl CardInfo for CardPossibilityTable {
impl fmt::Display for CardPossibilityTable {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for (card, weight) in &self.possible {
f.write_str(&format!("{} {}, ", weight, card))?;
write!(f, "{weight} {card}, ")?;
}
Ok(())
}
}
#[derive(Clone,Eq,PartialEq)]
pub struct HandInfo<T> where T: CardInfo {
pub hand_info: Vec<T>
#[derive(Clone, Eq, PartialEq)]
pub struct HandInfo<T>
where
T: CardInfo,
{
pub hand_info: Vec<T>,
}
impl <T> HandInfo<T> where T: CardInfo {
impl<T> HandInfo<T>
where
T: CardInfo,
{
pub fn new(hand_size: u32) -> Self {
let hand_info = (0..hand_size).map(|_| T::new()).collect::<Vec<_>>();
HandInfo {
hand_info,
}
HandInfo { hand_info }
}
// update for hint to me
pub fn update_for_hint(&mut self, hinted: &Hinted, matches: &[bool]) {
match hinted {
match *hinted {
Hinted::Color(color) => {
for (card_info, &matched) in self.hand_info.iter_mut().zip(matches.iter()) {
card_info.mark_color(*color, matched);
card_info.mark_color(color, matched);
}
}
Hinted::Value(value) => {
for (card_info, &matched) in self.hand_info.iter_mut().zip(matches.iter()) {
card_info.mark_value(*value, matched);
card_info.mark_value(value, matched);
}
}
}
}
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<T> { self.hand_info.iter_mut() }
pub fn iter(&self) -> slice::Iter<T> { 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<T> {
self.hand_info.iter_mut()
}
pub fn iter(&self) -> slice::Iter<T> {
self.hand_info.iter()
}
pub fn len(&self) -> usize {
self.hand_info.len()
}
}
impl <T> Index<usize> for HandInfo<T> where T: CardInfo {
impl<T> Index<usize> for HandInfo<T>
where
T: CardInfo,
{
type Output = T;
fn index(&self, index: usize) -> &T {
&self.hand_info[index]
}
}
impl <T> IndexMut<usize> for HandInfo<T> where T: CardInfo {
impl<T> IndexMut<usize> for HandInfo<T>
where
T: CardInfo,
{
fn index_mut(&mut self, index: usize) -> &mut T {
&mut self.hand_info[index]
}

View file

@ -1,18 +1,18 @@
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;
mod helpers;
mod game;
mod helpers;
mod simulator;
mod strategy;
mod strategies {
pub mod examples;
pub mod cheating;
pub mod examples;
mod hat_helpers;
pub mod information;
}
@ -33,46 +33,60 @@ 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() {
let args: Vec<String> = std::env::args().collect();
let program = args[0].clone();
let mut opts = Options::new();
opts.optopt("l", "loglevel",
"Log level, one of 'trace', 'debug', 'info', 'warn', and 'error'",
"LOGLEVEL");
opts.optopt("n", "ntrials",
"Number of games to simulate (default 1)",
"NTRIALS");
opts.optopt("o", "output",
"Number of games after which to print an update",
"OUTPUT_FREQ");
opts.optopt("t", "nthreads",
"Number of threads to use for simulation (default 1)",
"NTHREADS");
opts.optopt("s", "seed",
"Seed for PRNG (default random)",
"SEED");
opts.optopt("p", "nplayers",
"Number of players",
"NPLAYERS");
opts.optopt("g", "strategy",
"Which strategy to use. One of 'random', 'cheat', and 'info'",
"STRATEGY");
opts.optflag("h", "help",
"Print this help menu");
opts.optflag("", "results-table",
"Print a table of results for each strategy");
opts.optflag("", "write-results-table",
"Update the results table in README.md");
opts.optopt(
"l",
"loglevel",
"Log level, one of 'trace', 'debug', 'info', 'warn', and 'error'",
"LOGLEVEL",
);
opts.optopt(
"n",
"ntrials",
"Number of games to simulate (default 1)",
"NTRIALS",
);
opts.optopt(
"o",
"output",
"Number of games after which to print an update",
"OUTPUT_FREQ",
);
opts.optopt(
"t",
"nthreads",
"Number of threads to use for simulation (default 1)",
"NTHREADS",
);
opts.optopt("s", "seed", "Seed for PRNG (default random)", "SEED");
opts.optopt("p", "nplayers", "Number of players", "NPLAYERS");
opts.optopt(
"g",
"strategy",
"Which strategy to use. One of 'random', 'cheat', and 'info'",
"STRATEGY",
);
opts.optflag("h", "help", "Print this help menu");
opts.optflag(
"",
"results-table",
"Print a table of results for each strategy",
);
opts.optflag(
"",
"write-results-table",
"Update the results table in README.md",
);
let matches = match opts.parse(&args[1..]) {
Ok(m) => { m }
Ok(m) => m,
Err(f) => {
print_usage(&program, opts);
panic!("{}", f)
@ -91,42 +105,65 @@ 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 }
"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);
panic!("Unexpected log level argument {log_level_str}");
}
};
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 n_threads = u32::from_str(&matches.opt_str("t").unwrap_or("1".to_string())).unwrap();
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 n_trials = u32::from_str(matches.opt_str("n").as_deref().unwrap_or("1")).unwrap();
let seed = matches
.opt_str("s")
.map(|seed_str| u32::from_str(&seed_str).unwrap());
let progress_info = matches
.opt_str("o")
.map(|freq_str| u32::from_str(&freq_str).unwrap());
let n_threads = u32::from_str(matches.opt_str("t").as_deref().unwrap_or("1")).unwrap();
let n_players = u32::from_str(matches.opt_str("p").as_deref().unwrap_or("4")).unwrap();
let g_opt = matches.opt_str("g");
let strategy_str: &str = g_opt.as_deref().unwrap_or("cheat");
sim_games(n_players, strategy_str, seed, n_trials, n_threads, progress_info).info();
sim_games(
n_players,
strategy_str,
seed,
n_trials,
n_threads,
progress_info,
)
.info();
}
fn sim_games(n_players: u32, strategy_str: &str, seed: Option<u32>, n_trials: u32, n_threads: u32, progress_info: Option<u32>)
-> simulator::SimResult {
fn sim_games(
n_players: u32,
strategy_str: &str,
seed: Option<u32>,
n_trials: u32,
n_threads: u32,
progress_info: Option<u32>,
) -> 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 {
@ -138,26 +175,27 @@ fn sim_games(n_players: u32, strategy_str: &str, seed: Option<u32>, n_trials: u3
allow_empty_hints: false,
};
let strategy_config : Box<dyn strategy::GameStrategyConfig + Sync> = match strategy_str {
"random" => {
Box::new(strategies::examples::RandomStrategyConfig {
hint_probability: 0.4,
play_probability: 0.2,
}) as Box<dyn strategy::GameStrategyConfig + Sync>
},
"cheat" => {
Box::new(strategies::cheating::CheatingStrategyConfig::new())
as Box<dyn strategy::GameStrategyConfig + Sync>
},
"info" => {
Box::new(strategies::information::InformationStrategyConfig::new())
as Box<dyn strategy::GameStrategyConfig + Sync>
},
let strategy_config: Box<dyn strategy::GameStrategyConfig + Sync> = match strategy_str {
"random" => Box::new(strategies::examples::RandomStrategyConfig {
hint_probability: 0.4,
play_probability: 0.2,
}) as Box<dyn strategy::GameStrategyConfig + Sync>,
"cheat" => Box::new(strategies::cheating::CheatingStrategyConfig::new())
as Box<dyn strategy::GameStrategyConfig + Sync>,
"info" => Box::new(strategies::information::InformationStrategyConfig::new())
as Box<dyn strategy::GameStrategyConfig + Sync>,
_ => {
panic!("Unexpected strategy argument {}", strategy_str);
},
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,
)
}
fn get_results_table() -> String {
@ -167,39 +205,64 @@ 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 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 intro = format!(
"On the first {n_trials} seeds, we have these scores and win rates (average ± standard error):\n\n"
);
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: &[u32], head: TwoLines, make_block: &dyn Fn(u32) -> TwoLines) -> TwoLines {
let mut blocks = player_nums.iter().cloned().map(make_block).collect::<Vec<_>>();
fn make_twolines(
player_nums: &[u32],
head: TwoLines,
make_block: &dyn Fn(u32) -> TwoLines,
) -> TwoLines {
let mut blocks = player_nums
.iter()
.cloned()
.map(make_block)
.collect::<Vec<_>>();
blocks.insert(0, head);
fn combine(items: Vec<String>) -> 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<TwoLines>) -> 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),
&|n_players| (format_players(n_players), dashes_long.clone()));
let mut body = strategies.iter().map(|strategy| {
make_twolines(&player_nums, (format_name(strategy), space.clone()), &|n_players| {
let simresult = sim_games(n_players, strategy, Some(seed), n_trials, n_threads, None);
(
format_score(simresult.average_score(), simresult.score_stderr()),
format_percent(simresult.percent_perfect(), simresult.percent_perfect_stderr())
let header = make_twolines(&player_nums, (space.clone(), dashes), &|n_players| {
(format_players(n_players), dashes_long.clone())
});
let mut body = strategies
.iter()
.map(|strategy| {
make_twolines(
&player_nums,
(format_name(strategy), space.clone()),
&|n_players| {
let simresult =
sim_games(n_players, strategy, Some(seed), n_trials, n_threads, None);
(
format_score(simresult.average_score(), simresult.score_stderr()),
format_percent(
simresult.percent_perfect(),
simresult.percent_perfect_stderr(),
),
)
},
)
})
}).collect::<Vec<_>>();
.collect::<Vec<_>>();
body.insert(0, header);
intro + &concat_twolines(body)
}
@ -224,7 +287,7 @@ time cargo run --release -- --write-results-table
let readme_init = {
let parts = readme_contents.splitn(2, separator).collect::<Vec<_>>();
if parts.len() != 2 {
panic!("{} has been modified in the Results section!", readme);
panic!("{readme} has been modified in the Results section!");
}
parts[0]
};

View file

@ -1,10 +1,9 @@
use rand::{self, Rng, SeedableRng};
use fnv::FnvHashMap;
use rand::{self, Rng, SeedableRng};
use std::fmt;
use crossbeam;
use game::*;
use strategy::*;
use crate::game::*;
use crate::strategy::*;
fn new_deck(seed: u32) -> Cards {
let mut deck: Cards = Cards::new();
@ -15,7 +14,7 @@ fn new_deck(seed: u32) -> Cards {
deck.push(Card::new(color, value));
}
}
};
}
rand::ChaChaRng::from_seed(&[seed]).shuffle(&mut deck[..]);
debug!("Deck: {:?}", deck);
@ -23,17 +22,23 @@ fn new_deck(seed: u32) -> Cards {
}
pub fn simulate_once(
opts: &GameOptions,
game_strategy: Box<dyn GameStrategy>,
seed: u32,
) -> GameState {
opts: &GameOptions,
game_strategy: Box<dyn GameStrategy>,
seed: u32,
) -> GameState {
let deck = new_deck(seed);
let mut game = GameState::new(opts, deck);
let mut strategies = game.get_players().map(|player| {
(player, game_strategy.initialize(player, &game.get_view(player)))
}).collect::<FnvHashMap<Player, Box<dyn PlayerStrategy>>>();
let mut strategies = game
.get_players()
.map(|player| {
(
player,
game_strategy.initialize(player, &game.get_view(player)),
)
})
.collect::<FnvHashMap<Player, Box<dyn PlayerStrategy>>>();
while !game.is_over() {
let player = game.board.player;
@ -44,7 +49,6 @@ pub fn simulate_once(
debug!("=======================================================");
debug!("{}", game);
let choice = {
let strategy = strategies.get_mut(&player).unwrap();
strategy.decide(&game.get_view(player))
@ -56,7 +60,6 @@ pub fn simulate_once(
let strategy = strategies.get_mut(&player).unwrap();
strategy.update(&turn, &game.get_view(player));
}
}
debug!("");
debug!("=======================================================");
@ -82,7 +85,7 @@ 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) {
@ -119,24 +122,23 @@ impl fmt::Display for Histogram {
let mut keys = self.hist.keys().collect::<Vec<_>>();
keys.sort();
for val in keys {
f.write_str(&format!(
"\n{}: {}", val, self.get_count(val),
))?;
write!(f, "\n{}: {}", val, self.get_count(val))?;
}
Ok(())
}
}
pub fn simulate<T: ?Sized>(
opts: &GameOptions,
strat_config: Box<T>,
first_seed_opt: Option<u32>,
n_trials: u32,
n_threads: u32,
progress_info: Option<u32>,
) -> SimResult
where T: GameStrategyConfig + Sync {
opts: &GameOptions,
strat_config: Box<T>,
first_seed_opt: Option<u32>,
n_trials: u32,
n_threads: u32,
progress_info: Option<u32>,
) -> 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;
@ -144,7 +146,7 @@ pub fn simulate<T: ?Sized>(
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);
@ -156,10 +158,13 @@ pub fn simulate<T: ?Sized>(
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
);
}
@ -168,7 +173,9 @@ pub fn simulate<T: ?Sized>(
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 progress_info.is_some() {
info!("Thread {} done", i);
@ -177,21 +184,22 @@ pub fn simulate<T: ?Sized>(
}));
}
let mut non_perfect_seeds : Vec<u32> = Vec::new();
let mut non_perfect_seeds: Vec<u32> = 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);
}
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(),
}
})
}
@ -209,7 +217,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
}

View file

@ -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 crate::game::*;
use crate::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<dyn PlayerStrategy> {
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)).sum()
hand.iter()
.map(|card| self.card_play_value(view, card))
.sum()
}
// 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);
@ -108,7 +110,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);
}
}
}
@ -139,9 +141,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::<Vec<_>>();
let playable_cards = my_hand
.iter()
.enumerate()
.filter(|&(_, card)| view.board.is_playable(card))
.collect::<Vec<_>>();
if !playable_cards.is_empty() {
// play the best playable card
@ -156,15 +160,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 {
@ -204,6 +207,5 @@ impl PlayerStrategy for CheatingPlayerStrategy {
}
TurnChoice::Discard(index)
}
fn update(&mut self, _: &TurnRecord, _: &BorrowedGameView) {
}
fn update(&mut self, _: &TurnRecord, _: &BorrowedGameView) {}
}

View file

@ -1,5 +1,5 @@
use strategy::*;
use game::*;
use crate::game::*;
use crate::strategy::*;
use rand::{self, Rng};
// dummy, terrible strategy, as an example
@ -44,7 +44,9 @@ impl PlayerStrategy for RandomStrategyPlayer {
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
@ -66,6 +68,5 @@ impl PlayerStrategy for RandomStrategyPlayer {
TurnChoice::Discard(0)
}
}
fn update(&mut self, _: &TurnRecord, _: &BorrowedGameView) {
}
fn update(&mut self, _: &TurnRecord, _: &BorrowedGameView) {}
}

View file

@ -1,7 +1,7 @@
use game::*;
use helpers::*;
use crate::game::*;
use crate::helpers::*;
#[derive(Debug,Clone)]
#[derive(Debug, Clone)]
pub struct ModulusInformation {
pub modulus: u32,
pub value: u32,
@ -9,10 +9,7 @@ pub struct ModulusInformation {
impl ModulusInformation {
pub fn new(modulus: u32, value: u32) -> Self {
assert!(value < modulus);
ModulusInformation {
modulus,
value,
}
ModulusInformation { modulus, value }
}
pub fn none() -> Self {
@ -83,21 +80,21 @@ pub trait Question {
fn answer(&self, hand: &Cards, board: &BoardState) -> u32;
// process the answer to this question, updating card info
fn acknowledge_answer(
&self, value: u32, hand_info: &mut HandInfo<CardPossibilityTable>, board: &BoardState
&self,
value: u32,
hand_info: &mut HandInfo<CardPossibilityTable>,
board: &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<CardPossibilityTable>,
board: &BoardState
board: &BoardState,
) {
assert!(self.info_amount() == answer.modulus);
self.acknowledge_answer(answer.value, hand_info, board);
@ -112,8 +109,7 @@ pub trait PublicInformation: Clone {
fn set_board(&mut self, board: &BoardState);
/// If we store more state than just `HandInfo<CardPossibilityTable>`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 +122,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: &Player, hand_info: &HandInfo<CardPossibilityTable>, total_info: u32) -> Option<Box<dyn Question>>;
fn ask_question(
&self,
player: &Player,
hand_info: &HandInfo<CardPossibilityTable>,
total_info: u32,
) -> Option<Box<dyn Question>>;
fn ask_question_wrapper(&self, player: &Player, hand_info: &HandInfo<CardPossibilityTable>, total_info: u32)
-> Option<Box<dyn Question>>
{
fn ask_question_wrapper(
&self,
player: &Player,
hand_info: &HandInfo<CardPossibilityTable>,
total_info: u32,
) -> Option<Box<dyn Question>> {
assert!(total_info > 0);
if total_info == 1 {
None
@ -138,8 +142,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 +164,17 @@ pub trait PublicInformation: Clone {
}
fn get_hat_info_for_player(
&self, player: &Player, hand_info: &mut HandInfo<CardPossibilityTable>, total_info: u32, view: &OwnedGameView
&self,
player: &Player,
hand_info: &mut HandInfo<CardPossibilityTable>,
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 +201,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, 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, 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 +225,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, 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, hand_info))
})
.unzip();
for other_info in other_infos {
info.subtract(&other_info);
}
@ -232,7 +253,7 @@ pub trait PublicInformation: Clone {
fn get_private_info(&self, view: &OwnedGameView) -> HandInfo<CardPossibilityTable> {
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);
}

View file

@ -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 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
@ -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 }
let card = &hand[self.index];
if (self.property)(board, card) {
1
} else {
0
}
}
fn acknowledge_answer(
&self,
@ -32,22 +36,30 @@ impl Question for CardHasProperty
hand_info: &mut HandInfo<CardPossibilityTable>,
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); }
if answer == 0 {
card_table.mark_false(card);
}
} else 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 +74,11 @@ struct AdditiveComboQuestion {
}
impl Question for AdditiveComboQuestion {
fn info_amount(&self) -> u32 {
self.questions.iter().map(|q| { q.info_amount() - 1 }).sum::<u32>() + 1
self.questions
.iter()
.map(|q| q.info_amount() - 1)
.sum::<u32>()
+ 1
}
fn answer(&self, hand: &Cards, board: &BoardState) -> u32 {
let mut toadd = 1;
@ -88,7 +104,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 +123,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();
@ -140,14 +159,14 @@ 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);
@ -159,9 +178,11 @@ 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];
let card = &hand[self.index];
*self.partition.get(card).unwrap()
}
fn acknowledge_answer(
@ -170,7 +191,7 @@ impl Question for CardPossibilityPartition {
hand_info: &mut HandInfo<CardPossibilityTable>,
_: &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 {
@ -180,11 +201,11 @@ impl Question for CardPossibilityPartition {
}
}
#[derive(Eq,PartialEq,Clone)]
#[derive(Eq, PartialEq, Clone)]
struct MyPublicInformation {
hand_info: FnvHashMap<Player, HandInfo<CardPossibilityTable>>,
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 +218,7 @@ impl MyPublicInformation {
fn get_other_players_starting_after(&self, player: Player) -> Vec<Player> {
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.
@ -206,21 +227,21 @@ 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().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)));
if !may_be_all_one_color && !may_be_all_one_number { 4 } else { 3 }
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,11 +263,15 @@ 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::<Vec<_>>();
scores.sort();
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::<Vec<_>>();
scores.sort_unstable();
scores[0].1
}
@ -266,14 +291,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 +381,55 @@ impl MyPublicInformation {
}
}
};
hint_option_set.into_iter().collect::<FnvHashSet<_>>().into_iter().map(|hinted| {
Hint {
hint_option_set
.into_iter()
.collect::<FnvHashSet<_>>()
.into_iter()
.map(|hinted| Hint {
player: hint_player,
hinted,
}
}).collect()
})
.collect()
}
fn decode_hint_choice(&self, hint: &Hint, result: &[bool]) -> 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).sum::<u32>();
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);
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 +447,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 +489,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 +518,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::<FnvHashMap<_,_>>();
let hand_info = board
.get_players()
.map(|player| {
let hand_info = HandInfo::new(board.hand_size);
(player, hand_info)
})
.collect::<FnvHashMap<_, _>>();
MyPublicInformation {
hand_info,
card_counts: CardCounts::new(),
@ -522,32 +557,50 @@ 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::<Vec<_>>();
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()
.map(|(i, card_table)| {
let p_play = card_table.probability_is_playable(&self.board);
let p_dead = card_table.probability_is_dead(&self.board);
(i, p_play, p_dead)
})
.collect::<Vec<_>>();
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 || hand_info[i].is_determined() { false }
else { true }
}).collect::<Vec<_>>();
let augmented_hand_info = augmented_hand_info_raw
.into_iter()
.filter(|&(i, _, p_dead)| p_dead != 1.0 && !hand_info[i].is_determined())
.collect::<Vec<_>>();
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))
}
}));
}
@ -555,7 +608,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);
}
@ -563,31 +616,37 @@ 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<dyn Question> {
if ask_dead { Box::new(q_is_dead(i)) }
else { Box::new(q_is_playable(i)) }
}).collect::<Vec<_>>();
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<dyn Question> {
if ask_dead {
Box::new(q_is_dead(i))
} else {
Box::new(q_is_playable(i))
}
})
.collect::<Vec<_>>();
if !questions.is_empty() {
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::<Vec<_>>();
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::<Vec<_>>();
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) {
ask_partition.sort_by_key(|&(i, _, p_death)| (FloatOrd(p_death), i));
if let Some(&(i, _, _)) = ask_partition.first() {
let question = CardPossibilityPartition::new(i, total_info, &hand_info[i], &self.board);
Some(Box::new(question))
} else {
@ -596,8 +655,6 @@ impl PublicInformation for MyPublicInformation {
}
}
pub struct InformationStrategyConfig;
impl InformationStrategyConfig {
@ -640,8 +697,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)
}
@ -657,7 +718,13 @@ impl InformationPlayerStrategy {
(10.0 - card.value as f32) / (num_with as f32)
}
fn find_useless_cards(&self, board: &BoardState, hand: &HandInfo<CardPossibilityTable>) -> Vec<usize> {
fn find_useless_cards(
&self,
board: &BoardState,
hand: &HandInfo<CardPossibilityTable>,
) -> Vec<usize> {
use std::collections::hash_map::Entry::{Occupied, Vacant};
let mut useless: FnvHashSet<usize> = FnvHashSet::default();
let mut seen: FnvHashMap<Card, usize> = FnvHashMap::default();
@ -665,17 +732,20 @@ impl InformationPlayerStrategy {
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) {
// found a duplicate card
useless.insert(i);
useless.insert(*seen.get(&card).unwrap());
} else {
seen.insert(card, i);
match seen.entry(card) {
Occupied(e) => {
// found a duplicate card
useless.insert(i);
useless.insert(*e.get());
}
Vacant(e) => {
e.insert(i);
}
}
}
}
let mut useless_vec : Vec<usize> = useless.into_iter().collect();
useless_vec.sort();
let mut useless_vec: Vec<usize> = useless.into_iter().collect();
useless_vec.sort_unstable();
useless_vec
}
@ -701,17 +771,14 @@ 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);
let bonus = {
if card_table.is_determined() || 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
@ -729,13 +796,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::<Vec<_>>();
let mut hint_options = hints
.into_iter()
.map(|hint| (self.hint_goodness(&hint, view), hint))
.collect::<Vec<_>>();
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.is_empty() {
// NOTE: Technically possible, but never happens
@ -756,7 +822,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);
@ -771,39 +837,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::<Vec<_>>();
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::<Vec<_>>();
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)
if let Some(&(play_index, _)) = playable_cards.first() {
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::<Vec<_>>();
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::<Vec<_>>();
if !risky_playable_cards.is_empty() {
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 {
@ -812,19 +885,25 @@ 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.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 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);
@ -847,16 +926,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::<Vec<_>>();
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::<Vec<_>>();
cards_by_discard_value.sort_by_key(|&(i, compval)| (FloatOrd(-compval), i));
let (index, _) = cards_by_discard_value[0];
TurnChoice::Discard(index)
@ -876,13 +957,15 @@ impl InformationPlayerStrategy {
hint_matches: Option<&Vec<bool>>,
) {
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 {
@ -891,8 +974,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);
}
}
@ -912,39 +999,59 @@ 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 };
} 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) => {
if let &TurnResult::Hint(ref matches) = &turn_record.result {
TurnChoice::Hint(ref hint) => {
if let TurnResult::Hint(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);
if let TurnResult::Discard(card) = &turn_record.result {
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) => {
if let &TurnResult::Play(ref card, _) = &turn_record.result {
self.public_info.update_from_discard_or_play_result(view, &turn_record.player, index, card);
TurnChoice::Play(index) => {
if let TurnResult::Play(card, _) = &turn_record.result {
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
);
}
}
}

View file

@ -1,4 +1,4 @@
use game::*;
use crate::game::*;
// Traits to implement for any valid Hanabi strategy
@ -15,7 +15,7 @@ pub trait PlayerStrategy {
// Shouldn't do much, except store configuration parameters and
// possibility initialize some shared randomness between players
pub trait GameStrategy {
fn initialize(&self, player: Player, view: &BorrowedGameView) -> Box<dyn PlayerStrategy>;
fn initialize(&self, me: Player, view: &BorrowedGameView) -> Box<dyn PlayerStrategy>;
}
// Represents configuration for a strategy.
@ -23,4 +23,3 @@ pub trait GameStrategy {
pub trait GameStrategyConfig {
fn initialize(&self, opts: &GameOptions) -> Box<dyn GameStrategy>;
}