1use crate::game::{DrawError, DrawTileResult};
2use crate::meld::PossibleMeld;
3use crate::{Game, GamePhase, PlayerId, TileId, Wind, WINDS_ROUND_ORDER};
4use rand::seq::SliceRandom;
5use rand::thread_rng;
6use rustc_hash::FxHashSet;
7use strum_macros::EnumIter;
8
9mod best_drops;
10
11pub struct StandardAI<'a> {
13 ai_players: FxHashSet<PlayerId>,
14 pub auto_stop_claim_meld: FxHashSet<PlayerId>,
15 pub can_draw_round: bool,
16 pub can_pass_turn: bool,
17 pub dealer_order_deterministic: Option<bool>,
18 pub draw_tile_for_real_player: bool,
19 pub game: &'a mut Game,
20 pub shuffle_players: bool,
21 pub sort_on_draw: bool,
22 pub sort_on_initial_draw: bool,
23 pub with_dead_wall: bool,
24}
25
26#[derive(Debug, Eq, PartialEq, EnumIter, Clone)]
27pub enum PlayExitLocation {
28 AIPlayerTileDrawn,
29 AIPlayerTurnPassed,
30 AlreadyEnd,
31 AutoStoppedDrawMahjong,
32 AutoStoppedDrawNormal,
33 ClaimedTile,
34 CompletedPlayers,
35 CouldNotClaimTile,
36 DecidedDealer,
37 FinishedCharleston,
38 InitialDraw,
39 InitialDrawError(DrawError),
40 InitialShuffle,
41 MeldCreated,
42 NewRoundFromMeld,
43 NoAction,
44 NoAutoDrawTile,
45 RoundPassed,
46 StartGame,
47 SuccessMahjong,
48 TileDiscarded,
49 TileDrawn,
50 TurnPassed,
51 WaitingDealerOrder,
52 WaitingPlayers,
53}
54
55#[derive(Debug, PartialEq, Eq, Clone)]
57pub struct Metadata {}
58
59#[derive(Debug, Eq, PartialEq, Clone)]
60pub struct PlayActionResult {
61 pub changed: bool,
62 pub exit_location: PlayExitLocation,
63 pub metadata: Option<Metadata>,
64}
65
66pub fn sort_by_is_mahjong(a: &PossibleMeld, b: &PossibleMeld) -> std::cmp::Ordering {
67 if a.is_mahjong && !b.is_mahjong {
68 std::cmp::Ordering::Less
69 } else if !a.is_mahjong && b.is_mahjong {
70 std::cmp::Ordering::Greater
71 } else {
72 std::cmp::Ordering::Equal
73 }
74}
75
76impl StandardAI<'_> {
77 pub fn get_is_after_discard(&self) -> bool {
78 let current_player = self.game.get_current_player();
79 if current_player.is_none() {
80 return false;
81 }
82 let current_hand = self.game.table.hands.get(¤t_player.unwrap());
83
84 current_hand.unwrap().len() < self.game.style.tiles_after_claim()
85 && self.game.round.tile_claimed.is_some()
86 }
87}
88
89impl<'a> StandardAI<'a> {
90 pub fn new(
91 game: &'a mut Game,
92 ai_players: FxHashSet<PlayerId>,
93 auto_stop_claim_meld: FxHashSet<PlayerId>,
94 ) -> Self {
95 Self {
96 ai_players,
97 auto_stop_claim_meld,
98 can_draw_round: false,
99 can_pass_turn: true,
100 dealer_order_deterministic: None,
101 draw_tile_for_real_player: true,
102 game,
103 shuffle_players: false,
104 sort_on_draw: false,
105 sort_on_initial_draw: false,
106 with_dead_wall: false,
107 }
108 }
109
110 pub fn play_action(&mut self, with_metadata: bool) -> PlayActionResult {
111 let mut metadata: Option<Metadata> = None;
112
113 if with_metadata {
114 metadata = Some(Metadata {});
115 }
116
117 match self.game.phase {
118 GamePhase::Charleston => {
119 let finished_charleston = self.game.move_charleston();
120
121 if finished_charleston.is_ok() {
122 return PlayActionResult {
123 changed: true,
124 exit_location: PlayExitLocation::FinishedCharleston,
125 metadata,
126 };
127 }
128
129 return PlayActionResult {
130 changed: false,
131 exit_location: PlayExitLocation::NoAction,
132 metadata,
133 };
134 }
135 GamePhase::WaitingPlayers => {
136 return match self.game.complete_players(self.shuffle_players) {
137 Ok(_) => PlayActionResult {
138 changed: true,
139 exit_location: PlayExitLocation::CompletedPlayers,
140 metadata,
141 },
142 Err(_) => PlayActionResult {
143 changed: false,
144 exit_location: PlayExitLocation::WaitingPlayers,
145 metadata,
146 },
147 };
148 }
149 GamePhase::InitialShuffle => {
150 self.game.prepare_table(self.with_dead_wall);
151
152 return PlayActionResult {
153 changed: true,
154 exit_location: PlayExitLocation::InitialShuffle,
155 metadata,
156 };
157 }
158 GamePhase::DecidingDealer => {
159 if self.dealer_order_deterministic.is_some() {
160 let dealer_order_deterministic = self.dealer_order_deterministic.unwrap();
161
162 self.game
163 .round
164 .set_initial_winds(Some(if dealer_order_deterministic {
165 WINDS_ROUND_ORDER.clone()
166 } else {
167 let mut winds: [Wind; 4] = WINDS_ROUND_ORDER.clone();
168 winds.shuffle(&mut thread_rng());
169 winds
170 }))
171 .unwrap();
172 } else if self.game.round.initial_winds.is_none() {
173 return PlayActionResult {
174 metadata,
175 changed: false,
176 exit_location: PlayExitLocation::WaitingDealerOrder,
177 };
178 }
179
180 self.game.decide_dealer().unwrap();
181
182 return PlayActionResult {
183 changed: true,
184 metadata,
185 exit_location: PlayExitLocation::DecidedDealer,
186 };
187 }
188 GamePhase::Beginning => {
189 self.game.start(self.shuffle_players);
190
191 return PlayActionResult {
192 changed: true,
193 metadata,
194 exit_location: PlayExitLocation::StartGame,
195 };
196 }
197 GamePhase::InitialDraw => match self.game.initial_draw() {
198 Ok(_) => {
199 if self.sort_on_initial_draw {
200 for player in self.game.table.hands.0.clone().keys() {
201 self.game.table.hands.sort_player_hand(player);
202 }
203 }
204
205 return PlayActionResult {
206 changed: true,
207 exit_location: PlayExitLocation::InitialDraw,
208 metadata,
209 };
210 }
211 Err(e) => {
212 return PlayActionResult {
213 metadata,
214 changed: false,
215 exit_location: PlayExitLocation::InitialDrawError(e),
216 };
217 }
218 },
219 GamePhase::End => {
220 return PlayActionResult {
221 changed: false,
222 exit_location: PlayExitLocation::AlreadyEnd,
223 metadata,
224 };
225 }
226 GamePhase::Playing => {}
227 }
228
229 let mut melds = self.game.get_possible_melds(true);
231
232 let mut rng = thread_rng();
234 melds.shuffle(&mut rng);
235 melds.sort_by(sort_by_is_mahjong);
236
237 for meld in melds {
238 if self.ai_players.contains(&meld.player_id) {
239 if meld.is_mahjong {
240 let mahjong_success = self.game.say_mahjong(&meld.player_id);
241
242 if mahjong_success.is_ok() {
243 return PlayActionResult {
244 changed: true,
245 exit_location: PlayExitLocation::SuccessMahjong,
246 metadata,
247 };
248 }
249 }
250
251 let player_hand = self.game.table.hands.0.get(&meld.player_id).unwrap();
252 let missing_tile = meld
253 .tiles
254 .iter()
255 .find(|tile| !player_hand.get_has_tile(tile));
256
257 if let Some(missing_tile) = missing_tile {
258 if let Some(claimable_type) =
259 self.game.round.get_claimable_tile(&meld.player_id)
260 {
261 if claimable_type == *missing_tile {
262 let was_tile_claimed = self.game.claim_tile(&meld.player_id);
263
264 if was_tile_claimed {
265 return PlayActionResult {
266 changed: true,
267 exit_location: PlayExitLocation::ClaimedTile,
268 metadata,
269 };
270 } else {
271 return PlayActionResult {
273 changed: false,
274 exit_location: PlayExitLocation::CouldNotClaimTile,
275 metadata,
276 };
277 }
278 }
279 }
280 }
281
282 if meld.discard_tile.is_some() {
283 continue;
284 }
285
286 let phase_before = self.game.phase;
287
288 let meld_created = self.game.create_meld(
289 &meld.player_id,
290 &meld.tiles,
291 meld.is_upgrade,
292 meld.is_concealed,
293 );
294
295 if phase_before == GamePhase::Playing && self.game.phase != GamePhase::Playing {
296 return PlayActionResult {
297 changed: true,
298 exit_location: PlayExitLocation::NewRoundFromMeld,
299 metadata,
300 };
301 }
302
303 if meld_created.is_ok() {
304 return PlayActionResult {
305 changed: true,
306 exit_location: PlayExitLocation::MeldCreated,
307 metadata,
308 };
309 }
310 }
311 }
312
313 let current_player = self.game.get_current_player().unwrap();
314
315 if self.ai_players.contains(¤t_player) {
316 let is_tile_claimed = self.game.round.tile_claimed.is_some();
317
318 if !is_tile_claimed {
319 let tile_drawn = self.game.draw_tile_from_wall();
320
321 match tile_drawn {
322 DrawTileResult::Bonus(_) | DrawTileResult::Normal(_) => {
323 if let DrawTileResult::Normal(_) = tile_drawn {
324 if self.sort_on_initial_draw {
325 self.game.table.hands.sort_player_hand(¤t_player);
326 }
327 }
328
329 return PlayActionResult {
330 changed: true,
331 exit_location: PlayExitLocation::AIPlayerTileDrawn,
332 metadata,
333 };
334 }
335 DrawTileResult::AlreadyDrawn | DrawTileResult::WallExhausted => {}
336 };
337 }
338
339 let player_hand = self.game.table.hands.0.get(¤t_player).unwrap();
340 if player_hand.len() == self.game.style.tiles_after_claim() {
341 let mut tiles_without_meld = player_hand
342 .list
343 .iter()
344 .filter(|tile| tile.set_id.is_none())
345 .map(|tile| tile.id)
346 .collect::<Vec<TileId>>();
347
348 if !tiles_without_meld.is_empty() {
349 let tile_to_discard = 'a: {
350 if let Some(tile_claimed) = self.game.round.tile_claimed.clone() {
351 for tile in tiles_without_meld.iter() {
352 if tile_claimed.id == *tile {
353 break 'a tile_claimed.id;
354 }
355 }
356 }
357
358 tiles_without_meld.shuffle(&mut thread_rng());
359 tiles_without_meld[0]
360 };
361
362 let discarded = self.game.discard_tile_to_board(&tile_to_discard);
363
364 if discarded.is_ok() {
365 return PlayActionResult {
366 changed: true,
367 exit_location: PlayExitLocation::TileDiscarded,
368 metadata,
369 };
370 }
371 }
372 } else if self.can_pass_turn {
373 let auto_stop_claim_meld = self.auto_stop_claim_meld.clone();
374 if !auto_stop_claim_meld.is_empty() {
375 for player in auto_stop_claim_meld {
376 if player.is_empty() {
377 continue;
378 }
379 let (can_claim_tile, tile_claimed, _) =
380 self.game.get_can_claim_tile(&player);
381
382 if !can_claim_tile {
383 continue;
384 }
385
386 let tile_claimed = tile_claimed.unwrap();
387
388 let melds_mahjong = self.game.get_possible_melds_for_player(&player, true);
389 let melds_with_draw_mahjong = melds_mahjong
390 .iter()
391 .filter(|meld| meld.tiles.contains(&tile_claimed))
392 .collect::<Vec<&PossibleMeld>>();
393
394 if !melds_with_draw_mahjong.is_empty() {
395 return PlayActionResult {
396 changed: false,
397 exit_location: PlayExitLocation::AutoStoppedDrawMahjong,
398 metadata,
399 };
400 }
401
402 let melds_normal = self.game.get_possible_melds_for_player(&player, false);
403 let melds_with_draw_normal = melds_normal
404 .iter()
405 .filter(|meld| meld.tiles.contains(&tile_claimed))
406 .collect::<Vec<&PossibleMeld>>();
407
408 if !melds_with_draw_normal.is_empty() {
409 return PlayActionResult {
410 changed: false,
411 exit_location: PlayExitLocation::AutoStoppedDrawNormal,
412 metadata,
413 };
414 }
415 }
416 }
417 let success = self.game.round.next_turn(&self.game.table.hands);
418
419 if success.is_ok() {
420 return PlayActionResult {
421 changed: true,
422 exit_location: PlayExitLocation::AIPlayerTurnPassed,
423 metadata,
424 };
425 }
426 };
427 } else {
428 let is_tile_claimed = self.game.round.tile_claimed.is_some();
429
430 if !is_tile_claimed {
431 if !self.draw_tile_for_real_player {
432 return PlayActionResult {
433 changed: false,
434 exit_location: PlayExitLocation::NoAutoDrawTile,
435 metadata,
436 };
437 }
438
439 let tile_drawn = self.game.draw_tile_from_wall();
440
441 match tile_drawn {
442 DrawTileResult::Bonus(_) | DrawTileResult::Normal(_) => {
443 if let DrawTileResult::Normal(_) = tile_drawn {
444 if self.sort_on_draw {
445 self.game.table.hands.sort_player_hand(¤t_player);
446 }
447 }
448
449 return PlayActionResult {
450 changed: true,
451 exit_location: PlayExitLocation::TileDrawn,
452 metadata,
453 };
454 }
455 DrawTileResult::AlreadyDrawn | DrawTileResult::WallExhausted => {}
456 };
457 } else if self.can_pass_turn {
458 let player_hand = self.game.table.hands.0.get(¤t_player).unwrap();
459 if player_hand.len() < self.game.style.tiles_after_claim() {
460 let success = self.game.round.next_turn(&self.game.table.hands);
461
462 if success.is_ok() {
463 return PlayActionResult {
464 changed: true,
465 exit_location: PlayExitLocation::TurnPassed,
466 metadata,
467 };
468 }
469 }
470 }
471 }
472
473 if self.game.table.draw_wall.is_empty() && self.can_draw_round {
474 let round_passed = self.game.pass_null_round();
475
476 if round_passed.is_ok() {
477 return PlayActionResult {
478 changed: true,
479 exit_location: PlayExitLocation::RoundPassed,
480 metadata,
481 };
482 }
483 }
484
485 PlayActionResult {
486 changed: false,
487 exit_location: PlayExitLocation::NoAction,
488 metadata,
489 }
490 }
491}