use crate::heuristic::greedy_layoff;
use crate::{DrawAction, Layoff, Strategy, TurnAction, UpcardAction, View};
use gin_rummy::{Card, Hand, Rank, Suit, best_melds, deadwood};
use rand::{Rng, RngExt as _};
fn joins_a_meld(hand: Hand, top: Card) -> bool {
let with = hand | top.into();
let of_rank = Suit::ASC
.into_iter()
.filter(|&suit| {
with.contains(Card {
suit,
rank: top.rank,
})
})
.count();
if of_rank >= 3 {
return true;
}
let pivot = top.rank.get();
(pivot.saturating_sub(2).max(1)..=pivot.min(11)).any(|low| {
(low..low + 3).all(|rank| {
with.contains(Card {
suit: top.suit,
rank: Rank::new(rank),
})
})
})
}
#[derive(Debug, Clone)]
pub struct EaaiSimpleBot<R> {
rng: R,
pre_draw: Hand,
seen_pairs: Vec<(Card, Card)>,
}
impl<R: Rng> EaaiSimpleBot<R> {
#[must_use]
pub const fn new(rng: R) -> Self {
Self {
rng,
pre_draw: Hand::EMPTY,
seen_pairs: Vec::new(),
}
}
fn drawn(&self, hand: Hand) -> Option<Card> {
let fresh = hand - self.pre_draw;
let mut cards = fresh.iter();
match (cards.next(), cards.next()) {
(Some(card), None) => Some(card),
_ => None,
}
}
fn pick_discard(&mut self, hand: Hand, taken: Option<Card>, drawn: Option<Card>) -> (Card, u8) {
let mut min_deadwood = u8::MAX;
let mut candidates: Vec<Card> = Vec::new();
for card in hand {
if Some(card) == taken {
continue;
}
if let Some(drawn) = drawn
&& self.seen_pairs.contains(&(drawn, card))
{
continue;
}
let rest = deadwood(hand - card.into());
if rest < min_deadwood {
min_deadwood = rest;
candidates.clear();
}
if rest <= min_deadwood {
candidates.push(card);
}
}
let (card, rest) = if candidates.is_empty() {
self.seen_pairs.clear();
hand.iter()
.filter(|&card| Some(card) != taken)
.map(|card| (card, deadwood(hand - card.into())))
.min_by_key(|&(_, rest)| rest)
.expect("an 11-card hand always has a legal discard")
} else {
let index = self.rng.random_range(0..candidates.len());
(candidates[index], min_deadwood)
};
if let Some(drawn) = drawn {
self.seen_pairs.push((drawn, card));
}
(card, rest)
}
}
impl<R: Rng> Strategy for EaaiSimpleBot<R> {
fn offer_upcard(&mut self, view: &View<'_>) -> UpcardAction {
self.seen_pairs.clear();
self.pre_draw = view.hand();
let top = view.upcard().expect("the upcard offer has an upcard");
if joins_a_meld(view.hand(), top) {
UpcardAction::Take
} else {
UpcardAction::Pass
}
}
fn choose_draw(&mut self, view: &View<'_>) -> DrawAction {
if view.stock_len() == 31 {
self.seen_pairs.clear();
}
self.pre_draw = view.hand();
let top = view.upcard().expect("the pile is never empty on a draw");
if joins_a_meld(view.hand(), top) {
DrawAction::TakeDiscard
} else {
DrawAction::Stock
}
}
fn play_turn(&mut self, view: &View<'_>) -> TurnAction {
let hand = view.hand();
let drawn = view.taken_discard().or_else(|| self.drawn(hand));
let (card, rest) = self.pick_discard(hand, view.taken_discard(), drawn);
if rest <= view.knock_limit() {
TurnAction::Knock {
discard: card,
melds: best_melds(hand - card.into()),
}
} else {
TurnAction::Discard(card)
}
}
fn choose_layoff(&mut self, view: &View<'_>) -> Option<Layoff> {
greedy_layoff(view.hand(), view.spread()).map(|(card, meld)| Layoff { card, meld })
}
fn name(&self) -> &str {
"eaai-simple"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{HeuristicBot, play_game, play_round};
use gin_rummy::{Game, Player, Round, Rules};
use rand::SeedableRng as _;
use rand::rngs::StdRng;
fn card(text: &str) -> Card {
text.parse().expect("a valid card")
}
#[test]
fn draws_the_upcard_only_into_a_meld() {
let hand: Hand = "A27.7.34.89".parse().expect("a valid hand");
assert!(joins_a_meld(hand, card("♠7")));
assert!(joins_a_meld(hand, card("♥5")));
assert!(joins_a_meld(hand, card("♥2")));
assert!(!joins_a_meld(hand, card("♦3")));
assert!(!joins_a_meld(hand, card("♦K")));
assert!(joins_a_meld("QK...".parse().unwrap(), card("♣J")));
assert!(!joins_a_meld("2K...".parse().unwrap(), card("♣A")));
}
#[test]
fn discards_uniformly_among_minimal_deadwood_sheds() {
let hand: Hand = "A23.456.789.JK".parse().expect("a valid hand");
let mut bot = EaaiSimpleBot::new(StdRng::seed_from_u64(0));
let mut seen = Hand::EMPTY;
for _ in 0..64 {
bot.seen_pairs.clear();
let (shed, rest) = bot.pick_discard(hand, None, Some(card("♠K")));
assert_eq!(rest, 10);
seen.insert(shed);
}
assert_eq!(seen, "...JK".parse().expect("a valid hand"));
}
#[test]
fn refuses_a_repeated_draw_discard_pair() {
let hand: Hand = "A23.456.789.JK".parse().expect("a valid hand");
let mut bot = EaaiSimpleBot::new(StdRng::seed_from_u64(0));
bot.seen_pairs.push((card("♠K"), card("♠J")));
bot.seen_pairs.push((card("♠K"), card("♠K")));
assert_eq!(
bot.pick_discard(hand, None, Some(card("♠K"))),
(card("♣3"), 23),
);
let mut bot = EaaiSimpleBot::new(StdRng::seed_from_u64(0));
for shed in hand {
bot.seen_pairs.push((card("♠K"), shed));
}
let (_, rest) = bot.pick_discard(hand, None, Some(card("♠K")));
assert_eq!(rest, 10);
assert_eq!(bot.seen_pairs.len(), 1);
}
#[test]
fn knocks_at_the_first_opportunity() {
let hands: [Hand; 2] = [
"A23.456.789.J".parse().expect("a valid hand"),
"45.89J.JQK.23".parse().expect("a valid hand"),
];
let upcard = card("♠T");
let stock: Vec<Card> = Hand::ALL
.iter()
.filter(|&c| !hands[0].contains(c) && !hands[1].contains(c) && c != upcard)
.collect();
let round = Round::from_deal(Rules::default(), Player::Two, hands, upcard, stock)
.expect("a partitioned deck");
let mut bot = EaaiSimpleBot::new(StdRng::seed_from_u64(0));
let mut greedy = HeuristicBot::new();
let result = play_round(round, [&mut bot, &mut greedy]).expect("only legal actions");
assert_eq!(result.winner(), Some(Player::One));
}
#[test]
fn survives_whole_games_against_the_heuristic() {
let mut rules = Rules::default();
rules.big_gin_bonus = None;
for seed in 0..8 {
let mut rng = StdRng::seed_from_u64(seed);
let mut bot = EaaiSimpleBot::new(StdRng::seed_from_u64(seed));
let mut greedy = HeuristicBot::new();
let mut game = Game::new(rules, Player::One);
play_game(&mut game, [&mut bot, &mut greedy], &mut rng).expect("only legal actions");
}
}
}