From 81427e2dd587566e91b0e9576d499b2b01effe16 Mon Sep 17 00:00:00 2001 From: Jeff Wu Date: Fri, 1 Apr 2016 02:08:46 -0700 Subject: [PATCH] smart hinting, silencing/configuring of progress output --- .gitignore | 2 +- README.md | 27 ++++++---- src/game.rs | 2 +- src/info.rs | 17 +++--- src/main.rs | 7 ++- src/simulator.rs | 27 ++++++---- src/strategies/information.rs | 97 +++++++++++++++++++++++++++++------ 7 files changed, 134 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index c6262ea..7d0d0d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ target -*.swp +*.sw* diff --git a/README.md b/README.md index 684b0fb..e452100 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Some similar projects I am aware of: ## Setup -Install rust/rustc and cargo, and change the options in main.rs appropriately. +Install rust/rustc and cargo. Then, `cargo run -- -h` @@ -48,30 +48,37 @@ Options: For example, ``` -cargo run -- -n 10000 -s 0 -t 2 -p 5 -g cheat +cargo run -- -n 10000 -s 0 -p 5 -g cheat ``` Or, if the simulation is slow (as the info strategy is), ``` -cargo run --release -- -n 10000 -s 0 -t 2 -p 5 -g info +time cargo run --release -- -n 10000 -o 1000 -s 0 -t 4 -p 5 -g info +``` + +Or, to see a transcript of a single game: +``` +cargo run -- -s 2222 -p 5 -g info -l debug | less ``` ## Results -Currently, on seeds 0-9999, we have: +On seeds 0-9999, we have: + + | 2p | 3p | 4p | 5p | +----------|---------|---------|---------|---------| +cheating | 24.8600 | 24.9781 | 24.9715 | 24.9583 | +info | 18.5909 | 24.1655 | 24.7922 | 24.8784 | - | 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 +n=10000 # number of rounds to simulate +t=4 # number of threads 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; + time cargo run --release -- -n $n -s 0 -t $t -p $p -g $strategy; done done ``` diff --git a/src/game.rs b/src/game.rs index 35b0d39..9e50609 100644 --- a/src/game.rs +++ b/src/game.rs @@ -9,7 +9,7 @@ pub use cards::*; pub type Player = u32; -#[derive(Debug,Clone)] +#[derive(Debug,Clone,Hash,PartialEq,Eq)] pub enum Hinted { Color(Color), Value(Value), diff --git a/src/info.rs b/src/info.rs index 70379f8..6675bbd 100644 --- a/src/info.rs +++ b/src/info.rs @@ -34,12 +34,17 @@ pub trait CardInfo { } 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); - v.push((card, weight)); - } - v + self.get_possibilities().into_iter() + .map(|card| { + let weight = self.get_weight(&card); + (card, weight) + }).collect::>() + } + + fn total_weight(&self) -> f32 { + self.get_possibilities().iter() + .map(|card| self.get_weight(&card)) + .fold(0.0, |a, b| a+b) } fn weighted_score(&self, score_fn: &Fn(&Card) -> T) -> f32 diff --git a/src/main.rs b/src/main.rs index fa11e5d..c2daf1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,6 +47,9 @@ fn main() { opts.optopt("n", "ntrials", "Number of games to simulate (default 1)", "NTRIALS"); + opts.optopt("o", "output", + "Number of games after which to print an update", + "OUTPUT_FREQ"); opts.optopt("t", "nthreads", "Number of threads to use for simulation (default 1)", "NTHREADS"); @@ -97,6 +100,8 @@ fn main() { let seed = matches.opt_str("s").map(|seed_str| { u32::from_str(&seed_str).unwrap() }); + let progress_info = matches.opt_str("o").map(|freq_str| { u32::from_str(&freq_str).unwrap() }); + let n_threads = u32::from_str(&matches.opt_str("t").unwrap_or("1".to_string())).unwrap(); let n_players = u32::from_str(&matches.opt_str("p").unwrap_or("4".to_string())).unwrap(); @@ -138,5 +143,5 @@ fn main() { panic!("Unexpected strategy argument {}", strategy_str); }, }; - simulator::simulate(&game_opts, strategy_config, seed, n, n_threads); + simulator::simulate(&game_opts, strategy_config, seed, n, n_threads, progress_info); } diff --git a/src/simulator.rs b/src/simulator.rs index a99044c..5206eaf 100644 --- a/src/simulator.rs +++ b/src/simulator.rs @@ -73,6 +73,7 @@ pub fn simulate_once( debug!(""); debug!("======================================================="); debug!("Final state:\n{}", game); + debug!("SCORE: {:?}", game.score()); game } @@ -120,7 +121,7 @@ impl fmt::Display for Histogram { keys.sort(); for val in keys { try!(f.write_str(&format!( - "{}: {}\n", val, self.get_count(val), + "\n{}: {}", val, self.get_count(val), ))); } Ok(()) @@ -133,6 +134,7 @@ pub fn simulate( first_seed_opt: Option, n_trials: u32, n_threads: u32, + progress_info: Option, ) where T: GameStrategyConfig + Sync { let first_seed = first_seed_opt.unwrap_or(rand::thread_rng().next_u32()); @@ -144,28 +146,33 @@ pub fn simulate( let start = first_seed + ((n_trials * i) / n_threads); let end = first_seed + ((n_trials * (i+1)) / n_threads); join_handles.push(scope.spawn(move || { - info!("Thread {} spawned: seeds {} to {}", i, start, end); + if progress_info.is_some() { + info!("Thread {} spawned: seeds {} to {}", i, start, end); + } let mut non_perfect_seeds = Vec::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: {}, Stats so far: {} score, {} lives, {}% win", - i, seed-start, score_histogram.average(), lives_histogram.average(), - score_histogram.percentage_with(&PERFECT_SCORE) * 100.0 - ); + if let Some(progress_info_frequency) = progress_info { + if (seed > start) && ((seed-start) % progress_info_frequency == 0) { + info!( + "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 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); + if progress_info.is_some() { + info!("Thread {} done", i); + } (non_perfect_seeds, score_histogram, lives_histogram) })); } diff --git a/src/strategies/information.rs b/src/strategies/information.rs index 8338e92..b480423 100644 --- a/src/strategies/information.rs +++ b/src/strategies/information.rs @@ -262,9 +262,9 @@ impl InformationPlayerStrategy { let mut augmented_hand_info = hand_info.iter().enumerate() .filter(|&(_, card_table)| { - if card_table.is_determined() { + if card_table.probability_is_dead(&view.board) == 1.0 { false - } else if card_table.probability_is_dead(&view.board) == 1.0 { + } else if card_table.is_determined() { false } else { true @@ -372,10 +372,6 @@ impl InformationPlayerStrategy { question.acknowledge_answer_info(answer_info, &mut hand_info, view); } } - debug!("Current state of hand_info for {}:", me); - for (i, card_table) in hand_info.iter().enumerate() { - debug!(" Card {}: {}", i, card_table); - } self.return_public_info(&me, hand_info); } @@ -554,6 +550,9 @@ impl InformationPlayerStrategy { if card_table.probability_is_dead(view.get_board()) == 1.0 { return 0; } + if card_table.is_determined() { + return 0; + } // Do something more intelligent? let mut score = 1; if !card_table.color_determined() { @@ -574,6 +573,53 @@ impl InformationPlayerStrategy { scores[0].1 } + // how good is it to give this hint to this player? + fn hint_goodness(&self, hinted: &Hinted, hint_player: &Player, view: &OwnedGameView) -> f32 { + let hand = view.get_hand(&hint_player); + + // get post-hint hand_info + let mut hand_info = self.get_player_public_info(hint_player).clone(); + let total_info = 3 * (view.board.num_players - 1); + let questions = Self::get_questions(total_info, view, &hand_info); + for question in questions { + let answer = question.answer(hand, view); + question.acknowledge_answer(answer, &mut hand_info, view); + } + + let mut goodness = 1.0; + for (i, card_table) in hand_info.iter_mut().enumerate() { + let card = &hand[i]; + if card_table.probability_is_dead(&view.board) == 1.0 { + continue; + } + if card_table.is_determined() { + continue; + } + let old_weight = card_table.total_weight(); + match *hinted { + Hinted::Color(color) => { + card_table.mark_color(color, color == card.color) + } + Hinted::Value(value) => { + card_table.mark_value(value, value == card.value) + } + }; + let new_weight = card_table.total_weight(); + assert!(new_weight <= old_weight); + let bonus = { + if card_table.is_determined() { + 2 + } else if card_table.probability_is_dead(&view.board) == 1.0 { + 2 + } else { + 1 + } + }; + goodness *= (bonus as f32) * (old_weight / new_weight); + } + goodness + } + fn get_hint(&self) -> TurnChoice { let view = &self.last_view; @@ -608,22 +654,34 @@ impl InformationPlayerStrategy { Hinted::Color(hint_card.color) } 2 => { - let mut hinted_opt = None; + // NOTE: this doesn't do that much better than just hinting + // the first thing that doesn't match the hint_card + let mut hint_option_set = HashSet::new(); for card in hand { if card.color != hint_card.color { - hinted_opt = Some(Hinted::Color(card.color)); - break; + hint_option_set.insert(Hinted::Color(card.color)); } if card.value != hint_card.value { - hinted_opt = Some(Hinted::Value(card.value)); - break; + hint_option_set.insert(Hinted::Value(card.value)); } } - if let Some(hinted) = hinted_opt { - hinted + // using hint goodness barely helps + let mut hint_options = hint_option_set.into_iter().map(|hinted| { + (self.hint_goodness(&hinted, &hint_player, view), hinted) + }).collect::>(); + + hint_options.sort_by(|h1, h2| { + h2.0.partial_cmp(&h1.0).unwrap_or(Ordering::Equal) + }); + + if hint_options.len() == 0 { + // NOTE: Technically possible, but never happens + Hinted::Color(hint_card.color) } else { - // TODO: Technically possible, but never happens - panic!("Found nothing to hint!") + if hint_options.len() > 1 { + debug!("Choosing amongst hint options: {:?}", hint_options); + } + hint_options.remove(0).1 } } _ => { @@ -667,6 +725,14 @@ impl PlayerStrategy for InformationPlayerStrategy { // we already stored the view let view = &self.last_view; + for player in view.board.get_players().iter() { + let hand_info = self.get_player_public_info(player); + debug!("Current state of hand_info for {}:", player); + for (i, card_table) in hand_info.iter().enumerate() { + debug!(" Card {}: {}", i, card_table); + } + } + let private_info = self.get_private_info(view); // debug!("My info:"); // for (i, card_table) in private_info.iter().enumerate() { @@ -699,7 +765,6 @@ impl PlayerStrategy for InformationPlayerStrategy { - (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 && view.board.discard_size() <= discard_threshold