mahjong_core/game/
mod.rs

1pub use self::creation::GameNewOpts;
2pub use self::definition::{DrawTileResult, Game, GameId, GamePhase, GameStyle, GameVersion};
3use self::errors::DecideDealerError;
4pub use self::errors::{
5    BreakMeldError, CreateMeldError, DiscardTileError, DrawError, PassNullRoundError,
6};
7pub use self::players::{PlayerId, Players, PlayersVec};
8use crate::hand::KongTile;
9use crate::table::PositionTilesOpts;
10use crate::{
11    deck::DEFAULT_DECK,
12    hand::CanSayMahjongError,
13    meld::{
14        get_is_chow, get_is_kong, get_is_pung, get_tile_claimed_id_for_user, PlayerDiff,
15        PossibleMeld, SetCheckOpts,
16    },
17    round::{Round, RoundTileClaimed},
18    Hand, HandTile, TileId,
19};
20use crate::{Tile, Wind, WINDS_ROUND_ORDER};
21use rustc_hash::FxHashSet;
22use uuid::Uuid;
23
24mod charleston;
25mod creation;
26mod definition;
27mod errors;
28mod players;
29
30impl Game {
31    // If `check_for_mahjong` is true, then it will only check for mahjong, if is false, then it
32    // will check for melds that are not mahjong (they are exclusive)
33    pub fn get_possible_melds_for_player(
34        &self,
35        player: &PlayerId,
36        check_for_mahjong: bool,
37    ) -> Vec<PossibleMeld> {
38        let mut melds: Vec<PossibleMeld> = vec![];
39
40        let (can_claim_tile, tile_claimed, player_hand) = self.get_can_claim_tile(player);
41
42        let hand = if can_claim_tile {
43            let mut hand = player_hand.unwrap().clone();
44            hand.push(HandTile {
45                concealed: true,
46                id: tile_claimed.unwrap(),
47                set_id: None,
48            });
49            hand
50        } else {
51            player_hand.unwrap().clone()
52        };
53
54        let mut round = self.round.clone();
55        if can_claim_tile {
56            round.tile_claimed = Some(RoundTileClaimed {
57                by: Some(player.clone()),
58                from: round.tile_claimed.unwrap().from,
59                id: tile_claimed.unwrap(),
60            });
61        }
62
63        let board_tile_player_diff =
64            self.get_board_tile_player_diff(Some(&round), Some(&hand), player);
65        let claimed_tile = get_tile_claimed_id_for_user(player, &round.tile_claimed);
66
67        let possible_melds =
68            hand.get_possible_melds(board_tile_player_diff, claimed_tile, check_for_mahjong);
69
70        for meld in possible_melds {
71            melds.push(PossibleMeld {
72                discard_tile: None,
73                is_concealed: meld.is_concealed,
74                is_mahjong: meld.is_mahjong,
75                is_upgrade: meld.is_upgrade,
76                player_id: player.clone(),
77                tiles: meld.tiles,
78            });
79        }
80
81        melds
82    }
83
84    pub fn get_can_claim_tile(&self, player: &PlayerId) -> (bool, Option<TileId>, Option<&Hand>) {
85        let tile_claimed = self.round.get_claimable_tile(player);
86        let player_hand = self.table.hands.0.get(player);
87
88        if player_hand.is_none() {
89            return (false, None, None);
90        }
91
92        let player_hand = player_hand.unwrap();
93        let can_claim_tile =
94            tile_claimed.is_some() && player_hand.len() < self.style.tiles_after_claim();
95
96        (can_claim_tile, tile_claimed, Some(player_hand))
97    }
98
99    pub fn get_possible_melds(&self, early_return: bool) -> Vec<PossibleMeld> {
100        let mut melds: Vec<PossibleMeld> = vec![];
101        let mut players = self.players.clone();
102
103        if early_return {
104            players.shuffle();
105        }
106
107        for player in &players.0 {
108            let mut player_melds = self.get_possible_melds_for_player(player, true);
109
110            if early_return && !player_melds.is_empty() {
111                return player_melds;
112            }
113
114            melds.append(&mut player_melds);
115        }
116
117        for player in &players.0 {
118            let mut player_melds = self.get_possible_melds_for_player(player, false);
119
120            if early_return && !player_melds.is_empty() {
121                return player_melds;
122            }
123
124            melds.append(&mut player_melds);
125        }
126
127        melds
128    }
129
130    pub fn get_possible_melds_by_discard(&self) -> Vec<PossibleMeld> {
131        let mut melds = self.get_possible_melds(false);
132
133        let player_index = self
134            .players
135            .iter()
136            .position(|p| self.table.hands.get(p).unwrap().len() == self.style.tiles_after_claim());
137
138        if player_index.is_none() {
139            return melds;
140        }
141
142        let player_index = player_index.unwrap();
143        let player_id = self.players.get(player_index).unwrap().clone();
144
145        let player_hand: Vec<HandTile> = self
146            .table
147            .hands
148            .get(&player_id)
149            .unwrap()
150            .list
151            .into_iter()
152            .filter(|t| t.set_id.is_none())
153            .collect();
154
155        player_hand.iter().for_each(|hand_tile| {
156            let mut game_copy = self.clone();
157
158            game_copy
159                .discard_tile_to_board(&hand_tile.id)
160                .unwrap_or_default();
161
162            let new_melds = game_copy.get_possible_melds(false);
163
164            new_melds
165                .iter()
166                .filter(|m| m.player_id != player_id)
167                .for_each(|meld| {
168                    if !meld.tiles.iter().any(|t| t == &hand_tile.id) {
169                        return;
170                    }
171
172                    melds.push(PossibleMeld {
173                        discard_tile: Some(hand_tile.id),
174                        is_concealed: meld.is_concealed,
175                        is_mahjong: meld.is_mahjong,
176                        is_upgrade: meld.is_upgrade,
177                        player_id: meld.player_id.clone(),
178                        tiles: meld.tiles.clone(),
179                    });
180                });
181        });
182
183        melds
184    }
185
186    pub fn get_current_player(&self) -> Option<PlayerId> {
187        self.players.get(self.round.player_index).cloned()
188    }
189
190    pub fn get_board_tile_player_diff(
191        &self,
192        round: Option<&Round>,
193        hand: Option<&Hand>,
194        player_id: &PlayerId,
195    ) -> PlayerDiff {
196        let round = round.unwrap_or(&self.round);
197        let tile_claimed = round.tile_claimed.clone();
198
199        if let Some(tile_claimed) = tile_claimed {
200            tile_claimed.by?;
201
202            let hand = match hand {
203                Some(hand) => hand,
204                None => self.table.hands.0.get(player_id).unwrap(),
205            };
206            if !hand.list.iter().any(|h| h.id == tile_claimed.id) {
207                return None;
208            }
209
210            let player_index = self.players.iter().position(|p| p == player_id);
211            let other_player_index = self.players.iter().position(|p| *p == tile_claimed.from);
212
213            if player_index.is_none() || other_player_index.is_none() {
214                return None;
215            }
216
217            let player_index = player_index.unwrap();
218            let other_player_index = other_player_index.unwrap();
219            let raw_diff = player_index as i32 - other_player_index as i32;
220
221            return Some(if raw_diff == -3 { 1 } else { raw_diff });
222        }
223
224        None
225    }
226
227    pub fn get_dealer(&self) -> Option<&PlayerId> {
228        self.players.get(self.round.dealer_player_index)
229    }
230
231    pub fn get_player_wind(&self) -> Wind {
232        let current_player = self.get_current_player().unwrap();
233
234        self.round.get_player_wind(&self.players.0, &current_player)
235    }
236}
237
238impl Game {
239    pub fn set_players(&mut self, players: &Players) {
240        if players.len() != self.players.len() {
241            return;
242        }
243
244        let current_players = self.players.clone();
245
246        players.clone_into(&mut self.players);
247
248        for (index, player_id) in self.players.iter().enumerate() {
249            let current_player = current_players.get(index).unwrap();
250            let player_hand = self.table.hands.remove(current_player);
251            let score = self.score.remove(current_player);
252
253            self.table.hands.insert(player_id, player_hand);
254            self.score.insert(player_id, score);
255        }
256    }
257
258    pub fn say_mahjong(&mut self, player_id: &PlayerId) -> Result<(), CanSayMahjongError> {
259        let hand = self.table.hands.get(player_id).unwrap();
260
261        hand.can_say_mahjong()?;
262
263        self.calculate_hand_score(player_id);
264        let player_index = self.players.iter().position(|p| p == player_id);
265
266        if player_index.is_none() {
267            return Err(CanSayMahjongError::PlayerNotFound);
268        }
269
270        let player_index = player_index.unwrap();
271
272        self.round.move_after_win(&mut self.phase, player_index);
273
274        if self.phase != GamePhase::End {
275            self.phase = GamePhase::InitialShuffle;
276        }
277
278        Ok(())
279    }
280
281    pub fn pass_null_round(&mut self) -> Result<(), PassNullRoundError> {
282        if self.table.draw_wall.can_draw() {
283            return Err(PassNullRoundError::WallNotEmpty);
284        }
285
286        if self.round.tile_claimed.is_some() {
287            for hand in self.table.hands.0.values() {
288                if hand.can_drop_tile() {
289                    return Err(PassNullRoundError::HandCanDropTile);
290                }
291
292                if hand.can_say_mahjong().is_ok() {
293                    return Err(PassNullRoundError::HandCanSayMahjong);
294                }
295            }
296        }
297
298        self.round.move_after_draw(&mut self.phase);
299
300        if self.phase != GamePhase::End {
301            self.phase = GamePhase::InitialShuffle;
302        }
303
304        Ok(())
305    }
306
307    pub fn start(&mut self, shuffle_players: bool) {
308        if self.phase != GamePhase::Beginning {
309            return;
310        }
311
312        self.phase = GamePhase::WaitingPlayers;
313
314        self.complete_players(shuffle_players).unwrap_or_default();
315    }
316
317    pub fn decide_dealer(&mut self) -> Result<(), DecideDealerError> {
318        let mut new_players = self.players.clone();
319        let winds = self.round.get_initial_winds_slice();
320
321        winds.iter(|(player_index, player_wind)| {
322            let wind_index = WINDS_ROUND_ORDER
323                .iter()
324                .position(|w| w == player_wind)
325                .unwrap();
326
327            new_players.0[wind_index].clone_from(&self.players.0[player_index]);
328        });
329
330        self.round.dealer_player_index = 0;
331        self.round.east_player_index = 0;
332        self.round.player_index = 0;
333        self.round.tile_claimed = None;
334        self.players = new_players;
335
336        self.phase = GamePhase::InitialShuffle;
337
338        Ok(())
339    }
340
341    pub fn prepare_table(&mut self, with_dead_wall: bool) {
342        self.table = DEFAULT_DECK.create_table(&self.players);
343        self.table.draw_wall.position_tiles(Some(PositionTilesOpts {
344            shuffle: Some(true),
345            dead_wall: Some(with_dead_wall),
346        }));
347        self.phase = GamePhase::InitialDraw;
348    }
349
350    fn draw_tile_for_player(&mut self, player_id: &PlayerId) -> Result<(), DrawError> {
351        let player_wind = self.round.get_player_wind(&self.players.0, player_id);
352
353        loop {
354            let tile_id = self.table.draw_wall.pop_for_wind(&player_wind);
355
356            if tile_id.is_none() {
357                return Err(DrawError::NotEnoughTiles);
358            }
359
360            let tile_id = tile_id.unwrap();
361            let is_bonus = DEFAULT_DECK.0[tile_id].is_bonus();
362
363            if is_bonus {
364                let bonus_tiles = self.table.bonus_tiles.get_or_create(player_id);
365
366                bonus_tiles.push(tile_id);
367                continue;
368            }
369
370            let hand = self.table.hands.0.get_mut(player_id).unwrap();
371            hand.push(HandTile::from_id(tile_id));
372            return Ok(());
373        }
374    }
375
376    pub fn initial_draw(&mut self) -> Result<(), DrawError> {
377        let tiles_after_claim = self.style.tiles_after_claim();
378
379        for player_id in self.players.0.clone() {
380            'loop_label: loop {
381                let hand = self.table.hands.0.get(&player_id).unwrap();
382                if hand.len() == tiles_after_claim - 1 {
383                    break 'loop_label;
384                }
385
386                self.draw_tile_for_player(&player_id)?;
387            }
388        }
389
390        self.phase = GamePhase::Playing;
391
392        Ok(())
393    }
394
395    pub fn draw_tile_from_wall(&mut self) -> DrawTileResult {
396        if self.table.draw_wall.is_empty() {
397            return DrawTileResult::WallExhausted;
398        }
399
400        if self.round.wall_tile_drawn.is_some() {
401            return DrawTileResult::AlreadyDrawn;
402        }
403
404        let player_wind = self.get_player_wind();
405        let tile_id = self.table.draw_wall.pop_for_wind(&player_wind);
406
407        if tile_id.is_none() {
408            return DrawTileResult::WallExhausted;
409        }
410
411        let tile_id = tile_id.unwrap();
412
413        let tile = &DEFAULT_DECK.0[tile_id];
414
415        if tile.is_bonus() {
416            let bonus_tiles = self
417                .table
418                .bonus_tiles
419                .get_or_create(&self.get_current_player().unwrap());
420
421            bonus_tiles.push(tile_id);
422
423            return DrawTileResult::Bonus(tile_id);
424        }
425
426        let wall_tile_drawn = Some(tile_id);
427        self.round.wall_tile_drawn = wall_tile_drawn;
428        let player_id = self.get_current_player().unwrap();
429
430        let hand = self.table.hands.0.get_mut(&player_id).unwrap();
431        hand.push(HandTile::from_id(tile_id));
432
433        DrawTileResult::Normal(tile_id)
434    }
435
436    pub fn discard_tile_to_board(&mut self, tile_id: &TileId) -> Result<(), DiscardTileError> {
437        let player_with_max_tiles = self
438            .players
439            .iter()
440            .find(|p| self.table.hands.get(p).unwrap().len() == self.style.tiles_after_claim());
441
442        if player_with_max_tiles.is_none() {
443            return Err(DiscardTileError::NoPlayerCanDiscard);
444        }
445
446        let player_id = player_with_max_tiles.unwrap().clone();
447        let player_hand = self.table.hands.0.get_mut(&player_id).unwrap();
448        let tiles_with_id = player_hand
449            .list
450            .iter()
451            .filter(|t| t.id == *tile_id)
452            .collect::<Vec<_>>();
453        let tile_index = player_hand
454            .list
455            .iter()
456            .position(|t| &t.id == tile_id && (tiles_with_id.len() == 1 || t.set_id.is_none()));
457
458        if tile_index.is_none() {
459            return Err(DiscardTileError::PlayerHasNoTile);
460        }
461
462        let tile_index = tile_index.unwrap();
463        let tile = player_hand.get(tile_index);
464
465        if !tile.concealed {
466            return Err(DiscardTileError::TileIsExposed);
467        }
468
469        if tile.set_id.is_some() {
470            return Err(DiscardTileError::TileIsPartOfMeld);
471        }
472
473        if let Some(tile_claimed) = self.round.tile_claimed.clone() {
474            if let Some(by) = tile_claimed.by {
475                if by == player_id
476                    && tile.id != tile_claimed.id
477                    && player_hand
478                        .list
479                        .iter()
480                        .find(|t| t.id == tile_claimed.id)
481                        .unwrap()
482                        .set_id
483                        .is_none()
484                {
485                    return Err(DiscardTileError::ClaimedAnotherTile);
486                }
487            }
488        }
489
490        self.table.board.0.push(tile.id);
491
492        self.round.tile_claimed = Some(RoundTileClaimed {
493            from: player_id.clone(),
494            id: tile.id,
495            by: None,
496        });
497
498        player_hand.list.remove(tile_index);
499        
500        Ok(())
501    }
502
503    pub fn create_meld(
504        &mut self,
505        player_id: &PlayerId,
506        tiles: &[TileId],
507        is_upgrade: bool,
508        is_concealed: bool,
509    ) -> Result<(), CreateMeldError> {
510        let tiles_set = tiles.iter().cloned().collect::<FxHashSet<TileId>>();
511        let hand = self.table.hands.get(player_id);
512        let sub_hand_tiles = hand
513            .unwrap()
514            .list
515            .iter()
516            .filter(|t| tiles_set.contains(&t.id))
517            .cloned()
518            .collect::<Vec<HandTile>>();
519
520        if !is_upgrade
521            && sub_hand_tiles
522                .iter()
523                .any(|t| t.set_id.is_some() || !t.concealed)
524        {
525            return Err(CreateMeldError::TileIsPartOfMeld);
526        }
527
528        let sub_hand = Hand::new(sub_hand_tiles);
529
530        let board_tile_player_diff =
531            self.get_board_tile_player_diff(None, Some(&sub_hand), player_id);
532
533        let opts_claimed_tile = get_tile_claimed_id_for_user(player_id, &self.round.tile_claimed);
534        let tiles_full: Vec<&Tile> = tiles.iter().map(|t| &DEFAULT_DECK.0[*t]).collect();
535
536        let opts = SetCheckOpts {
537            board_tile_player_diff,
538            claimed_tile: opts_claimed_tile,
539            sub_hand: &tiles_full,
540        };
541
542        let mut is_kong = false;
543
544        if get_is_pung(&opts) || get_is_chow(&opts) || {
545            is_kong = get_is_kong(&opts);
546            is_kong
547        } {
548            if (is_upgrade && !is_kong) || (is_concealed && opts_claimed_tile.is_some()) {
549                return Err(CreateMeldError::NotMeld);
550            }
551
552            let set_id = Uuid::new_v4().to_string();
553            let player_hand = self.table.hands.0.get_mut(player_id).unwrap();
554
555            for tile in tiles.iter() {
556                let tile = player_hand.list.iter().find(|t| t.id == *tile);
557
558                if tile.is_none() {
559                    return Err(CreateMeldError::NotMeld);
560                }
561            }
562
563            player_hand
564                .list
565                .iter_mut()
566                .filter(|t| tiles.contains(&t.id))
567                .for_each(|tile| {
568                    tile.concealed = is_concealed;
569                    tile.set_id = Some(set_id.clone());
570                });
571
572            if is_kong {
573                let moved_tile = player_hand
574                    .list
575                    .iter()
576                    .find(|t| t.set_id == Some(set_id.clone()))
577                    .unwrap()
578                    .clone();
579
580                self.draw_tile_for_player(player_id)
581                    .map_err(|_| match self.pass_null_round() {
582                        Ok(_) => CreateMeldError::EndRound,
583                        Err(_) => CreateMeldError::NotMeld,
584                    })?;
585
586                let next_player_hand = self.table.hands.0.get_mut(player_id).unwrap();
587
588                let position = next_player_hand
589                    .list
590                    .iter()
591                    .position(|t| t.id == moved_tile.id)
592                    .unwrap();
593                next_player_hand.list.remove(position);
594                next_player_hand.kong_tiles.insert(KongTile {
595                    set_id: set_id.clone(),
596                    concealed: is_concealed,
597                    id: moved_tile.id,
598                });
599            }
600
601            return Ok(());
602        }
603
604        Err(CreateMeldError::NotMeld)
605    }
606
607    pub fn break_meld(
608        &mut self,
609        player_id: &PlayerId,
610        set_id: &String,
611    ) -> Result<(), BreakMeldError> {
612        let hand = self.table.hands.0.get_mut(player_id);
613
614        if hand.is_none() {
615            return Err(BreakMeldError::MissingHand);
616        }
617
618        let hand = hand.unwrap();
619
620        if hand.kong_tiles.iter().any(|t| t.set_id == set_id.clone()) {
621            return Err(BreakMeldError::MeldIsKong);
622        }
623
624        for hand_tile in hand.list.iter_mut() {
625            if hand_tile.set_id.is_some() && hand_tile.set_id.as_ref() == Some(set_id) {
626                if !hand_tile.concealed {
627                    return Err(BreakMeldError::TileIsExposed);
628                }
629
630                hand_tile.set_id = None;
631            }
632        }
633
634        Ok(())
635    }
636
637    pub fn claim_tile(&mut self, player_id: &PlayerId) -> bool {
638        let player_hand = self.table.hands.0.get_mut(player_id);
639        if player_hand.is_none() {
640            return false;
641        }
642        let player_hand = player_hand.unwrap();
643
644        if player_hand.len() != self.style.tiles_after_claim() - 1
645            || self.round.tile_claimed.is_none()
646            || self.table.board.0.is_empty()
647        {
648            return false;
649        }
650
651        let tile = self.table.board.0.pop().unwrap();
652
653        let mut tile_claimed = self.round.tile_claimed.clone().unwrap();
654        tile_claimed.by = Some(player_id.clone());
655
656        self.round.tile_claimed = Some(tile_claimed);
657        self.round.player_index = self.players.iter().position(|p| p == player_id).unwrap();
658
659        player_hand.push(HandTile {
660            concealed: true,
661            id: tile,
662            set_id: None,
663        });
664
665        true
666    }
667
668    pub fn update_version(&mut self) {
669        self.version = Uuid::new_v4().to_string();
670    }
671
672    pub fn update_id(&mut self, id: Option<&str>) {
673        self.id = id.map_or_else(
674            || Uuid::new_v4().to_string(),
675            |inner_id| inner_id.to_string(),
676        );
677    }
678
679    pub fn complete_players(&mut self, shuffle_players: bool) -> Result<(), &'static str> {
680        if self.phase != GamePhase::WaitingPlayers {
681            return Err("Game is not waiting for players");
682        }
683
684        let players_num = Self::get_players_num(&self.style);
685
686        if self.players.len() != players_num {
687            return Err("Not enough players");
688        }
689
690        if shuffle_players {
691            self.players.shuffle();
692        }
693
694        for player_id in self.players.0.clone() {
695            self.table.hands.insert(player_id.clone(), Hand::default());
696            self.score.insert(player_id, 0);
697        }
698
699        self.phase = GamePhase::DecidingDealer;
700
701        Ok(())
702    }
703
704    pub fn set_wind_for_player(&mut self, player_id: &PlayerId, wind: &Wind) {
705        let current_player_with_wind = self
706            .players
707            .0
708            .iter()
709            .find(|p| self.round.get_player_wind(&self.players.0, p) == *wind)
710            .unwrap()
711            .clone();
712
713        if &current_player_with_wind == player_id {
714            return;
715        }
716
717        self.players.swap(player_id, &current_player_with_wind);
718    }
719}