1use crate::{
2 deck::DEFAULT_DECK,
3 game::{GameStyle, GameVersion, Players},
4 meld::{PlayerDiff, PossibleMeld},
5 table::BonusTiles,
6 Board, Game, GameId, GamePhase, Hand, HandTile, Hands, PlayerId, Score, TileId, Wind,
7 WINDS_ROUND_ORDER,
8};
9use rustc_hash::{FxHashMap, FxHashSet};
10use serde::{Deserialize, Serialize};
11use ts_rs::TS;
12
13#[derive(Debug, Clone, Serialize, Deserialize, TS)]
14#[ts(export)]
15pub struct RoundSummary {
16 consecutive_same_seats: usize,
17 pub dealer_player_index: usize,
18 east_player_index: usize,
19 pub discarded_tile: Option<TileId>,
20 pub round_index: u32,
21 pub player_index: usize,
22 wind: Wind,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, TS)]
26#[ts(export)]
27pub struct VisibleMeld {
28 set_id: String,
29 tiles: Vec<TileId>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, TS)]
33pub struct OtherPlayerHand {
34 pub tiles: usize,
35 pub visible: Hand,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, TS)]
39#[ts(export)]
40pub struct HandTileStat {
41 in_other_melds: usize,
42 in_board: usize,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, TS)]
46pub struct OtherPlayerHands(pub FxHashMap<PlayerId, OtherPlayerHand>);
47
48impl OtherPlayerHands {
49 pub fn from_hands(hands: &Hands, player_id: &PlayerId) -> Self {
50 let mut other_hands = FxHashMap::default();
51
52 for (id, hand) in hands.0.iter() {
53 if id != player_id {
54 let visible_tiles: Vec<HandTile> =
55 hand.list.iter().filter(|t| !t.concealed).cloned().collect();
56 other_hands.insert(
57 id.clone(),
58 OtherPlayerHand {
59 tiles: hand.len(),
60 visible: Hand::new(visible_tiles),
61 },
62 );
63 }
64 }
65
66 Self(other_hands)
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, TS)]
71#[ts(export)]
72pub struct GameSummary {
73 pub board: Board,
74 pub bonus_tiles: BonusTiles,
75 pub draw_wall_count: usize,
76 pub hand: Option<Hand>,
77 pub id: GameId,
78 pub other_hands: OtherPlayerHands,
79 pub phase: GamePhase,
80 pub player_id: PlayerId,
81 pub players: Players,
82 pub round: RoundSummary,
83 pub score: Score,
84 pub style: GameStyle,
85 pub version: GameVersion,
86}
87
88impl GameSummary {
89 pub fn from_game(game: &Game, player_id: &PlayerId) -> Option<Self> {
90 let discarded_tile = if let Some(tile_claimed) = game.round.tile_claimed.clone() {
91 Some(tile_claimed.id)
92 } else {
93 None
94 };
95
96 let round = RoundSummary {
97 dealer_player_index: game.round.dealer_player_index,
98 east_player_index: game.round.east_player_index,
99 discarded_tile,
100 consecutive_same_seats: game.round.consecutive_same_seats,
101 player_index: game.round.player_index,
102 wind: game.round.wind.clone(),
103 round_index: game.round.round_index,
104 };
105
106 let draw_wall_count = game.table.draw_wall.len();
107 let other_hands = OtherPlayerHands::from_hands(&game.table.hands, player_id);
108
109 Some(Self {
110 board: game.table.board.clone(),
111 bonus_tiles: game.table.bonus_tiles.clone(),
112 draw_wall_count,
113 hand: game.table.hands.get(player_id),
114 id: game.id.clone(),
115 other_hands,
116 phase: game.phase,
117 player_id: player_id.clone(),
118 players: game.players.clone(),
119 round,
120 score: game.score.clone(),
121 style: game.style.clone(),
122 version: game.version.clone(),
123 })
124 }
125
126 pub fn get_current_player(&self) -> &PlayerId {
127 &self.players.0[self.round.player_index]
128 }
129
130 pub fn get_can_claim_tile(&self) -> bool {
131 if self.hand.is_none() {
132 return false;
133 }
134
135 let tiles_after_claim = self.style.tiles_after_claim();
136 self.hand.clone().unwrap().len() < tiles_after_claim
137 && self
138 .other_hands
139 .0
140 .iter()
141 .all(|(_, hand)| hand.tiles < tiles_after_claim)
142 && self.round.discarded_tile.is_some()
143 }
144
145 pub fn get_can_pass_turn(&self) -> bool {
146 if self.get_can_claim_tile() {
147 return true;
148 }
149
150 self.phase == GamePhase::Playing
151 && self.hand.is_some()
152 && self.hand.as_ref().unwrap().len() == self.style.tiles_after_claim() - 1
153 && self.get_current_player() == &self.player_id
154 }
155
156 pub fn get_can_discard_tile(&self) -> bool {
157 self.hand.is_some() && self.hand.clone().unwrap().len() == self.style.tiles_after_claim()
158 }
159
160 pub fn get_possible_melds(&self) -> Vec<PossibleMeld> {
161 let tested_hand = self.hand.clone();
162 if tested_hand.is_none() {
163 return vec![];
164 }
165
166 let mut tested_hand = tested_hand.unwrap();
167
168 let mut possible_melds: Vec<PossibleMeld> = vec![];
169 let can_claim_tile = self.get_can_claim_tile();
170
171 let claimed_tile: Option<TileId> = self.round.discarded_tile;
172 let mut player_diff: PlayerDiff = None;
173 let player_index = self
174 .players
175 .iter()
176 .position(|p| p == &self.player_id)
177 .unwrap();
178 let current_player_index = self.round.player_index;
179
180 if can_claim_tile {
181 let tile = HandTile {
182 concealed: true,
183 id: claimed_tile.unwrap(),
184 set_id: None,
185 };
186
187 tested_hand.push(tile);
188 player_diff = Some(match player_index as i32 - current_player_index as i32 {
189 -3 => 1,
190 val => val,
191 });
192 }
193
194 let mut raw_melds = tested_hand.get_possible_melds(player_diff, claimed_tile, true);
195
196 tested_hand
197 .get_possible_melds(player_diff, claimed_tile, false)
198 .iter()
199 .for_each(|m| {
200 raw_melds.push(m.clone());
201 });
202
203 for raw_meld in raw_melds {
204 let possible_meld = PossibleMeld {
205 discard_tile: None,
206 is_concealed: raw_meld.is_concealed,
207 is_mahjong: raw_meld.is_mahjong,
208 is_upgrade: raw_meld.is_upgrade,
209 player_id: self.player_id.clone(),
210 tiles: raw_meld.tiles.clone(),
211 };
212
213 possible_melds.push(possible_meld);
214 }
215
216 possible_melds
217 }
218
219 pub fn get_players_winds(&self) -> FxHashMap<PlayerId, Wind> {
220 let mut winds = FxHashMap::default();
221
222 let east_index = WINDS_ROUND_ORDER
223 .iter()
224 .position(|w| w == &Wind::East)
225 .unwrap();
226
227 for (index, player_id) in self.players.iter().enumerate() {
228 let wind_index = (east_index + index) % WINDS_ROUND_ORDER.len();
229 let wind = WINDS_ROUND_ORDER[wind_index].clone();
230 winds.insert(player_id.clone(), wind);
231 }
232
233 winds
234 }
235
236 pub fn get_players_visible_melds(&self) -> FxHashMap<PlayerId, Vec<VisibleMeld>> {
237 let mut visible_melds_set = FxHashMap::default();
238
239 fn get_visible_melds(player_hand: &Hand) -> Vec<VisibleMeld> {
240 let mut visible_melds = vec![];
241 let player_melds = player_hand
242 .list
243 .iter()
244 .filter(|t| !t.concealed)
245 .filter_map(|t| t.set_id.clone())
246 .collect::<FxHashSet<_>>();
247
248 for meld_id in player_melds {
249 let mut tiles = player_hand
250 .list
251 .iter()
252 .filter(|t| t.set_id.as_ref() == Some(&meld_id))
253 .map(|t| t.id)
254 .collect::<Vec<_>>();
255
256 let kong_tile = player_hand
257 .kong_tiles
258 .iter()
259 .find(|t| t.set_id.as_ref() == meld_id);
260
261 if let Some(kong_tile) = kong_tile {
262 tiles.push(kong_tile.id);
263 }
264
265 visible_melds.push(VisibleMeld {
266 set_id: meld_id.clone(),
267 tiles,
268 })
269 }
270 visible_melds
271 }
272
273 if self.hand.is_none() {
274 return visible_melds_set;
275 }
276
277 visible_melds_set.insert(
278 self.player_id.clone(),
279 get_visible_melds(&self.hand.clone().unwrap()),
280 );
281
282 for (player_id, other_player_hand) in self.other_hands.0.iter() {
283 let hand = &other_player_hand.visible;
284
285 visible_melds_set.insert(player_id.clone(), get_visible_melds(hand));
286 }
287
288 visible_melds_set
289 }
290
291 pub fn get_can_pass_round(&self) -> bool {
292 let tiles_after_claim = self.style.tiles_after_claim();
293
294 self.phase == GamePhase::Playing
295 && self.hand.is_some()
296 && self.draw_wall_count == 0
297 && self.hand.as_ref().unwrap().len() < tiles_after_claim
298 && self
299 .other_hands
300 .0
301 .iter()
302 .all(|(_, hand)| hand.tiles < tiles_after_claim)
303 }
304
305 pub fn get_can_draw_tile(&self) -> bool {
306 self.phase == GamePhase::Playing
307 && self.hand.is_some()
308 && self.hand.as_ref().unwrap().len() < self.style.tiles_after_claim()
309 && self.draw_wall_count > 0
310 && self.get_current_player() == &self.player_id
311 }
312
313 pub fn get_can_say_mahjong(&self) -> bool {
314 self.phase == GamePhase::Playing
315 && self.hand.is_some()
316 && self.hand.as_ref().unwrap().can_say_mahjong().is_ok()
317 }
318
319 pub fn get_hand_stats(&self) -> FxHashMap<TileId, HandTileStat> {
320 let mut hand_stats = FxHashMap::default();
321
322 if self.hand.is_none() {
323 return hand_stats;
324 }
325
326 let hand = self.hand.as_ref().unwrap();
327
328 let mut own_meld_tiles = hand
329 .list
330 .iter()
331 .filter(|t| t.set_id.is_some())
332 .map(|t| t.id)
333 .collect::<Vec<_>>();
334
335 for kong_tile in hand.kong_tiles.iter() {
336 own_meld_tiles.push(kong_tile.id);
337 }
338
339 for hand_tile in hand.list.iter() {
340 if hand_tile.set_id.is_some() {
341 continue;
342 }
343
344 let mut stat = HandTileStat {
345 in_other_melds: 0,
346 in_board: 0,
347 };
348
349 let hand_tile_full = &DEFAULT_DECK.0[hand_tile.id];
350
351 for own_meld_tile in own_meld_tiles.iter() {
352 let tile = &DEFAULT_DECK.0[*own_meld_tile];
353
354 if tile.is_same_content(hand_tile_full) {
355 stat.in_other_melds += 1;
356 }
357 }
358
359 for (_, other_hand) in self.other_hands.0.iter() {
360 other_hand.visible.list.iter().for_each(|t| {
361 let tile = &DEFAULT_DECK.0[t.id];
362
363 if tile.is_same_content(hand_tile_full) {
364 stat.in_other_melds += 1;
365 }
366 })
367 }
368
369 for board_tile in self.board.0.iter() {
370 let tile = &DEFAULT_DECK.0[*board_tile];
371
372 if tile.is_same_content(hand_tile_full) {
373 stat.in_board += 1;
374 }
375 }
376
377 hand_stats.insert(hand_tile.id, stat);
378 }
379
380 hand_stats
381 }
382}