mahjong_core/
game_summary.rs

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}