mahjong_core/
hand.rs

1use crate::{
2    deck::DEFAULT_DECK,
3    game::GameStyle,
4    meld::{
5        get_is_chow, get_is_kong, get_is_pair, get_is_pung, MeldType, PlayerDiff, PossibleMeld,
6        SetCheckOpts,
7    },
8    PlayerId, Tile, TileId,
9};
10use rustc_hash::{FxHashMap, FxHashSet};
11use serde::{Deserialize, Serialize};
12use strum_macros::EnumIter;
13use ts_rs::TS;
14
15pub type SetIdContent = String;
16pub type SetId = Option<SetIdContent>;
17
18#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
19pub struct HandPossibleMeld {
20    pub is_mahjong: bool,
21    pub is_concealed: bool,
22    pub is_upgrade: bool,
23    pub tiles: Vec<TileId>,
24}
25
26impl From<PossibleMeld> for HandPossibleMeld {
27    fn from(meld: PossibleMeld) -> Self {
28        Self {
29            is_concealed: meld.is_concealed,
30            is_mahjong: meld.is_mahjong,
31            is_upgrade: meld.is_upgrade,
32            tiles: meld.tiles.clone(),
33        }
34    }
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
38#[ts(export)]
39pub struct KongTile {
40    pub concealed: bool,
41    pub id: TileId,
42    pub set_id: SetIdContent,
43}
44
45#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
46#[ts(export)]
47pub struct HandTile {
48    pub concealed: bool,
49    pub id: TileId,
50    pub set_id: SetId,
51}
52
53impl HandTile {
54    pub fn from_id(id: TileId) -> Self {
55        Self {
56            id,
57            set_id: None,
58            concealed: true,
59        }
60    }
61    pub fn from_tile(tile: &Tile) -> Self {
62        Self {
63            id: tile.get_id(),
64            set_id: None,
65            concealed: true,
66        }
67    }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
71pub struct Hand {
72    pub list: Vec<HandTile>,
73    pub kong_tiles: FxHashSet<KongTile>,
74    #[serde(skip)]
75    pub style: Option<GameStyle>,
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TS)]
79#[ts(export)]
80pub struct HandMeld {
81    pub meld_type: MeldType,
82    pub tiles: Vec<TileId>,
83}
84
85#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq, TS)]
86#[ts(export)]
87pub struct HandMelds {
88    pub melds: Vec<HandMeld>,
89    pub tiles_without_meld: usize,
90}
91
92// Proxied
93impl Hand {
94    pub fn get(&self, index: usize) -> &HandTile {
95        self.list.get(index).unwrap()
96    }
97
98    pub fn len(&self) -> usize {
99        self.list.len()
100    }
101
102    pub fn is_empty(&self) -> bool {
103        self.list.is_empty()
104    }
105
106    pub fn push(&mut self, tile: HandTile) {
107        self.list.push(tile)
108    }
109}
110
111#[derive(Debug, EnumIter, Eq, PartialEq, Clone)]
112pub enum SortHandError {
113    NotSortedMissingTile,
114}
115
116#[derive(Debug, EnumIter, Eq, PartialEq, Clone)]
117pub enum CanSayMahjongError {
118    CantDrop,
119    NotPair,
120    PlayerNotFound,
121}
122
123impl Hand {
124    pub fn new(list: Vec<HandTile>) -> Self {
125        Self {
126            list,
127            style: None,
128            kong_tiles: FxHashSet::default(),
129        }
130    }
131
132    pub fn from_ref_vec(tiles: &[&HandTile]) -> Self {
133        Self {
134            kong_tiles: FxHashSet::default(),
135            list: tiles.iter().cloned().cloned().collect(),
136            style: None,
137        }
138    }
139
140    pub fn from_ids(tiles: &[TileId]) -> Self {
141        Self {
142            list: tiles.iter().cloned().map(HandTile::from_id).collect(),
143            kong_tiles: FxHashSet::default(),
144            style: None,
145        }
146    }
147
148    pub fn sort_default(&mut self) {
149        self.list.sort_by(|a, b| {
150            let tile_a = &DEFAULT_DECK.0.get(a.id);
151            let tile_b = &DEFAULT_DECK.0.get(b.id);
152
153            if tile_a.is_none() || tile_b.is_none() {
154                return std::cmp::Ordering::Equal;
155            }
156
157            tile_a.unwrap().cmp_custom(tile_b.unwrap())
158        });
159    }
160
161    // `tiles` can be a sub-set of the whole hand
162    pub fn sort_by_tiles(&mut self, tiles: &[TileId]) -> Result<(), SortHandError> {
163        let hand_copy = self
164            .list
165            .clone()
166            .iter()
167            .map(|t| t.id)
168            .collect::<Vec<TileId>>();
169        let hand_set = hand_copy.iter().collect::<FxHashSet<&TileId>>();
170
171        if tiles.iter().any(|t| !hand_set.contains(&t)) {
172            return Err(SortHandError::NotSortedMissingTile);
173        }
174
175        self.list.sort_by(|a, b| {
176            let tile_a = tiles.iter().position(|t| *t == a.id);
177            let tile_b = tiles.iter().position(|t| *t == b.id);
178
179            if tile_a.is_none() && tile_b.is_none() {
180                return std::cmp::Ordering::Equal;
181            }
182
183            if tile_a.is_none() {
184                return std::cmp::Ordering::Greater;
185            }
186
187            if tile_b.is_none() {
188                return std::cmp::Ordering::Less;
189            }
190
191            let (tile_a, tile_b) = (tile_a.unwrap(), tile_b.unwrap());
192
193            tile_a.cmp(&tile_b)
194        });
195
196        Ok(())
197    }
198
199    pub fn get_melds(&self) -> HandMelds {
200        let mut melds = HandMelds::default();
201        let sets_groups = self.get_sets_groups();
202
203        for (set, tiles) in sets_groups.iter() {
204            if set.is_none() {
205                melds.tiles_without_meld = tiles.len();
206                continue;
207            }
208
209            let meld_type = MeldType::from_tiles(tiles);
210
211            if meld_type.is_none() {
212                continue;
213            }
214
215            let meld_type = meld_type.unwrap();
216
217            melds.melds.push(HandMeld {
218                meld_type,
219                tiles: tiles.clone(),
220            });
221        }
222
223        melds
224    }
225
226    pub fn can_say_mahjong(&self) -> Result<(), CanSayMahjongError> {
227        if !self.can_drop_tile() {
228            return Err(CanSayMahjongError::CantDrop);
229        }
230
231        let tiles_without_meld: Vec<&Tile> = self
232            .list
233            .iter()
234            .filter(|t| t.set_id.is_none())
235            .map(|t| &DEFAULT_DECK.0[t.id])
236            .collect();
237
238        let is_pair = get_is_pair(&tiles_without_meld);
239
240        if !is_pair {
241            return Err(CanSayMahjongError::NotPair);
242        }
243
244        Ok(())
245    }
246
247    fn get_pungs_tiles(&self) -> Vec<(Tile, SetId)> {
248        let sets_ids: FxHashSet<SetIdContent> =
249            self.list.iter().filter_map(|t| t.set_id.clone()).collect();
250        let mut pungs: Vec<(Tile, SetId)> = vec![];
251        let existing_kongs = self
252            .kong_tiles
253            .iter()
254            .map(|t| t.set_id.clone())
255            .collect::<FxHashSet<SetIdContent>>();
256
257        for set_id in sets_ids {
258            let tiles: Vec<&Tile> = self
259                .list
260                .iter()
261                .filter(|t| t.set_id == Some(set_id.clone()))
262                .map(|t| &DEFAULT_DECK.0[t.id])
263                .collect();
264
265            let is_pung = get_is_pung(&SetCheckOpts {
266                board_tile_player_diff: PlayerDiff::default(),
267                claimed_tile: None,
268                sub_hand: &tiles,
269            });
270
271            if is_pung && !existing_kongs.contains(&set_id) {
272                pungs.push((tiles[0].clone(), Some(set_id)));
273            }
274        }
275
276        pungs
277    }
278
279    pub fn get_possible_melds(
280        &self,
281        board_tile_player_diff: PlayerDiff,
282        claimed_tile: Option<TileId>,
283        check_for_mahjong: bool,
284    ) -> Vec<HandPossibleMeld> {
285        let hand_filtered: Vec<&HandTile> =
286            self.list.iter().filter(|h| h.set_id.is_none()).collect();
287        let mut melds: Vec<HandPossibleMeld> = vec![];
288        let existing_pungs = self.get_pungs_tiles();
289
290        if check_for_mahjong {
291            if self.can_say_mahjong().is_ok() {
292                let tiles = self
293                    .list
294                    .iter()
295                    .filter(|t| t.set_id.is_none())
296                    .map(|t| t.id)
297                    .collect();
298                let meld = HandPossibleMeld {
299                    is_upgrade: false,
300                    is_concealed: false,
301                    is_mahjong: true,
302                    tiles,
303                };
304
305                melds.push(meld);
306            }
307
308            return melds;
309        }
310
311        for first_tile_index in 0..hand_filtered.len() {
312            let first_tile = hand_filtered[first_tile_index].id;
313            let first_tile_full = &DEFAULT_DECK.0[first_tile];
314
315            for second_tile_index in (first_tile_index + 1)..hand_filtered.len() {
316                let second_tile = hand_filtered[second_tile_index].id;
317                let second_tile_full = &DEFAULT_DECK.0[second_tile];
318
319                if !first_tile_full.is_same_type(second_tile_full) {
320                    continue;
321                }
322
323                for third_tile_index in (second_tile_index + 1)..hand_filtered.len() {
324                    let third_tile = hand_filtered[third_tile_index].id;
325                    let third_tile_full = &DEFAULT_DECK.0[third_tile];
326                    if !first_tile_full.is_same_type(third_tile_full) {
327                        continue;
328                    }
329
330                    let sub_hand = [first_tile_full, second_tile_full, third_tile_full];
331
332                    let opts = SetCheckOpts {
333                        board_tile_player_diff,
334                        claimed_tile,
335                        sub_hand: &sub_hand,
336                    };
337
338                    if get_is_pung(&opts) || get_is_chow(&opts) {
339                        let is_concealed = claimed_tile.is_none();
340
341                        let meld = HandPossibleMeld {
342                            is_concealed,
343                            is_mahjong: false,
344                            is_upgrade: false,
345                            tiles: vec![first_tile, second_tile, third_tile],
346                        };
347                        melds.push(meld);
348                    }
349
350                    for forth_tile in hand_filtered.iter().skip(third_tile_index + 1) {
351                        let forth_tile_full = &DEFAULT_DECK.0[forth_tile.id];
352
353                        let mut opts = opts.clone();
354                        let sub_hand_inner = [
355                            first_tile_full,
356                            second_tile_full,
357                            third_tile_full,
358                            forth_tile_full,
359                        ];
360                        opts.sub_hand = &sub_hand_inner;
361
362                        if get_is_kong(&opts) {
363                            let is_concealed = claimed_tile.is_none();
364
365                            let meld = HandPossibleMeld {
366                                is_concealed,
367                                is_mahjong: false,
368                                is_upgrade: false,
369                                tiles: vec![first_tile, second_tile, third_tile, forth_tile.id],
370                            };
371                            melds.push(meld);
372                        }
373                    }
374                }
375            }
376
377            for (concealed_pung_tile, set_id) in existing_pungs.iter() {
378                if first_tile_full.is_same_content(concealed_pung_tile) {
379                    let is_concealed = claimed_tile.is_none();
380                    let mut tiles: Vec<TileId> = self
381                        .list
382                        .iter()
383                        .filter(|t| t.set_id == *set_id)
384                        .map(|t| t.id)
385                        .collect();
386                    tiles.push(first_tile);
387
388                    let meld = HandPossibleMeld {
389                        is_mahjong: false,
390                        is_upgrade: true,
391                        is_concealed,
392                        tiles,
393                    };
394                    melds.push(meld);
395                }
396            }
397        }
398
399        melds
400    }
401
402    pub fn get_has_tile(&self, tile_id: &TileId) -> bool {
403        self.list.iter().any(|t| t.id == *tile_id)
404    }
405
406    pub fn get_sets_groups(&self) -> FxHashMap<SetId, Vec<TileId>> {
407        let mut sets: FxHashMap<SetId, Vec<TileId>> = FxHashMap::default();
408
409        for tile in &self.list {
410            let set_id = tile.set_id.clone();
411
412            sets.entry(set_id.clone()).or_default().push(tile.id);
413        }
414
415        for kong_tile in &self.kong_tiles {
416            sets.entry(Some(kong_tile.set_id.clone()))
417                .or_default()
418                .push(kong_tile.id);
419        }
420
421        sets
422    }
423
424    pub fn can_drop_tile(&self) -> bool {
425        self.list.len()
426            == self
427                .style
428                .as_ref()
429                .unwrap_or(&GameStyle::HongKong)
430                .tiles_after_claim()
431    }
432}
433
434impl From<Hand> for Vec<TileId> {
435    fn from(hand: Hand) -> Self {
436        hand.list.iter().map(|t| t.id).collect()
437    }
438}
439
440pub type HandsMap = FxHashMap<PlayerId, Hand>;
441#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default, TS)]
442#[ts(export)]
443pub struct Hands(pub HandsMap);
444
445// Proxied
446impl Hands {
447    pub fn get(&self, player: &PlayerId) -> Option<Hand> {
448        self.0.get(player).cloned()
449    }
450}
451
452// Proxied
453impl Hands {
454    pub fn remove(&mut self, player: &PlayerId) -> Hand {
455        self.0.remove(player).unwrap()
456    }
457
458    pub fn insert(&mut self, player: impl AsRef<str>, hand: Hand) -> Option<Hand> {
459        self.0.insert(player.as_ref().to_string(), hand)
460    }
461}
462
463impl Hands {
464    pub fn get_player_hand_len(&self, player: &str) -> usize {
465        self.0.get(player).unwrap().len()
466    }
467
468    pub fn get_style(&self) -> GameStyle {
469        self.0
470            .values()
471            .next()
472            .cloned()
473            .unwrap()
474            .style
475            .unwrap_or_default()
476    }
477}
478
479impl Hands {
480    pub fn insert_ids(&mut self, player: &str, tiles: &[TileId]) -> &mut Self {
481        self.0.insert(player.to_string(), Hand::from_ids(tiles));
482        self
483    }
484
485    pub fn sort_player_hand(&mut self, player: &PlayerId) {
486        let mut hand = self.0.get(player).unwrap().clone();
487        hand.sort_default();
488        self.0.insert(player.clone(), hand);
489    }
490}