various cleanups, fixes

This commit is contained in:
Jeff Wu 2016-04-01 00:14:13 -07:00
parent ec1fd2eb07
commit 7f5e32699e
7 changed files with 148 additions and 131 deletions

View File

@ -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
```

View File

@ -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<Value> {
pub fn needed_value(&self) -> Option<Value> {
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;

View File

@ -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,

View File

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

View File

@ -33,7 +33,7 @@ pub fn simulate_once(
opts: &GameOptions,
game_strategy: Box<GameStrategy>,
seed_opt: Option<u32>,
) -> 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<Score, u32>,
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<T: ?Sized>(
first_seed_opt: Option<u32>,
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<T: ?Sized>(
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<T: ?Sized>(
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());
})
}

View File

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

View File

@ -123,23 +123,23 @@ struct CardPossibilityPartition {
partition: HashMap<Card, u32>,
}
impl CardPossibilityPartition {
fn new<T>(
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<T>(
fn get_questions(
total_info: u32,
view: &T,
view: &OwnedGameView,
hand_info: &Vec<CardPossibilityTable>,
) -> Vec<Box<Question>>
where T: GameView
{
) -> Vec<Box<Question>> {
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::<Vec<_>>();
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::<Vec<_>>();
// 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<T>(&self, view: &T, hand: &Vec<CardPossibilityTable>) -> Vec<usize>
where T: GameView
{
fn find_useless_cards(&self, view: &OwnedGameView, hand: &Vec<CardPossibilityTable>) -> Vec<usize> {
let mut useless: HashSet<usize> = HashSet::new();
let mut seen: HashMap<Card, usize> = 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<CardPossibilityTable> {
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<CardPossibilityTable> {
// let me = self.me.clone();
// self.get_player_public_info_mut(&me)
// }
fn get_player_public_info(&self, player: &Player) -> &Vec<CardPossibilityTable> {
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<T>(&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<T>(&self, info: &Vec<CardPossibilityTable>, view: &T) -> usize
where T: GameView
{
fn get_index_for_hint(&self, info: &Vec<CardPossibilityTable>, 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::<Vec<_>>();
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 {:?}",