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
85const 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}