smart hinting, silencing/configuring of progress output
This commit is contained in:
parent
7f5e32699e
commit
81427e2dd5
7 changed files with 134 additions and 45 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
||||||
target
|
target
|
||||||
*.swp
|
*.sw*
|
||||||
|
|
23
README.md
23
README.md
|
@ -21,7 +21,7 @@ Some similar projects I am aware of:
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Install rust/rustc and cargo, and change the options in main.rs appropriately.
|
Install rust/rustc and cargo. Then,
|
||||||
|
|
||||||
`cargo run -- -h`
|
`cargo run -- -h`
|
||||||
|
|
||||||
|
@ -48,30 +48,37 @@ Options:
|
||||||
For example,
|
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),
|
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
|
## Results
|
||||||
|
|
||||||
Currently, on seeds 0-9999, we have:
|
On seeds 0-9999, we have:
|
||||||
|
|
||||||
| 2p | 3p | 4p | 5p |
|
| 2p | 3p | 4p | 5p |
|
||||||
------------|---------|---------|---------|---------|
|
----------|---------|---------|---------|---------|
|
||||||
cheating | 24.8600 | 24.9781 | 24.9715 | 24.9583 |
|
cheating | 24.8600 | 24.9781 | 24.9715 | 24.9583 |
|
||||||
information | 18.5726 | 23.8806 | 24.7722 | 24.8756 |
|
info | 18.5909 | 24.1655 | 24.7922 | 24.8784 |
|
||||||
|
|
||||||
|
|
||||||
To reproduce:
|
To reproduce:
|
||||||
```
|
```
|
||||||
n=1000000
|
n=10000 # number of rounds to simulate
|
||||||
|
t=4 # number of threads
|
||||||
for strategy in info cheat; do
|
for strategy in info cheat; do
|
||||||
for p in $(seq 2 5); 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
|
||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
|
@ -9,7 +9,7 @@ pub use cards::*;
|
||||||
|
|
||||||
pub type Player = u32;
|
pub type Player = u32;
|
||||||
|
|
||||||
#[derive(Debug,Clone)]
|
#[derive(Debug,Clone,Hash,PartialEq,Eq)]
|
||||||
pub enum Hinted {
|
pub enum Hinted {
|
||||||
Color(Color),
|
Color(Color),
|
||||||
Value(Value),
|
Value(Value),
|
||||||
|
|
13
src/info.rs
13
src/info.rs
|
@ -34,12 +34,17 @@ pub trait CardInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_weighted_possibilities(&self) -> Vec<(Card, f32)> {
|
fn get_weighted_possibilities(&self) -> Vec<(Card, f32)> {
|
||||||
let mut v = Vec::new();
|
self.get_possibilities().into_iter()
|
||||||
for card in self.get_possibilities() {
|
.map(|card| {
|
||||||
let weight = self.get_weight(&card);
|
let weight = self.get_weight(&card);
|
||||||
v.push((card, weight));
|
(card, weight)
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
v
|
|
||||||
|
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<T>(&self, score_fn: &Fn(&Card) -> T) -> f32
|
fn weighted_score<T>(&self, score_fn: &Fn(&Card) -> T) -> f32
|
||||||
|
|
|
@ -47,6 +47,9 @@ fn main() {
|
||||||
opts.optopt("n", "ntrials",
|
opts.optopt("n", "ntrials",
|
||||||
"Number of games to simulate (default 1)",
|
"Number of games to simulate (default 1)",
|
||||||
"NTRIALS");
|
"NTRIALS");
|
||||||
|
opts.optopt("o", "output",
|
||||||
|
"Number of games after which to print an update",
|
||||||
|
"OUTPUT_FREQ");
|
||||||
opts.optopt("t", "nthreads",
|
opts.optopt("t", "nthreads",
|
||||||
"Number of threads to use for simulation (default 1)",
|
"Number of threads to use for simulation (default 1)",
|
||||||
"NTHREADS");
|
"NTHREADS");
|
||||||
|
@ -97,6 +100,8 @@ fn main() {
|
||||||
|
|
||||||
let seed = matches.opt_str("s").map(|seed_str| { u32::from_str(&seed_str).unwrap() });
|
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_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();
|
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);
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,7 @@ pub fn simulate_once(
|
||||||
debug!("");
|
debug!("");
|
||||||
debug!("=======================================================");
|
debug!("=======================================================");
|
||||||
debug!("Final state:\n{}", game);
|
debug!("Final state:\n{}", game);
|
||||||
|
debug!("SCORE: {:?}", game.score());
|
||||||
game
|
game
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +121,7 @@ impl fmt::Display for Histogram {
|
||||||
keys.sort();
|
keys.sort();
|
||||||
for val in keys {
|
for val in keys {
|
||||||
try!(f.write_str(&format!(
|
try!(f.write_str(&format!(
|
||||||
"{}: {}\n", val, self.get_count(val),
|
"\n{}: {}", val, self.get_count(val),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -133,6 +134,7 @@ pub fn simulate<T: ?Sized>(
|
||||||
first_seed_opt: Option<u32>,
|
first_seed_opt: Option<u32>,
|
||||||
n_trials: u32,
|
n_trials: u32,
|
||||||
n_threads: u32,
|
n_threads: u32,
|
||||||
|
progress_info: Option<u32>,
|
||||||
) where T: GameStrategyConfig + Sync {
|
) where T: GameStrategyConfig + Sync {
|
||||||
|
|
||||||
let first_seed = first_seed_opt.unwrap_or(rand::thread_rng().next_u32());
|
let first_seed = first_seed_opt.unwrap_or(rand::thread_rng().next_u32());
|
||||||
|
@ -144,28 +146,33 @@ pub fn simulate<T: ?Sized>(
|
||||||
let start = first_seed + ((n_trials * i) / n_threads);
|
let start = first_seed + ((n_trials * i) / n_threads);
|
||||||
let end = first_seed + ((n_trials * (i+1)) / n_threads);
|
let end = first_seed + ((n_trials * (i+1)) / n_threads);
|
||||||
join_handles.push(scope.spawn(move || {
|
join_handles.push(scope.spawn(move || {
|
||||||
|
if progress_info.is_some() {
|
||||||
info!("Thread {} spawned: seeds {} to {}", i, start, end);
|
info!("Thread {} spawned: seeds {} to {}", i, start, end);
|
||||||
|
}
|
||||||
let mut non_perfect_seeds = Vec::new();
|
let mut non_perfect_seeds = Vec::new();
|
||||||
|
|
||||||
let mut score_histogram = Histogram::new();
|
let mut score_histogram = Histogram::new();
|
||||||
let mut lives_histogram = Histogram::new();
|
let mut lives_histogram = Histogram::new();
|
||||||
|
|
||||||
for seed in start..end {
|
for seed in start..end {
|
||||||
if (seed > start) && ((seed-start) % 1000 == 0) {
|
if let Some(progress_info_frequency) = progress_info {
|
||||||
|
if (seed > start) && ((seed-start) % progress_info_frequency == 0) {
|
||||||
info!(
|
info!(
|
||||||
"Thread {}, Trials: {}, Stats so far: {} score, {} lives, {}% win",
|
"Thread {}, Trials: {}, Stats so far: {} score, {} lives, {}% win",
|
||||||
i, seed-start, score_histogram.average(), lives_histogram.average(),
|
i, seed-start, score_histogram.average(), lives_histogram.average(),
|
||||||
score_histogram.percentage_with(&PERFECT_SCORE) * 100.0
|
score_histogram.percentage_with(&PERFECT_SCORE) * 100.0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let game = simulate_once(&opts, strat_config_ref.initialize(&opts), Some(seed));
|
let game = simulate_once(&opts, strat_config_ref.initialize(&opts), Some(seed));
|
||||||
let score = game.score();
|
let score = game.score();
|
||||||
debug!("SCORED: {:?}", score);
|
|
||||||
lives_histogram.insert(game.board.lives_remaining);
|
lives_histogram.insert(game.board.lives_remaining);
|
||||||
score_histogram.insert(score);
|
score_histogram.insert(score);
|
||||||
if score != PERFECT_SCORE { non_perfect_seeds.push((score, seed)); }
|
if score != PERFECT_SCORE { non_perfect_seeds.push((score, seed)); }
|
||||||
}
|
}
|
||||||
|
if progress_info.is_some() {
|
||||||
info!("Thread {} done", i);
|
info!("Thread {} done", i);
|
||||||
|
}
|
||||||
(non_perfect_seeds, score_histogram, lives_histogram)
|
(non_perfect_seeds, score_histogram, lives_histogram)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -262,9 +262,9 @@ impl InformationPlayerStrategy {
|
||||||
|
|
||||||
let mut augmented_hand_info = hand_info.iter().enumerate()
|
let mut augmented_hand_info = hand_info.iter().enumerate()
|
||||||
.filter(|&(_, card_table)| {
|
.filter(|&(_, card_table)| {
|
||||||
if card_table.is_determined() {
|
if card_table.probability_is_dead(&view.board) == 1.0 {
|
||||||
false
|
false
|
||||||
} else if card_table.probability_is_dead(&view.board) == 1.0 {
|
} else if card_table.is_determined() {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
|
@ -372,10 +372,6 @@ impl InformationPlayerStrategy {
|
||||||
question.acknowledge_answer_info(answer_info, &mut hand_info, view);
|
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);
|
self.return_public_info(&me, hand_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -554,6 +550,9 @@ impl InformationPlayerStrategy {
|
||||||
if card_table.probability_is_dead(view.get_board()) == 1.0 {
|
if card_table.probability_is_dead(view.get_board()) == 1.0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
if card_table.is_determined() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
// Do something more intelligent?
|
// Do something more intelligent?
|
||||||
let mut score = 1;
|
let mut score = 1;
|
||||||
if !card_table.color_determined() {
|
if !card_table.color_determined() {
|
||||||
|
@ -574,6 +573,53 @@ impl InformationPlayerStrategy {
|
||||||
scores[0].1
|
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 {
|
fn get_hint(&self) -> TurnChoice {
|
||||||
let view = &self.last_view;
|
let view = &self.last_view;
|
||||||
|
|
||||||
|
@ -608,22 +654,34 @@ impl InformationPlayerStrategy {
|
||||||
Hinted::Color(hint_card.color)
|
Hinted::Color(hint_card.color)
|
||||||
}
|
}
|
||||||
2 => {
|
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 {
|
for card in hand {
|
||||||
if card.color != hint_card.color {
|
if card.color != hint_card.color {
|
||||||
hinted_opt = Some(Hinted::Color(card.color));
|
hint_option_set.insert(Hinted::Color(card.color));
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
if card.value != hint_card.value {
|
if card.value != hint_card.value {
|
||||||
hinted_opt = Some(Hinted::Value(card.value));
|
hint_option_set.insert(Hinted::Value(card.value));
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(hinted) = hinted_opt {
|
// using hint goodness barely helps
|
||||||
hinted
|
let mut hint_options = hint_option_set.into_iter().map(|hinted| {
|
||||||
|
(self.hint_goodness(&hinted, &hint_player, view), hinted)
|
||||||
|
}).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
// TODO: Technically possible, but never happens
|
if hint_options.len() > 1 {
|
||||||
panic!("Found nothing to hint!")
|
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
|
// we already stored the view
|
||||||
let view = &self.last_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);
|
let private_info = self.get_private_info(view);
|
||||||
// debug!("My info:");
|
// debug!("My info:");
|
||||||
// for (i, card_table) in private_info.iter().enumerate() {
|
// for (i, card_table) in private_info.iter().enumerate() {
|
||||||
|
@ -699,7 +765,6 @@ impl PlayerStrategy for InformationPlayerStrategy {
|
||||||
- (COLORS.len() * VALUES.len()) as u32
|
- (COLORS.len() * VALUES.len()) as u32
|
||||||
- (view.board.num_players * view.board.hand_size);
|
- (view.board.num_players * view.board.hand_size);
|
||||||
|
|
||||||
|
|
||||||
// make a possibly risky play
|
// make a possibly risky play
|
||||||
if view.board.lives_remaining > 1 &&
|
if view.board.lives_remaining > 1 &&
|
||||||
view.board.discard_size() <= discard_threshold
|
view.board.discard_size() <= discard_threshold
|
||||||
|
|
Loading…
Reference in a new issue