beginnings of information strategy

This commit is contained in:
Jeff Wu 2016-03-27 10:47:58 -07:00
parent f09dd58cda
commit efba24d6e8
6 changed files with 451 additions and 44 deletions

View file

@ -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);
}
}

View file

@ -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<T>(
&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<T>(&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<T>(&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<T>(&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<Player> {

View file

@ -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<Card> {
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<T>(&self, score_fn: &Fn(&Card) -> T) -> f32
where f32: From<T>
{
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<Card, u32>,
}
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::<Vec<_>>()
}
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(())
}

View file

@ -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<simulator::GameStrategyConfig + Sync>
},
"info" => {
Box::new(strategies::information::InformationStrategyConfig::new())
as Box<simulator::GameStrategyConfig + Sync>
},
_ => {
print_usage(&program, opts);
panic!("Unexpected strategy argument {}", strategy_str);

View file

@ -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::*;

View file

@ -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<GameStrategy> {
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<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<_>>();
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<Player, Vec<CardPossibilityTable>>,
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<Color, <Value, usize>> */) -> 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<usize> {
let mut set: HashSet<Card> = 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<CardPossibilityTable> {
self.public_info.get(player).unwrap()
}
fn get_player_public_info_mut(&mut self, player: &Player) -> &mut Vec<CardPossibilityTable> {
self.public_info.get_mut(player).unwrap()
}
fn update_public_info_for_hint(&mut self, hint: &Hint, matches: &Vec<bool>) {
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<CardPossibilityTable> {
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::<Vec<_>>();
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);
}
}
}
}
}