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 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(¤t_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}