information strategy, working!
This commit is contained in:
parent
efba24d6e8
commit
adf98e0e4a
7 changed files with 604 additions and 181 deletions
|
@ -92,7 +92,7 @@ impl fmt::Display for CardCounts {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug,Clone)]
|
||||
pub struct Discard {
|
||||
pub cards: Cards,
|
||||
counts: CardCounts,
|
||||
|
|
131
src/game.rs
131
src/game.rs
|
@ -66,7 +66,7 @@ pub struct GameOptions {
|
|||
}
|
||||
|
||||
// The state of a given player: all other players may see this
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug,Clone)]
|
||||
pub struct PlayerState {
|
||||
// the player's actual hand
|
||||
pub hand: Cards,
|
||||
|
@ -154,7 +154,7 @@ fn new_deck(seed: u32) -> Cards {
|
|||
|
||||
// State of everything except the player's hands
|
||||
// Is all completely common knowledge
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug,Clone)]
|
||||
pub struct BoardState {
|
||||
deck: Cards,
|
||||
pub total_cards: u32,
|
||||
|
@ -377,9 +377,50 @@ impl fmt::Display for BoardState {
|
|||
}
|
||||
|
||||
// complete game view of a given player
|
||||
// state will be borrowed GameState
|
||||
pub trait GameView {
|
||||
fn me(&self) -> Player;
|
||||
fn my_info(&self) -> &Vec<SimpleCardInfo>;
|
||||
fn get_state(&self, player: &Player) -> &PlayerState;
|
||||
fn get_board(&self) -> &BoardState;
|
||||
|
||||
fn get_hand(&self, player: &Player) -> &Cards {
|
||||
assert!(self.me() != *player, "Cannot query about your own cards!");
|
||||
&self.get_state(player).hand
|
||||
}
|
||||
|
||||
fn hand_size(&self, player: &Player) -> usize {
|
||||
if self.me() == *player {
|
||||
self.my_info().len()
|
||||
} else {
|
||||
self.get_hand(player).len()
|
||||
}
|
||||
}
|
||||
|
||||
fn has_card(&self, player: &Player, card: &Card) -> bool {
|
||||
for other_card in self.get_hand(player) {
|
||||
if *card == *other_card {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn can_see(&self, card: &Card) -> bool {
|
||||
for other_player in self.get_board().get_players() {
|
||||
if self.me() == other_player {
|
||||
continue
|
||||
}
|
||||
if self.has_card(&other_player, card) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// version of game view that is borrowed. used in simulator for efficiency,
|
||||
#[derive(Debug)]
|
||||
pub struct GameStateView<'a> {
|
||||
pub struct BorrowedGameView<'a> {
|
||||
// the player whose view it is
|
||||
pub player: Player,
|
||||
// what is known about their own hand (and thus common knowledge)
|
||||
|
@ -389,30 +430,70 @@ pub struct GameStateView<'a> {
|
|||
// board state
|
||||
pub board: &'a BoardState,
|
||||
}
|
||||
impl <'a> GameStateView<'a> {
|
||||
pub fn get_hand(&self, player: &Player) -> &Cards {
|
||||
assert!(self.player != *player, "Cannot query about your own cards!");
|
||||
&self.other_player_states.get(player).unwrap().hand
|
||||
impl <'a> GameView for BorrowedGameView<'a> {
|
||||
fn me(&self) -> Player {
|
||||
self.player
|
||||
}
|
||||
|
||||
pub fn has_card(&self, player: &Player, card: &Card) -> bool {
|
||||
for other_card in self.get_hand(player) {
|
||||
if *card == *other_card {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
fn my_info(&self) -> &Vec<SimpleCardInfo> {
|
||||
self.info
|
||||
}
|
||||
pub fn can_see(&self, card: &Card) -> bool {
|
||||
for other_player in self.other_player_states.keys() {
|
||||
if self.has_card(other_player, card) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
fn get_state(&self, player: &Player) -> &PlayerState {
|
||||
assert!(self.me() != *player, "Cannot query about your own state!");
|
||||
self.other_player_states.get(player).unwrap()
|
||||
}
|
||||
fn get_board(&self) -> &BoardState {
|
||||
self.board
|
||||
}
|
||||
}
|
||||
|
||||
// version of game view, may be useful to strategies
|
||||
#[derive(Debug)]
|
||||
pub struct OwnedGameView {
|
||||
// the player whose view it is
|
||||
pub player: Player,
|
||||
// what is known about their own hand (and thus common knowledge)
|
||||
pub info: Vec<SimpleCardInfo>,
|
||||
// the cards of the other players, as well as the information they have
|
||||
pub other_player_states: HashMap<Player, PlayerState>,
|
||||
// board state
|
||||
pub board: BoardState,
|
||||
}
|
||||
impl OwnedGameView {
|
||||
pub fn clone_from(borrowed_view: &BorrowedGameView) -> OwnedGameView {
|
||||
let mut info : Vec<SimpleCardInfo> = Vec::new();
|
||||
for card_info in borrowed_view.info.iter() {
|
||||
info.push((*card_info).clone());
|
||||
}
|
||||
let mut other_player_states : HashMap<Player, PlayerState> = HashMap::new();
|
||||
for (other_player, player_state) in &borrowed_view.other_player_states {
|
||||
other_player_states.insert(*other_player, (*player_state).clone());
|
||||
}
|
||||
|
||||
OwnedGameView {
|
||||
player: borrowed_view.player.clone(),
|
||||
info: info,
|
||||
other_player_states: other_player_states,
|
||||
board: (*borrowed_view.board).clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl GameView for OwnedGameView {
|
||||
fn me(&self) -> Player {
|
||||
self.player
|
||||
}
|
||||
fn my_info(&self) -> &Vec<SimpleCardInfo> {
|
||||
&self.info
|
||||
}
|
||||
fn get_state(&self, player: &Player) -> &PlayerState {
|
||||
assert!(self.me() != *player, "Cannot query about your own state!");
|
||||
self.other_player_states.get(player).unwrap()
|
||||
}
|
||||
fn get_board(&self) -> &BoardState {
|
||||
&self.board
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// complete game state (known to nobody!)
|
||||
#[derive(Debug)]
|
||||
pub struct GameState {
|
||||
|
@ -471,14 +552,14 @@ impl GameState {
|
|||
}
|
||||
|
||||
// get the game state view of a particular player
|
||||
pub fn get_view(&self, player: Player) -> GameStateView {
|
||||
pub fn get_view(&self, player: Player) -> BorrowedGameView {
|
||||
let mut other_player_states = HashMap::new();
|
||||
for (other_player, state) in &self.player_states {
|
||||
if player != *other_player {
|
||||
other_player_states.insert(*other_player, state);
|
||||
}
|
||||
}
|
||||
GameStateView {
|
||||
BorrowedGameView {
|
||||
player: player,
|
||||
info: &self.player_states.get(&player).unwrap().info,
|
||||
other_player_states: other_player_states,
|
||||
|
|
29
src/info.rs
29
src/info.rs
|
@ -151,7 +151,7 @@ pub trait Info<T> where T: Hash + Eq + Clone {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug,Clone)]
|
||||
pub struct ColorInfo(HashSet<Color>);
|
||||
impl ColorInfo {
|
||||
pub fn new() -> ColorInfo { ColorInfo(ColorInfo::initialize()) }
|
||||
|
@ -162,7 +162,7 @@ impl Info<Color> for ColorInfo {
|
|||
fn get_mut_possibility_set(&mut self) -> &mut HashSet<Color> { &mut self.0 }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug,Clone)]
|
||||
pub struct ValueInfo(HashSet<Value>);
|
||||
impl ValueInfo {
|
||||
pub fn new() -> ValueInfo { ValueInfo(ValueInfo::initialize()) }
|
||||
|
@ -175,7 +175,7 @@ impl Info<Value> for ValueInfo {
|
|||
|
||||
// represents information only of the form:
|
||||
// this color is/isn't possible, this value is/isn't possible
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug,Clone)]
|
||||
pub struct SimpleCardInfo {
|
||||
pub color_info: ColorInfo,
|
||||
pub value_info: ValueInfo,
|
||||
|
@ -260,10 +260,25 @@ impl CardPossibilityTable {
|
|||
}
|
||||
|
||||
pub fn decrement_weight(&mut self, card: &Card) {
|
||||
let weight =
|
||||
self.possible.get_mut(card)
|
||||
.expect(&format!("Decrementing weight for impossible card: {}", card));
|
||||
*weight -= 1;
|
||||
let remove = {
|
||||
let weight =
|
||||
self.possible.get_mut(card)
|
||||
.expect(&format!("Decrementing weight for impossible card: {}", card));
|
||||
*weight -= 1;
|
||||
*weight == 0
|
||||
};
|
||||
if remove {
|
||||
self.possible.remove(card);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_card(&self) -> Option<Card> {
|
||||
let possibilities = self.get_possibilities();
|
||||
if possibilities.len() == 1 {
|
||||
Some(possibilities[0].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
impl <'a> From<&'a CardCounts> for CardPossibilityTable {
|
||||
|
|
|
@ -10,17 +10,17 @@ use game::*;
|
|||
// Represents the strategy of a given player
|
||||
pub trait PlayerStrategy {
|
||||
// A function to decide what to do on the player's turn.
|
||||
// Given a GameStateView, outputs their choice.
|
||||
fn decide(&mut self, &GameStateView) -> TurnChoice;
|
||||
// Given a BorrowedGameView, outputs their choice.
|
||||
fn decide(&mut self, &BorrowedGameView) -> TurnChoice;
|
||||
// A function to update internal state after other players' turns.
|
||||
// Given what happened last turn, and the new state.
|
||||
fn update(&mut self, &Turn, &GameStateView);
|
||||
fn update(&mut self, &Turn, &BorrowedGameView);
|
||||
}
|
||||
// Represents the overall strategy for a game
|
||||
// Shouldn't do much, except store configuration parameters and
|
||||
// possibility initialize some shared randomness between players
|
||||
pub trait GameStrategy {
|
||||
fn initialize(&self, Player, &GameStateView) -> Box<PlayerStrategy>;
|
||||
fn initialize(&self, Player, &BorrowedGameView) -> Box<PlayerStrategy>;
|
||||
}
|
||||
|
||||
// Represents configuration for a strategy.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::rc::Rc;
|
||||
use std::cell::{RefCell};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
|
||||
use simulator::*;
|
||||
use game::*;
|
||||
|
@ -43,7 +43,7 @@ impl CheatingStrategy {
|
|||
}
|
||||
}
|
||||
impl GameStrategy for CheatingStrategy {
|
||||
fn initialize(&self, player: Player, view: &GameStateView) -> Box<PlayerStrategy> {
|
||||
fn initialize(&self, player: Player, view: &BorrowedGameView) -> Box<PlayerStrategy> {
|
||||
for (player, state) in &view.other_player_states {
|
||||
self.player_states_cheat.borrow_mut().insert(
|
||||
*player, state.hand.clone()
|
||||
|
@ -62,7 +62,7 @@ pub struct CheatingPlayerStrategy {
|
|||
}
|
||||
impl CheatingPlayerStrategy {
|
||||
// last player might've drawn a new card, let him know!
|
||||
fn inform_last_player_cards(&self, view: &GameStateView) {
|
||||
fn inform_last_player_cards(&self, view: &BorrowedGameView) {
|
||||
let next = view.board.player_to_right(&self.me);
|
||||
self.player_states_cheat.borrow_mut().insert(
|
||||
next, view.other_player_states.get(&next).unwrap().hand.clone()
|
||||
|
@ -70,7 +70,7 @@ impl CheatingPlayerStrategy {
|
|||
}
|
||||
|
||||
// give a throwaway hint - we only do this when we have nothing to do
|
||||
fn throwaway_hint(&self, view: &GameStateView) -> TurnChoice {
|
||||
fn throwaway_hint(&self, view: &BorrowedGameView) -> TurnChoice {
|
||||
let hint_player = view.board.player_to_left(&self.me);
|
||||
let hint_card = &view.get_hand(&hint_player).first().unwrap();
|
||||
TurnChoice::Hint(Hint {
|
||||
|
@ -80,7 +80,7 @@ impl CheatingPlayerStrategy {
|
|||
}
|
||||
|
||||
// given a hand of cards, represents how badly it will need to play things
|
||||
fn hand_play_value(&self, view: &GameStateView, hand: &Cards/*, all_viewable: HashMap<Color, <Value, u32>> */) -> u32 {
|
||||
fn hand_play_value(&self, view: &BorrowedGameView, hand: &Cards/*, all_viewable: HashMap<Color, <Value, u32>> */) -> u32 {
|
||||
// dead = 0 points
|
||||
// indispensible = 5 + (5 - value) points
|
||||
// playable, not in another hand = 2 point
|
||||
|
@ -102,7 +102,7 @@ impl CheatingPlayerStrategy {
|
|||
}
|
||||
|
||||
// how badly do we need to play a particular card
|
||||
fn get_play_score(&self, view: &GameStateView, card: &Card) -> i32 {
|
||||
fn get_play_score(&self, view: &BorrowedGameView, card: &Card) -> i32 {
|
||||
let states = self.player_states_cheat.borrow();
|
||||
let my_hand = states.get(&self.me).unwrap();
|
||||
|
||||
|
@ -124,7 +124,7 @@ impl CheatingPlayerStrategy {
|
|||
20 - (card.value as i32)
|
||||
}
|
||||
|
||||
fn find_useless_card(&self, view: &GameStateView, hand: &Cards) -> Option<usize> {
|
||||
fn find_useless_card(&self, view: &BorrowedGameView, hand: &Cards) -> Option<usize> {
|
||||
let mut set: HashSet<Card> = HashSet::new();
|
||||
|
||||
for (i, card) in hand.iter().enumerate() {
|
||||
|
@ -140,7 +140,7 @@ impl CheatingPlayerStrategy {
|
|||
return None
|
||||
}
|
||||
|
||||
fn someone_else_can_play(&self, view: &GameStateView) -> bool {
|
||||
fn someone_else_can_play(&self, view: &BorrowedGameView) -> bool {
|
||||
for player in view.board.get_players() {
|
||||
if player != self.me {
|
||||
for card in view.get_hand(&player) {
|
||||
|
@ -154,7 +154,7 @@ impl CheatingPlayerStrategy {
|
|||
}
|
||||
}
|
||||
impl PlayerStrategy for CheatingPlayerStrategy {
|
||||
fn decide(&mut self, view: &GameStateView) -> TurnChoice {
|
||||
fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice {
|
||||
self.inform_last_player_cards(view);
|
||||
|
||||
let states = self.player_states_cheat.borrow();
|
||||
|
@ -180,63 +180,64 @@ impl PlayerStrategy for CheatingPlayerStrategy {
|
|||
let index = my_cards.iter().position(|card| {
|
||||
card == play_card.unwrap()
|
||||
}).unwrap();
|
||||
TurnChoice::Play(index)
|
||||
} else {
|
||||
// discard threshold is how many cards we're willing to discard
|
||||
// such that if we only played,
|
||||
// we would not reach the final countdown round
|
||||
// e.g. 50 total, 25 to play, 20 in hand
|
||||
let discard_threshold =
|
||||
view.board.total_cards
|
||||
- (COLORS.len() * VALUES.len()) as u32
|
||||
- (view.board.num_players * view.board.hand_size);
|
||||
if view.board.discard_size() <= discard_threshold {
|
||||
// if anything is totally useless, discard it
|
||||
if let Some(i) = self.find_useless_card(view, my_cards) {
|
||||
return TurnChoice::Discard(i);
|
||||
}
|
||||
}
|
||||
|
||||
// hinting is better than discarding dead cards
|
||||
// (probably because it stalls the deck-drawing).
|
||||
if view.board.hints_remaining > 0 {
|
||||
if self.someone_else_can_play(view) {
|
||||
return self.throwaway_hint(view);
|
||||
}
|
||||
}
|
||||
return TurnChoice::Play(index)
|
||||
}
|
||||
|
||||
// discard threshold is how many cards we're willing to discard
|
||||
// such that if we only played,
|
||||
// we would not reach the final countdown round
|
||||
// e.g. 50 total, 25 to play, 20 in hand
|
||||
let discard_threshold =
|
||||
view.board.total_cards
|
||||
- (COLORS.len() * VALUES.len()) as u32
|
||||
- (view.board.num_players * view.board.hand_size);
|
||||
if view.board.discard_size() <= discard_threshold {
|
||||
// if anything is totally useless, discard it
|
||||
if let Some(i) = self.find_useless_card(view, my_cards) {
|
||||
return TurnChoice::Discard(i);
|
||||
}
|
||||
}
|
||||
|
||||
// All cards are plausibly useful.
|
||||
// Play the best discardable card, according to the ordering induced by comparing
|
||||
// (is in another hand, is dispensable, value)
|
||||
// The higher, the better to discard
|
||||
let mut discard_card = None;
|
||||
let mut compval = (false, false, 0);
|
||||
for card in my_cards {
|
||||
let my_compval = (
|
||||
view.can_see(card),
|
||||
view.board.is_dispensable(card),
|
||||
card.value,
|
||||
);
|
||||
if my_compval > compval {
|
||||
discard_card = Some(card);
|
||||
compval = my_compval;
|
||||
}
|
||||
}
|
||||
if let Some(card) = discard_card {
|
||||
let index = my_cards.iter().position(|iter_card| {
|
||||
card == iter_card
|
||||
}).unwrap();
|
||||
TurnChoice::Discard(index)
|
||||
} else {
|
||||
panic!("This shouldn't happen! No discardable card");
|
||||
// hinting is better than discarding dead cards
|
||||
// (probably because it stalls the deck-drawing).
|
||||
if view.board.hints_remaining > 0 {
|
||||
if self.someone_else_can_play(view) {
|
||||
return self.throwaway_hint(view);
|
||||
}
|
||||
}
|
||||
|
||||
// if anything is totally useless, discard it
|
||||
if let Some(i) = self.find_useless_card(view, my_cards) {
|
||||
return TurnChoice::Discard(i);
|
||||
}
|
||||
|
||||
// All cards are plausibly useful.
|
||||
// Play the best discardable card, according to the ordering induced by comparing
|
||||
// (is in another hand, is dispensable, value)
|
||||
// The higher, the better to discard
|
||||
let mut discard_card = None;
|
||||
let mut compval = (false, false, 0);
|
||||
for card in my_cards {
|
||||
let my_compval = (
|
||||
view.can_see(card),
|
||||
view.board.is_dispensable(card),
|
||||
card.value,
|
||||
);
|
||||
if my_compval > compval {
|
||||
discard_card = Some(card);
|
||||
compval = my_compval;
|
||||
}
|
||||
}
|
||||
if let Some(card) = discard_card {
|
||||
let index = my_cards.iter().position(|iter_card| {
|
||||
card == iter_card
|
||||
}).unwrap();
|
||||
TurnChoice::Discard(index)
|
||||
} else {
|
||||
panic!("This shouldn't happen! No discardable card");
|
||||
}
|
||||
}
|
||||
fn update(&mut self, _: &Turn, _: &GameStateView) {
|
||||
fn update(&mut self, _: &Turn, _: &BorrowedGameView) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ pub struct RandomStrategy {
|
|||
play_probability: f64,
|
||||
}
|
||||
impl GameStrategy for RandomStrategy {
|
||||
fn initialize(&self, player: Player, _: &GameStateView) -> Box<PlayerStrategy> {
|
||||
fn initialize(&self, player: Player, _: &BorrowedGameView) -> Box<PlayerStrategy> {
|
||||
Box::new(RandomStrategyPlayer {
|
||||
hint_probability: self.hint_probability,
|
||||
play_probability: self.play_probability,
|
||||
|
@ -40,7 +40,7 @@ pub struct RandomStrategyPlayer {
|
|||
}
|
||||
|
||||
impl PlayerStrategy for RandomStrategyPlayer {
|
||||
fn decide(&mut self, view: &GameStateView) -> TurnChoice {
|
||||
fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice {
|
||||
let p = rand::random::<f64>();
|
||||
if p < self.hint_probability {
|
||||
if view.board.hints_remaining > 0 {
|
||||
|
@ -67,6 +67,6 @@ impl PlayerStrategy for RandomStrategyPlayer {
|
|||
TurnChoice::Discard(0)
|
||||
}
|
||||
}
|
||||
fn update(&mut self, _: &Turn, _: &GameStateView) {
|
||||
fn update(&mut self, _: &Turn, _: &BorrowedGameView) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use rand::{self, Rng};
|
||||
|
||||
use simulator::*;
|
||||
use game::*;
|
||||
|
@ -13,41 +12,150 @@ use game::*;
|
|||
// - only 9 + 8 hints total. each player goes through 12.5 cards
|
||||
//
|
||||
// For any given player with at least 4 cards, and index i, there are at least 3 hints that can be given.
|
||||
// 1. a value hint on card i
|
||||
// 2. a color hint on card i
|
||||
// 3. any hint not involving card i
|
||||
// 0. a value hint on card i
|
||||
// 1. a color hint on card i
|
||||
// 2. any hint not involving card i
|
||||
//
|
||||
// for 4 players, can give 6 distinct hints
|
||||
|
||||
// TODO: currently, you need to be very careful due to
|
||||
// answers changing from the old view to the new view
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
|
||||
enum Question {
|
||||
IsPlayable(usize),
|
||||
IsDead(usize),
|
||||
}
|
||||
pub fn none() -> Self {
|
||||
Self::new(1, 0)
|
||||
}
|
||||
|
||||
fn answer_question(question: Question, hand: &Cards, view: &GameStateView) -> ModulusInformation {
|
||||
match question {
|
||||
Question::IsPlayable(index) => {
|
||||
let ref card = hand[index];
|
||||
ModulusInformation {
|
||||
modulus: 2,
|
||||
value: if view.board.is_playable(card) { 1 } else { 0 },
|
||||
}
|
||||
},
|
||||
Question::IsDead(index) => {
|
||||
let ref card = hand[index];
|
||||
ModulusInformation {
|
||||
modulus: 2,
|
||||
value: if view.board.is_dead(card) { 1 } else { 0 },
|
||||
}
|
||||
},
|
||||
pub fn combine(&mut self, other: Self) {
|
||||
self.value = self.value * other.modulus + other.value;
|
||||
self.modulus = self.modulus * other.modulus;
|
||||
}
|
||||
|
||||
pub fn emit(&mut self, modulus: u32) -> Self {
|
||||
assert!(self.modulus >= modulus);
|
||||
assert!(self.modulus % modulus == 0);
|
||||
self.modulus = self.modulus / modulus;
|
||||
let value = self.value / self.modulus;
|
||||
assert!((self.value - value) % modulus == 0);
|
||||
self.value = (self.value - value) / modulus;
|
||||
|
||||
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, Box<&GameView>) -> u32;
|
||||
fn answer_info(&self, hand: &Cards, view: Box<&GameView>) -> ModulusInformation {
|
||||
ModulusInformation::new(
|
||||
self.info_amount(),
|
||||
self.answer(hand, view)
|
||||
)
|
||||
}
|
||||
// process the answer to this question, updating card info
|
||||
fn acknowledge_answer(
|
||||
&self, value: u32, &mut Vec<CardPossibilityTable>, Box<&GameView>
|
||||
);
|
||||
|
||||
fn acknowledge_answer_info(
|
||||
&self,
|
||||
answer: ModulusInformation,
|
||||
hand_info: &mut Vec<CardPossibilityTable>,
|
||||
view: Box<&GameView>
|
||||
) {
|
||||
assert!(self.info_amount() == answer.modulus);
|
||||
self.acknowledge_answer(answer.value, hand_info, view);
|
||||
}
|
||||
}
|
||||
struct IsPlayable {
|
||||
index: usize,
|
||||
}
|
||||
impl Question for IsPlayable {
|
||||
fn info_amount(&self) -> u32 { 2 }
|
||||
fn answer(&self, hand: &Cards, view: Box<&GameView>) -> u32 {
|
||||
let ref card = hand[self.index];
|
||||
if view.get_board().is_playable(card) { 1 } else { 0 }
|
||||
}
|
||||
fn acknowledge_answer(
|
||||
&self,
|
||||
answer: u32,
|
||||
hand_info: &mut Vec<CardPossibilityTable>,
|
||||
view: Box<&GameView>,
|
||||
) {
|
||||
let ref mut card_table = hand_info[self.index];
|
||||
let possible = card_table.get_possibilities();
|
||||
for card in &possible {
|
||||
if view.get_board().is_playable(card) {
|
||||
if answer == 0 { card_table.mark_false(card); }
|
||||
} else {
|
||||
if answer == 1 { card_table.mark_false(card); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// struct IsDead {
|
||||
// index: usize,
|
||||
// }
|
||||
// impl Question for IsDead {
|
||||
// fn info_amount(&self) -> u32 { 2 }
|
||||
// fn answer(&self, hand: &Cards, view: &Box<GameView>) -> u32 {
|
||||
// let ref card = hand[self.index];
|
||||
// if view.get_board().is_dead(card) { 1 } else { 0 }
|
||||
// }
|
||||
// fn acknowledge_answer(
|
||||
// &self,
|
||||
// answer: u32,
|
||||
// hand_info: &mut Vec<CardPossibilityTable>,
|
||||
// view: &Box<GameView>,
|
||||
// ) {
|
||||
// let ref mut card_table = hand_info[self.index];
|
||||
// let possible = card_table.get_possibilities();
|
||||
// for card in &possible {
|
||||
// if view.get_board().is_dead(card) {
|
||||
// if answer == 0 { card_table.mark_false(card); }
|
||||
// } else {
|
||||
// if answer == 1 { card_table.mark_false(card); }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct InformationStrategyConfig;
|
||||
|
||||
|
@ -73,7 +181,7 @@ impl InformationStrategy {
|
|||
}
|
||||
}
|
||||
impl GameStrategy for InformationStrategy {
|
||||
fn initialize(&self, player: Player, view: &GameStateView) -> Box<PlayerStrategy> {
|
||||
fn initialize(&self, player: Player, view: &BorrowedGameView) -> Box<PlayerStrategy> {
|
||||
let mut public_info = HashMap::new();
|
||||
for player in view.board.get_players() {
|
||||
let hand_info = (0..view.board.hand_size).map(|_| { CardPossibilityTable::new() }).collect::<Vec<_>>();
|
||||
|
@ -83,6 +191,7 @@ impl GameStrategy for InformationStrategy {
|
|||
me: player,
|
||||
public_info: public_info,
|
||||
public_counts: CardCounts::new(),
|
||||
last_view: OwnedGameView::clone_from(view),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -91,10 +200,147 @@ pub struct InformationPlayerStrategy {
|
|||
me: Player,
|
||||
public_info: HashMap<Player, Vec<CardPossibilityTable>>,
|
||||
public_counts: CardCounts, // what any newly drawn card should be
|
||||
last_view: OwnedGameView, // the view on the previous turn
|
||||
}
|
||||
impl InformationPlayerStrategy {
|
||||
|
||||
fn get_questions<T>(
|
||||
total_info: u32,
|
||||
view: &T,
|
||||
hand_info: &Vec<CardPossibilityTable>,
|
||||
) -> Vec<Box<Question>>
|
||||
where T: GameView
|
||||
{
|
||||
let mut questions = Vec::new();
|
||||
let mut info_remaining = total_info;
|
||||
|
||||
while info_remaining > 1 {
|
||||
let mut question = None;
|
||||
for (i, card_table) in hand_info.iter().enumerate() {
|
||||
let p = view.get_board().probability_is_playable(card_table);
|
||||
if (p != 0.0) && (p != 1.0) {
|
||||
question = Some(Box::new(IsPlayable {index: i}) as Box<Question>);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(q) = question {
|
||||
info_remaining = info_remaining / q.info_amount();
|
||||
questions.push(q);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
questions
|
||||
}
|
||||
|
||||
fn answer_questions<T>(
|
||||
questions: &Vec<Box<Question>>, hand: &Cards, view: &T
|
||||
) -> ModulusInformation
|
||||
where T: GameView
|
||||
{
|
||||
let mut info = ModulusInformation::none();
|
||||
for question in questions {
|
||||
let answer_info = question.answer_info(hand, Box::new(view as &GameView));
|
||||
info.combine(answer_info);
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
fn get_hint_info_for_player<T>(
|
||||
&self, player: &Player, total_info: u32, view: &T
|
||||
) -> ModulusInformation where T: GameView
|
||||
{
|
||||
assert!(player != &self.me);
|
||||
let hand_info = self.get_player_public_info(player);
|
||||
let questions = Self::get_questions(total_info, view, hand_info);
|
||||
trace!("Getting hint for player {}, questions {:?}", player, questions.len());
|
||||
let mut answer = Self::answer_questions(&questions, view.get_hand(player), view);
|
||||
answer.cast_up(total_info);
|
||||
trace!("Resulting answer {:?}", answer);
|
||||
answer
|
||||
}
|
||||
|
||||
fn get_hint_sum<T>(&self, total_info: u32, view: &T) -> ModulusInformation
|
||||
where T: GameView
|
||||
{
|
||||
let mut sum = ModulusInformation::new(total_info, 0);
|
||||
for player in view.get_board().get_players() {
|
||||
if player != self.me {
|
||||
let answer = self.get_hint_info_for_player(&player, total_info, view);
|
||||
sum.add(&answer);
|
||||
}
|
||||
}
|
||||
trace!("Summed answer {:?}\n", sum);
|
||||
sum
|
||||
}
|
||||
|
||||
fn infer_own_from_hint_sum(&mut self, hint: ModulusInformation) {
|
||||
let mut hint = hint;
|
||||
let questions = {
|
||||
let view = &self.last_view;
|
||||
let hand_info = self.get_my_public_info();
|
||||
Self::get_questions(hint.modulus, view, hand_info)
|
||||
};
|
||||
trace!("{}: Questions {:?}", self.me, questions.len());
|
||||
let product = questions.iter().fold(1, |a, ref b| a * b.info_amount());
|
||||
trace!("{}: Product {}, hint: {:?}", self.me, product, hint);
|
||||
hint.cast_down(product);
|
||||
trace!("{}: Inferred for myself {:?}", self.me, hint);
|
||||
|
||||
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.emit(question.info_amount());
|
||||
question.acknowledge_answer_info(answer_info, &mut hand_info, Box::new(view as &GameView));
|
||||
}
|
||||
}
|
||||
self.return_public_info(&me, hand_info);
|
||||
}
|
||||
|
||||
fn update_from_hint_sum(&mut self, hint: ModulusInformation) {
|
||||
let hinter = self.last_view.board.player;
|
||||
let players = {
|
||||
let view = &self.last_view;
|
||||
view.board.get_players()
|
||||
};
|
||||
trace!("{}: inferring for myself, starting with {:?}", self.me, hint);
|
||||
let mut hint = hint;
|
||||
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);
|
||||
trace!("{}: subtracted for {}, now {:?}", self.me, player, hint);
|
||||
}
|
||||
|
||||
// *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, Box::new(view as &GameView));
|
||||
question.acknowledge_answer(answer, &mut hand_info, Box::new(view as &GameView));
|
||||
}
|
||||
}
|
||||
self.return_public_info(&player, hand_info);
|
||||
}
|
||||
}
|
||||
if self.me == hinter {
|
||||
assert!(hint.value == 0);
|
||||
} else {
|
||||
self.infer_own_from_hint_sum(hint);
|
||||
}
|
||||
}
|
||||
|
||||
// given a hand of cards, represents how badly it will need to play things
|
||||
fn hand_play_value(&self, view: &GameStateView, hand: &Cards/*, all_viewable: HashMap<Color, <Value, usize>> */) -> u32 {
|
||||
fn hand_play_value(&self, view: &BorrowedGameView, hand: &Cards/*, all_viewable: HashMap<Color, <Value, usize>> */) -> u32 {
|
||||
// dead = 0 points
|
||||
// indispensible = 5 + (5 - value) points
|
||||
// playable = 1 point
|
||||
|
@ -112,19 +358,20 @@ impl InformationPlayerStrategy {
|
|||
value
|
||||
}
|
||||
|
||||
fn estimate_hand_play_value(&self, view: &GameStateView) -> u32 {
|
||||
fn estimate_hand_play_value(&self, view: &BorrowedGameView) -> u32 {
|
||||
// TODO: fix this
|
||||
0
|
||||
}
|
||||
|
||||
// how badly do we need to play a particular card
|
||||
fn get_average_play_score(&self, view: &GameStateView, card_table: &CardPossibilityTable) -> f32 {
|
||||
fn get_average_play_score(&self, view: &BorrowedGameView, card_table: &CardPossibilityTable) -> f32 {
|
||||
let f = |card: &Card| {
|
||||
self.get_play_score(view, card) as f32
|
||||
};
|
||||
card_table.weighted_score(&f)
|
||||
}
|
||||
|
||||
fn get_play_score(&self, view: &GameStateView, card: &Card) -> i32 {
|
||||
fn get_play_score(&self, view: &BorrowedGameView, card: &Card) -> i32 {
|
||||
let my_hand_value = self.estimate_hand_play_value(view);
|
||||
|
||||
for player in view.board.get_players() {
|
||||
|
@ -143,23 +390,25 @@ impl InformationPlayerStrategy {
|
|||
5 + (5 - (card.value as i32))
|
||||
}
|
||||
|
||||
fn find_useless_card(&self, view: &GameStateView, hand: &Cards) -> Option<usize> {
|
||||
fn find_useless_card(&self, view: &BorrowedGameView, hand: &Vec<CardPossibilityTable>) -> Option<usize> {
|
||||
let mut set: HashSet<Card> = HashSet::new();
|
||||
|
||||
for (i, card) in hand.iter().enumerate() {
|
||||
if view.board.is_dead(card) {
|
||||
for (i, card_table) in hand.iter().enumerate() {
|
||||
if view.board.probability_is_dead(card_table) == 1.0 {
|
||||
return Some(i);
|
||||
}
|
||||
if set.contains(card) {
|
||||
// found a duplicate card
|
||||
return Some(i);
|
||||
if let Some(card) = card_table.get_card() {
|
||||
if set.contains(&card) {
|
||||
// found a duplicate card
|
||||
return Some(i);
|
||||
}
|
||||
set.insert(card);
|
||||
}
|
||||
set.insert(card.clone());
|
||||
}
|
||||
return None
|
||||
}
|
||||
|
||||
fn someone_else_can_play(&self, view: &GameStateView) -> bool {
|
||||
fn someone_else_can_play(&self, view: &BorrowedGameView) -> bool {
|
||||
for player in view.board.get_players() {
|
||||
if player != self.me {
|
||||
for card in view.get_hand(&player) {
|
||||
|
@ -172,6 +421,18 @@ impl InformationPlayerStrategy {
|
|||
false
|
||||
}
|
||||
|
||||
fn take_public_info(&mut self, player: &Player) -> Vec<CardPossibilityTable> {
|
||||
self.public_info.remove(player).unwrap()
|
||||
}
|
||||
|
||||
fn return_public_info(&mut self, player: &Player, card_info: Vec<CardPossibilityTable>) {
|
||||
self.public_info.insert(*player, card_info);
|
||||
}
|
||||
|
||||
fn get_my_public_info(&self) -> &Vec<CardPossibilityTable> {
|
||||
self.get_player_public_info(&self.me)
|
||||
}
|
||||
|
||||
fn get_player_public_info(&self, player: &Player) -> &Vec<CardPossibilityTable> {
|
||||
self.public_info.get(player).unwrap()
|
||||
}
|
||||
|
@ -200,7 +461,7 @@ impl InformationPlayerStrategy {
|
|||
|
||||
fn update_public_info_for_discard_or_play(
|
||||
&mut self,
|
||||
view: &GameStateView,
|
||||
view: &BorrowedGameView,
|
||||
player: &Player,
|
||||
index: usize,
|
||||
card: &Card
|
||||
|
@ -212,7 +473,7 @@ impl InformationPlayerStrategy {
|
|||
info.remove(index);
|
||||
|
||||
// push *before* incrementing public counts
|
||||
if info.len() < view.info.len() {
|
||||
if info.len() < view.hand_size(&player) {
|
||||
info.push(new_card_table);
|
||||
}
|
||||
}
|
||||
|
@ -220,7 +481,7 @@ impl InformationPlayerStrategy {
|
|||
// 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 mut info = self.get_player_public_info_mut(&other_player);
|
||||
let info = self.get_player_public_info_mut(&other_player);
|
||||
for card_table in info {
|
||||
card_table.decrement_weight_if_possible(card);
|
||||
}
|
||||
|
@ -229,10 +490,10 @@ impl InformationPlayerStrategy {
|
|||
self.public_counts.increment(card);
|
||||
}
|
||||
|
||||
fn get_private_info(&self, view: &GameStateView) -> Vec<CardPossibilityTable> {
|
||||
let mut info = self.get_player_public_info(&self.me).clone();
|
||||
fn get_private_info(&self, view: &BorrowedGameView) -> Vec<CardPossibilityTable> {
|
||||
let mut info = self.get_my_public_info().clone();
|
||||
for card_table in info.iter_mut() {
|
||||
for (other_player, state) in &view.other_player_states {
|
||||
for (_, state) in &view.other_player_states {
|
||||
for card in &state.hand {
|
||||
card_table.decrement_weight_if_possible(card);
|
||||
}
|
||||
|
@ -241,9 +502,81 @@ impl InformationPlayerStrategy {
|
|||
info
|
||||
}
|
||||
|
||||
fn get_hint(&self, view: &BorrowedGameView) -> TurnChoice {
|
||||
let total_info = 3 * (view.board.num_players - 1);
|
||||
|
||||
let hint_info = self.get_hint_sum(total_info, view);
|
||||
|
||||
let hint_type = hint_info.value % 3;
|
||||
let player_amt = (hint_info.value - hint_type) / 3;
|
||||
|
||||
let hint_player = (self.me + 1 + player_amt) % view.board.num_players;
|
||||
let card_index = 0;
|
||||
|
||||
let hand = view.get_hand(&hint_player);
|
||||
let hint_card = &hand[card_index];
|
||||
|
||||
let hinted = match hint_type {
|
||||
0 => {
|
||||
Hinted::Value(hint_card.value)
|
||||
}
|
||||
1 => {
|
||||
Hinted::Color(hint_card.color)
|
||||
}
|
||||
2 => {
|
||||
let mut hinted_opt = None;
|
||||
for card in hand {
|
||||
if card.color != hint_card.color {
|
||||
hinted_opt = Some(Hinted::Color(card.color));
|
||||
break;
|
||||
}
|
||||
if card.value != hint_card.value {
|
||||
hinted_opt = Some(Hinted::Value(card.value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(hinted) = hinted_opt {
|
||||
hinted
|
||||
} else {
|
||||
panic!("Found nothing to hint!")
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
panic!("Invalid hint type")
|
||||
}
|
||||
};
|
||||
|
||||
TurnChoice::Hint(Hint {
|
||||
player: hint_player,
|
||||
hinted: hinted,
|
||||
})
|
||||
}
|
||||
|
||||
fn infer_from_hint(&mut self, view: &BorrowedGameView, hint: &Hint, result: &Vec<bool>) {
|
||||
let total_info = 3 * (view.board.num_players - 1);
|
||||
|
||||
let hinter = self.last_view.board.player;
|
||||
let player_amt = (view.board.num_players + hint.player - hinter - 1) % view.board.num_players;
|
||||
|
||||
let hint_type = if result[0] {
|
||||
match hint.hinted {
|
||||
Hinted::Value(_) => 0,
|
||||
Hinted::Color(_) => 1,
|
||||
}
|
||||
} else {
|
||||
2
|
||||
};
|
||||
|
||||
let hint_value = player_amt * 3 + 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, view: &GameStateView) -> TurnChoice {
|
||||
fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice {
|
||||
let private_info = self.get_private_info(view);
|
||||
// debug!("My info:");
|
||||
// for (i, card_table) in private_info.iter().enumerate() {
|
||||
|
@ -268,48 +601,33 @@ impl PlayerStrategy for InformationPlayerStrategy {
|
|||
}
|
||||
}
|
||||
|
||||
TurnChoice::Play(play_index)
|
||||
} else {
|
||||
if view.board.hints_remaining > 0 {
|
||||
let hint_player = view.board.player_to_left(&self.me);
|
||||
let hint_card = rand::thread_rng().choose(&view.get_hand(&hint_player)).unwrap();
|
||||
let hinted = {
|
||||
if rand::random() {
|
||||
// hint a color
|
||||
Hinted::Color(hint_card.color)
|
||||
} else {
|
||||
Hinted::Value(hint_card.value)
|
||||
}
|
||||
};
|
||||
TurnChoice::Hint(Hint {
|
||||
player: hint_player,
|
||||
hinted: hinted,
|
||||
})
|
||||
} else {
|
||||
TurnChoice::Discard(0)
|
||||
return TurnChoice::Play(play_index)
|
||||
}
|
||||
|
||||
let discard_threshold =
|
||||
view.board.total_cards
|
||||
- (COLORS.len() * VALUES.len()) as u32
|
||||
- (view.board.num_players * view.board.hand_size);
|
||||
|
||||
if view.board.discard_size() <= discard_threshold {
|
||||
// if anything is totally useless, discard it
|
||||
if let Some(i) = self.find_useless_card(view, &private_info) {
|
||||
return TurnChoice::Discard(i);
|
||||
}
|
||||
}
|
||||
|
||||
// // 50 total, 25 to play, 20 in hand
|
||||
// if view.board.discard.cards.len() < 6 {
|
||||
// // if anything is totally useless, discard it
|
||||
// if let Some(i) = self.find_useless_card(view) {
|
||||
// return TurnChoice::Discard(i);
|
||||
// }
|
||||
// }
|
||||
// hinting is better than discarding dead cards
|
||||
// (probably because it stalls the deck-drawing).
|
||||
if view.board.hints_remaining > 1 {
|
||||
if self.someone_else_can_play(view) {
|
||||
return self.get_hint(view);
|
||||
}
|
||||
}
|
||||
|
||||
// // hinting is better than discarding dead cards
|
||||
// // (probably because it stalls the deck-drawing).
|
||||
// if view.board.hints_remaining > 1 {
|
||||
// if self.someone_else_can_play(view) {
|
||||
// return self.throwaway_hint(view);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // if anything is totally useless, discard it
|
||||
// if let Some(i) = self.find_useless_card(view) {
|
||||
// return TurnChoice::Discard(i);
|
||||
// }
|
||||
// if anything is totally useless, discard it
|
||||
if let Some(i) = self.find_useless_card(view, &private_info) {
|
||||
return TurnChoice::Discard(i);
|
||||
}
|
||||
|
||||
// // All cards are plausibly useful.
|
||||
// // Play the best discardable card, according to the ordering induced by comparing
|
||||
|
@ -343,12 +661,19 @@ impl PlayerStrategy for InformationPlayerStrategy {
|
|||
// panic!("This shouldn't happen! No discardable card");
|
||||
// }
|
||||
// }
|
||||
|
||||
if view.board.hints_remaining > 0 {
|
||||
self.get_hint(view)
|
||||
} else {
|
||||
TurnChoice::Discard(0)
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, turn: &Turn, view: &GameStateView) {
|
||||
fn update(&mut self, turn: &Turn, view: &BorrowedGameView) {
|
||||
match turn.choice {
|
||||
TurnChoice::Hint(ref hint) => {
|
||||
if let &TurnResult::Hint(ref matches) = &turn.result {
|
||||
self.infer_from_hint(view, hint, matches);
|
||||
self.update_public_info_for_hint(hint, matches);
|
||||
} else {
|
||||
panic!("Got turn choice {:?}, but turn result {:?}",
|
||||
|
@ -364,7 +689,7 @@ impl PlayerStrategy for InformationPlayerStrategy {
|
|||
}
|
||||
}
|
||||
TurnChoice::Play(index) => {
|
||||
if let &TurnResult::Play(ref card, played) = &turn.result {
|
||||
if let &TurnResult::Play(ref card, _) = &turn.result {
|
||||
self.update_public_info_for_discard_or_play(view, &turn.player, index, card);
|
||||
} else {
|
||||
panic!("Got turn choice {:?}, but turn result {:?}",
|
||||
|
@ -372,5 +697,6 @@ impl PlayerStrategy for InformationPlayerStrategy {
|
|||
}
|
||||
}
|
||||
}
|
||||
self.last_view = OwnedGameView::clone_from(view);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue