From 385feeb6ba4a556d08650b2f41036e0ec6fce92a Mon Sep 17 00:00:00 2001 From: Felix Bauckholt Date: Wed, 20 Feb 2019 15:17:43 +0100 Subject: [PATCH] Slight tweaks that increase 3-player win rate to 75% - When there are less than 5 players, and we're near the discard threshold, prefer hinting over discarding, even if there are known useless cards. - We now ask questions like "what's the first playable card in this list?" This means that if a playable card is in the asking player's list, the player will learn that it's playable, and that every card before it is not playable. Additionally, if a player doesn't know of any dead cards in their hand and there is enough information available, we use this mechanism so that if the player doesn't have a playable card, they will learn about one dead card in their hand. (These were two commits that got joined in a rebase accident, sorry.) --- README.md | 4 +- src/strategies/information.rs | 128 +++++++++++++++++++++++++++++++--- 2 files changed, 120 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a5450fb..05f0b25 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,8 @@ On seeds 0-9999, we have these average scores and win rates: |-------|---------|---------|---------|---------| |cheat | 24.8600 | 24.9781 | 24.9715 | 24.9570 | | | 90.52 % | 98.12 % | 97.74 % | 96.57 % | -|info | 18.6956 | 24.2234 | 24.8335 | 24.8928 | -| | 00.03 % | 50.70 % | 87.37 % | 91.32 % | +|info | 20.9745 | 24.6041 | 24.8543 | 24.8942 | +| | 04.40 % | 75.07 % | 89.59 % | 91.53 % | To reproduce: diff --git a/src/strategies/information.rs b/src/strategies/information.rs index 313d84c..b9ec34f 100644 --- a/src/strategies/information.rs +++ b/src/strategies/information.rs @@ -95,14 +95,19 @@ trait Question { self.acknowledge_answer(answer.value, hand_info, view); } } -struct IsPlayable { + +type PropertyPredicate = fn(&BoardState, &Card) -> bool; +struct CardHasProperty +{ index: usize, + property: PropertyPredicate, } -impl Question for IsPlayable { +impl Question for CardHasProperty +{ fn info_amount(&self) -> u32 { 2 } fn answer(&self, hand: &Cards, view: &OwnedGameView) -> u32 { let ref card = hand[self.index]; - if view.get_board().is_playable(card) { 1 } else { 0 } + if (self.property)(view.get_board(), card) { 1 } else { 0 } } fn acknowledge_answer( &self, @@ -113,7 +118,7 @@ impl Question for IsPlayable { let ref mut card_table = hand_info[self.index]; let possible = card_table.get_possibilities(); for card in &possible { - if view.get_board().is_playable(card) { + if (self.property)(view.get_board(), card) { if answer == 0 { card_table.mark_false(card); } } else { if answer == 1 { card_table.mark_false(card); } @@ -121,6 +126,61 @@ impl Question for IsPlayable { } } } +fn q_is_playable(index: usize) -> CardHasProperty { + CardHasProperty {index, property: |board, card| board.is_playable(card)} +} +fn q_is_dead(index: usize) -> CardHasProperty { + CardHasProperty {index, property: |board, card| board.is_dead(card)} +} + +struct AdditiveComboQuestion { + /// For some list of questions l, the question `AdditiveComboQuestion { questions : l }` asks: + /// "What is the first question in the list `l` that has a nonzero answer, and what is its + /// answer?" + /// If all questions in `l` have the answer `0`, this question has the answer `0` as well. + /// + /// It's named that way because the `info_amount` grows additively with the `info_amount`s of + /// the questions in `l`. + questions: Vec>, +} +impl Question for AdditiveComboQuestion { + fn info_amount(&self) -> u32 { + self.questions.iter().map(|q| { q.info_amount() - 1 }).sum::() + 1 + } + fn answer(&self, hand: &Cards, view: &OwnedGameView) -> u32 { + let mut toadd = 1; + for q in &self.questions { + let q_answer = q.answer(hand, view); + if q_answer != 0 { + return toadd + q_answer - 1; + } + toadd += q.info_amount() - 1; + } + assert!(toadd == self.info_amount()); + 0 + } + fn acknowledge_answer( + &self, + mut answer: u32, + hand_info: &mut HandInfo, + view: &OwnedGameView, + ) { + if answer == 0 { + answer = self.info_amount(); + } + answer -= 1; + for q in &self.questions { + if answer < q.info_amount() - 1 { + q.acknowledge_answer(answer+1, hand_info, view); + return; + } else { + q.acknowledge_answer(0, hand_info, view); + answer -= q.info_amount() - 1; + } + } + assert!(answer == 0); + } +} struct CardPossibilityPartition { index: usize, @@ -278,8 +338,13 @@ impl InformationPlayerStrategy { let known_playable = augmented_hand_info.iter().filter(|&&(_, _, p_play, _, _)| { p_play == 1.0 }).collect::>().len(); + let known_dead = augmented_hand_info.iter().filter(|&&(_, _, _, p_dead, _)| { + p_dead == 1.0 + }).collect::>().len(); - if known_playable == 0 { + if known_playable == 0 { // TODO: changing this to "if true {" slightly improves the three-player game and + // very slightly worsens the other cases. There probably is some + // other way to make this decision that's better in all cases. let mut ask_play = augmented_hand_info.iter() .filter(|&&(_, _, p_play, p_dead, is_determined)| { if is_determined { return false; } @@ -289,7 +354,9 @@ impl InformationPlayerStrategy { }).collect::>(); // sort by probability of play, then by index ask_play.sort_by(|&&(_, i1, p1, _, _), &&(_, i2, p2, _, _)| { - // *higher* probabilities are better + // It's better to include higher-probability-of-playability + // cards into our combo question, since that maximizes our + // chance of finding out about a playable card. let result = p2.partial_cmp(&p1); if result == None || result == Some(Ordering::Equal) { i1.cmp(&i2) @@ -298,9 +365,45 @@ impl InformationPlayerStrategy { } }); - - for &(_, i, _, _, _) in ask_play { - if add_question(&mut questions, &mut info_remaining, IsPlayable {index: i}) { + if view.board.num_players == 5 { + for &(_, i, _, _, _) in ask_play { + if add_question(&mut questions, &mut info_remaining, q_is_playable(i)) { + return questions; + } + } + } else { + let mut rest_combo = AdditiveComboQuestion {questions: Vec::new()}; + for &(_, i, _, _, _) in ask_play { + if rest_combo.info_amount() < info_remaining { + rest_combo.questions.push(Box::new(q_is_playable(i))); + } + } + rest_combo.questions.reverse(); // It's better to put lower-probability-of-playability + // cards first: The difference only matters if we + // find a playable card, and conditional on that, + // it's better to find out about as many non-playable + // cards as possible. + if rest_combo.info_amount() < info_remaining && known_dead == 0 { + let mut ask_dead = augmented_hand_info.iter() + .filter(|&&(_, _, _, p_dead, _)| { + p_dead > 0.0 && p_dead < 1.0 + }).collect::>(); + // sort by probability of death, then by index + ask_dead.sort_by(|&&(_, i1, _, d1, _), &&(_, i2, _, d2, _)| { + let result = d2.partial_cmp(&d1); + if result == None || result == Some(Ordering::Equal) { + i1.cmp(&i2) + } else { + result.unwrap() + } + }); + for &(_, i, _, _, _) in ask_dead { + if rest_combo.info_amount() < info_remaining { + rest_combo.questions.push(Box::new(q_is_dead(i))); + } + } + } + if add_question(&mut questions, &mut info_remaining, rest_combo) { return questions; } } @@ -869,6 +972,11 @@ impl PlayerStrategy for InformationPlayerStrategy { view.board.total_cards - (COLORS.len() * VALUES.len()) as u32 - (view.board.num_players * view.board.hand_size); + let soft_discard_threshold = if view.board.num_players < 5 { + discard_threshold - 5 + } else { + discard_threshold + }; // TODO something more principled. // make a possibly risky play // TODO: consider removing this, if we improve information transfer @@ -900,7 +1008,7 @@ impl PlayerStrategy for InformationPlayerStrategy { let public_useless_indices = self.find_useless_cards(view, &self.get_my_public_info()); let useless_indices = self.find_useless_cards(view, &private_info); - if view.board.discard_size() <= discard_threshold { + if view.board.discard_size() <= soft_discard_threshold { // 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);