Refactor out a "public information object"

One important change is that now, when deciding which questions to ask, they can see the answer to the last question before asking the next one.

Some design choices:
- Questions now take a BoardState instead of an OwnedGameView.
- When deciding which questions to ask (in ask_questions), we get an immutable public information object
  (representing the public information before any questions were asked), and a mutable HandInfo<CardPossibilityTable>
  that gets updated as we ask questions. That HandInfo<CardPossibilityTable> was copied instead of taken.
- In ask_questions, we also get some &mut u32 representing "info_remaining" that gets updated for us.
  This will later allow for cases where "info_remaining" depends on the answers to previous questions.
- Both get_hint_sum and update_from_hint_sum change the public information object. If you want to compute the
  hint sum but aren't sure if you actually want to give the hint, you'll have to clone the public information
  object!
- Over time, in the code to decide on a move, we'll be able to build an increasingly complicated tree of
  "public information object operations" that will have to be matched exactly in the code to update on a move.
  In order to make this less scary, I moved most of the code into
  "decide_wrapped" and "update_wrapped". If the call to update_wrapped
  (for the player who just made the move) changes the public information
  object in different ways than the previous call to decide_wrapped, we
  detect this and panic.

This commit should be purely refactoring; all changes to win-rates are
due to bugs.
This commit is contained in:
Felix Bauckholt 2019-03-04 17:24:24 +01:00
parent e510f7cc5e
commit 493631dad0
6 changed files with 746 additions and 651 deletions

View File

@ -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 | | cheat | 24.8594 | 24.9785 | 24.9720 | 24.9557 |
| | 90.59 % | 98.17 % | 97.76 % | 96.42 % | | | 90.59 % | 98.17 % | 97.76 % | 96.42 % |
| info | 22.3316 | 24.7246 | 24.8892 | 24.8969 | | info | 22.2908 | 24.7171 | 24.8875 | 24.8957 |
| | 09.81 % | 80.38 % | 91.45 % | 91.94 % | | | 09.40 % | 79.94 % | 91.32 % | 91.83 % |

View File

@ -44,7 +44,7 @@ impl fmt::Debug for Card {
} }
} }
#[derive(Debug,Clone)] #[derive(Debug,Clone,Eq,PartialEq)]
pub struct CardCounts { pub struct CardCounts {
counts: FnvHashMap<Card, u32>, counts: FnvHashMap<Card, u32>,
} }
@ -99,7 +99,7 @@ impl fmt::Display for CardCounts {
pub type Cards = Vec<Card>; pub type Cards = Vec<Card>;
#[derive(Debug,Clone)] #[derive(Debug,Clone,Eq,PartialEq)]
pub struct Discard { pub struct Discard {
pub cards: Cards, pub cards: Cards,
counts: CardCounts, counts: CardCounts,
@ -137,7 +137,7 @@ impl fmt::Display for Discard {
pub type Score = u32; pub type Score = u32;
pub const PERFECT_SCORE: Score = (NUM_COLORS * NUM_VALUES) as u32; pub const PERFECT_SCORE: Score = (NUM_COLORS * NUM_VALUES) as u32;
#[derive(Debug,Clone)] #[derive(Debug,Clone,Eq,PartialEq)]
pub struct Firework { pub struct Firework {
pub color: Color, pub color: Color,
pub top: Value, pub top: Value,
@ -198,14 +198,14 @@ impl fmt::Display for Hinted {
} }
} }
#[derive(Debug,Clone)] #[derive(Debug,Clone,Eq,PartialEq)]
pub struct Hint { pub struct Hint {
pub player: Player, pub player: Player,
pub hinted: Hinted, pub hinted: Hinted,
} }
// represents the choice a player made in a given turn // represents the choice a player made in a given turn
#[derive(Debug,Clone)] #[derive(Debug,Clone,Eq,PartialEq)]
pub enum TurnChoice { pub enum TurnChoice {
Hint(Hint), Hint(Hint),
Discard(usize), // index of card to discard Discard(usize), // index of card to discard
@ -213,7 +213,7 @@ pub enum TurnChoice {
} }
// represents what happened in a turn // represents what happened in a turn
#[derive(Debug,Clone)] #[derive(Debug,Clone,Eq,PartialEq)]
pub enum TurnResult { pub enum TurnResult {
Hint(Vec<bool>), // vector of whether each was in the hint Hint(Vec<bool>), // vector of whether each was in the hint
Discard(Card), // card discarded Discard(Card), // card discarded
@ -221,7 +221,7 @@ pub enum TurnResult {
} }
// represents a turn taken in the game // represents a turn taken in the game
#[derive(Debug,Clone)] #[derive(Debug,Clone,Eq,PartialEq)]
pub struct TurnRecord { pub struct TurnRecord {
pub player: Player, pub player: Player,
pub choice: TurnChoice, pub choice: TurnChoice,
@ -243,7 +243,7 @@ pub struct GameOptions {
// State of everything except the player's hands // State of everything except the player's hands
// Is all completely common knowledge // Is all completely common knowledge
#[derive(Debug,Clone)] #[derive(Debug,Clone,Eq,PartialEq)]
pub struct BoardState { pub struct BoardState {
pub deck_size: u32, pub deck_size: u32,
pub total_cards: u32, pub total_cards: u32,

View File

@ -246,7 +246,7 @@ impl fmt::Display for SimpleCardInfo {
// Can represent information of the form: // Can represent information of the form:
// this card is/isn't possible // this card is/isn't possible
// also, maintains integer weights for the cards // also, maintains integer weights for the cards
#[derive(Clone,Debug)] #[derive(Clone,Debug,Eq,PartialEq)]
pub struct CardPossibilityTable { pub struct CardPossibilityTable {
possible: HashMap<Card, u32>, possible: HashMap<Card, u32>,
} }
@ -369,7 +369,7 @@ impl fmt::Display for CardPossibilityTable {
} }
} }
#[derive(Clone)] #[derive(Clone,Eq,PartialEq)]
pub struct HandInfo<T> where T: CardInfo { pub struct HandInfo<T> where T: CardInfo {
pub hand_info: Vec<T> pub hand_info: Vec<T>
} }

View File

@ -12,6 +12,7 @@ mod strategy;
mod strategies { mod strategies {
pub mod examples; pub mod examples;
pub mod cheating; pub mod cheating;
mod hat_helpers;
pub mod information; pub mod information;
} }

View File

@ -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<CardPossibilityTable>, &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<CardPossibilityTable>,
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<CardPossibilityTable>;
fn set_player_info(&mut self, &Player, HandInfo<CardPossibilityTable>);
fn new(&BoardState) -> Self;
fn set_board(&mut self, &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 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<CardPossibilityTable>` 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<Callback>(&self, &Player, &mut HandInfo<CardPossibilityTable>, Callback, total_info: u32)
where Callback: FnMut(&mut HandInfo<CardPossibilityTable>, &mut u32, Box<Question>);
fn set_player_infos(&mut self, infos: Vec<(Player, HandInfo<CardPossibilityTable>)>) {
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<CardPossibilityTable>, total_info: u32, view: &OwnedGameView
) -> ModulusInformation {
assert!(player != &view.player);
let mut answer_info = ModulusInformation::none();
{
let callback = |hand_info: &mut HandInfo<CardPossibilityTable>, info_remaining: &mut u32, question: Box<Question>| {
*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<CardPossibilityTable>,
board: &BoardState,
mut info: ModulusInformation,
) {
let total_info = info.modulus;
let callback = |hand_info: &mut HandInfo<CardPossibilityTable>, info_remaining: &mut u32, question: Box<Question>| {
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<CardPossibilityTable> {
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
}
}

File diff suppressed because it is too large Load Diff