From 7f5e32699e547c98d4e4196d5ad4cbfc1af2c47f Mon Sep 17 00:00:00 2001 From: Jeff Wu Date: Fri, 1 Apr 2016 00:14:13 -0700 Subject: [PATCH] various cleanups, fixes --- README.md | 20 +++-- src/cards.rs | 5 +- src/game.rs | 27 ++++-- src/main.rs | 1 - src/simulator.rs | 49 +++++----- src/strategies/cheating.rs | 15 +--- src/strategies/information.rs | 162 +++++++++++++++++----------------- 7 files changed, 148 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index a4ad2ec..684b0fb 100644 --- a/README.md +++ b/README.md @@ -54,16 +54,24 @@ cargo run -- -n 10000 -s 0 -t 2 -p 5 -g cheat Or, if the simulation is slow (as the info strategy is), ``` -cargo build --release -time ./target/release/rust_hanabi -n 10000 -s 0 -t 2 -p 5 -g info +cargo run --release -- -n 10000 -s 0 -t 2 -p 5 -g info ``` ## Results Currently, on seeds 0-9999, we have: - | 2p | 3p | 4p | 5p | -----------|---------|---------|---------|---------| -cheating | 24.8600 | 24.9781 | 24.9715 | 24.9583 | -info | 18.5959 | 23.8846 | 24.7753 | 24.8719 | + | 2p | 3p | 4p | 5p | +------------|---------|---------|---------|---------| +cheating | 24.8600 | 24.9781 | 24.9715 | 24.9583 | +information | 18.5726 | 23.8806 | 24.7722 | 24.8756 | +To reproduce: +``` +n=1000000 +for strategy in info cheat; do + for p in $(seq 2 5); do + time cargo run --release -- -n $n -s 0 -t 4 -p $p -g $strategy; + done +done +``` diff --git a/src/cards.rs b/src/cards.rs index 4dc4e10..75e2423 100644 --- a/src/cards.rs +++ b/src/cards.rs @@ -125,6 +125,7 @@ impl fmt::Display for Discard { } pub type Score = u32; +pub const PERFECT_SCORE: Score = 25; #[derive(Debug,Clone)] pub struct Firework { @@ -139,7 +140,7 @@ impl Firework { } } - pub fn desired_value(&self) -> Option { + pub fn needed_value(&self) -> Option { if self.complete() { None } else { Some(self.top + 1) } } @@ -157,7 +158,7 @@ impl Firework { "Attempted to place card on firework of wrong color!" ); assert!( - Some(card.value) == self.desired_value(), + Some(card.value) == self.needed_value(), "Attempted to place card of wrong value on firework!" ); self.top = card.value; diff --git a/src/game.rs b/src/game.rs index faa6db4..35b0d39 100644 --- a/src/game.rs +++ b/src/game.rs @@ -223,7 +223,7 @@ impl BoardState { // returns whether a card would place on a firework pub fn is_playable(&self, card: &Card) -> bool { - Some(card.value) == self.get_firework(card.color).desired_value() + Some(card.value) == self.get_firework(card.color).needed_value() } // best possible value we can get for firework of that color, @@ -233,10 +233,10 @@ impl BoardState { if firework.complete() { return FINAL_VALUE; } - let desired = firework.desired_value().unwrap(); + let needed = firework.needed_value().unwrap(); for &value in VALUES.iter() { - if value < desired { + if value < needed { // already have these cards continue } @@ -255,8 +255,8 @@ impl BoardState { if firework.complete() { true } else { - let desired = firework.desired_value().unwrap(); - if card.value < desired { + let needed = firework.needed_value().unwrap(); + if card.value < needed { true } else { card.value > self.highest_attainable(card.color) @@ -270,8 +270,8 @@ impl BoardState { if firework.complete() { true } else { - let desired = firework.desired_value().unwrap(); - if card.value < desired { + let needed = firework.needed_value().unwrap(); + if card.value < needed { true } else { if card.value > self.highest_attainable(card.color) { @@ -392,6 +392,19 @@ pub trait GameView { } false } + + fn someone_else_can_play(&self) -> bool { + for player in self.get_board().get_players() { + if player != self.me() { + for card in self.get_hand(&player) { + if self.get_board().is_playable(card) { + return true; + } + } + } + } + false + } } // version of game view that is borrowed. used in simulator for efficiency, diff --git a/src/main.rs b/src/main.rs index 598766c..fa11e5d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,7 +33,6 @@ impl log::Log for SimpleLogger { fn print_usage(program: &str, opts: Options) { print!("{}", opts.usage(&format!("Usage: {} [options]", program))); - // for p in $(seq 5 2); do time cargo run -- -n 1000 -s 0 -t 2 -p $p -g info; done } diff --git a/src/simulator.rs b/src/simulator.rs index 1d0b20e..a99044c 100644 --- a/src/simulator.rs +++ b/src/simulator.rs @@ -33,7 +33,7 @@ pub fn simulate_once( opts: &GameOptions, game_strategy: Box, seed_opt: Option, - ) -> Score { + ) -> GameState { let seed = seed_opt.unwrap_or(rand::thread_rng().next_u32()); @@ -73,11 +73,10 @@ pub fn simulate_once( debug!(""); debug!("======================================================="); debug!("Final state:\n{}", game); - let score = game.score(); - debug!("SCORED: {:?}", score); - score + game } +#[derive(Debug)] struct Histogram { pub hist: HashMap, pub sum: Score, @@ -103,6 +102,9 @@ impl Histogram { pub fn get_count(&self, val: &Score) -> u32 { *self.hist.get(&val).unwrap_or(&0) } + pub fn percentage_with(&self, val: &Score) -> f32 { + self.get_count(val) as f32 / self.total_count as f32 + } pub fn average(&self) -> f32 { (self.sum as f32) / (self.total_count as f32) } @@ -131,7 +133,7 @@ pub fn simulate( first_seed_opt: Option, n_trials: u32, n_threads: u32, - ) -> f32 where T: GameStrategyConfig + Sync { + ) where T: GameStrategyConfig + Sync { let first_seed = first_seed_opt.unwrap_or(rand::thread_rng().next_u32()); @@ -145,33 +147,40 @@ pub fn simulate( info!("Thread {} spawned: seeds {} to {}", i, start, end); let mut non_perfect_seeds = Vec::new(); - let mut histogram = Histogram::new(); + let mut score_histogram = Histogram::new(); + let mut lives_histogram = Histogram::new(); for seed in start..end { if (seed > start) && ((seed-start) % 1000 == 0) { info!( - "Thread {}, Trials: {}, Average so far: {}", - i, seed-start, histogram.average() + "Thread {}, Trials: {}, Stats so far: {} score, {} lives, {}% win", + i, seed-start, score_histogram.average(), lives_histogram.average(), + score_histogram.percentage_with(&PERFECT_SCORE) * 100.0 ); } - let score = simulate_once(&opts, strat_config_ref.initialize(&opts), Some(seed)); - histogram.insert(score); - if score != 25 { non_perfect_seeds.push((score, seed)); } + let game = simulate_once(&opts, strat_config_ref.initialize(&opts), Some(seed)); + let score = game.score(); + debug!("SCORED: {:?}", score); + lives_histogram.insert(game.board.lives_remaining); + score_histogram.insert(score); + if score != PERFECT_SCORE { non_perfect_seeds.push((score, seed)); } } info!("Thread {} done", i); - (non_perfect_seeds, histogram) + (non_perfect_seeds, score_histogram, lives_histogram) })); } let mut non_perfect_seeds : Vec<(Score,u32)> = Vec::new(); - let mut histogram = Histogram::new(); + let mut score_histogram = Histogram::new(); + let mut lives_histogram = Histogram::new(); for join_handle in join_handles { - let (thread_non_perfect_seeds, thread_histogram) = join_handle.join(); + let (thread_non_perfect_seeds, thread_score_histogram, thread_lives_histogram) = join_handle.join(); non_perfect_seeds.extend(thread_non_perfect_seeds.iter()); - histogram.merge(thread_histogram); + score_histogram.merge(thread_score_histogram); + lives_histogram.merge(thread_lives_histogram); } - info!("Score histogram:\n{}", histogram); + info!("Score histogram:\n{}", score_histogram); non_perfect_seeds.sort(); // info!("Seeds with non-perfect score: {:?}", non_perfect_seeds); @@ -180,10 +189,8 @@ pub fn simulate( non_perfect_seeds.get(0).unwrap().1); } - let percentage = (n_trials - non_perfect_seeds.len() as u32) as f32 / n_trials as f32; - info!("Percentage perfect: {:?}%", percentage * 100.0); - let average = histogram.average(); - info!("Average score: {:?}", average); - average + info!("Percentage perfect: {:?}%", score_histogram.percentage_with(&PERFECT_SCORE) * 100.0); + info!("Average score: {:?}", score_histogram.average()); + info!("Average lives: {:?}", lives_histogram.average()); }) } diff --git a/src/strategies/cheating.rs b/src/strategies/cheating.rs index 184c72f..aec01d3 100644 --- a/src/strategies/cheating.rs +++ b/src/strategies/cheating.rs @@ -134,19 +134,6 @@ impl CheatingPlayerStrategy { } return None } - - 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) { - if view.board.is_playable(card) { - return true; - } - } - } - } - false - } } impl PlayerStrategy for CheatingPlayerStrategy { fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice { @@ -197,7 +184,7 @@ impl PlayerStrategy for CheatingPlayerStrategy { // 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) { + if view.someone_else_can_play() { return self.throwaway_hint(view); } } diff --git a/src/strategies/information.rs b/src/strategies/information.rs index 5657796..8338e92 100644 --- a/src/strategies/information.rs +++ b/src/strategies/information.rs @@ -123,23 +123,23 @@ struct CardPossibilityPartition { partition: HashMap, } impl CardPossibilityPartition { - fn new( - index: usize, max_n_partitions: u32, card_table: &CardPossibilityTable, view: &T - ) -> CardPossibilityPartition where T: GameView { + fn new( + index: usize, max_n_partitions: u32, card_table: &CardPossibilityTable, view: &OwnedGameView + ) -> CardPossibilityPartition { let mut cur_block = 0; let mut partition = HashMap::new(); let mut n_partitions = 0; - let has_dead = card_table.probability_is_dead(view.get_board()) != 0.0; + let has_dead = card_table.probability_is_dead(&view.board) != 0.0; // TODO: group things of different colors and values? - let effective_max = if has_dead { - max_n_partitions - 1 - } else { - max_n_partitions + let mut effective_max = max_n_partitions; + if has_dead { + effective_max -= 1; }; + for card in card_table.get_possibilities() { - if !view.get_board().is_dead(&card) { + if !view.board.is_dead(&card) { partition.insert(card.clone(), cur_block); cur_block = (cur_block + 1) % effective_max; if n_partitions < effective_max { @@ -150,13 +150,24 @@ impl CardPossibilityPartition { if has_dead { for card in card_table.get_possibilities() { - if view.get_board().is_dead(&card) { + if view.board.is_dead(&card) { partition.insert(card.clone(), n_partitions); } } n_partitions += 1; } + // let mut s : String = "Partition: |".to_string(); + // for i in 0..n_partitions { + // for (card, block) in partition.iter() { + // if *block == i { + // s = s + &format!(" {}", card); + // } + // } + // s = s + &format!(" |"); + // } + // debug!("{}", s); + CardPossibilityPartition { index: index, n_partitions: n_partitions, @@ -231,13 +242,11 @@ pub struct InformationPlayerStrategy { } impl InformationPlayerStrategy { - fn get_questions( + fn get_questions( total_info: u32, - view: &T, + view: &OwnedGameView, hand_info: &Vec, - ) -> Vec> - where T: GameView - { + ) -> Vec> { let mut questions = Vec::new(); let mut info_remaining = total_info; @@ -251,10 +260,21 @@ impl InformationPlayerStrategy { *info_remaining <= 1 } - let mut augmented_hand_info = hand_info.iter().enumerate().map(|(i, card_table)| { - let p = card_table.probability_is_playable(view.get_board()); - (p, card_table, i) - }).collect::>(); + let mut augmented_hand_info = hand_info.iter().enumerate() + .filter(|&(_, card_table)| { + if card_table.is_determined() { + false + } else if card_table.probability_is_dead(&view.board) == 1.0 { + false + } else { + true + } + }) + .map(|(i, card_table)| { + let p = card_table.probability_is_playable(&view.board); + (p, card_table, i) + }) + .collect::>(); // sort by probability of play, then by index augmented_hand_info.sort_by(|&(p1, _, i1), &(p2, _, i2)| { @@ -284,12 +304,6 @@ impl InformationPlayerStrategy { // } for &(_, card_table, i) in &augmented_hand_info { - if card_table.is_determined() { - continue; - } - if card_table.probability_is_dead(view.get_board()) == 1.0 { - continue; - } let question = CardPossibilityPartition::new(i, info_remaining, card_table, view); if add_question(&mut questions, &mut info_remaining, question) { return questions; @@ -406,28 +420,25 @@ 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) as f32 - }; + let f = |card: &Card| { self.get_play_score(view, card) }; card_table.weighted_score(&f) } - fn get_play_score(&self, view: &OwnedGameView, card: &Card) -> i32 { + fn get_play_score(&self, view: &OwnedGameView, card: &Card) -> f32 { + let mut num_with = 1; if view.board.deck_size() > 0 { for player in view.board.get_players() { if player != self.me { if view.has_card(&player, card) { - return 1; + num_with += 1; } } } } - 5 + (5 - (card.value as i32)) + (10.0 - card.value as f32) / (num_with as f32) } - fn find_useless_cards(&self, view: &T, hand: &Vec) -> Vec - where T: GameView - { + fn find_useless_cards(&self, view: &OwnedGameView, hand: &Vec) -> Vec { let mut useless: HashSet = HashSet::new(); let mut seen: HashMap = HashMap::new(); @@ -451,19 +462,6 @@ impl InformationPlayerStrategy { return useless_vec; } - fn someone_else_can_play(&self, view: &OwnedGameView) -> 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 take_public_info(&mut self, player: &Player) -> Vec { self.public_info.remove(player).unwrap() } @@ -476,6 +474,11 @@ impl InformationPlayerStrategy { self.get_player_public_info(&self.me) } + // fn get_my_public_info_mut(&mut self) -> &mut Vec { + // let me = self.me.clone(); + // self.get_player_public_info_mut(&me) + // } + fn get_player_public_info(&self, player: &Player) -> &Vec { self.public_info.get(player).unwrap() } @@ -521,8 +524,7 @@ impl InformationPlayerStrategy { } } - // TODO: decrement weight counts for fully determined cards, - // ahead of time + // 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 @@ -548,9 +550,7 @@ impl InformationPlayerStrategy { info } - fn get_hint_index_score(&self, card_table: &CardPossibilityTable, view: &T) -> i32 - where T: GameView - { + 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; } @@ -565,9 +565,7 @@ impl InformationPlayerStrategy { return score; } - fn get_index_for_hint(&self, info: &Vec, view: &T) -> usize - where T: GameView - { + fn get_index_for_hint(&self, info: &Vec, 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) @@ -680,7 +678,6 @@ impl PlayerStrategy for InformationPlayerStrategy { }).collect::>(); if playable_cards.len() > 0 { - // TODO: try playing things that have no chance of being indispensable // play the best playable card // the higher the play_score, the better to play let mut play_score = -1.0; @@ -697,8 +694,16 @@ impl PlayerStrategy for InformationPlayerStrategy { 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); + + // make a possibly risky play - if view.board.lives_remaining > 1 { + if view.board.lives_remaining > 1 && + view.board.discard_size() <= discard_threshold + { let mut risky_playable_cards = private_info.iter().enumerate().filter(|&(_, card_table)| { // card is either playable or dead card_table.probability_of_predicate(&|card| { @@ -711,21 +716,16 @@ impl PlayerStrategy for InformationPlayerStrategy { if risky_playable_cards.len() > 0 { risky_playable_cards.sort_by(|c1, c2| { - c1.2.partial_cmp(&c2.2).unwrap_or(Ordering::Equal) + c2.2.partial_cmp(&c1.2).unwrap_or(Ordering::Equal) }); let maybe_play = risky_playable_cards[0]; - if maybe_play.2 > 0.7 { + if maybe_play.2 > 0.75 { return TurnChoice::Play(maybe_play.0); } } } - let discard_threshold = - view.board.total_cards - - (COLORS.len() * VALUES.len()) as u32 - - (view.board.num_players * view.board.hand_size); - let public_useless_indices = self.find_useless_cards(view, &self.get_my_public_info()); let useless_indices = self.find_useless_cards(view, &private_info); @@ -742,16 +742,11 @@ impl PlayerStrategy for InformationPlayerStrategy { // 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) { + if view.someone_else_can_play() { return self.get_hint(); - } else { - // print!("This actually happens"); } } - // TODO: if they discarded a non-useless card, despite there being hints remaining - // infer that we have no playable cards - // 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); @@ -760,6 +755,10 @@ impl PlayerStrategy for InformationPlayerStrategy { return TurnChoice::Discard(useless_indices[0]); } + // NOTE: the only conditions under which we would discard a potentially useful card: + // - we have no known useless cards + // - there are no hints remaining OR nobody else can play + // Play the best discardable card let mut compval = 0.0; let mut index = 0; @@ -792,17 +791,20 @@ impl PlayerStrategy for InformationPlayerStrategy { } } TurnChoice::Discard(index) => { + let known_useless_indices = self.find_useless_cards( + &self.last_view, &self.get_player_public_info(&turn.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 let &TurnResult::Discard(ref card) = &turn.result { - let public_useless_indices = self.find_useless_cards( - &self.last_view, &self.get_player_public_info(&turn.player)); - if public_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 = public_useless_indices.iter().position(|&i| i == index).unwrap(); - self.update_from_hint_sum(ModulusInformation::new( - public_useless_indices.len() as u32, value as u32 - )); - } self.update_public_info_for_discard_or_play(view, &turn.player, index, card); } else { panic!("Got turn choice {:?}, but turn result {:?}",