From efba24d6e832e84dbca44c865603bb3e7c859f62 Mon Sep 17 00:00:00 2001 From: Jeff Wu Date: Sun, 27 Mar 2016 10:47:58 -0700 Subject: [PATCH] beginnings of information strategy --- src/cards.rs | 4 +- src/game.rs | 30 +-- src/info.rs | 78 +++++-- src/main.rs | 5 + src/strategies/cheating.rs | 2 +- src/strategies/information.rs | 376 ++++++++++++++++++++++++++++++++++ 6 files changed, 451 insertions(+), 44 deletions(-) create mode 100644 src/strategies/information.rs diff --git a/src/cards.rs b/src/cards.rs index c27b1fa..7ddcd9e 100644 --- a/src/cards.rs +++ b/src/cards.rs @@ -65,7 +65,7 @@ impl CardCounts { get_count_for_value(&card.value) - count } - pub fn add(&mut self, card: &Card) { + pub fn increment(&mut self, card: &Card) { let count = self.counts.get_mut(card).unwrap(); *count += 1; } @@ -118,7 +118,7 @@ impl Discard { } pub fn place(&mut self, card: Card) { - self.counts.add(&card); + self.counts.increment(&card); self.cards.push(card); } } diff --git a/src/game.rs b/src/game.rs index a5fd53f..8ab79aa 100644 --- a/src/game.rs +++ b/src/game.rs @@ -33,8 +33,8 @@ pub struct Hint { #[derive(Debug,Clone)] pub enum TurnChoice { Hint(Hint), - Discard(usize), - Play(usize), + Discard(usize), // index of card to discard + Play(usize), // index of card to play } // represents what happened in a turn @@ -292,33 +292,19 @@ impl BoardState { } } - fn probability_of_predicate( - &self, - card_info: &T, - predicate: &Fn(&Self, &Card) -> bool - ) -> f32 where T: CardInfo { - let mut total_weight = 0; - let mut playable_weight = 0; - for card in card_info.get_possibilities() { - let weight = card_info.get_weight(&card); - if predicate(&self, &card) { - playable_weight += weight; - } - total_weight += weight; - } - (playable_weight as f32) / (total_weight as f32) - } - pub fn probability_is_playable(&self, card_info: &T) -> f32 where T: CardInfo { - self.probability_of_predicate(card_info, &Self::is_playable) + let f = |card: &Card| { self.is_playable(card) }; + card_info.probability_of_predicate(&f) } pub fn probability_is_dead(&self, card_info: &T) -> f32 where T: CardInfo { - self.probability_of_predicate(card_info, &Self::is_dead) + let f = |card: &Card| { self.is_dead(card) }; + card_info.probability_of_predicate(&f) } pub fn probability_is_dispensable(&self, card_info: &T) -> f32 where T: CardInfo { - self.probability_of_predicate(card_info, &Self::is_dispensable) + let f = |card: &Card| { self.is_dispensable(card) }; + card_info.probability_of_predicate(&f) } pub fn get_players(&self) -> Vec { diff --git a/src/info.rs b/src/info.rs index da9e53f..0293287 100644 --- a/src/info.rs +++ b/src/info.rs @@ -2,6 +2,7 @@ use std::cmp::Eq; use std::collections::{HashMap, HashSet}; use std::fmt; use std::hash::Hash; +use std::convert::From; use cards::*; @@ -20,8 +21,6 @@ pub trait CardInfo { // whether the card is possible fn is_possible(&self, card: &Card) -> bool; - // TODO: have a borrow_possibilities to allow for more efficiency? - // mark all current possibilities for the card fn get_possibilities(&self) -> Vec { let mut v = Vec::new(); @@ -37,10 +36,10 @@ pub trait CardInfo { } // get probability weight for the card #[allow(unused_variables)] - fn get_weight(&self, card: &Card) -> u32 { - 1 + fn get_weight(&self, card: &Card) -> f32 { + 1 as f32 } - fn get_weighted_possibilities(&self) -> Vec<(Card, u32)> { + fn get_weighted_possibilities(&self) -> Vec<(Card, f32)> { let mut v = Vec::new(); for card in self.get_possibilities() { let weight = self.get_weight(&card); @@ -48,6 +47,25 @@ pub trait CardInfo { } v } + fn weighted_score(&self, score_fn: &Fn(&Card) -> T) -> f32 + where f32: From + { + let mut total_score = 0.; + let mut total_weight = 0.; + for card in self.get_possibilities() { + let weight = self.get_weight(&card); + let score = f32::from(score_fn(&card)); + total_weight += weight; + total_score += weight * score; + } + total_score / total_weight + } + fn probability_of_predicate(&self, predicate: &Fn(&Card) -> bool) -> f32 { + let f = |card: &Card| { + if predicate(card) { 1.0 } else { 0.0 } + }; + self.weighted_score(&f) + } // mark a whole color as false fn mark_color_false(&mut self, color: &Color); @@ -215,31 +233,53 @@ impl fmt::Display for SimpleCardInfo { // Can represent information of the form: // this card is/isn't possible -// also, maintains weights for the cards +// also, maintains integer weights for the cards #[derive(Clone)] pub struct CardPossibilityTable { possible: HashMap, } impl CardPossibilityTable { pub fn new() -> CardPossibilityTable { + Self::from(&CardCounts::new()) + } + + // mark a possible card as false + pub fn mark_false(&mut self, card: &Card) { + self.possible.remove(card); + } + + // a bit more efficient + pub fn borrow_possibilities<'a>(&'a self) -> Vec<&'a Card> { + self.possible.keys().collect::>() + } + + pub fn decrement_weight_if_possible(&mut self, card: &Card) { + if self.is_possible(card) { + self.decrement_weight(card); + } + } + + 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; + } +} +impl <'a> From<&'a CardCounts> for CardPossibilityTable { + fn from(counts: &'a CardCounts) -> CardPossibilityTable { let mut possible = HashMap::new(); for &color in COLORS.iter() { for &value in VALUES.iter() { - possible.insert( - Card::new(color, value), - get_count_for_value(&value) - ); + let card = Card::new(color, value); + let count = counts.remaining(&card); + possible.insert(card, count); } } CardPossibilityTable { possible: possible, } } - - // mark a possible card as false - fn mark_false(&mut self, card: &Card) { - self.possible.remove(card); - } } impl CardInfo for CardPossibilityTable { fn is_possible(&self, card: &Card) -> bool { @@ -261,14 +301,14 @@ impl CardInfo for CardPossibilityTable { self.mark_false(&Card::new(color, value.clone())); } } - fn get_weight(&self, card: &Card) -> u32 { - *self.possible.get(card).unwrap_or(&0) + fn get_weight(&self, card: &Card) -> f32 { + *self.possible.get(card).unwrap_or(&0) as f32 } } impl fmt::Display for CardPossibilityTable { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for card in self.get_possibilities() { - try!(f.write_str(&format!("{}, ", card))); + for (card, weight) in &self.possible { + try!(f.write_str(&format!("{} {}, ", weight, card))); } Ok(()) } diff --git a/src/main.rs b/src/main.rs index 8002e28..94ee340 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ mod simulator; mod strategies { pub mod examples; pub mod cheating; + pub mod information; } use getopts::Options; @@ -128,6 +129,10 @@ fn main() { Box::new(strategies::cheating::CheatingStrategyConfig::new()) as Box }, + "info" => { + Box::new(strategies::information::InformationStrategyConfig::new()) + as Box + }, _ => { print_usage(&program, opts); panic!("Unexpected strategy argument {}", strategy_str); diff --git a/src/strategies/cheating.rs b/src/strategies/cheating.rs index 8bc6aaf..06c0527 100644 --- a/src/strategies/cheating.rs +++ b/src/strategies/cheating.rs @@ -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::*; diff --git a/src/strategies/information.rs b/src/strategies/information.rs new file mode 100644 index 0000000..10f1746 --- /dev/null +++ b/src/strategies/information.rs @@ -0,0 +1,376 @@ +use std::collections::{HashMap, HashSet}; +use rand::{self, Rng}; + +use simulator::*; +use game::*; + +// strategy that recommends other players an action. +// +// 50 cards, 25 plays, 25 left +// with 5 players: +// - only 5 + 8 hints total. each player goes through 10 cards +// with 4 players: +// - 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 +// +// for 4 players, can give 6 distinct hints + +struct ModulusInformation { + modulus: u32, + value: u32, +} + +enum Question { + IsPlayable(usize), + IsDead(usize), +} + +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 }, + } + }, + } +} + +#[allow(dead_code)] +pub struct InformationStrategyConfig; + +impl InformationStrategyConfig { + pub fn new() -> InformationStrategyConfig { + InformationStrategyConfig + } +} +impl GameStrategyConfig for InformationStrategyConfig { + fn initialize(&self, opts: &GameOptions) -> Box { + if opts.num_players < 4 { + panic!("Information strategy doesn't work with less than 4 players"); + } + Box::new(InformationStrategy::new()) + } +} + +pub struct InformationStrategy; + +impl InformationStrategy { + pub fn new() -> InformationStrategy { + InformationStrategy + } +} +impl GameStrategy for InformationStrategy { + fn initialize(&self, player: Player, view: &GameStateView) -> Box { + 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::>(); + public_info.insert(player, hand_info); + } + Box::new(InformationPlayerStrategy { + me: player, + public_info: public_info, + public_counts: CardCounts::new(), + }) + } +} + +pub struct InformationPlayerStrategy { + me: Player, + public_info: HashMap>, + public_counts: CardCounts, // what any newly drawn card should be +} +impl InformationPlayerStrategy { + // 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> */) -> u32 { + // dead = 0 points + // indispensible = 5 + (5 - value) points + // playable = 1 point + let mut value = 0; + for card in hand { + if view.board.is_dead(card) { + continue + } + if !view.board.is_dispensable(card) { + value += 10 - card.value; + } else { + value += 1; + } + } + value + } + + fn estimate_hand_play_value(&self, view: &GameStateView) -> u32 { + 0 + } + + // how badly do we need to play a particular card + fn get_average_play_score(&self, view: &GameStateView, 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 { + let my_hand_value = self.estimate_hand_play_value(view); + + for player in view.board.get_players() { + if player != self.me { + if view.has_card(&player, card) { + let their_hand_value = self.hand_play_value(view, view.get_hand(&player)); + // they can play this card, and have less urgent plays than i do + if their_hand_value <= my_hand_value { + return 1; + } + } + } + } + // there are no hints + // maybe value 5s more? + 5 + (5 - (card.value as i32)) + } + + fn find_useless_card(&self, view: &GameStateView, hand: &Cards) -> Option { + let mut set: HashSet = HashSet::new(); + + for (i, card) in hand.iter().enumerate() { + if view.board.is_dead(card) { + return Some(i); + } + if set.contains(card) { + // found a duplicate card + return Some(i); + } + set.insert(card.clone()); + } + return None + } + + fn someone_else_can_play(&self, view: &GameStateView) -> bool { + for player in view.board.get_players() { + if player != self.me { + for card in view.get_hand(&player) { + if view.board.is_playable(card) { + return true; + } + } + } + } + false + } + + fn get_player_public_info(&self, player: &Player) -> &Vec { + self.public_info.get(player).unwrap() + } + + fn get_player_public_info_mut(&mut self, player: &Player) -> &mut Vec { + self.public_info.get_mut(player).unwrap() + } + + fn update_public_info_for_hint(&mut self, hint: &Hint, matches: &Vec) { + let mut info = self.get_player_public_info_mut(&hint.player); + let zip_iter = info.iter_mut().zip(matches); + match hint.hinted { + Hinted::Color(ref color) => { + for (card_info, matched) in zip_iter { + card_info.mark_color(color, *matched); + } + } + Hinted::Value(ref value) => { + for (card_info, matched) in zip_iter { + card_info.mark_value(value, *matched); + } + } + + } + } + + fn update_public_info_for_discard_or_play( + &mut self, + view: &GameStateView, + player: &Player, + index: usize, + card: &Card + ) { + let new_card_table = CardPossibilityTable::from(&self.public_counts); + { + let mut 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.info.len() { + info.push(new_card_table); + } + } + + // 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); + for card_table in info { + card_table.decrement_weight_if_possible(card); + } + } + + self.public_counts.increment(card); + } + + fn get_private_info(&self, view: &GameStateView) -> Vec { + let mut info = self.get_player_public_info(&self.me).clone(); + for card_table in info.iter_mut() { + for (other_player, state) in &view.other_player_states { + for card in &state.hand { + card_table.decrement_weight_if_possible(card); + } + } + } + info + } + +} +impl PlayerStrategy for InformationPlayerStrategy { + fn decide(&mut self, view: &GameStateView) -> TurnChoice { + let private_info = self.get_private_info(view); + // debug!("My info:"); + // for (i, card_table) in private_info.iter().enumerate() { + // debug!("{}: {}", i, card_table); + // } + + let playable_cards = private_info.iter().enumerate().filter(|&(_, card_table)| { + view.board.probability_is_playable(card_table) == 1.0 + }).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; + } + } + + 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) + } + } + + // // 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.throwaway_hint(view); + // } + // } + + // // if anything is totally useless, discard it + // if let Some(i) = self.find_useless_card(view) { + // 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 { + // if view.board.hints_remaining > 0 { + // if !view.can_see(card) { + // return self.throwaway_hint(view); + // } + // } + + // 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: &Turn, view: &GameStateView) { + match turn.choice { + TurnChoice::Hint(ref hint) => { + if let &TurnResult::Hint(ref matches) = &turn.result { + self.update_public_info_for_hint(hint, matches); + } else { + panic!("Got turn choice {:?}, but turn result {:?}", + turn.choice, turn.result); + } + } + TurnChoice::Discard(index) => { + if let &TurnResult::Discard(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 {:?}", + turn.choice, turn.result); + } + } + TurnChoice::Play(index) => { + if let &TurnResult::Play(ref card, played) = &turn.result { + self.update_public_info_for_discard_or_play(view, &turn.player, index, card); + } else { + panic!("Got turn choice {:?}, but turn result {:?}", + turn.choice, turn.result); + } + } + } + } +}