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.)
This commit is contained in:
parent
737085948e
commit
385feeb6ba
2 changed files with 120 additions and 12 deletions
|
@ -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 |
|
|cheat | 24.8600 | 24.9781 | 24.9715 | 24.9570 |
|
||||||
| | 90.52 % | 98.12 % | 97.74 % | 96.57 % |
|
| | 90.52 % | 98.12 % | 97.74 % | 96.57 % |
|
||||||
|info | 18.6956 | 24.2234 | 24.8335 | 24.8928 |
|
|info | 20.9745 | 24.6041 | 24.8543 | 24.8942 |
|
||||||
| | 00.03 % | 50.70 % | 87.37 % | 91.32 % |
|
| | 04.40 % | 75.07 % | 89.59 % | 91.53 % |
|
||||||
|
|
||||||
|
|
||||||
To reproduce:
|
To reproduce:
|
||||||
|
|
|
@ -95,14 +95,19 @@ trait Question {
|
||||||
self.acknowledge_answer(answer.value, hand_info, view);
|
self.acknowledge_answer(answer.value, hand_info, view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
struct IsPlayable {
|
|
||||||
|
type PropertyPredicate = fn(&BoardState, &Card) -> bool;
|
||||||
|
struct CardHasProperty
|
||||||
|
{
|
||||||
index: usize,
|
index: usize,
|
||||||
|
property: PropertyPredicate,
|
||||||
}
|
}
|
||||||
impl Question for IsPlayable {
|
impl Question for CardHasProperty
|
||||||
|
{
|
||||||
fn info_amount(&self) -> u32 { 2 }
|
fn info_amount(&self) -> u32 { 2 }
|
||||||
fn answer(&self, hand: &Cards, view: &OwnedGameView) -> u32 {
|
fn answer(&self, hand: &Cards, view: &OwnedGameView) -> u32 {
|
||||||
let ref card = hand[self.index];
|
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(
|
fn acknowledge_answer(
|
||||||
&self,
|
&self,
|
||||||
|
@ -113,7 +118,7 @@ impl Question for IsPlayable {
|
||||||
let ref mut card_table = hand_info[self.index];
|
let ref mut card_table = hand_info[self.index];
|
||||||
let possible = card_table.get_possibilities();
|
let possible = card_table.get_possibilities();
|
||||||
for card in &possible {
|
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); }
|
if answer == 0 { card_table.mark_false(card); }
|
||||||
} else {
|
} else {
|
||||||
if answer == 1 { card_table.mark_false(card); }
|
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<Box<Question>>,
|
||||||
|
}
|
||||||
|
impl Question for AdditiveComboQuestion {
|
||||||
|
fn info_amount(&self) -> u32 {
|
||||||
|
self.questions.iter().map(|q| { q.info_amount() - 1 }).sum::<u32>() + 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<CardPossibilityTable>,
|
||||||
|
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 {
|
struct CardPossibilityPartition {
|
||||||
index: usize,
|
index: usize,
|
||||||
|
@ -278,8 +338,13 @@ impl InformationPlayerStrategy {
|
||||||
let known_playable = augmented_hand_info.iter().filter(|&&(_, _, p_play, _, _)| {
|
let known_playable = augmented_hand_info.iter().filter(|&&(_, _, p_play, _, _)| {
|
||||||
p_play == 1.0
|
p_play == 1.0
|
||||||
}).collect::<Vec<_>>().len();
|
}).collect::<Vec<_>>().len();
|
||||||
|
let known_dead = augmented_hand_info.iter().filter(|&&(_, _, _, p_dead, _)| {
|
||||||
|
p_dead == 1.0
|
||||||
|
}).collect::<Vec<_>>().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()
|
let mut ask_play = augmented_hand_info.iter()
|
||||||
.filter(|&&(_, _, p_play, p_dead, is_determined)| {
|
.filter(|&&(_, _, p_play, p_dead, is_determined)| {
|
||||||
if is_determined { return false; }
|
if is_determined { return false; }
|
||||||
|
@ -289,7 +354,9 @@ impl InformationPlayerStrategy {
|
||||||
}).collect::<Vec<_>>();
|
}).collect::<Vec<_>>();
|
||||||
// sort by probability of play, then by index
|
// sort by probability of play, then by index
|
||||||
ask_play.sort_by(|&&(_, i1, p1, _, _), &&(_, i2, p2, _, _)| {
|
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);
|
let result = p2.partial_cmp(&p1);
|
||||||
if result == None || result == Some(Ordering::Equal) {
|
if result == None || result == Some(Ordering::Equal) {
|
||||||
i1.cmp(&i2)
|
i1.cmp(&i2)
|
||||||
|
@ -298,9 +365,45 @@ impl InformationPlayerStrategy {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if view.board.num_players == 5 {
|
||||||
for &(_, i, _, _, _) in ask_play {
|
for &(_, i, _, _, _) in ask_play {
|
||||||
if add_question(&mut questions, &mut info_remaining, IsPlayable {index: i}) {
|
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::<Vec<_>>();
|
||||||
|
// 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;
|
return questions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -869,6 +972,11 @@ impl PlayerStrategy for InformationPlayerStrategy {
|
||||||
view.board.total_cards
|
view.board.total_cards
|
||||||
- (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);
|
||||||
|
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
|
// make a possibly risky play
|
||||||
// TODO: consider removing this, if we improve information transfer
|
// 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 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);
|
||||||
|
|
||||||
if view.board.discard_size() <= discard_threshold {
|
if view.board.discard_size() <= soft_discard_threshold {
|
||||||
// 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);
|
||||||
|
|
Loading…
Reference in a new issue