mahjong_core/
meld.rs

1use crate::{
2    deck::DEFAULT_DECK, round::TileClaimed, HandTile, PlayerId, SetId, Suit, SuitTile, Tile, TileId,
3};
4use serde::{Deserialize, Serialize};
5use ts_rs::TS;
6
7pub type PlayerDiff = Option<i32>;
8
9#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TS)]
10#[ts(export)]
11pub enum MeldType {
12    Chow,
13    Kong,
14    Pair,
15    Pung,
16}
17
18impl MeldType {
19    pub fn from_tiles(tiles: &[TileId]) -> Option<Self> {
20        if tiles.len() < 2 || tiles.len() > 4 {
21            return None;
22        }
23
24        let tiles = tiles
25            .iter()
26            .map(|t| &DEFAULT_DECK.0[*t])
27            .collect::<Vec<&Tile>>();
28
29        let opts = SetCheckOpts {
30            board_tile_player_diff: None,
31            claimed_tile: None,
32            sub_hand: &tiles,
33        };
34
35        if get_is_pung(&opts) {
36            return Some(Self::Pung);
37        }
38
39        if get_is_chow(&opts) {
40            return Some(Self::Chow);
41        }
42
43        if get_is_kong(&opts) {
44            return Some(Self::Kong);
45        }
46
47        if get_is_pair(opts.sub_hand) {
48            return Some(Self::Pair);
49        }
50
51        None
52    }
53}
54
55#[derive(Debug, Clone)]
56pub struct SetCheckOpts<'a> {
57    pub board_tile_player_diff: PlayerDiff,
58    pub claimed_tile: Option<TileId>,
59    pub sub_hand: &'a [&'a Tile],
60}
61
62pub fn get_is_pung(opts: &SetCheckOpts) -> bool {
63    if opts.sub_hand.len() != 3 {
64        return false;
65    }
66
67    let mut last_tile = opts.sub_hand[0];
68    if last_tile.is_bonus() {
69        return false;
70    }
71
72    for tile_index in 1..3 {
73        let tile = opts.sub_hand[tile_index];
74
75        if !tile.is_same_content(last_tile) {
76            return false;
77        }
78
79        last_tile = tile;
80    }
81
82    true
83}
84
85// This approach is used for performance
86const DUMMY_SUIT: SuitTile = SuitTile {
87    id: 0,
88    value: 0,
89    suit: crate::Suit::Dots,
90};
91
92pub fn get_is_chow(opts: &SetCheckOpts) -> bool {
93    if opts.sub_hand.len() != 3 {
94        return false;
95    };
96
97    if let Some(board_tile_player_diff) = opts.board_tile_player_diff {
98        if let Some(claimed_tile) = opts.claimed_tile {
99            if board_tile_player_diff != 1 {
100                let has_same_claimed_tile =
101                    opts.sub_hand.iter().any(|t| t.get_id() == claimed_tile);
102
103                if has_same_claimed_tile {
104                    return false;
105                }
106            }
107        }
108    }
109
110    let mut suit_tiles: [&SuitTile; 3] = [&DUMMY_SUIT, &DUMMY_SUIT, &DUMMY_SUIT];
111    let mut suit: Option<Suit> = None;
112
113    for (idx, tile) in opts.sub_hand.iter().enumerate() {
114        match tile {
115            Tile::Suit(suit_tile) => {
116                if suit.is_some() {
117                    if Some(suit_tile.suit) != suit {
118                        return false;
119                    }
120                } else {
121                    suit = Some(suit_tile.suit);
122                }
123                suit_tiles[idx] = suit_tile
124            }
125            _ => {
126                return false;
127            }
128        }
129    }
130
131    suit_tiles.sort_by(|a, b| a.value.cmp(&b.value));
132
133    let mut last_tile = suit_tiles[0];
134
135    for tile in suit_tiles.iter().skip(1).take(2) {
136        if last_tile.value + 1 != tile.value {
137            return false;
138        }
139
140        last_tile = tile;
141    }
142
143    true
144}
145
146pub fn get_is_kong(opts: &SetCheckOpts) -> bool {
147    if opts.sub_hand.len() != 4 {
148        return false;
149    }
150
151    let mut last_tile = opts.sub_hand[0];
152    if last_tile.is_bonus() {
153        return false;
154    }
155
156    for tile_index in 1..4 {
157        let tile = opts.sub_hand[tile_index];
158
159        if !tile.is_same_content(last_tile) {
160            return false;
161        }
162
163        last_tile = tile;
164    }
165
166    true
167}
168
169pub fn get_tile_claimed_id_for_user(
170    player_id: &PlayerId,
171    tile_claimed: &TileClaimed,
172) -> Option<TileId> {
173    if tile_claimed.is_none() {
174        return None;
175    }
176
177    let tile_claimed = tile_claimed.clone().unwrap();
178
179    tile_claimed.clone().by?;
180
181    if tile_claimed.by.unwrap() == *player_id {
182        return Some(tile_claimed.id);
183    }
184
185    None
186}
187
188pub struct RemoveMeldOpts {
189    hand: Vec<HandTile>,
190    set_id: SetId,
191}
192
193pub fn remove_meld(opts: RemoveMeldOpts) {
194    let mut meld_tiles = opts
195        .hand
196        .iter()
197        .filter(|h| h.set_id == opts.set_id)
198        .cloned()
199        .collect::<Vec<HandTile>>();
200
201    for meld_tile in meld_tiles.clone() {
202        if !meld_tile.concealed {
203            return;
204        }
205    }
206
207    meld_tiles.iter_mut().for_each(|t| {
208        t.set_id = None;
209    });
210}
211
212pub fn get_is_pair(hand: &[&Tile]) -> bool {
213    if hand.len() != 2 {
214        return false;
215    }
216
217    hand[0].is_same_content(hand[1])
218}
219
220#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TS)]
221#[ts(export)]
222pub struct PossibleMeld {
223    pub discard_tile: Option<TileId>,
224    pub is_concealed: bool,
225    pub is_mahjong: bool,
226    pub is_upgrade: bool,
227    pub player_id: PlayerId,
228    pub tiles: Vec<TileId>,
229}
230
231impl PossibleMeld {
232    pub fn sort_tiles(&mut self) {
233        self.tiles.sort_by(|a, b| {
234            let tile_a = &DEFAULT_DECK.0[*a];
235            let tile_b = &DEFAULT_DECK.0[*b];
236
237            tile_a.cmp_custom(tile_b)
238        });
239    }
240}