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), Or, if the simulation is slow (as the info strategy is),
``` ```
cargo build --release cargo run --release -- -n 10000 -s 0 -t 2 -p 5 -g info
time ./target/release/rust_hanabi -n 10000 -s 0 -t 2 -p 5 -g info
``` ```
## Results ## Results
Currently, on seeds 0-9999, we have: Currently, 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 |
info | 18.5959 | 23.8846 | 24.7753 | 24.8719 | 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 type Score = u32;
pub const PERFECT_SCORE: Score = 25;
#[derive(Debug,Clone)] #[derive(Debug,Clone)]
pub struct Firework { 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) } if self.complete() { None } else { Some(self.top + 1) }
} }
@ -157,7 +158,7 @@ impl Firework {
"Attempted to place card on firework of wrong color!" "Attempted to place card on firework of wrong color!"
); );
assert!( assert!(
Some(card.value) == self.desired_value(), Some(card.value) == self.needed_value(),
"Attempted to place card of wrong value on firework!" "Attempted to place card of wrong value on firework!"
); );
self.top = card.value; self.top = card.value;

View File

@ -223,7 +223,7 @@ impl BoardState {
// returns whether a card would place on a firework // returns whether a card would place on a firework
pub fn is_playable(&self, card: &Card) -> bool { 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, // best possible value we can get for firework of that color,
@ -233,10 +233,10 @@ impl BoardState {
if firework.complete() { if firework.complete() {
return FINAL_VALUE; return FINAL_VALUE;
} }
let desired = firework.desired_value().unwrap(); let needed = firework.needed_value().unwrap();
for &value in VALUES.iter() { for &value in VALUES.iter() {
if value < desired { if value < needed {
// already have these cards // already have these cards
continue continue
} }
@ -255,8 +255,8 @@ impl BoardState {
if firework.complete() { if firework.complete() {
true true
} else { } else {
let desired = firework.desired_value().unwrap(); let needed = firework.needed_value().unwrap();
if card.value < desired { if card.value < needed {
true true
} else { } else {
card.value > self.highest_attainable(card.color) card.value > self.highest_attainable(card.color)
@ -270,8 +270,8 @@ impl BoardState {
if firework.complete() { if firework.complete() {
true true
} else { } else {
let desired = firework.desired_value().unwrap(); let needed = firework.needed_value().unwrap();
if card.value < desired { if card.value < needed {
true true
} else { } else {
if card.value > self.highest_attainable(card.color) { if card.value > self.highest_attainable(card.color) {
@ -392,6 +392,19 @@ pub trait GameView {
} }
false 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, // 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) { fn print_usage(program: &str, opts: Options) {
print!("{}", opts.usage(&format!("Usage: {} [options]", program))); 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, opts: &GameOptions,
game_strategy: Box<GameStrategy>, game_strategy: Box<GameStrategy>,
seed_opt: Option<u32>, seed_opt: Option<u32>,
) -> Score { ) -> GameState {
let seed = seed_opt.unwrap_or(rand::thread_rng().next_u32()); let seed = seed_opt.unwrap_or(rand::thread_rng().next_u32());
@ -73,11 +73,10 @@ pub fn simulate_once(
debug!(""); debug!("");
debug!("======================================================="); debug!("=======================================================");
debug!("Final state:\n{}", game); debug!("Final state:\n{}", game);
let score = game.score(); game
debug!("SCORED: {:?}", score);
score
} }
#[derive(Debug)]
struct Histogram { struct Histogram {
pub hist: HashMap<Score, u32>, pub hist: HashMap<Score, u32>,
pub sum: Score, pub sum: Score,
@ -103,6 +102,9 @@ impl Histogram {
pub fn get_count(&self, val: &Score) -> u32 { pub fn get_count(&self, val: &Score) -> u32 {
*self.hist.get(&val).unwrap_or(&0) *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 { pub fn average(&self) -> f32 {
(self.sum as f32) / (self.total_count as f32) (self.sum as f32) / (self.total_count as f32)
} }
@ -131,7 +133,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,
) -> f32 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());
@ -145,33 +147,40 @@ pub fn simulate<T: ?Sized>(
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 histogram = Histogram::new(); let mut score_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 (seed > start) && ((seed-start) % 1000 == 0) {
info!( info!(
"Thread {}, Trials: {}, Average so far: {}", "Thread {}, Trials: {}, Stats so far: {} score, {} lives, {}% win",
i, seed-start, histogram.average() 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)); let game = simulate_once(&opts, strat_config_ref.initialize(&opts), Some(seed));
histogram.insert(score); let score = game.score();
if score != 25 { non_perfect_seeds.push((score, seed)); } 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); 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 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 { 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()); 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(); non_perfect_seeds.sort();
// info!("Seeds with non-perfect score: {:?}", non_perfect_seeds); // 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); 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: {:?}%", score_histogram.percentage_with(&PERFECT_SCORE) * 100.0);
info!("Percentage perfect: {:?}%", percentage * 100.0); info!("Average score: {:?}", score_histogram.average());
let average = histogram.average(); info!("Average lives: {:?}", lives_histogram.average());
info!("Average score: {:?}", average);
average
}) })
} }

View File

@ -134,19 +134,6 @@ impl CheatingPlayerStrategy {
} }
return None 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 { impl PlayerStrategy for CheatingPlayerStrategy {
fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice { fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice {
@ -197,7 +184,7 @@ impl PlayerStrategy for CheatingPlayerStrategy {
// hinting is better than discarding dead cards // hinting is better than discarding dead cards
// (probably because it stalls the deck-drawing). // (probably because it stalls the deck-drawing).
if view.board.hints_remaining > 0 { if view.board.hints_remaining > 0 {
if self.someone_else_can_play(view) { if view.someone_else_can_play() {
return self.throwaway_hint(view); return self.throwaway_hint(view);
} }
} }

View File

@ -123,23 +123,23 @@ struct CardPossibilityPartition {
partition: HashMap<Card, u32>, partition: HashMap<Card, u32>,
} }
impl CardPossibilityPartition { impl CardPossibilityPartition {
fn new<T>( fn new(
index: usize, max_n_partitions: u32, card_table: &CardPossibilityTable, view: &T index: usize, max_n_partitions: u32, card_table: &CardPossibilityTable, view: &OwnedGameView
) -> CardPossibilityPartition where T: GameView { ) -> CardPossibilityPartition {
let mut cur_block = 0; let mut cur_block = 0;
let mut partition = HashMap::new(); let mut partition = HashMap::new();
let mut n_partitions = 0; 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? // TODO: group things of different colors and values?
let effective_max = if has_dead { let mut effective_max = max_n_partitions;
max_n_partitions - 1 if has_dead {
} else { effective_max -= 1;
max_n_partitions
}; };
for card in card_table.get_possibilities() { 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); partition.insert(card.clone(), cur_block);
cur_block = (cur_block + 1) % effective_max; cur_block = (cur_block + 1) % effective_max;
if n_partitions < effective_max { if n_partitions < effective_max {
@ -150,13 +150,24 @@ impl CardPossibilityPartition {
if has_dead { if has_dead {
for card in card_table.get_possibilities() { 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); partition.insert(card.clone(), n_partitions);
} }
} }
n_partitions += 1; 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 { CardPossibilityPartition {
index: index, index: index,
n_partitions: n_partitions, n_partitions: n_partitions,
@ -231,13 +242,11 @@ pub struct InformationPlayerStrategy {
} }
impl InformationPlayerStrategy { impl InformationPlayerStrategy {
fn get_questions<T>( fn get_questions(
total_info: u32, total_info: u32,
view: &T, view: &OwnedGameView,
hand_info: &Vec<CardPossibilityTable>, hand_info: &Vec<CardPossibilityTable>,
) -> Vec<Box<Question>> ) -> Vec<Box<Question>> {
where T: GameView
{
let mut questions = Vec::new(); let mut questions = Vec::new();
let mut info_remaining = total_info; let mut info_remaining = total_info;
@ -251,10 +260,21 @@ impl InformationPlayerStrategy {
*info_remaining <= 1 *info_remaining <= 1
} }
let mut augmented_hand_info = hand_info.iter().enumerate().map(|(i, card_table)| { let mut augmented_hand_info = hand_info.iter().enumerate()
let p = card_table.probability_is_playable(view.get_board()); .filter(|&(_, card_table)| {
(p, card_table, i) if card_table.is_determined() {
}).collect::<Vec<_>>(); 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 // sort by probability of play, then by index
augmented_hand_info.sort_by(|&(p1, _, i1), &(p2, _, i2)| { augmented_hand_info.sort_by(|&(p1, _, i1), &(p2, _, i2)| {
@ -284,12 +304,6 @@ impl InformationPlayerStrategy {
// } // }
for &(_, card_table, i) in &augmented_hand_info { 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); let question = CardPossibilityPartition::new(i, info_remaining, card_table, view);
if add_question(&mut questions, &mut info_remaining, question) { if add_question(&mut questions, &mut info_remaining, question) {
return questions; return questions;
@ -406,28 +420,25 @@ impl InformationPlayerStrategy {
// how badly do we need to play a particular card // how badly do we need to play a particular card
fn get_average_play_score(&self, view: &OwnedGameView, card_table: &CardPossibilityTable) -> f32 { fn get_average_play_score(&self, view: &OwnedGameView, card_table: &CardPossibilityTable) -> f32 {
let f = |card: &Card| { let f = |card: &Card| { self.get_play_score(view, card) };
self.get_play_score(view, card) as f32
};
card_table.weighted_score(&f) 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 { if view.board.deck_size() > 0 {
for player in view.board.get_players() { for player in view.board.get_players() {
if player != self.me { if player != self.me {
if view.has_card(&player, card) { 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> fn find_useless_cards(&self, view: &OwnedGameView, hand: &Vec<CardPossibilityTable>) -> Vec<usize> {
where T: GameView
{
let mut useless: HashSet<usize> = HashSet::new(); let mut useless: HashSet<usize> = HashSet::new();
let mut seen: HashMap<Card, usize> = HashMap::new(); let mut seen: HashMap<Card, usize> = HashMap::new();
@ -451,19 +462,6 @@ impl InformationPlayerStrategy {
return useless_vec; 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> { fn take_public_info(&mut self, player: &Player) -> Vec<CardPossibilityTable> {
self.public_info.remove(player).unwrap() self.public_info.remove(player).unwrap()
} }
@ -476,6 +474,11 @@ impl InformationPlayerStrategy {
self.get_player_public_info(&self.me) 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> { fn get_player_public_info(&self, player: &Player) -> &Vec<CardPossibilityTable> {
self.public_info.get(player).unwrap() self.public_info.get(player).unwrap()
} }
@ -521,8 +524,7 @@ impl InformationPlayerStrategy {
} }
} }
// TODO: decrement weight counts for fully determined cards, // TODO: decrement weight counts for fully determined cards, ahead of time
// ahead of time
// note: other_player could be player, as well // note: other_player could be player, as well
// in particular, we will decrement the newly drawn card // in particular, we will decrement the newly drawn card
@ -548,9 +550,7 @@ impl InformationPlayerStrategy {
info info
} }
fn get_hint_index_score<T>(&self, card_table: &CardPossibilityTable, view: &T) -> i32 fn get_hint_index_score(&self, card_table: &CardPossibilityTable, view: &OwnedGameView) -> i32 {
where T: GameView
{
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;
} }
@ -565,9 +565,7 @@ impl InformationPlayerStrategy {
return score; return score;
} }
fn get_index_for_hint<T>(&self, info: &Vec<CardPossibilityTable>, view: &T) -> usize fn get_index_for_hint(&self, info: &Vec<CardPossibilityTable>, view: &OwnedGameView) -> usize {
where T: GameView
{
let mut scores = info.iter().enumerate().map(|(i, card_table)| { let mut scores = info.iter().enumerate().map(|(i, card_table)| {
let score = self.get_hint_index_score(card_table, view); let score = self.get_hint_index_score(card_table, view);
(-score, i) (-score, i)
@ -680,7 +678,6 @@ impl PlayerStrategy for InformationPlayerStrategy {
}).collect::<Vec<_>>(); }).collect::<Vec<_>>();
if playable_cards.len() > 0 { if playable_cards.len() > 0 {
// TODO: try playing things that have no chance of being indispensable
// play the best playable card // play the best playable card
// the higher the play_score, the better to play // the higher the play_score, the better to play
let mut play_score = -1.0; let mut play_score = -1.0;
@ -697,8 +694,16 @@ impl PlayerStrategy for InformationPlayerStrategy {
return TurnChoice::Play(play_index) 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 // 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)| { let mut risky_playable_cards = private_info.iter().enumerate().filter(|&(_, card_table)| {
// card is either playable or dead // card is either playable or dead
card_table.probability_of_predicate(&|card| { card_table.probability_of_predicate(&|card| {
@ -711,21 +716,16 @@ impl PlayerStrategy for InformationPlayerStrategy {
if risky_playable_cards.len() > 0 { if risky_playable_cards.len() > 0 {
risky_playable_cards.sort_by(|c1, c2| { 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]; 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); 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 public_useless_indices = self.find_useless_cards(view, &self.get_my_public_info());
let useless_indices = self.find_useless_cards(view, &private_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 // hinting is better than discarding dead cards
// (probably because it stalls the deck-drawing). // (probably because it stalls the deck-drawing).
if view.board.hints_remaining > 0 { if view.board.hints_remaining > 0 {
if self.someone_else_can_play(view) { if view.someone_else_can_play() {
return self.get_hint(); 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 anything is totally useless, discard it
if public_useless_indices.len() > 1 { if public_useless_indices.len() > 1 {
let info = self.get_hint_sum_info(public_useless_indices.len() as u32, view); 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]); 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 // Play the best discardable card
let mut compval = 0.0; let mut compval = 0.0;
let mut index = 0; let mut index = 0;
@ -792,17 +791,20 @@ impl PlayerStrategy for InformationPlayerStrategy {
} }
} }
TurnChoice::Discard(index) => { 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 { 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); self.update_public_info_for_discard_or_play(view, &turn.player, index, card);
} else { } else {
panic!("Got turn choice {:?}, but turn result {:?}", panic!("Got turn choice {:?}, but turn result {:?}",