1use crate::heuristic::greedy_layoff;
11use crate::{DrawAction, Layoff, Strategy, TurnAction, UpcardAction, View};
12use gin_rummy::{Card, Hand, Rank, Suit, best_melds, deadwood};
13use rand::{Rng, RngExt as _};
14
15fn joins_a_meld(hand: Hand, top: Card) -> bool {
18 let with = hand | top.into();
19 let of_rank = Suit::ASC
20 .into_iter()
21 .filter(|&suit| {
22 with.contains(Card {
23 suit,
24 rank: top.rank,
25 })
26 })
27 .count();
28 if of_rank >= 3 {
29 return true;
30 }
31
32 let pivot = top.rank.get();
35 (pivot.saturating_sub(2).max(1)..=pivot.min(11)).any(|low| {
36 (low..low + 3).all(|rank| {
37 with.contains(Card {
38 suit: top.suit,
39 rank: Rank::new(rank),
40 })
41 })
42 })
43}
44
45#[derive(Debug, Clone)]
68pub struct EaaiSimpleBot<R> {
69 rng: R,
70 pre_draw: Hand,
73 seen_pairs: Vec<(Card, Card)>,
76}
77
78impl<R: Rng> EaaiSimpleBot<R> {
79 #[must_use]
81 pub const fn new(rng: R) -> Self {
82 Self {
83 rng,
84 pre_draw: Hand::EMPTY,
85 seen_pairs: Vec::new(),
86 }
87 }
88
89 fn drawn(&self, hand: Hand) -> Option<Card> {
91 let fresh = hand - self.pre_draw;
92 let mut cards = fresh.iter();
93 match (cards.next(), cards.next()) {
94 (Some(card), None) => Some(card),
95 _ => None,
96 }
97 }
98
99 fn pick_discard(&mut self, hand: Hand, taken: Option<Card>, drawn: Option<Card>) -> (Card, u8) {
103 let mut min_deadwood = u8::MAX;
104 let mut candidates: Vec<Card> = Vec::new();
105 for card in hand {
106 if Some(card) == taken {
107 continue;
108 }
109 if let Some(drawn) = drawn
110 && self.seen_pairs.contains(&(drawn, card))
111 {
112 continue;
113 }
114 let rest = deadwood(hand - card.into());
115 if rest < min_deadwood {
116 min_deadwood = rest;
117 candidates.clear();
118 }
119 if rest <= min_deadwood {
120 candidates.push(card);
121 }
122 }
123
124 let (card, rest) = if candidates.is_empty() {
127 self.seen_pairs.clear();
128 hand.iter()
129 .filter(|&card| Some(card) != taken)
130 .map(|card| (card, deadwood(hand - card.into())))
131 .min_by_key(|&(_, rest)| rest)
132 .expect("an 11-card hand always has a legal discard")
133 } else {
134 let index = self.rng.random_range(0..candidates.len());
135 (candidates[index], min_deadwood)
136 };
137 if let Some(drawn) = drawn {
138 self.seen_pairs.push((drawn, card));
139 }
140 (card, rest)
141 }
142}
143
144impl<R: Rng> Strategy for EaaiSimpleBot<R> {
145 fn offer_upcard(&mut self, view: &View<'_>) -> UpcardAction {
146 self.seen_pairs.clear();
148 self.pre_draw = view.hand();
149 let top = view.upcard().expect("the upcard offer has an upcard");
150 if joins_a_meld(view.hand(), top) {
151 UpcardAction::Take
152 } else {
153 UpcardAction::Pass
154 }
155 }
156
157 fn choose_draw(&mut self, view: &View<'_>) -> DrawAction {
158 if view.stock_len() == 31 {
164 self.seen_pairs.clear();
165 }
166 self.pre_draw = view.hand();
167 let top = view.upcard().expect("the pile is never empty on a draw");
168 if joins_a_meld(view.hand(), top) {
169 DrawAction::TakeDiscard
170 } else {
171 DrawAction::Stock
172 }
173 }
174
175 fn play_turn(&mut self, view: &View<'_>) -> TurnAction {
176 let hand = view.hand();
177 let drawn = view.taken_discard().or_else(|| self.drawn(hand));
178 let (card, rest) = self.pick_discard(hand, view.taken_discard(), drawn);
179
180 if rest <= view.knock_limit() {
183 TurnAction::Knock {
184 discard: card,
185 melds: best_melds(hand - card.into()),
186 }
187 } else {
188 TurnAction::Discard(card)
189 }
190 }
191
192 fn choose_layoff(&mut self, view: &View<'_>) -> Option<Layoff> {
193 greedy_layoff(view.hand(), view.spread()).map(|(card, meld)| Layoff { card, meld })
194 }
195
196 fn name(&self) -> &str {
197 "eaai-simple"
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::{HeuristicBot, play_game, play_round};
205 use gin_rummy::{Game, Player, Round, Rules};
206 use rand::SeedableRng as _;
207 use rand::rngs::StdRng;
208
209 fn card(text: &str) -> Card {
210 text.parse().expect("a valid card")
211 }
212
213 #[test]
214 fn draws_the_upcard_only_into_a_meld() {
215 let hand: Hand = "A27.7.34.89".parse().expect("a valid hand");
217 assert!(joins_a_meld(hand, card("♠7")));
219 assert!(joins_a_meld(hand, card("♥5")));
220 assert!(joins_a_meld(hand, card("♥2")));
221 assert!(!joins_a_meld(hand, card("♦3")));
224 assert!(!joins_a_meld(hand, card("♦K")));
225 assert!(joins_a_meld("QK...".parse().unwrap(), card("♣J")));
227 assert!(!joins_a_meld("2K...".parse().unwrap(), card("♣A")));
228 }
229
230 #[test]
231 fn discards_uniformly_among_minimal_deadwood_sheds() {
232 let hand: Hand = "A23.456.789.JK".parse().expect("a valid hand");
234 let mut bot = EaaiSimpleBot::new(StdRng::seed_from_u64(0));
235 let mut seen = Hand::EMPTY;
236 for _ in 0..64 {
237 bot.seen_pairs.clear();
238 let (shed, rest) = bot.pick_discard(hand, None, Some(card("♠K")));
239 assert_eq!(rest, 10);
240 seen.insert(shed);
241 }
242 assert_eq!(seen, "...JK".parse().expect("a valid hand"));
244 }
245
246 #[test]
247 fn refuses_a_repeated_draw_discard_pair() {
248 let hand: Hand = "A23.456.789.JK".parse().expect("a valid hand");
249 let mut bot = EaaiSimpleBot::new(StdRng::seed_from_u64(0));
252 bot.seen_pairs.push((card("♠K"), card("♠J")));
253 bot.seen_pairs.push((card("♠K"), card("♠K")));
254 assert_eq!(
255 bot.pick_discard(hand, None, Some(card("♠K"))),
256 (card("♣3"), 23),
257 );
258
259 let mut bot = EaaiSimpleBot::new(StdRng::seed_from_u64(0));
261 for shed in hand {
262 bot.seen_pairs.push((card("♠K"), shed));
263 }
264 let (_, rest) = bot.pick_discard(hand, None, Some(card("♠K")));
265 assert_eq!(rest, 10);
266 assert_eq!(bot.seen_pairs.len(), 1);
267 }
268
269 #[test]
270 fn knocks_at_the_first_opportunity() {
271 let hands: [Hand; 2] = [
274 "A23.456.789.J".parse().expect("a valid hand"),
275 "45.89J.JQK.23".parse().expect("a valid hand"),
276 ];
277 let upcard = card("♠T");
278 let stock: Vec<Card> = Hand::ALL
279 .iter()
280 .filter(|&c| !hands[0].contains(c) && !hands[1].contains(c) && c != upcard)
281 .collect();
282 let round = Round::from_deal(Rules::default(), Player::Two, hands, upcard, stock)
283 .expect("a partitioned deck");
284
285 let mut bot = EaaiSimpleBot::new(StdRng::seed_from_u64(0));
286 let mut greedy = HeuristicBot::new();
287 let result = play_round(round, [&mut bot, &mut greedy]).expect("only legal actions");
288 assert_eq!(result.winner(), Some(Player::One));
289 }
290
291 #[test]
292 fn survives_whole_games_against_the_heuristic() {
293 let mut rules = Rules::default();
296 rules.big_gin_bonus = None;
297 for seed in 0..8 {
298 let mut rng = StdRng::seed_from_u64(seed);
299 let mut bot = EaaiSimpleBot::new(StdRng::seed_from_u64(seed));
300 let mut greedy = HeuristicBot::new();
301 let mut game = Game::new(rules, Player::One);
302 play_game(&mut game, [&mut bot, &mut greedy], &mut rng).expect("only legal actions");
303 }
304 }
305}