From e510f7cc5e6fa942ffec07780058a383410165b3 Mon Sep 17 00:00:00 2001 From: Felix Bauckholt Date: Sun, 3 Mar 2019 22:50:20 +0100 Subject: [PATCH 1/5] Switch around ordering of question combining This has the advantage that for some ModulusInformation object m and some modulus, ``` a = m.split(modulus); m.cast_down(x); ``` is equivalent to ``` m.cast_down(x*modulus); a = m.split(modulus); ``` This means that when using a ModulusInformation object to answer questions, we can answer the first question without needing to know how many questions there are. --- README.md | 4 ++-- src/strategies/information.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c1e8a05..e47e9fc 100644 --- a/README.md +++ b/README.md @@ -73,5 +73,5 @@ On the first 20000 seeds, we have these average scores and win rates: |---------|---------|---------|---------|---------| | cheat | 24.8594 | 24.9785 | 24.9720 | 24.9557 | | | 90.59 % | 98.17 % | 97.76 % | 96.42 % | -| info | 22.3249 | 24.7278 | 24.8919 | 24.8961 | -| | 09.81 % | 80.54 % | 91.67 % | 91.90 % | +| info | 22.3316 | 24.7246 | 24.8892 | 24.8969 | +| | 09.81 % | 80.38 % | 91.45 % | 91.94 % | diff --git a/src/strategies/information.rs b/src/strategies/information.rs index 0d6ed70..49cf466 100644 --- a/src/strategies/information.rs +++ b/src/strategies/information.rs @@ -29,7 +29,7 @@ impl ModulusInformation { } pub fn combine(&mut self, other: Self) { - self.value = self.value * other.modulus + other.value; + self.value = self.value + self.modulus * other.value; self.modulus = self.modulus * other.modulus; } @@ -39,10 +39,10 @@ impl ModulusInformation { let original_modulus = self.modulus; let original_value = self.value; self.modulus = self.modulus / modulus; - let value = self.value / self.modulus; - self.value = self.value - value * self.modulus; + let value = self.value % modulus; + self.value = self.value / modulus; assert!(original_modulus == modulus * self.modulus); - assert!(original_value == value * self.modulus + self.value); + assert!(original_value == value + modulus * self.value); Self::new(modulus, value) } From 493631dad01ff037046a5c6f2854bbf2c0020942 Mon Sep 17 00:00:00 2001 From: Felix Bauckholt Date: Mon, 4 Mar 2019 17:24:24 +0100 Subject: [PATCH 2/5] Refactor out a "public information object" One important change is that now, when deciding which questions to ask, they can see the answer to the last question before asking the next one. Some design choices: - Questions now take a BoardState instead of an OwnedGameView. - When deciding which questions to ask (in ask_questions), we get an immutable public information object (representing the public information before any questions were asked), and a mutable HandInfo that gets updated as we ask questions. That HandInfo was copied instead of taken. - In ask_questions, we also get some &mut u32 representing "info_remaining" that gets updated for us. This will later allow for cases where "info_remaining" depends on the answers to previous questions. - Both get_hint_sum and update_from_hint_sum change the public information object. If you want to compute the hint sum but aren't sure if you actually want to give the hint, you'll have to clone the public information object! - Over time, in the code to decide on a move, we'll be able to build an increasingly complicated tree of "public information object operations" that will have to be matched exactly in the code to update on a move. In order to make this less scary, I moved most of the code into "decide_wrapped" and "update_wrapped". If the call to update_wrapped (for the player who just made the move) changes the public information object in different ways than the previous call to decide_wrapped, we detect this and panic. This commit should be purely refactoring; all changes to win-rates are due to bugs. --- README.md | 4 +- src/game.rs | 16 +- src/helpers.rs | 4 +- src/main.rs | 1 + src/strategies/hat_helpers.rs | 218 +++++++ src/strategies/information.rs | 1154 +++++++++++++++------------------ 6 files changed, 746 insertions(+), 651 deletions(-) create mode 100644 src/strategies/hat_helpers.rs diff --git a/README.md b/README.md index e47e9fc..05def07 100644 --- a/README.md +++ b/README.md @@ -73,5 +73,5 @@ On the first 20000 seeds, we have these average scores and win rates: |---------|---------|---------|---------|---------| | cheat | 24.8594 | 24.9785 | 24.9720 | 24.9557 | | | 90.59 % | 98.17 % | 97.76 % | 96.42 % | -| info | 22.3316 | 24.7246 | 24.8892 | 24.8969 | -| | 09.81 % | 80.38 % | 91.45 % | 91.94 % | +| info | 22.2908 | 24.7171 | 24.8875 | 24.8957 | +| | 09.40 % | 79.94 % | 91.32 % | 91.83 % | diff --git a/src/game.rs b/src/game.rs index ce6d388..e973a3c 100644 --- a/src/game.rs +++ b/src/game.rs @@ -44,7 +44,7 @@ impl fmt::Debug for Card { } } -#[derive(Debug,Clone)] +#[derive(Debug,Clone,Eq,PartialEq)] pub struct CardCounts { counts: FnvHashMap, } @@ -99,7 +99,7 @@ impl fmt::Display for CardCounts { pub type Cards = Vec; -#[derive(Debug,Clone)] +#[derive(Debug,Clone,Eq,PartialEq)] pub struct Discard { pub cards: Cards, counts: CardCounts, @@ -137,7 +137,7 @@ impl fmt::Display for Discard { pub type Score = u32; pub const PERFECT_SCORE: Score = (NUM_COLORS * NUM_VALUES) as u32; -#[derive(Debug,Clone)] +#[derive(Debug,Clone,Eq,PartialEq)] pub struct Firework { pub color: Color, pub top: Value, @@ -198,14 +198,14 @@ impl fmt::Display for Hinted { } } -#[derive(Debug,Clone)] +#[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)] +#[derive(Debug,Clone,Eq,PartialEq)] pub enum TurnChoice { Hint(Hint), Discard(usize), // index of card to discard @@ -213,7 +213,7 @@ pub enum TurnChoice { } // represents what happened in a turn -#[derive(Debug,Clone)] +#[derive(Debug,Clone,Eq,PartialEq)] pub enum TurnResult { Hint(Vec), // vector of whether each was in the hint Discard(Card), // card discarded @@ -221,7 +221,7 @@ pub enum TurnResult { } // represents a turn taken in the game -#[derive(Debug,Clone)] +#[derive(Debug,Clone,Eq,PartialEq)] pub struct TurnRecord { pub player: Player, pub choice: TurnChoice, @@ -243,7 +243,7 @@ pub struct GameOptions { // State of everything except the player's hands // Is all completely common knowledge -#[derive(Debug,Clone)] +#[derive(Debug,Clone,Eq,PartialEq)] pub struct BoardState { pub deck_size: u32, pub total_cards: u32, diff --git a/src/helpers.rs b/src/helpers.rs index 504319a..43aa3b7 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -246,7 +246,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)] +#[derive(Clone,Debug,Eq,PartialEq)] pub struct CardPossibilityTable { possible: HashMap, } @@ -369,7 +369,7 @@ impl fmt::Display for CardPossibilityTable { } } -#[derive(Clone)] +#[derive(Clone,Eq,PartialEq)] pub struct HandInfo where T: CardInfo { pub hand_info: Vec } diff --git a/src/main.rs b/src/main.rs index c322ba9..12a05f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod strategy; mod strategies { pub mod examples; pub mod cheating; + mod hat_helpers; pub mod information; } diff --git a/src/strategies/hat_helpers.rs b/src/strategies/hat_helpers.rs new file mode 100644 index 0000000..ad50a04 --- /dev/null +++ b/src/strategies/hat_helpers.rs @@ -0,0 +1,218 @@ +use game::*; +use helpers::*; + +#[derive(Debug,Clone)] +pub struct ModulusInformation { + pub modulus: u32, + pub value: u32, +} +impl ModulusInformation { + pub fn new(modulus: u32, value: u32) -> Self { + assert!(value < modulus); + ModulusInformation { + modulus: modulus, + value: value, + } + } + + pub fn none() -> Self { + Self::new(1, 0) + } + + pub fn combine(&mut self, other: Self) { + self.value = self.value + self.modulus * other.value; + self.modulus = self.modulus * other.modulus; + } + + pub fn split(&mut self, modulus: u32) -> Self { + assert!(self.modulus >= modulus); + assert!(self.modulus % modulus == 0); + let original_modulus = self.modulus; + let original_value = self.value; + self.modulus = self.modulus / modulus; + let value = self.value % modulus; + self.value = self.value / modulus; + assert!(original_modulus == modulus * self.modulus); + assert!(original_value == value + modulus * self.value); + Self::new(modulus, value) + } + + pub fn cast_up(&mut self, modulus: u32) { + assert!(self.modulus <= modulus); + self.modulus = modulus; + } + + pub fn cast_down(&mut self, modulus: u32) { + assert!(self.modulus >= modulus); + assert!(self.value < modulus); + self.modulus = modulus; + } + + pub fn add(&mut self, other: &Self) { + assert!(self.modulus == other.modulus); + self.value = (self.value + other.value) % self.modulus; + } + + pub fn subtract(&mut self, other: &Self) { + assert!(self.modulus == other.modulus); + self.value = (self.modulus + self.value - other.value) % self.modulus; + } +} + +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; + // process the answer to this question, updating card info + fn acknowledge_answer( + &self, value: u32, &mut HandInfo, &BoardState + ); + + fn answer_info(&self, hand: &Cards, board: &BoardState) -> ModulusInformation { + ModulusInformation::new( + self.info_amount(), + self.answer(hand, board) + ) + } + + fn acknowledge_answer_info( + &self, + answer: ModulusInformation, + hand_info: &mut HandInfo, + board: &BoardState + ) { + assert!(self.info_amount() == answer.modulus); + self.acknowledge_answer(answer.value, hand_info, board); + } +} + +pub trait PublicInformation: Clone { + fn get_player_info(&self, &Player) -> HandInfo; + fn set_player_info(&mut self, &Player, HandInfo); + + fn new(&BoardState) -> Self; + fn set_board(&mut self, &BoardState); + + /// If we store more state than just `HandInfo`s, update it after `set_player_info` has been called. + fn update_other_info(&mut self) { + } + + fn agrees_with(&self, other: Self) -> bool; + + /// By defining `ask_questions`, we decides which `Question`s a player learns the answers to. + /// + /// A player "asks" a question by calling the callback. Questions can depend on the answers to + /// earlier questions: We are given a `&mut HandInfo` that we'll have to pass + /// to that callback; there, it will be modified to reflect the answer to the question. Note that `self` + /// is not modified and thus reflects the state before any player "asked" any question. + /// + /// The product of the `info_amount()`s of all questions we have may not exceed `total_info`. + /// For convenience, we pass a `&mut u32` to the callback, and it will be updated to the + /// "remaining" information amount. + fn ask_questions(&self, &Player, &mut HandInfo, Callback, total_info: u32) + where Callback: FnMut(&mut HandInfo, &mut u32, Box); + + fn set_player_infos(&mut self, infos: Vec<(Player, HandInfo)>) { + for (player, new_hand_info) in infos { + self.set_player_info(&player, new_hand_info); + } + self.update_other_info(); + } + + fn get_hat_info_for_player( + &self, player: &Player, hand_info: &mut HandInfo, total_info: u32, view: &OwnedGameView + ) -> ModulusInformation { + assert!(player != &view.player); + let mut answer_info = ModulusInformation::none(); + { + let callback = |hand_info: &mut HandInfo, info_remaining: &mut u32, question: Box| { + *info_remaining = *info_remaining / question.info_amount(); + 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); + }; + self.ask_questions(player, hand_info, callback, total_info); + } + answer_info.cast_up(total_info); + answer_info + } + + fn update_from_hat_info_for_player( + &self, + player: &Player, + hand_info: &mut HandInfo, + board: &BoardState, + mut info: ModulusInformation, + ) { + let total_info = info.modulus; + let callback = |hand_info: &mut HandInfo, info_remaining: &mut u32, question: Box| { + let q_info_amount = question.info_amount(); + *info_remaining = *info_remaining / q_info_amount; + let info_modulus = info.modulus; + // Instead of casting down the ModulusInformation to the product of all the questions before + // answering any, we now have to cast down the ModulusInformation question-by-question. + info.cast_down((info_modulus / q_info_amount) * q_info_amount); + let answer_info = info.split(question.info_amount()); + question.acknowledge_answer_info(answer_info, hand_info, board); + }; + self.ask_questions(player, hand_info, callback, total_info); + } + + /// When deciding on a move, if we can choose between `total_info` choices, + /// `self.get_hat_sum(total_info, view)` tells us which choice to take, and at the same time + /// mutates `self` to simulate the choice becoming common knowledge. + fn get_hat_sum(&mut self, total_info: u32, view: &OwnedGameView) -> ModulusInformation { + let (infos, new_player_hands): (Vec<_>, Vec<_>) = view.get_other_players().iter().map(|player| { + let mut hand_info = self.get_player_info(player); + let info = self.get_hat_info_for_player(player, &mut hand_info, total_info, view); + (info, (player.clone(), hand_info)) + }).unzip(); + 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 + } + ) + } + + /// When updating on a move, if we infer that the player making the move called `get_hat_sum()` + /// and got the result `info`, we can call `self.update_from_hat_sum(info, view)` to update + /// from that fact. + fn update_from_hat_sum(&mut self, mut info: ModulusInformation, view: &OwnedGameView) { + let info_source = view.board.player; + let (other_infos, mut new_player_hands): (Vec<_>, Vec<_>) = view.get_other_players().into_iter().filter(|player| { + *player != info_source + }).map(|player| { + let mut hand_info = self.get_player_info(&player); + let player_info = self.get_hat_info_for_player(&player, &mut hand_info, info.modulus, view); + (player_info, (player.clone(), hand_info)) + }).unzip(); + for other_info in other_infos { + info.subtract(&other_info); + } + let me = view.player; + if me == info_source { + assert!(info.value == 0); + } else { + let mut my_hand = self.get_player_info(&me); + self.update_from_hat_info_for_player(&me, &mut my_hand, &view.board, info); + new_player_hands.push((me, my_hand)); + } + self.set_player_infos(new_player_hands); + } + + 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 card in hand { + card_table.decrement_weight_if_possible(card); + } + } + } + info + } +} diff --git a/src/strategies/information.rs b/src/strategies/information.rs index 49cf466..ea739e2 100644 --- a/src/strategies/information.rs +++ b/src/strategies/information.rs @@ -4,99 +4,15 @@ use std::cmp::Ordering; use strategy::*; use game::*; use helpers::*; +use strategies::hat_helpers::*; // TODO: use random extra information - i.e. when casting up and down, // we sometimes have 2 choices of value to choose // TODO: guess very aggressively at very end of game (first, see whether // situation ever occurs) -#[derive(Debug,Clone)] -struct ModulusInformation { - modulus: u32, - value: u32, -} -impl ModulusInformation { - pub fn new(modulus: u32, value: u32) -> Self { - assert!(value < modulus); - ModulusInformation { - modulus: modulus, - value: value, - } - } - - pub fn none() -> Self { - Self::new(1, 0) - } - - pub fn combine(&mut self, other: Self) { - self.value = self.value + self.modulus * other.value; - self.modulus = self.modulus * other.modulus; - } - - pub fn split(&mut self, modulus: u32) -> Self { - assert!(self.modulus >= modulus); - assert!(self.modulus % modulus == 0); - let original_modulus = self.modulus; - let original_value = self.value; - self.modulus = self.modulus / modulus; - let value = self.value % modulus; - self.value = self.value / modulus; - assert!(original_modulus == modulus * self.modulus); - assert!(original_value == value + modulus * self.value); - Self::new(modulus, value) - } - - pub fn cast_up(&mut self, modulus: u32) { - assert!(self.modulus <= modulus); - self.modulus = modulus; - } - - pub fn cast_down(&mut self, modulus: u32) { - assert!(self.modulus >= modulus); - assert!(self.value < modulus); - self.modulus = modulus; - } - - pub fn add(&mut self, other: &Self) { - assert!(self.modulus == other.modulus); - self.value = (self.value + other.value) % self.modulus; - } - - pub fn subtract(&mut self, other: &Self) { - assert!(self.modulus == other.modulus); - self.value = (self.modulus + self.value - other.value) % self.modulus; - } -} - -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, &OwnedGameView) -> u32; - // process the answer to this question, updating card info - fn acknowledge_answer( - &self, value: u32, &mut HandInfo, &OwnedGameView - ); - - fn answer_info(&self, hand: &Cards, view: &OwnedGameView) -> ModulusInformation { - ModulusInformation::new( - self.info_amount(), - self.answer(hand, view) - ) - } - - fn acknowledge_answer_info( - &self, - answer: ModulusInformation, - hand_info: &mut HandInfo, - view: &OwnedGameView - ) { - assert!(self.info_amount() == answer.modulus); - self.acknowledge_answer(answer.value, hand_info, view); - } -} - type PropertyPredicate = fn(&BoardState, &Card) -> bool; + struct CardHasProperty { index: usize, @@ -105,20 +21,20 @@ struct CardHasProperty impl Question for CardHasProperty { fn info_amount(&self) -> u32 { 2 } - fn answer(&self, hand: &Cards, view: &OwnedGameView) -> u32 { + fn answer(&self, hand: &Cards, board: &BoardState) -> u32 { let ref card = hand[self.index]; - if (self.property)(view.get_board(), card) { 1 } else { 0 } + if (self.property)(board, card) { 1 } else { 0 } } fn acknowledge_answer( &self, answer: u32, hand_info: &mut HandInfo, - view: &OwnedGameView, + board: &BoardState, ) { let ref mut card_table = hand_info[self.index]; let possible = card_table.get_possibilities(); for card in &possible { - if (self.property)(view.get_board(), card) { + if (self.property)(board, card) { if answer == 0 { card_table.mark_false(card); } } else { if answer == 1 { card_table.mark_false(card); } @@ -133,24 +49,24 @@ fn q_is_dead(index: usize) -> CardHasProperty { CardHasProperty {index, property: |board, card| board.is_dead(card)} } +/// For some list of questions l, the question `AdditiveComboQuestion { questions : l }` asks: +/// "What is the first question in the list `l` that has a nonzero answer, and what is its +/// answer?" +/// If all questions in `l` have the answer `0`, this question has the answer `0` as well. +/// +/// It's named that way because the `info_amount` grows additively with the `info_amount`s of +/// the questions in `l`. struct AdditiveComboQuestion { - /// For some list of questions l, the question `AdditiveComboQuestion { questions : l }` asks: - /// "What is the first question in the list `l` that has a nonzero answer, and what is its - /// answer?" - /// If all questions in `l` have the answer `0`, this question has the answer `0` as well. - /// - /// It's named that way because the `info_amount` grows additively with the `info_amount`s of - /// the questions in `l`. questions: Vec>, } impl Question for AdditiveComboQuestion { fn info_amount(&self) -> u32 { self.questions.iter().map(|q| { q.info_amount() - 1 }).sum::() + 1 } - fn answer(&self, hand: &Cards, view: &OwnedGameView) -> u32 { + fn answer(&self, hand: &Cards, board: &BoardState) -> u32 { let mut toadd = 1; for q in &self.questions { - let q_answer = q.answer(hand, view); + let q_answer = q.answer(hand, board); if q_answer != 0 { return toadd + q_answer - 1; } @@ -163,7 +79,7 @@ impl Question for AdditiveComboQuestion { &self, mut answer: u32, hand_info: &mut HandInfo, - view: &OwnedGameView, + board: &BoardState, ) { if answer == 0 { answer = self.info_amount(); @@ -171,10 +87,10 @@ impl Question for AdditiveComboQuestion { answer -= 1; for q in &self.questions { if answer < q.info_amount() - 1 { - q.acknowledge_answer(answer+1, hand_info, view); + q.acknowledge_answer(answer+1, hand_info, board); return; } else { - q.acknowledge_answer(0, hand_info, view); + q.acknowledge_answer(0, hand_info, board); answer -= q.info_amount() - 1; } } @@ -182,6 +98,7 @@ impl Question for AdditiveComboQuestion { } } +#[derive(Debug)] struct CardPossibilityPartition { index: usize, n_partitions: u32, @@ -189,13 +106,13 @@ struct CardPossibilityPartition { } impl CardPossibilityPartition { fn new( - index: usize, max_n_partitions: u32, card_table: &CardPossibilityTable, view: &OwnedGameView + index: usize, max_n_partitions: u32, card_table: &CardPossibilityTable, board: &BoardState ) -> CardPossibilityPartition { let mut cur_block = 0; let mut partition = FnvHashMap::default(); let mut n_partitions = 0; - let has_dead = card_table.probability_is_dead(&view.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; @@ -204,7 +121,7 @@ impl CardPossibilityPartition { }; for card in card_table.get_possibilities() { - if !view.board.is_dead(&card) { + if !board.is_dead(&card) { partition.insert(card.clone(), cur_block); cur_block = (cur_block + 1) % effective_max; if n_partitions < effective_max { @@ -215,7 +132,7 @@ impl CardPossibilityPartition { if has_dead { for card in card_table.get_possibilities() { - if view.board.is_dead(&card) { + if board.is_dead(&card) { partition.insert(card.clone(), n_partitions); } } @@ -242,7 +159,7 @@ impl CardPossibilityPartition { } impl Question for CardPossibilityPartition { fn info_amount(&self) -> u32 { self.n_partitions } - fn answer(&self, hand: &Cards, _: &OwnedGameView) -> u32 { + fn answer(&self, hand: &Cards, _: &BoardState) -> u32 { let ref card = hand[self.index]; *self.partition.get(&card).unwrap() } @@ -250,7 +167,7 @@ impl Question for CardPossibilityPartition { &self, answer: u32, hand_info: &mut HandInfo, - _: &OwnedGameView, + _: &BoardState, ) { let ref mut card_table = hand_info[self.index]; let possible = card_table.get_possibilities(); @@ -262,74 +179,353 @@ impl Question for CardPossibilityPartition { } } -pub struct InformationStrategyConfig; - -impl InformationStrategyConfig { - pub fn new() -> InformationStrategyConfig { - InformationStrategyConfig - } -} -impl GameStrategyConfig for InformationStrategyConfig { - fn initialize(&self, _: &GameOptions) -> Box { - Box::new(InformationStrategy::new()) - } +#[derive(Eq,PartialEq,Clone)] +struct MyPublicInformation { + hand_info: FnvHashMap>, + card_counts: CardCounts, // what any newly drawn card should be + board: BoardState, // TODO: maybe we should store an appropriately lifetimed reference? } -pub struct InformationStrategy; - -impl InformationStrategy { - pub fn new() -> InformationStrategy { - InformationStrategy +impl MyPublicInformation { + fn get_player_info_mut(&mut self, player: &Player) -> &mut HandInfo { + self.hand_info.get_mut(player).unwrap() + } + fn take_player_info(&mut self, player: &Player) -> HandInfo { + self.hand_info.remove(player).unwrap() } -} -impl GameStrategy for InformationStrategy { - fn initialize(&self, player: Player, view: &BorrowedGameView) -> Box { - let public_info = - view.board.get_players().map(|player| { - let hand_info = HandInfo::new(view.board.hand_size); - (player, hand_info) - }).collect::>(); - Box::new(InformationPlayerStrategy { - me: player, - public_info: public_info, - public_counts: CardCounts::new(), - last_view: OwnedGameView::clone_from(view), + 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() + } + + // Returns the number of ways to hint the player. + fn get_info_per_player(&self, player: Player) -> u32 { + // Determine if both: + // - 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 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) + }) + }); + + return if !may_be_all_one_color && !may_be_all_one_number { 4 } else { 3 } + } + + fn get_hint_index_score(&self, card_table: &CardPossibilityTable) -> i32 { + if card_table.probability_is_dead(&self.board) == 1.0 { + return 0; + } + if card_table.is_determined() { + return 0; + } + // Do something more intelligent? + let mut score = 1; + if !card_table.color_determined() { + score += 1; + } + if !card_table.value_determined() { + score += 1; + } + return score; + } + + 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::>(); + scores.sort(); + scores[0].1 + } + + // TODO: refactor out the common parts of get_hint and update_from_hint_choice + fn get_hint(&mut self, view: &OwnedGameView) -> Vec { + // Can give up to 3(n-1) hints + // For any given player with at least 4 cards, and index i, there are at least 3 hints that can be given. + // 0. a value hint on card i + // 1. a color hint on card i + // 2. any hint not involving card i + // However, if it is public info that the player has at least two colors + // and at least two numbers, then instead we do + // 2. any color hint not involving i + // 3. any color hint not involving i + + // TODO: make it so space of hints is larger when there is + // 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 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 hint_info = self.get_hat_sum(total_info, view); + + //let hint_type = hint_info.value % 3; + //let player_amt = (hint_info.value - hint_type) / 3; + let mut hint_type = hint_info.value; + let mut player_amt = 0; + while hint_type >= info_per_player[player_amt] { + hint_type -= info_per_player[player_amt]; + player_amt += 1; + } + let hint_info_we_can_give_to_this_player = info_per_player[player_amt]; + + let hint_player = (hinter + 1 + (player_amt as u32)) % view.board.num_players; + + let hand = view.get_hand(&hint_player); + let card_index = card_indices[player_amt]; + let hint_card = &hand[card_index]; + + let hint_option_set = if hint_info_we_can_give_to_this_player == 3 { + match hint_type { + 0 => { + vec![Hinted::Value(hint_card.value)] + } + 1 => { + vec![Hinted::Color(hint_card.color)] + } + 2 => { + // NOTE: this doesn't do that much better than just hinting + // the first thing that doesn't match the hint_card + let mut hint_option_set = Vec::new(); + for card in hand { + if card.color != hint_card.color { + hint_option_set.push(Hinted::Color(card.color)); + } + if card.value != hint_card.value { + hint_option_set.push(Hinted::Value(card.value)); + } + } + hint_option_set + } + _ => { + panic!("Invalid hint type") + } + } + } else { + match hint_type { + 0 => { + vec![Hinted::Value(hint_card.value)] + } + 1 => { + vec![Hinted::Color(hint_card.color)] + } + 2 => { + // Any value hint for a card other than the first + let mut hint_option_set = Vec::new(); + for card in hand { + if card.value != hint_card.value { + hint_option_set.push(Hinted::Value(card.value)); + } + } + hint_option_set + } + 3 => { + // Any color hint for a card other than the first + let mut hint_option_set = Vec::new(); + for card in hand { + if card.color != hint_card.color { + hint_option_set.push(Hinted::Color(card.color)); + } + } + hint_option_set + } + _ => { + panic!("Invalid hint type") + } + } + }; + hint_option_set.into_iter().collect::>().into_iter().map(|hinted| { + Hint { + player: hint_player, + hinted: hinted, + } + }).collect() + } + + fn decode_hint_choice(&self, hint: &Hint, result: &Vec) -> ModulusInformation { + let hinter = self.board.player; + + let info_per_player: Vec<_> = self.get_other_players_starting_after(hinter).into_iter().map( + |player| { self.get_info_per_player(player) } + ).collect(); + let total_info = info_per_player.iter().sum(); + + let n = self.board.num_players; + + let player_amt = (n + hint.player - hinter - 1) % n; + + let amt_from_prev_players = info_per_player.iter().take(player_amt as usize).fold(0, |a, b| a + b); + let 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 + } + } 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; + + ModulusInformation::new(total_info, hint_value) + } + + fn update_from_hint_choice(&mut self, hint: &Hint, matches: &Vec, 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) { + let info = self.get_player_info_mut(&hint.player); + info.update_for_hint(&hint.hinted, matches); + } + + fn knows_playable_card(&self, player: &Player) -> bool { + 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) + }); + has_playable_card && !self.knows_playable_card(&player) }) } -} -pub struct InformationPlayerStrategy { - me: Player, - public_info: FnvHashMap>, - public_counts: CardCounts, // what any newly drawn card should be - last_view: OwnedGameView, // the view on the previous turn -} + fn update_noone_else_needs_hint(&mut self) { + // If it becomes public knowledge that someone_else_needs_hint() returns false, + // update accordingly. + for player in self.board.get_players() { + if player != self.board.player && !self.knows_playable_card(&player) { + // If player doesn't know any playable cards, player doesn't have any playable + // cards. + let mut hand_info = self.take_player_info(&player); + for ref mut card_table in hand_info.iter_mut() { + let possible = card_table.get_possibilities(); + for card in &possible { + if self.board.is_playable(card) { + card_table.mark_false(card); + } + } + } + self.set_player_info(&player, hand_info); + } + } + } -impl InformationPlayerStrategy { + fn update_from_discard_or_play_result( + &mut self, + new_view: &BorrowedGameView, + player: &Player, + index: usize, + card: &Card + ) { + let new_card_table = CardPossibilityTable::from(&self.card_counts); + { + let info = self.get_player_info_mut(player); + assert!(info[index].is_possible(card)); + info.remove(index); - fn get_questions( - total_info: u32, - view: &OwnedGameView, - hand_info: &HandInfo, - ) -> Vec> { - let mut questions = Vec::new(); - let mut info_remaining = total_info; - - fn add_question( - questions: &mut Vec>, info_remaining: &mut u32, question: T - ) -> bool where T: Question { - *info_remaining = *info_remaining / question.info_amount(); - questions.push(Box::new(question) as Box); - - // if there's no more info to give, return that we should stop - *info_remaining <= 1 + // push *before* incrementing public counts + if info.len() < new_view.hand_size(&player) { + info.push(new_card_table); + } } - let augmented_hand_info = hand_info.iter().enumerate() + // TODO: decrement weight counts for fully determined cards, ahead of time + + for player in self.board.get_players() { + let info = self.get_player_info_mut(&player); + for card_table in info.iter_mut() { + card_table.decrement_weight_if_possible(card); + } + } + + self.card_counts.increment(card); + } +} + +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::>(); + MyPublicInformation { + hand_info: hand_info, + card_counts: CardCounts::new(), + board: board.clone(), + } + } + + fn set_board(&mut self, board: &BoardState) { + self.board = board.clone(); + } + + fn get_player_info(&self, player: &Player) -> HandInfo { + self.hand_info[player].clone() + } + + fn set_player_info(&mut self, player: &Player, hand_info: HandInfo) { + self.hand_info.insert(*player, hand_info); + } + + fn agrees_with(&self, other: Self) -> bool { + *self == other + } + + fn ask_questions( + &self, + _player: &Player, + hand_info: &mut HandInfo, + mut ask_question: Callback, + mut info_remaining: u32, + ) where Callback: FnMut(&mut HandInfo, &mut u32, Box) { + // Changing anything inside this function will not break the information transfer + // mechanisms! + + let augmented_hand_info = hand_info.iter().cloned().enumerate() .map(|(i, card_table)| { - let p_play = card_table.probability_is_playable(&view.board); - let p_dead = card_table.probability_is_dead(&view.board); + let p_play = card_table.probability_is_playable(&self.board); + let p_dead = card_table.probability_is_dead(&self.board); let is_determined = card_table.is_determined(); (card_table, i, p_play, p_dead, is_determined) }) @@ -363,13 +559,12 @@ impl InformationPlayerStrategy { } else { result.unwrap() } - }); + }); - if view.board.num_players == 5 { + if self.board.num_players == 5 { for &(_, i, _, _, _) in ask_play { - if add_question(&mut questions, &mut info_remaining, q_is_playable(i)) { - return questions; - } + ask_question(hand_info, &mut info_remaining, Box::new(q_is_playable(i))); + if info_remaining <= 1 { return; } } } else { let mut rest_combo = AdditiveComboQuestion {questions: Vec::new()}; @@ -396,16 +591,15 @@ impl InformationPlayerStrategy { } else { result.unwrap() } - }); + }); for &(_, i, _, _, _) in ask_dead { if rest_combo.info_amount() < info_remaining { rest_combo.questions.push(Box::new(q_is_dead(i))); } } } - if add_question(&mut questions, &mut info_remaining, rest_combo) { - return questions; - } + ask_question(hand_info, &mut info_remaining, Box::new(rest_combo)); + if info_remaining <= 1 { return; } } } @@ -425,110 +619,59 @@ impl InformationPlayerStrategy { } else { result.unwrap() } - }); + }); - for &(card_table, i, _, _, _) in ask_partition { - let question = CardPossibilityPartition::new(i, info_remaining, card_table, view); - if add_question(&mut questions, &mut info_remaining, question) { - return questions; - } + for &(ref card_table, i, _, _, _) in ask_partition { + let question = CardPossibilityPartition::new(i, info_remaining, &card_table, &self.board); + ask_question(hand_info, &mut info_remaining, Box::new(question)); + if info_remaining <= 1 { return; } } - - return questions } +} - fn answer_questions( - questions: &Vec>, hand: &Cards, view: &OwnedGameView - ) -> ModulusInformation { - questions.iter() - .fold( - ModulusInformation::none(), - |mut answer_info, question| { - let new_answer_info = question.answer_info(hand, view); - answer_info.combine(new_answer_info); - answer_info - }) + + +pub struct InformationStrategyConfig; + +impl InformationStrategyConfig { + pub fn new() -> InformationStrategyConfig { + InformationStrategyConfig } - - fn get_hint_info_for_player( - &self, player: &Player, total_info: u32, view: &OwnedGameView - ) -> ModulusInformation { - assert!(player != &self.me); - let hand_info = self.get_player_public_info(player); - let questions = Self::get_questions(total_info, view, hand_info); - let mut answer = Self::answer_questions(&questions, view.get_hand(player), view); - answer.cast_up(total_info); - answer +} +impl GameStrategyConfig for InformationStrategyConfig { + fn initialize(&self, _: &GameOptions) -> Box { + Box::new(InformationStrategy::new()) } +} - fn get_hint_sum_info(&self, total_info: u32, view: &OwnedGameView) -> ModulusInformation { - view.get_other_players().iter().fold( - ModulusInformation::new(total_info, 0), - |mut sum_info, player| { - let answer = self.get_hint_info_for_player(&player, total_info, view); - sum_info.add(&answer); - sum_info +pub struct InformationStrategy; + +impl InformationStrategy { + pub fn new() -> InformationStrategy { + InformationStrategy + } +} +impl GameStrategy for InformationStrategy { + fn initialize(&self, player: Player, view: &BorrowedGameView) -> Box { + Box::new(InformationPlayerStrategy { + me: player, + public_info: MyPublicInformation::new(view.board), + new_public_info: None, + last_view: OwnedGameView::clone_from(view), }) } +} - fn infer_own_from_hint_sum(&mut self, mut hint: ModulusInformation) { - let questions = { - let view = &self.last_view; - let hand_info = self.get_my_public_info(); - Self::get_questions(hint.modulus, view, hand_info) - }; - let product = questions.iter().fold(1, |a, ref b| a * b.info_amount()); - hint.cast_down(product); - - let me = self.me.clone(); - let mut hand_info = self.take_public_info(&me); - - { - let view = &self.last_view; - for question in questions { - let answer_info = hint.split(question.info_amount()); - question.acknowledge_answer_info(answer_info, &mut hand_info, view); - } - } - self.return_public_info(&me, hand_info); - } - - fn update_from_hint_sum(&mut self, mut hint: ModulusInformation) { - let hinter = self.last_view.board.player; - let players = { - let view = &self.last_view; - view.board.get_players() - }; - for player in players { - if (player != hinter) && (player != self.me) { - { - let view = &self.last_view; - let hint_info = self.get_hint_info_for_player(&player, hint.modulus, view); - hint.subtract(&hint_info); - } - - // *take* instead of borrowing mutably, because of borrow rules... - let mut hand_info = self.take_public_info(&player); - - { - let view = &self.last_view; - let hand = view.get_hand(&player); - let questions = Self::get_questions(hint.modulus, view, &mut hand_info); - for question in questions { - let answer = question.answer(hand, view); - question.acknowledge_answer(answer, &mut hand_info, view); - } - } - self.return_public_info(&player, hand_info); - } - } - if self.me == hinter { - assert!(hint.value == 0); - } else { - self.infer_own_from_hint_sum(hint); - } - } +pub struct InformationPlayerStrategy { + me: Player, + public_info: MyPublicInformation, + // Inside decide(), modify a copy of public_info and put it here. After that, when + // calling update, check that the updated public_info matches new_public_info. + new_public_info: Option, + last_view: OwnedGameView, // the view on the previous turn +} +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) }; @@ -549,12 +692,12 @@ impl InformationPlayerStrategy { (10.0 - card.value as f32) / (num_with as f32) } - fn find_useless_cards(&self, view: &OwnedGameView, hand: &HandInfo) -> Vec { + fn find_useless_cards(&self, board: &BoardState, hand: &HandInfo) -> Vec { let mut useless: FnvHashSet = FnvHashSet::default(); let mut seen: FnvHashMap = FnvHashMap::default(); for (i, card_table) in hand.iter().enumerate() { - if card_table.probability_is_dead(view.get_board()) == 1.0 { + if card_table.probability_is_dead(board) == 1.0 { useless.insert(i); } else { if let Some(card) = card_table.get_card() { @@ -573,161 +716,16 @@ impl InformationPlayerStrategy { return useless_vec; } - fn take_public_info(&mut self, player: &Player) -> HandInfo { - self.public_info.remove(player).unwrap() - } - - fn return_public_info(&mut self, player: &Player, card_info: HandInfo) { - self.public_info.insert(*player, card_info); - } - - fn get_my_public_info(&self) -> &HandInfo { - self.get_player_public_info(&self.me) - } - - // fn get_my_public_info_mut(&mut self) -> &mut HandInfo { - // let me = self.me.clone(); - // self.get_player_public_info_mut(&me) - // } - - fn get_player_public_info(&self, player: &Player) -> &HandInfo { - self.public_info.get(player).unwrap() - } - - fn get_player_public_info_mut(&mut self, player: &Player) -> &mut HandInfo { - self.public_info.get_mut(player).unwrap() - } - - fn update_public_info_for_hint(&mut self, hint: &Hint, matches: &Vec) { - let info = self.get_player_public_info_mut(&hint.player); - info.update_for_hint(&hint.hinted, matches); - } - - fn knows_playable_card(&self, player: &Player) -> bool { - self.get_player_public_info(&player).iter().any(|table| { - table.probability_is_playable(self.last_view.get_board()) == 1.0 - }) - } - - fn someone_else_needs_hint(&self) -> bool { - // Does another player have a playable card, but doesn't know it? - let view = &self.last_view; - view.get_other_players().iter().any(|player| { - 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) - }) - } - - fn update_noone_else_needs_hint(&mut self) { - // If it becomes public knowledge that someone_else_needs_hint() returns false, - // update accordingly. - for player in self.last_view.board.get_players() { - if player != self.last_view.board.player { - if !self.knows_playable_card(&player) { - // If player doesn't know any playable cards, player doesn't have any playable - // cards. - let mut hand_info = self.take_public_info(&player); - for ref mut card_table in hand_info.iter_mut() { - let view = &self.last_view; - let possible = card_table.get_possibilities(); - for card in &possible { - if view.get_board().is_playable(card) { - card_table.mark_false(card); - } - } - } - self.return_public_info(&player, hand_info); - } - } - } - } - - fn update_public_info_for_discard_or_play( - &mut self, - view: &BorrowedGameView, - player: &Player, - index: usize, - card: &Card - ) { - let new_card_table = CardPossibilityTable::from(&self.public_counts); - { - let info = self.get_player_public_info_mut(&player); - assert!(info[index].is_possible(card)); - info.remove(index); - - // push *before* incrementing public counts - if info.len() < view.hand_size(&player) { - info.push(new_card_table); - } - } - - // TODO: decrement weight counts for fully determined cards, ahead of time - - // note: other_player could be player, as well - // in particular, we will decrement the newly drawn card - for other_player in view.board.get_players() { - let info = self.get_player_public_info_mut(&other_player); - for card_table in info.iter_mut() { - card_table.decrement_weight_if_possible(card); - } - } - - self.public_counts.increment(card); - } - - fn get_private_info(&self, view: &OwnedGameView) -> HandInfo { - let mut info = self.get_my_public_info().clone(); - for card_table in info.iter_mut() { - for (_, hand) in &view.other_hands { - for card in hand { - card_table.decrement_weight_if_possible(card); - } - } - } - info - } - - fn get_hint_index_score(&self, card_table: &CardPossibilityTable, view: &OwnedGameView) -> i32 { - if card_table.probability_is_dead(view.get_board()) == 1.0 { - return 0; - } - if card_table.is_determined() { - return 0; - } - // Do something more intelligent? - let mut score = 1; - if !card_table.color_determined() { - score += 1; - } - if !card_table.value_determined() { - score += 1; - } - return score; - } - - fn get_index_for_hint(&self, info: &HandInfo, view: &OwnedGameView) -> usize { - let mut scores = info.iter().enumerate().map(|(i, card_table)| { - let score = self.get_hint_index_score(card_table, view); - (-score, i) - }).collect::>(); - scores.sort(); - scores[0].1 - } - // how good is it to give this hint to this player? - fn hint_goodness(&self, hinted: &Hinted, hint_player: &Player, view: &OwnedGameView) -> f32 { - let hand = view.get_hand(&hint_player); + fn hint_goodness(&self, hint: &Hint, view: &OwnedGameView) -> f32 { + // This gets called after self.public_info.get_hint(), which modifies the public + // info to include information gained through question answering. Therefore, we only + // simulate information gained through the hint result here. - // get post-hint hand_info - let mut hand_info = self.get_player_public_info(hint_player).clone(); - let total_info = view.get_other_players().into_iter().map(|player| self.get_info_per_player(player)).sum(); - let questions = Self::get_questions(total_info, view, &hand_info); - for question in questions { - let answer = question.answer(hand, view); - question.acknowledge_answer(answer, &mut hand_info, view); - } + 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 mut goodness = 1.0; for (i, card_table) in hand_info.iter_mut().enumerate() { @@ -763,41 +761,15 @@ impl InformationPlayerStrategy { goodness } - // Returns the number of ways to hint the player. - fn get_info_per_player(&self, player: Player) -> u32 { - let info = self.get_player_public_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_number = VALUES.iter().any(|value| { - info.iter().all(|card| { - card.can_be_value(*value) - }) - }); - - return if !may_be_all_one_color && !may_be_all_one_number { 4 } else { 3 } - - // Determine if both: - // - it is public that there are at least two colors - // - it is public that there are at least two numbers - } - - fn get_other_players_starting_after(&self, player: Player) -> Vec { - let view = &self.last_view; - let n = view.board.num_players; - (0 .. n - 1).into_iter().map(|i| { (player + 1 + i) % n }).collect() - } - - fn get_best_hint_of_options(&self, hint_player: Player, hint_option_set: FnvHashSet) -> Hinted { + fn get_best_hint_of_options(&self, mut hints: Vec) -> Hint { + if hints.len() == 1 { + return hints.remove(0); + } let view = &self.last_view; // using hint goodness barely helps - let mut hint_options = hint_option_set.into_iter().map(|hinted| { - (self.hint_goodness(&hinted, &hint_player, view), hinted) + let mut hint_options = hints.into_iter().map(|hint| { + (self.hint_goodness(&hint, view), hint) }).collect::>(); hint_options.sort_by(|h1, h2| { @@ -814,173 +786,25 @@ impl InformationPlayerStrategy { hint_options.remove(0).1 } - fn get_hint(&self) -> TurnChoice { - let view = &self.last_view; - - // Can give up to 3(n-1) hints - // For any given player with at least 4 cards, and index i, there are at least 3 hints that can be given. - // 0. a value hint on card i - // 1. a color hint on card i - // 2. any hint not involving card i - // However, if it is public info that the player has at least two colors - // and at least two numbers, then instead we do - // 2. any color hint not involving i - // 3. any color hint not involving i - - // TODO: make it so space of hints is larger when there is - // knowledge about the cards? - - let info_per_player: Vec = self.get_other_players_starting_after(self.me).iter().map( - |player| { self.get_info_per_player(*player) } - ).collect(); - let total_info = info_per_player.iter().fold(0, |a, b| a + b); - - let hint_info = self.get_hint_sum_info(total_info, view); - - //let hint_type = hint_info.value % 3; - //let player_amt = (hint_info.value - hint_type) / 3; - let mut hint_type = hint_info.value; - let mut player_amt = 0; - while hint_type >= info_per_player[player_amt] { - hint_type -= info_per_player[player_amt]; - player_amt += 1; - } - let hint_info_we_can_give_to_this_player = info_per_player[player_amt]; - - let hint_player = (self.me + 1 + (player_amt as u32)) % view.board.num_players; - - let hand = view.get_hand(&hint_player); - let card_index = self.get_index_for_hint(self.get_player_public_info(&hint_player), view); - let hint_card = &hand[card_index]; - - let hinted = if hint_info_we_can_give_to_this_player == 3 { - match hint_type { - 0 => { - Hinted::Value(hint_card.value) - } - 1 => { - Hinted::Color(hint_card.color) - } - 2 => { - // NOTE: this doesn't do that much better than just hinting - // the first thing that doesn't match the hint_card - let mut hint_option_set = FnvHashSet::default(); - for card in hand { - if card.color != hint_card.color { - hint_option_set.insert(Hinted::Color(card.color)); - } - if card.value != hint_card.value { - hint_option_set.insert(Hinted::Value(card.value)); - } - } - self.get_best_hint_of_options(hint_player, hint_option_set) - } - _ => { - panic!("Invalid hint type") - } - } - } else { - match hint_type { - 0 => { - Hinted::Value(hint_card.value) - } - 1 => { - Hinted::Color(hint_card.color) - } - 2 => { - // Any value hint for a card other than the first - let mut hint_option_set = FnvHashSet::default(); - for card in hand { - if card.value != hint_card.value { - hint_option_set.insert(Hinted::Value(card.value)); - } - } - self.get_best_hint_of_options(hint_player, hint_option_set) - } - 3 => { - // Any color hint for a card other than the first - let mut hint_option_set = FnvHashSet::default(); - for card in hand { - if card.color != hint_card.color { - hint_option_set.insert(Hinted::Color(card.color)); - } - } - self.get_best_hint_of_options(hint_player, hint_option_set) - } - _ => { - panic!("Invalid hint type") - } - } - }; - - TurnChoice::Hint(Hint { - player: hint_player, - hinted: hinted, - }) - } - - fn infer_from_hint(&mut self, hint: &Hint, result: &Vec) { - let hinter = self.last_view.board.player; - - let info_per_player: Vec = self.get_other_players_starting_after(hinter).iter().map( - |player| { self.get_info_per_player(*player) } - ).collect(); - let total_info = info_per_player.iter().fold(0, |a, b| a + b); - - let n = self.last_view.board.num_players; - - let player_amt = (n + hint.player - hinter - 1) % n; - - let amt_from_prev_players = info_per_player.iter().take(player_amt as usize).fold(0, |a, b| a + b); - let hint_info_we_can_give_to_this_player = info_per_player[player_amt as usize]; - - let card_index = self.get_index_for_hint(self.get_player_public_info(&hint.player), &self.last_view); - 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 - } - } 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; - - let mod_info = ModulusInformation::new(total_info, hint_value); - - self.update_from_hint_sum(mod_info); - } -} - -impl PlayerStrategy for InformationPlayerStrategy { - fn decide(&mut self, _: &BorrowedGameView) -> TurnChoice { + /// Decide on a move. At the same time, simulate the impact of that move on the public + /// information state by modifying `public_info`. Since `self` is immutable and since our + /// public information state change will be compared against the change in the corresponding + /// call to `update_wrapped`, nothing we do here will let our public information state silently + /// get out of sync with other players' public information state! + fn decide_wrapped(&mut self, public_info: &mut MyPublicInformation) -> TurnChoice { // we already stored the view let view = &self.last_view; + let me = &view.player; for player in view.board.get_players() { - let hand_info = self.get_player_public_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); } } - let private_info = self.get_private_info(view); + let private_info = public_info.get_private_info(view); // debug!("My info:"); // for (i, card_table) in private_info.iter().enumerate() { // debug!("{}: {}", i, card_table); @@ -1039,12 +863,12 @@ impl PlayerStrategy for InformationPlayerStrategy { } } - let public_useless_indices = self.find_useless_cards(view, &self.get_my_public_info()); - let useless_indices = self.find_useless_cards(view, &private_info); + 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 && self.someone_else_needs_hint() { true } + 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). @@ -1054,13 +878,24 @@ impl PlayerStrategy for InformationPlayerStrategy { else { false }; if will_hint { - return self.get_hint(); + let hint_set = public_info.get_hint(view); + let hint = self.get_best_hint_of_options(hint_set); + return TurnChoice::Hint(hint); + } + + // We update on the discard choice before updating on the fact that we're discarding to + // match pre-refactor behavior. + // TODO: change this in the next commit! + let discard_info = if public_useless_indices.len() > 1 { + Some(public_info.get_hat_sum(public_useless_indices.len() as u32, view)) + } else { None }; + if self.last_view.board.hints_remaining > 0 { + public_info.update_noone_else_needs_hint(); } // if anything is totally useless, discard it if public_useless_indices.len() > 1 { - let info = self.get_hint_sum_info(public_useless_indices.len() as u32, view); - return TurnChoice::Discard(public_useless_indices[info.value as usize]); + return TurnChoice::Discard(public_useless_indices[discard_info.unwrap().value as usize]); } else if useless_indices.len() > 0 { // TODO: have opponents infer that i knew a card was useless // TODO: after that, potentially prefer useless indices that arent public @@ -1087,39 +922,79 @@ impl PlayerStrategy for InformationPlayerStrategy { TurnChoice::Discard(index) } + /// Update the public information. The "update" operations on the public information state have to + /// exactly match the corresponding "choice" operations in `decide_wrapped()`. + /// + /// We don't have to update on the "turn result" here. If the turn was a hint, we get the + /// matches in order to understand the "intention" behind the hint, but we do not need to + /// update on what the hint says about the hinted player's cards directly. (This is done in the + /// call to `update_hint_matches()` inside `update()`. + fn update_wrapped( + &mut self, + turn_player: &Player, + turn_choice: &TurnChoice, + hint_matches: Option<&Vec>, + ) { + match turn_choice { + TurnChoice::Hint(ref hint) => { + let matches = hint_matches.unwrap(); + 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) + ); + + // TODO: reorder these blocks in the next commit! + 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); + self.public_info.update_from_hat_sum(info, &self.last_view); + } + if self.last_view.board.hints_remaining > 0 { + self.public_info.update_noone_else_needs_hint(); + } + } + TurnChoice::Play(_index) => { + // TODO: Maybe we can transfer information through plays as well? + } + } + } +} + +impl PlayerStrategy for InformationPlayerStrategy { + fn decide(&mut self, _: &BorrowedGameView) -> TurnChoice { + let mut public_info = self.public_info.clone(); + let turn_choice = self.decide_wrapped(&mut public_info); + self.new_public_info = Some(public_info); + turn_choice + } + fn update(&mut self, turn_record: &TurnRecord, view: &BorrowedGameView) { + let hint_matches = if let &TurnResult::Hint(ref matches) = &turn_record.result { + Some(matches) + } else { None }; + 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!"); + } + } match turn_record.choice { TurnChoice::Hint(ref hint) => { if let &TurnResult::Hint(ref matches) = &turn_record.result { - self.infer_from_hint(hint, matches); - self.update_public_info_for_hint(hint, matches); + self.public_info.update_from_hint_matches(hint, matches); } else { panic!("Got turn choice {:?}, but turn result {:?}", turn_record.choice, turn_record.result); } } TurnChoice::Discard(index) => { - let known_useless_indices = self.find_useless_cards( - &self.last_view, &self.get_player_public_info(&turn_record.player) - ); - - 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(); - self.update_from_hint_sum(ModulusInformation::new( - known_useless_indices.len() as u32, value as u32 - )); - } - if self.last_view.board.hints_remaining > 0 { - // TODO It would be more information-efficient to do this before the call to - // update_from_hint_sum(). To do that, we would have to restructure decide() - // as well. - self.update_noone_else_needs_hint(); - } - if let &TurnResult::Discard(ref card) = &turn_record.result { - self.update_public_info_for_discard_or_play(view, &turn_record.player, index, card); + self.public_info.update_from_discard_or_play_result(view, &turn_record.player, index, card); } else { panic!("Got turn choice {:?}, but turn result {:?}", turn_record.choice, turn_record.result); @@ -1127,7 +1002,7 @@ impl PlayerStrategy for InformationPlayerStrategy { } TurnChoice::Play(index) => { if let &TurnResult::Play(ref card, _) = &turn_record.result { - self.update_public_info_for_discard_or_play(view, &turn_record.player, index, card); + self.public_info.update_from_discard_or_play_result(view, &turn_record.player, index, card); } else { panic!("Got turn choice {:?}, but turn result {:?}", turn_record.choice, turn_record.result); @@ -1135,5 +1010,6 @@ impl PlayerStrategy for InformationPlayerStrategy { } } self.last_view = OwnedGameView::clone_from(view); + self.public_info.set_board(view.board); } } From 8ed01d47cad5cc02f406036e890684ff1d5136c7 Mon Sep 17 00:00:00 2001 From: Felix Bauckholt Date: Mon, 4 Mar 2019 18:53:20 +0100 Subject: [PATCH 3/5] Slight changes to be more information-efficient - If a player discards because nobody else needed a hint, that information is "transmitted" before they transmit other info through hat stuff. - The "card possibility partition" questions now take into account info learned through earlier questions. --- README.md | 4 +-- src/strategies/information.rs | 48 +++++++++++++++++------------------ 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 05def07..f83f2bf 100644 --- a/README.md +++ b/README.md @@ -73,5 +73,5 @@ On the first 20000 seeds, we have these average scores and win rates: |---------|---------|---------|---------|---------| | cheat | 24.8594 | 24.9785 | 24.9720 | 24.9557 | | | 90.59 % | 98.17 % | 97.76 % | 96.42 % | -| info | 22.2908 | 24.7171 | 24.8875 | 24.8957 | -| | 09.40 % | 79.94 % | 91.32 % | 91.83 % | +| info | 22.3427 | 24.7488 | 24.9055 | 24.9051 | +| | 10.23 % | 81.67 % | 92.53 % | 92.62 % | diff --git a/src/strategies/information.rs b/src/strategies/information.rs index ea739e2..8f1bfcc 100644 --- a/src/strategies/information.rs +++ b/src/strategies/information.rs @@ -420,6 +420,12 @@ impl MyPublicInformation { }) } + fn knows_dead_card(&self, player: &Player) -> bool { + self.hand_info[player].iter().any(|table| { + table.probability_is_dead(&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| { @@ -514,7 +520,7 @@ impl PublicInformation for MyPublicInformation { fn ask_questions( &self, - _player: &Player, + me: &Player, hand_info: &mut HandInfo, mut ask_question: Callback, mut info_remaining: u32, @@ -522,25 +528,21 @@ impl PublicInformation for MyPublicInformation { // Changing anything inside this function will not break the information transfer // mechanisms! - let augmented_hand_info = hand_info.iter().cloned().enumerate() + let compute_augmented_hand_info = |hand_info: &HandInfo| { + 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); let is_determined = card_table.is_determined(); (card_table, i, p_play, p_dead, is_determined) }) - .collect::>(); + .collect::>() + }; - let known_playable = augmented_hand_info.iter().filter(|&&(_, _, p_play, _, _)| { - p_play == 1.0 - }).collect::>().len(); - let known_dead = augmented_hand_info.iter().filter(|&&(_, _, _, p_dead, _)| { - p_dead == 1.0 - }).collect::>().len(); - - if known_playable == 0 { // TODO: changing this to "if true {" slightly improves the three-player game and - // very slightly worsens the other cases. There probably is some - // other way to make this decision that's better in all cases. + if !self.knows_playable_card(me) { // TODO: changing this to "if true {" slightly improves the three-player game and + // very slightly worsens the other cases. There probably is some + // other way to make this decision that's better in all cases. + let augmented_hand_info = compute_augmented_hand_info(hand_info); let mut ask_play = augmented_hand_info.iter() .filter(|&&(_, _, p_play, p_dead, is_determined)| { if is_determined { return false; } @@ -578,7 +580,7 @@ impl PublicInformation for MyPublicInformation { // find a playable card, and conditional on that, // it's better to find out about as many non-playable // cards as possible. - if rest_combo.info_amount() < info_remaining && known_dead == 0 { + if rest_combo.info_amount() < info_remaining && !self.knows_dead_card(me) { let mut ask_dead = augmented_hand_info.iter() .filter(|&&(_, _, _, p_dead, _)| { p_dead > 0.0 && p_dead < 1.0 @@ -603,6 +605,8 @@ impl PublicInformation for MyPublicInformation { } } + // Recompute augmented_hand_info, incorporating the things we learned when asking questions + let augmented_hand_info = compute_augmented_hand_info(hand_info); let mut ask_partition = augmented_hand_info.iter() .filter(|&&(_, _, _, p_dead, is_determined)| { if is_determined { return false } @@ -883,19 +887,14 @@ impl InformationPlayerStrategy { return TurnChoice::Hint(hint); } - // We update on the discard choice before updating on the fact that we're discarding to - // match pre-refactor behavior. - // TODO: change this in the next commit! - let discard_info = if public_useless_indices.len() > 1 { - Some(public_info.get_hat_sum(public_useless_indices.len() as u32, view)) - } else { None }; if self.last_view.board.hints_remaining > 0 { public_info.update_noone_else_needs_hint(); } // if anything is totally useless, discard it if public_useless_indices.len() > 1 { - return TurnChoice::Discard(public_useless_indices[discard_info.unwrap().value as usize]); + 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 { // TODO: have opponents infer that i knew a card was useless // TODO: after that, potentially prefer useless indices that arent public @@ -945,7 +944,9 @@ impl InformationPlayerStrategy { &self.last_view.board, &self.public_info.get_player_info(turn_player) ); - // TODO: reorder these blocks in the next commit! + if self.last_view.board.hints_remaining > 0 { + self.public_info.update_noone_else_needs_hint(); + } 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 @@ -953,9 +954,6 @@ impl InformationPlayerStrategy { let info = ModulusInformation::new(known_useless_indices.len() as u32, value as u32); self.public_info.update_from_hat_sum(info, &self.last_view); } - if self.last_view.board.hints_remaining > 0 { - self.public_info.update_noone_else_needs_hint(); - } } TurnChoice::Play(_index) => { // TODO: Maybe we can transfer information through plays as well? From 051ac7a097682a2278dea573220cc27659e3c03e Mon Sep 17 00:00:00 2001 From: Felix Bauckholt Date: Tue, 5 Mar 2019 15:48:32 +0100 Subject: [PATCH 4/5] Modulus magic! Suppose we have `total_information` choices, and we first use them to encode the answer `x` to a question with `m` answers. That answer is encoded by the choice we take modulo `m`. How much "information" do we have left? That depends on the number of numbers less than `total_information` that are equal to `x` modulo `m`. Depending on the value of `x`, this is either `floor(total_information/m)` or `floor(total_information/m) + 1`. We now use all of this information as opposed to just `floor(total_information/m)`, at the cost of making our math not a lot more complicated but pretty confusing. --- README.md | 4 +-- src/strategies/hat_helpers.rs | 61 ++++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f83f2bf..baa7397 100644 --- a/README.md +++ b/README.md @@ -73,5 +73,5 @@ On the first 20000 seeds, we have these average scores and win rates: |---------|---------|---------|---------|---------| | cheat | 24.8594 | 24.9785 | 24.9720 | 24.9557 | | | 90.59 % | 98.17 % | 97.76 % | 96.42 % | -| info | 22.3427 | 24.7488 | 24.9055 | 24.9051 | -| | 10.23 % | 81.67 % | 92.53 % | 92.62 % | +| info | 22.3736 | 24.7840 | 24.9261 | 24.9160 | +| | 10.41 % | 84.14 % | 94.33 % | 93.49 % | diff --git a/src/strategies/hat_helpers.rs b/src/strategies/hat_helpers.rs index ad50a04..5ba7c6f 100644 --- a/src/strategies/hat_helpers.rs +++ b/src/strategies/hat_helpers.rs @@ -19,20 +19,37 @@ impl ModulusInformation { Self::new(1, 0) } - pub fn combine(&mut self, other: Self) { + 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.modulus = self.modulus * other.modulus; + self.modulus = std::cmp::min(max_modulus, self.modulus * other.modulus); + assert!(self.value < self.modulus); + } + + pub fn info_remaining(&self, max_modulus: u32) -> u32 { + // We want to find the largest number `result` such that + // `self.combine(other, max_modulus)` works whenever `other.modulus == result`. + // `other.value` can be up to `result - 1`, so calling combine could increase our value to + // up to `self.value + self.modulus * (result - 1)`, which must always be less than + // `max_modulus`. + // Therefore, we compute the largest number `result` such that + // `self.value + self.modulus * (result - 1) < max_modulus`. + let result = (max_modulus - self.value - 1) / self.modulus + 1; + assert!(self.value + self.modulus * (result - 1) < max_modulus); + assert!(self.value + self.modulus * ((result + 1) - 1) >= max_modulus); + result } pub fn split(&mut self, modulus: u32) -> Self { assert!(self.modulus >= modulus); - assert!(self.modulus % modulus == 0); let original_modulus = self.modulus; let original_value = self.value; - self.modulus = self.modulus / modulus; let value = self.value % modulus; self.value = self.value / modulus; - assert!(original_modulus == modulus * self.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 + self.modulus = (original_modulus - value - 1) / modulus + 1; assert!(original_value == value + modulus * self.value); Self::new(modulus, value) } @@ -42,11 +59,11 @@ impl ModulusInformation { self.modulus = modulus; } - pub fn cast_down(&mut self, modulus: u32) { - assert!(self.modulus >= modulus); - assert!(self.value < modulus); - self.modulus = modulus; - } + // pub fn cast_down(&mut self, modulus: u32) { + // assert!(self.modulus >= modulus); + // assert!(self.value < modulus); + // self.modulus = modulus; + // } pub fn add(&mut self, other: &Self) { assert!(self.modulus == other.modulus); @@ -127,10 +144,10 @@ pub trait PublicInformation: Clone { let mut answer_info = ModulusInformation::none(); { let callback = |hand_info: &mut HandInfo, info_remaining: &mut u32, question: Box| { - *info_remaining = *info_remaining / question.info_amount(); 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); + answer_info.combine(new_answer_info, total_info); + *info_remaining = answer_info.info_remaining(total_info); }; self.ask_questions(player, hand_info, callback, total_info); } @@ -146,17 +163,15 @@ pub trait PublicInformation: Clone { mut info: ModulusInformation, ) { let total_info = info.modulus; - let callback = |hand_info: &mut HandInfo, info_remaining: &mut u32, question: Box| { - let q_info_amount = question.info_amount(); - *info_remaining = *info_remaining / q_info_amount; - let info_modulus = info.modulus; - // Instead of casting down the ModulusInformation to the product of all the questions before - // answering any, we now have to cast down the ModulusInformation question-by-question. - info.cast_down((info_modulus / q_info_amount) * q_info_amount); - let answer_info = info.split(question.info_amount()); - question.acknowledge_answer_info(answer_info, hand_info, board); - }; - self.ask_questions(player, hand_info, callback, total_info); + { + let callback = |hand_info: &mut HandInfo, info_remaining: &mut u32, question: Box| { + let answer_info = info.split(question.info_amount()); + question.acknowledge_answer_info(answer_info, hand_info, board); + *info_remaining = info.modulus; + }; + self.ask_questions(player, hand_info, callback, total_info); + } + assert!(info.value == 0); } /// When deciding on a move, if we can choose between `total_info` choices, From 9364a0686242cf1544ee2ad211da31c3d7cde554 Mon Sep 17 00:00:00 2001 From: Felix Bauckholt Date: Fri, 8 Mar 2019 20:59:14 +0100 Subject: [PATCH 5/5] Improve ask_question strategy I also replaced the callback architecture behind `PublicInformation.ask_questions()` with a simpler method `PublicInformation.ask_question()` that gets called repeatedly. To make all float-based sorts easier, I used the `float-ord` package. I also used it to clean up some of the sorting in `decide_wrapped()`. --- Cargo.lock | 7 ++ Cargo.toml | 1 + README.md | 4 +- src/main.rs | 1 + src/strategies/hat_helpers.rs | 62 ++++++---- src/strategies/information.rs | 220 +++++++++++++--------------------- 6 files changed, 132 insertions(+), 163 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc376d2..e063b68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,6 +3,11 @@ name = "crossbeam" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "float-ord" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "fnv" version = "1.0.6" @@ -39,6 +44,7 @@ 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)", @@ -47,6 +53,7 @@ dependencies = [ [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 libc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "4870ef6725dde13394134e587e4ab4eca13cb92e916209a31c851b49131d3c75" diff --git a/Cargo.toml b/Cargo.toml index 85f0aae..79c0936 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ rand = "*" log = "*" getopts = "*" fnv = "*" +float-ord = "*" crossbeam = "0.2.5" diff --git a/README.md b/README.md index baa7397..b5cd0f5 100644 --- a/README.md +++ b/README.md @@ -73,5 +73,5 @@ On the first 20000 seeds, we have these average scores and win rates: |---------|---------|---------|---------|---------| | cheat | 24.8594 | 24.9785 | 24.9720 | 24.9557 | | | 90.59 % | 98.17 % | 97.76 % | 96.42 % | -| info | 22.3736 | 24.7840 | 24.9261 | 24.9160 | -| | 10.41 % | 84.14 % | 94.33 % | 93.49 % | +| info | 22.5194 | 24.7942 | 24.9354 | 24.9220 | +| | 12.58 % | 84.46 % | 95.03 % | 94.01 % | diff --git a/src/main.rs b/src/main.rs index 12a05f7..86ff353 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ extern crate log; extern crate rand; extern crate crossbeam; extern crate fnv; +extern crate float_ord; mod helpers; mod game; diff --git a/src/strategies/hat_helpers.rs b/src/strategies/hat_helpers.rs index 5ba7c6f..7550a5b 100644 --- a/src/strategies/hat_helpers.rs +++ b/src/strategies/hat_helpers.rs @@ -117,18 +117,37 @@ pub trait PublicInformation: Clone { fn agrees_with(&self, other: Self) -> bool; - /// By defining `ask_questions`, we decides which `Question`s a player learns the answers to. + /// By defining `ask_question`, we decides which `Question`s a player learns the answers to. /// - /// A player "asks" a question by calling the callback. Questions can depend on the answers to - /// earlier questions: We are given a `&mut HandInfo` that we'll have to pass - /// to that callback; there, it will be modified to reflect the answer to the question. Note that `self` - /// is not modified and thus reflects the state before any player "asked" any question. + /// Whenever we need to compute a "hat value", this method will be called repeatedly, either + /// until the information runs out, or until it returns `None`. These questions can depend on + /// the answers to earlier questions: We are given a `&HandInfo` that + /// reflect the answers of previous questions for the same "hat value computation". /// - /// The product of the `info_amount()`s of all questions we have may not exceed `total_info`. - /// For convenience, we pass a `&mut u32` to the callback, and it will be updated to the - /// "remaining" information amount. - fn ask_questions(&self, &Player, &mut HandInfo, Callback, total_info: u32) - where Callback: FnMut(&mut HandInfo, &mut u32, Box); + /// Note that `self` does not reflect the answers to previous questions; it reflects the state + /// before the entire "hat value" calculation. + fn ask_question(&self, &Player, &HandInfo, total_info: u32) -> Option>; + + fn ask_question_wrapper(&self, player: &Player, hand_info: &HandInfo, total_info: u32) + -> Option> + { + assert!(total_info > 0); + if total_info == 1 { + None + } else { + 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); + } + if question.info_amount() == 1 { + panic!("ask_question returned a trivial question!"); + } + } + result + } + } fn set_player_infos(&mut self, infos: Vec<(Player, HandInfo)>) { for (player, new_hand_info) in infos { @@ -142,14 +161,10 @@ pub trait PublicInformation: Clone { ) -> ModulusInformation { assert!(player != &view.player); let mut answer_info = ModulusInformation::none(); - { - let callback = |hand_info: &mut HandInfo, info_remaining: &mut u32, question: Box| { - 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); - *info_remaining = answer_info.info_remaining(total_info); - }; - self.ask_questions(player, hand_info, callback, 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); } answer_info.cast_up(total_info); answer_info @@ -162,14 +177,9 @@ pub trait PublicInformation: Clone { board: &BoardState, mut info: ModulusInformation, ) { - let total_info = info.modulus; - { - let callback = |hand_info: &mut HandInfo, info_remaining: &mut u32, question: Box| { - let answer_info = info.split(question.info_amount()); - question.acknowledge_answer_info(answer_info, hand_info, board); - *info_remaining = info.modulus; - }; - self.ask_questions(player, hand_info, callback, total_info); + while let Some(question) = self.ask_question_wrapper(player, hand_info, info.modulus) { + let answer_info = info.split(question.info_amount()); + question.acknowledge_answer_info(answer_info, hand_info, board); } assert!(info.value == 0); } diff --git a/src/strategies/information.rs b/src/strategies/information.rs index 8f1bfcc..e28951d 100644 --- a/src/strategies/information.rs +++ b/src/strategies/information.rs @@ -1,5 +1,6 @@ use fnv::{FnvHashMap, FnvHashSet}; use std::cmp::Ordering; +use float_ord::*; use strategy::*; use game::*; @@ -420,12 +421,6 @@ impl MyPublicInformation { }) } - fn knows_dead_card(&self, player: &Player) -> bool { - self.hand_info[player].iter().any(|table| { - table.probability_is_dead(&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| { @@ -518,117 +513,86 @@ impl PublicInformation for MyPublicInformation { *self == other } - fn ask_questions( + fn ask_question( &self, - me: &Player, - hand_info: &mut HandInfo, - mut ask_question: Callback, - mut info_remaining: u32, - ) where Callback: FnMut(&mut HandInfo, &mut u32, Box) { + _me: &Player, + hand_info: &HandInfo, + total_info: u32, + ) -> Option> { // Changing anything inside this function will not break the information transfer // mechanisms! - let compute_augmented_hand_info = |hand_info: &HandInfo| { - 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); - let is_determined = card_table.is_determined(); - (card_table, i, p_play, p_dead, is_determined) - }) - .collect::>() - }; + let augmented_hand_info_raw = hand_info.iter().cloned().enumerate().filter_map(|(i, card_table)| { + let p_play = card_table.probability_is_playable(&self.board); + let p_dead = card_table.probability_is_dead(&self.board); + Some((i, p_play, p_dead)) + }).collect::>(); + let know_playable_card = augmented_hand_info_raw.iter().any(|&(_, p_play, _)| p_play == 1.0); + let know_dead_card = augmented_hand_info_raw.iter().any(|&(_, _, p_dead)| p_dead == 1.0); - if !self.knows_playable_card(me) { // TODO: changing this to "if true {" slightly improves the three-player game and - // very slightly worsens the other cases. There probably is some - // other way to make this decision that's better in all cases. - let augmented_hand_info = compute_augmented_hand_info(hand_info); - let mut ask_play = augmented_hand_info.iter() - .filter(|&&(_, _, p_play, p_dead, is_determined)| { - if is_determined { return false; } - if p_dead == 1.0 { return false; } - if p_play == 1.0 || p_play < 0.2 { return false; } - true - }).collect::>(); - // sort by probability of play, then by index - ask_play.sort_by(|&&(_, i1, p1, _, _), &&(_, i2, p2, _, _)| { - // It's better to include higher-probability-of-playability - // cards into our combo question, since that maximizes our - // chance of finding out about a playable card. - let result = p2.partial_cmp(&p1); - if result == None || result == Some(Ordering::Equal) { - i1.cmp(&i2) - } else { - result.unwrap() - } - }); + // We don't need to find out anything about cards that are determined or dead. + let augmented_hand_info = augmented_hand_info_raw.into_iter().filter(|&(i, _, p_dead)| { + if p_dead == 1.0 { false } + else if hand_info[i].is_determined() { false } + else { true } + }).collect::>(); - if self.board.num_players == 5 { - for &(_, i, _, _, _) in ask_play { - ask_question(hand_info, &mut info_remaining, Box::new(q_is_playable(i))); - if info_remaining <= 1 { return; } - } - } else { - let mut rest_combo = AdditiveComboQuestion {questions: Vec::new()}; - for &(_, i, _, _, _) in ask_play { - if rest_combo.info_amount() < info_remaining { - rest_combo.questions.push(Box::new(q_is_playable(i))); - } - } - rest_combo.questions.reverse(); // It's better to put lower-probability-of-playability - // cards first: The difference only matters if we - // find a playable card, and conditional on that, - // it's better to find out about as many non-playable - // cards as possible. - if rest_combo.info_amount() < info_remaining && !self.knows_dead_card(me) { - let mut ask_dead = augmented_hand_info.iter() - .filter(|&&(_, _, _, p_dead, _)| { - p_dead > 0.0 && p_dead < 1.0 - }).collect::>(); - // sort by probability of death, then by index - ask_dead.sort_by(|&&(_, i1, _, d1, _), &&(_, i2, _, d2, _)| { - let result = d2.partial_cmp(&d1); - if result == None || result == Some(Ordering::Equal) { - i1.cmp(&i2) - } else { - result.unwrap() - } - }); - for &(_, i, _, _, _) in ask_dead { - if rest_combo.info_amount() < info_remaining { - rest_combo.questions.push(Box::new(q_is_dead(i))); - } - } - } - ask_question(hand_info, &mut info_remaining, Box::new(rest_combo)); - if info_remaining <= 1 { return; } + 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(); + 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)) } + })); + } + + let combo_question_capacity = (total_info - 1) as usize; + 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.truncate(combo_question_capacity); + } + + // Sort by type (ask_dead=false first), then by p_yes (smaller first), since it's + // better to put lower-probability-of-playability/death cards first: The difference + // only matters if we find a playable/dead card, and conditional on that, it's better + // to find out about as many non-playable/non-dead cards as possible. + to_ask.sort_by_key(|&(ask_dead, _, p_yes)| {(ask_dead, FloatOrd(p_yes))}); + let questions = to_ask.into_iter().map(|(ask_dead, i, _)| -> Box { + if ask_dead { Box::new(q_is_dead(i)) } + else { Box::new(q_is_playable(i)) } + }).collect::>(); + if questions.len() > 0 { + return Some(Box::new(AdditiveComboQuestion { questions })) } } - // Recompute augmented_hand_info, incorporating the things we learned when asking questions - let augmented_hand_info = compute_augmented_hand_info(hand_info); - let mut ask_partition = augmented_hand_info.iter() - .filter(|&&(_, _, _, p_dead, is_determined)| { - if is_determined { return false } - // TODO: possibly still valuable to ask? - if p_dead == 1.0 { return false } - true - }).collect::>(); - // sort by probability of play, then by index - ask_partition.sort_by(|&&(_, i1, p1, _, _), &&(_, i2, p2, _, _)| { - // *higher* probabilities are better - let result = p2.partial_cmp(&p1); - if result == None || result == Some(Ordering::Equal) { - i1.cmp(&i2) - } else { - result.unwrap() - } - }); + let ask_play_score = |p_play: f32| FloatOrd((p_play-0.7).abs()); + let mut ask_play = augmented_hand_info.iter().filter(|&&(_, p_play, _)| { + ask_play_score(p_play) < FloatOrd(0.2) + }).cloned().collect::>(); + ask_play.sort_by_key(|&(i, p_play, _)| (ask_play_score(p_play), i)); + if let Some(&(i, _, _)) = ask_play.get(0) { + return Some(Box::new(q_is_playable(i))); + } - for &(ref card_table, i, _, _, _) in ask_partition { - let question = CardPossibilityPartition::new(i, info_remaining, &card_table, &self.board); - ask_question(hand_info, &mut info_remaining, Box::new(question)); - if info_remaining <= 1 { return; } + 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) { + let question = CardPossibilityPartition::new(i, total_info, &hand_info[i], &self.board); + Some(Box::new(question)) + } else { + None } } } @@ -814,24 +778,14 @@ impl InformationPlayerStrategy { // debug!("{}: {}", i, card_table); // } - let playable_cards = private_info.iter().enumerate().filter(|&(_, card_table)| { - card_table.probability_is_playable(&view.board) == 1.0 + // 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::>(); - - if playable_cards.len() > 0 { - // play the best playable card - // the higher the play_score, the better to play - let mut play_score = -1.0; - let mut play_index = 0; - - for (index, card_table) in playable_cards { - let score = self.get_average_play_score(view, card_table); - if score > play_score { - play_score = score; - play_index = index; - } - } - + 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) } @@ -901,23 +855,19 @@ impl InformationPlayerStrategy { return TurnChoice::Discard(useless_indices[0]); } - // Play the best discardable card - let mut compval = 0.0; - let mut index = 0; - for (i, card_table) in private_info.iter().enumerate() { + // 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 my_compval = + let compval = 20.0 * probability_is_seen + 10.0 * card_table.probability_is_dispensable(&view.board) + card_table.average_value(); - - if my_compval > compval { - compval = my_compval; - index = i; - } - } + (i, compval) + }).collect::>(); + cards_by_discard_value.sort_by_key(|&(i, compval)| (FloatOrd(-compval), i)); + let (index, _) = cards_by_discard_value[0]; TurnChoice::Discard(index) }