mahjong_core/
summary_view.rs

1use std::{
2    fmt::{Display, Formatter},
3    str::FromStr,
4};
5
6use uuid::Uuid;
7
8use crate::{
9    deck::DEFAULT_DECK,
10    hand::{HandPossibleMeld, KongTile, SetIdContent},
11    round::{Round, RoundTileClaimed},
12    score::ScoringRule,
13    table::{BonusTiles, PositionTilesOpts},
14    Board, Deck, Dragon, DragonTile, DrawWall, Flower, FlowerTile, Game, GamePhase, Hand, HandTile,
15    Hands, Season, SeasonTile, Suit, SuitTile, Tile, TileId, Wind, WindTile,
16};
17
18pub fn print_game_tile(tile: &Tile) -> String {
19    let mut result = String::new();
20
21    match tile {
22        Tile::Dragon(tile) => {
23            let dragon_letter = match tile.value {
24                Dragon::Red => '中',
25                Dragon::Green => '發',
26                Dragon::White => '白',
27            };
28            result.push(dragon_letter);
29        }
30        Tile::Wind(tile) => {
31            let wind_letter = tile.value.to_string();
32            result.push_str(&wind_letter);
33        }
34        Tile::Flower(tile) => {
35            let flower_letter = match tile.value {
36                Flower::Plum => '梅',
37                Flower::Orchid => '蘭',
38                Flower::Chrysanthemum => '菊',
39                Flower::Bamboo => '竹',
40            };
41            result.push(flower_letter);
42        }
43        Tile::Season(tile) => {
44            let season_letter = match tile.value {
45                Season::Spring => '春',
46                Season::Summer => '夏',
47                Season::Autumn => '秋',
48                Season::Winter => '冬',
49            };
50            result.push(season_letter);
51        }
52        Tile::Suit(tile) => {
53            let value_str = match tile.value {
54                1 => '一',
55                2 => '二',
56                3 => '三',
57                4 => '四',
58                5 => '五',
59                6 => '六',
60                7 => '七',
61                8 => '八',
62                9 => '九',
63                _ => panic!("Invalid value"),
64            };
65            let suit_letter = match tile.suit {
66                Suit::Bamboo => '索',
67                Suit::Dots => '筒',
68                Suit::Characters => '萬',
69            };
70            result.push_str(&format!("{:}{:}", value_str, suit_letter));
71        }
72    }
73
74    result
75}
76
77impl FromStr for GamePhase {
78    type Err = ();
79
80    fn from_str(input: &str) -> Result<Self, Self::Err> {
81        match input {
82            "Beginning" => Ok(Self::Beginning),
83            "Deciding Dealer" => Ok(Self::DecidingDealer),
84            "End" => Ok(Self::End),
85            "Initial Draw" => Ok(Self::InitialDraw),
86            "Playing" => Ok(Self::Playing),
87            "Initial Shuffle" => Ok(Self::InitialShuffle),
88            "Waiting Players" => Ok(Self::WaitingPlayers),
89            _ => Err(()),
90        }
91    }
92}
93
94impl Game {
95    pub fn get_summary(&self) -> String {
96        let mut result = String::new();
97
98        for (pos, player) in self.players.iter().enumerate() {
99            let hand = self.table.hands.get(player);
100            if hand.is_none() {
101                continue;
102            }
103            let hand = hand.unwrap();
104            if hand.is_empty() {
105                continue;
106            }
107            result.push('\n');
108            result.push_str(&format!("- P{}: ", pos + 1));
109
110            result.push_str(&hand.to_summary_full());
111        }
112
113        if !self.table.draw_wall.is_empty() {
114            result.push_str("\nWall:");
115            let player_wind = self.get_player_wind();
116            if self.table.draw_wall.len() > 3 {
117                result.push_str(" ...");
118            }
119            result.push_str(&format!(
120                " {}",
121                self.table.draw_wall.summary_next(&player_wind)
122            ));
123        }
124
125        if !self.table.board.0.is_empty() {
126            result.push_str("\nBoard: ");
127            let mut parsed_board = self
128                .table
129                .board
130                .0
131                .iter()
132                .map(|tile| print_game_tile(&DEFAULT_DECK.0[*tile]))
133                .collect::<Vec<String>>();
134            parsed_board.reverse();
135            if parsed_board.len() > 2 {
136                result.push_str(&parsed_board[0..2].join(","));
137                result.push_str("...");
138            } else {
139                result.push_str(&parsed_board.join(","));
140            }
141        }
142
143        result.push_str("\nTurn: ");
144        result.push_str(&format!("P{}", self.round.player_index + 1));
145        result.push_str(", Dealer: ");
146        result.push_str(&format!("P{}", self.round.dealer_player_index + 1));
147        result.push_str(", Round: ");
148        result.push_str(&format!("{}", self.round.round_index + 1));
149        result.push_str(", Wind: ");
150        result.push_str(self.round.wind.to_string().as_str());
151        result.push_str(", Phase: ");
152        result.push_str(&format!("{:?}", self.phase));
153
154        result.push_str("\nConsecutive: ");
155        result.push_str(&format!("{}", self.round.consecutive_same_seats));
156        if let Some(tile) = self.round.tile_claimed.clone() {
157            result.push_str(", Discarded: ");
158            result.push_str(&print_game_tile(&DEFAULT_DECK.0[tile.id]));
159            if let Some(by) = tile.by {
160                result.push_str("(P");
161                let player_id = match by.parse::<usize>() {
162                    Ok(player_num) => (player_num + 1).to_string(),
163                    Err(_) => by.clone(),
164                };
165                result.push_str(&player_id);
166                result.push(')');
167            }
168        }
169        if let Some(tile) = self.round.wall_tile_drawn {
170            result.push_str(", Drawn: ");
171            result.push_str(&print_game_tile(&DEFAULT_DECK.0[tile]));
172        }
173
174        result.trim().to_string()
175    }
176
177    pub fn get_summary_sorted(&self) -> String {
178        let mut game = self.clone();
179
180        for hand in game.table.hands.0.values_mut() {
181            hand.sort_default();
182        }
183
184        game.get_summary()
185    }
186
187    pub fn from_summary(summary: &str) -> Self {
188        let mut game = Self::new(None);
189        let mut lines = summary.trim().lines();
190
191        let mut line = lines.next().unwrap().trim();
192
193        for idx in 0..Self::get_players_num(&game.style) {
194            let prefix_player = format!("- P{}: ", idx + 1);
195            if line.starts_with(&prefix_player) {
196                let new_player = (idx).to_string();
197                game.players.push(new_player.clone());
198            } else {
199                let prefix_no_player = format!("- XP{}", idx + 1);
200
201                if line.starts_with(&prefix_no_player) {
202                    line = lines.next().unwrap().trim();
203                } else {
204                    // This means that the was a `- XP` before
205                    if game.players.0.len() != idx {
206                        continue;
207                    }
208                    let new_player = (idx).to_string();
209                    game.players.push(new_player.clone());
210                    game.table.hands.update_player_hand(new_player, "");
211                }
212                continue;
213            }
214            let player_id = game.players.0.get(idx);
215            if player_id.is_none() {
216                continue;
217            }
218            let player_id = player_id.unwrap();
219            let hand = Hand::from_summary(&line[5..]);
220            game.table.hands.0.insert(player_id.clone(), hand);
221            game.table
222                .bonus_tiles
223                .set_from_summary(player_id, &line[5..]);
224
225            line = lines.next().unwrap_or("").trim();
226        }
227
228        let mut wall_line: Option<String> = None;
229        if let Some(w) = line.strip_prefix("Wall:") {
230            wall_line = Some(w.to_string());
231            line = lines.next().unwrap_or("");
232        } else {
233            game.table.draw_wall.clear();
234        }
235
236        if let Some(board_line) = line.trim().strip_prefix("Board: ") {
237            let board_line = board_line.replace("...", "");
238            game.table.board.push_by_summary(&board_line);
239            line = lines.next().unwrap_or("");
240        }
241
242        line.trim().split(", ").for_each(|fragment| {
243            if fragment.starts_with("Turn: ") {
244                let turn_player = fragment[7..].parse::<usize>().unwrap();
245                game.round.player_index = turn_player - 1;
246            } else if fragment.starts_with("Dealer: ") {
247                let dealer_player = fragment[9..].parse::<usize>().unwrap();
248                game.round.dealer_player_index = dealer_player - 1;
249            } else if let Some(round_num) = fragment.strip_prefix("Round: ") {
250                let round_index = round_num.parse::<u32>().unwrap();
251                game.round.round_index = round_index - 1;
252            } else if let Some(wind) = fragment.strip_prefix("Wind: ") {
253                game.round.wind = Wind::from_str(wind.trim()).unwrap();
254            } else if let Some(phase) = fragment.strip_prefix("Phase: ") {
255                game.phase = GamePhase::from_str(phase.trim()).unwrap();
256            } else if let Some(winds_str) = fragment.strip_prefix("Initial Winds: ") {
257                let mut winds: [Wind; 4] = [Wind::East, Wind::South, Wind::West, Wind::North];
258                winds_str.split(',').enumerate().for_each(|(i, w)| {
259                    winds[i] = Wind::from_str(w.trim()).unwrap();
260                });
261                game.round.set_initial_winds(Some(winds)).unwrap();
262            }
263        });
264
265        line = lines.next().unwrap_or("");
266
267        line.trim().split(", ").for_each(|fragment| {
268            if let Some(count) = fragment.strip_prefix("Consecutive: ") {
269                let consecutive = count.parse::<usize>().unwrap();
270                game.round.consecutive_same_seats = consecutive;
271            } else if let Some(tile) = fragment.strip_prefix("Drawn: ") {
272                let tile_id = Tile::id_from_summary(tile.trim());
273                game.round.wall_tile_drawn = Some(tile_id);
274            } else if fragment.starts_with("First East: ") {
275                let player_num = fragment[13..].parse::<usize>().unwrap();
276                game.round.east_player_index = player_num - 1;
277            } else if let Some(tile) = fragment.strip_prefix("Discarded: ") {
278                let (from, by) = if tile.contains('(') {
279                    let mut parts = tile.split('(');
280                    let from = parts.next().unwrap().trim();
281                    let by_str = parts
282                        .next()
283                        .unwrap()
284                        .trim()
285                        .strip_prefix('P')
286                        .unwrap()
287                        .strip_suffix(')')
288                        .unwrap();
289
290                    let by = match by_str.parse::<usize>() {
291                        Ok(player_num) => (player_num - 1).to_string(),
292                        Err(_) => by_str.to_string(),
293                    };
294
295                    (from, Some(by.to_string()))
296                } else {
297                    (tile, None)
298                };
299                game.round.tile_claimed = Some(RoundTileClaimed {
300                    by,
301                    from: game.players.0[game.round.player_index].clone(),
302                    id: Tile::id_from_summary(from),
303                });
304            }
305        });
306
307        if let Some(wall_line) = wall_line {
308            if wall_line.trim().starts_with("Random") {
309                game.table.draw_wall.position_tiles(Some(PositionTilesOpts {
310                    shuffle: Some(true),
311                    dead_wall: None,
312                }));
313            } else {
314                let wall_line = wall_line.trim().replace("... ", "");
315                if wall_line.is_empty() {
316                    game.table.draw_wall.clear();
317                } else {
318                    game.table.draw_wall.position_tiles(None);
319                    let current_wind = game.get_player_wind();
320                    game.table
321                        .draw_wall
322                        .replace_tail_summary(&current_wind, &wall_line);
323                }
324            }
325        }
326
327        game
328    }
329
330    pub fn get_meld_id_from_summary(&self, player_id: &str, summary: &str) -> String {
331        let tile_id = Tile::from_summary(summary).get_id();
332        let hand = self.table.hands.0.get(player_id).unwrap();
333
334        hand.list
335            .iter()
336            .find(|hand_tile| hand_tile.id == tile_id)
337            .unwrap()
338            .set_id
339            .clone()
340            .unwrap()
341    }
342}
343
344impl Tile {
345    pub fn from_summary(summary: &str) -> Self {
346        let first_char = summary.chars().nth(0).unwrap();
347        let tile = match first_char {
348            '一' | '二' | '三' | '四' | '五' | '六' | '七' | '八' | '九' => {
349                let value = match first_char {
350                    '一' => 1,
351                    '二' => 2,
352                    '三' => 3,
353                    '四' => 4,
354                    '五' => 5,
355                    '六' => 6,
356                    '七' => 7,
357                    '八' => 8,
358                    '九' => 9,
359                    _ => panic!("Invalid value"),
360                };
361                let suit = match summary.chars().nth(1).unwrap() {
362                    '索' => Suit::Bamboo,
363                    '筒' => Suit::Dots,
364                    '萬' => Suit::Characters,
365                    _ => panic!("Invalid suit"),
366                };
367                Self::Suit(SuitTile { id: 0, value, suit })
368            }
369            '東' | '南' | '西' | '北' => {
370                let value = Wind::from_str(&first_char.to_string()).unwrap();
371                Self::Wind(WindTile { id: 0, value })
372            }
373            '中' | '發' | '白' => {
374                let value = match first_char {
375                    '中' => Dragon::Red,
376                    '發' => Dragon::Green,
377                    '白' => Dragon::White,
378                    _ => panic!("Invalid dragon"),
379                };
380                Self::Dragon(DragonTile { id: 0, value })
381            }
382            '梅' | '蘭' | '菊' | '竹' => {
383                let value = match first_char {
384                    '梅' => Flower::Plum,
385                    '蘭' => Flower::Orchid,
386                    '菊' => Flower::Chrysanthemum,
387                    '竹' => Flower::Bamboo,
388                    _ => panic!("Invalid flower"),
389                };
390                Self::Flower(FlowerTile { id: 0, value })
391            }
392            '春' | '夏' | '秋' | '冬' => {
393                let value = match first_char {
394                    '春' => Season::Spring,
395                    '夏' => Season::Summer,
396                    '秋' => Season::Autumn,
397                    '冬' => Season::Winter,
398                    _ => panic!("Invalid season"),
399                };
400                Self::Season(SeasonTile { id: 0, value })
401            }
402            _ => panic!("Invalid summary: {summary}"),
403        };
404        Deck::find_tile_without_id(tile)
405    }
406
407    pub fn summary_from_ids(ids: &[TileId]) -> String {
408        ids.iter()
409            .map(|id| print_game_tile(DEFAULT_DECK.get_sure(*id)))
410            .collect::<Vec<String>>()
411            .join(",")
412    }
413
414    pub fn id_from_summary(summary: &str) -> TileId {
415        Self::from_summary(summary).get_id()
416    }
417
418    pub fn ids_from_summary(summary: &str) -> Vec<TileId> {
419        summary
420            .split(',')
421            .filter(|tile| !tile.is_empty())
422            .map(Self::id_from_summary)
423            .collect()
424    }
425}
426
427impl HandPossibleMeld {
428    pub fn from_summary(summary: &str) -> Self {
429        let summary_parts = summary.split(' ').collect::<Vec<&str>>();
430
431        match summary_parts.len() {
432            2 => Self {
433                is_mahjong: summary_parts[1] == "YES",
434                is_upgrade: false,
435                is_concealed: false,
436                tiles: Hand::from_summary(summary_parts[0])
437                    .list
438                    .iter()
439                    .map(|t| t.id)
440                    .collect(),
441            },
442            _ => panic!("Invalid summary: {}", summary),
443        }
444    }
445
446    pub fn from_summaries(summary: &[&str]) -> Vec<Self> {
447        summary.iter().map(|s| Self::from_summary(s)).collect()
448    }
449
450    pub fn to_summary(&self) -> String {
451        let result = Hand::from_ids(&self.tiles);
452        let mut result_summary = result.to_summary();
453
454        if self.is_mahjong {
455            result_summary.push_str(" YES");
456        } else {
457            result_summary.push_str(" NO");
458        }
459        result_summary
460    }
461}
462
463impl HandTile {
464    pub fn from_test_summary(summary: &str) -> Self {
465        Self::from_tile(&Tile::from_summary(summary))
466    }
467}
468
469impl Hand {
470    pub fn from_summary(summary: &str) -> Self {
471        let mut hand = Self::new(
472            summary
473                .split(' ')
474                .filter(|tile_set| !tile_set.is_empty())
475                .enumerate()
476                .flat_map(|(idx, tile_set)| {
477                    if tile_set == "_" {
478                        return vec![];
479                    }
480
481                    let set_id = if idx == 0 {
482                        None
483                    } else {
484                        Some(Uuid::new_v4().to_string())
485                    };
486                    let (concealed, parsed_set) =
487                        if let Some(tile_set_plain) = tile_set.strip_prefix('*') {
488                            (false, tile_set_plain.to_string())
489                        } else {
490                            (true, tile_set.to_string())
491                        };
492
493                    parsed_set
494                        .split(',')
495                        .filter(|tile| !tile.is_empty())
496                        .filter_map(|tile| {
497                            let tile = Tile::from_summary(tile);
498                            if tile.is_bonus() {
499                                return None;
500                            }
501                            let mut hand_tile = HandTile::from_tile(&tile);
502                            hand_tile.set_id.clone_from(&set_id);
503                            hand_tile.concealed = concealed;
504                            Some(hand_tile)
505                        })
506                        .collect::<Vec<HandTile>>()
507                })
508                .collect(),
509        );
510
511        let kong_sets = hand
512            .get_sets_groups()
513            .into_iter()
514            .filter(|(set_id, tiles)| set_id.is_some() && tiles.len() == 4)
515            .map(|(set_id, _)| set_id.clone().unwrap())
516            .collect::<Vec<SetIdContent>>();
517
518        for set_id in kong_sets {
519            let first_tile = hand
520                .list
521                .iter()
522                .find(|tile| tile.set_id == Some(set_id.clone()))
523                .unwrap()
524                .clone();
525
526            let position = hand
527                .list
528                .iter()
529                .position(|tile| tile.id == first_tile.id)
530                .unwrap();
531            hand.list.remove(position);
532            hand.kong_tiles.insert(KongTile {
533                concealed: first_tile.concealed,
534                id: first_tile.id,
535                set_id: set_id.clone(),
536            });
537        }
538
539        hand
540    }
541
542    pub fn to_summary(&self) -> String {
543        let sets_parsed = self
544            .list
545            .iter()
546            .map(|tile| print_game_tile(DEFAULT_DECK.get_sure(tile.id)))
547            .collect::<Vec<String>>();
548        sets_parsed.join(",")
549    }
550
551    pub fn to_summary_full(&self) -> String {
552        let mut result = String::new();
553        let mut hand_clone = self.clone();
554        for kong_tile in hand_clone.kong_tiles.iter() {
555            hand_clone.list.push(HandTile {
556                concealed: kong_tile.concealed,
557                id: kong_tile.id,
558                set_id: Some(kong_tile.set_id.clone()),
559            });
560        }
561        let sets_groups = hand_clone.get_sets_groups();
562
563        if let Some(tiles) = sets_groups.get(&None) {
564            let hand_tiles = hand_clone
565                .list
566                .iter()
567                .filter(|tile| tiles.contains(&tile.id))
568                .collect::<Vec<&HandTile>>();
569            result.push_str(&Self::from_ref_vec(&hand_tiles).to_summary());
570        }
571
572        for (_, tiles) in sets_groups.iter().filter(|(set_id, _)| set_id.is_some()) {
573            let hand_tiles = hand_clone
574                .list
575                .iter()
576                .filter(|tile| tiles.contains(&tile.id))
577                .collect::<Vec<&HandTile>>();
578            result.push(' ');
579            if tiles.len() > 1 && !hand_tiles[0].concealed {
580                result.push('*');
581            }
582
583            result.push_str(&Self::from_ref_vec(&hand_tiles).to_summary());
584        }
585
586        result
587    }
588}
589
590impl Hands {
591    pub fn update_player_hand(&mut self, player_id: impl AsRef<str>, summary: &str) -> &mut Self {
592        self.0
593            .insert(player_id.as_ref().to_string(), Hand::from_summary(summary));
594        self
595    }
596    pub fn update_players_hands(&mut self, summaries: &[&str]) -> &mut Self {
597        summaries.iter().enumerate().for_each(|(idx, summary)| {
598            self.update_player_hand(idx.to_string(), summary);
599        });
600        self
601    }
602}
603
604impl Board {
605    pub fn from_summary(summary: &str) -> Self {
606        let mut board = Self::default();
607        board.push_by_summary(summary);
608        board
609    }
610
611    pub fn to_summary(&self) -> String {
612        self.0
613            .iter()
614            .map(|tile| print_game_tile(DEFAULT_DECK.get_sure(*tile)))
615            .collect::<Vec<String>>()
616            .join(",")
617    }
618}
619
620impl Board {
621    pub fn push_by_summary(&mut self, summary: &str) {
622        summary
623            .split(',')
624            .filter(|tile| !tile.is_empty())
625            .for_each(|tile| {
626                self.0.push(Tile::id_from_summary(tile));
627            });
628    }
629}
630
631impl DrawWall {
632    pub fn summary_next(&self, wind: &Wind) -> String {
633        let next_tile = self.get_next(wind);
634
635        match next_tile {
636            Some(tile) => Tile::summary_from_ids(&[*tile]),
637            None => String::new(),
638        }
639    }
640}
641
642impl DrawWall {
643    pub fn replace_tail_summary(&mut self, wind: &Wind, summary: &str) {
644        let tile = Tile::id_from_summary(summary);
645
646        self.replace_tail(wind, &tile)
647    }
648}
649
650impl BonusTiles {
651    pub fn set_from_summary(&mut self, player_id: &str, summary: &str) {
652        self.0.insert(
653            player_id.to_string(),
654            summary
655                .replace('_', "")
656                .trim()
657                .replace(' ', ",")
658                .replace('*', "")
659                .split(',')
660                .filter(|s| !s.is_empty())
661                .map(|s| Tile::id_from_summary(s.trim().replace(' ', ",").as_ref()))
662                .filter(|tile_id| DEFAULT_DECK.get_sure(*tile_id).is_bonus())
663                .collect(),
664        );
665    }
666}
667
668impl Round {
669    pub fn from_summary(summary: &str) -> Self {
670        let game = Game::from_summary(summary);
671
672        game.round
673    }
674}
675
676impl Display for ScoringRule {
677    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
678        write!(f, "{:?}", self)
679    }
680}