mjlog/
model.rs

1//! # model
2//!
3//! Represents mjlog XML
4//!
5//! # Reference
6//!
7//! <https://m77.hatenablog.com/entry/2017/05/21/214529>
8
9use num_derive::FromPrimitive;
10use num_traits::FromPrimitive;
11use serde_derive::{Serialize, Deserialize};
12use thiserror::Error;
13
14/// Occurs when there is no corresponding identifier.
15#[derive(Debug, Error)]
16pub enum ParseError {
17    #[error("Invalid hai number")]
18    InvalidHaiNumber,
19    #[error("Invalid player number")]
20    InvalidPlayerNumber,
21    #[error("Invalid tenhou rank")]
22    InvalidTenhouRank,
23    #[error("Invalid extra ryuukyoku reason")]
24    InvalidExtraRyuukyokuReason,
25}
26
27/// Represents tiles numbered from 0 to 135.
28///
29/// When red 5 is enabled, it is assigned to the tile where mod 4 == 0. (16,52,88)
30///
31/// ```
32/// order:
33/// 1111..0555..9999m 1111..0555..9999p 1111..0555..9999s 1111..7777z
34/// (0m == red 5m)
35/// ```
36#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
37pub struct Hai(u8);
38
39/// Player index.
40#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
41pub struct Player(u8);
42
43/// GamePoint represents each player's score, which usually starts at 25,000 or 30,000.
44pub type GamePoint = i32;
45
46/// Represents the relative direction of a player based on the current player’s perspective.
47#[repr(u8)]
48#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, FromPrimitive)]
49pub enum Direction {
50    /// 自分(Self)
51    #[default]
52    SelfSeat,
53    /// 下家(Right)
54    Shimocha,
55    /// 対面(Across)
56    Toimen,
57    /// 上家(Left)
58    Kamicha,
59}
60
61/// Represents the room type in Tenhou.
62#[repr(u8)]
63#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, FromPrimitive)]
64pub enum TenhouRoom {
65    /// 一般卓
66    #[default]
67    Ippan,
68    /// 上級卓
69    Joukyu,
70    /// 特上卓
71    Tokujou,
72    /// 鳳凰卓
73    Houou,
74}
75
76/// Represents the rank type in Tenhou.
77#[repr(u8)]
78#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, FromPrimitive)]
79pub enum TenhouRank {
80    #[default]
81    Newcomer,
82    Kyu9,
83    Kyu8,
84    Kyu7,
85    Kyu6,
86    Kyu5,
87    Kyu4,
88    Kyu3,
89    Kyu2,
90    Kyu1,
91    Dan1,
92    Dan2,
93    Dan3,
94    Dan4,
95    Dan5,
96    Dan6,
97    Dan7,
98    Dan8,
99    Dan9,
100    Dan10,
101    Tenhou,
102}
103
104/// Game settings.
105#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
106pub struct GameSettings {
107    pub vs_human: bool,
108    pub no_red: bool,
109    pub no_kuitan: bool,
110    pub hanchan: bool,
111    pub sanma: bool,
112    pub soku: bool,
113    pub room: TenhouRoom,
114}
115
116/// Represents the initial settings for each round.
117#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
118pub struct InitSeed {
119    pub kyoku: u8,
120    pub honba: u8,
121    pub kyoutaku: u8,
122    pub dice: (u8, u8),
123    pub dora_hyouji: Hai,
124}
125
126/// Represents the details of a call (meld).
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub enum Meld {
129    Chii {
130        combination: (Hai, Hai, Hai),
131        // 0 == min, 1 == mid, 2 == max
132        called_position: u8,
133    },
134    Pon {
135        dir: Direction,
136        combination: (Hai, Hai, Hai),
137        called: Hai,
138        unused: Hai,
139    },
140    // Almost same as pon
141    Kakan {
142        dir: Direction,
143        combination: (Hai, Hai, Hai),
144        called: Hai,
145        added: Hai,
146    },
147    Daiminkan {
148        dir: Direction,
149        hai: Hai,
150    },
151    Ankan {
152        hai: Hai,
153    },
154}
155
156/// Represents special draw conditions.
157#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
158pub enum ExtraRyuukyokuReason {
159    /// 九種九牌
160    #[default]
161    KyuusyuKyuuhai,
162    /// 四家立直
163    SuuchaRiichi,
164    /// 三家和了
165    SanchaHoura,
166    /// 四槓散了
167    SuukanSanra,
168    /// 四風連打
169    SuufuuRenda,
170    /// 流し満貫
171    NagashiMangan,
172}
173
174/// Represents the winning hand rank, such as Mangan.
175#[repr(u8)]
176#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, FromPrimitive)]
177pub enum ScoreRank {
178    #[default]
179    Normal,
180    /// 満貫
181    Mangan,
182    /// 跳満
183    Haneman,
184    /// 倍満
185    Baiman,
186    /// 三倍満
187    Sanbaiman,
188    /// 役満
189    Yakuman,
190}
191
192/// Represents the name of a Yaku (winning hand combination).
193#[repr(u8)]
194#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, FromPrimitive)]
195pub enum Yaku {
196    #[default]
197    MenzenTsumo,
198    Riichi,
199    Ippatsu,
200    Chankan,
201    Rinshankaihou,
202    HaiteiTsumo,
203    HouteiRon,
204    Pinfu,
205    Tanyao,
206    Iipeikou,
207    PlayerWindTon,
208    PlayerWindNan,
209    PlayerWindSha,
210    PlayerWindPei,
211    FieldWindTon,
212    FieldWindNan,
213    FieldWindSha,
214    FieldWindPei,
215    YakuhaiHaku,
216    YakuhaiHatsu,
217    YakuhaiChun,
218    DoubleRiichi,
219    Chiitoitsu,
220    Chanta,
221    Ikkitsuukan,
222    SansyokuDoujun,
223    SanshokuDoukou,
224    Sankantsu,
225    Toitoi,
226    Sanannkou,
227    Shousangen,
228    Honroutou,
229    Ryanpeikou,
230    Junchan,
231    Honiisou,
232    Chiniisou,
233    Renhou,
234    Tenhou,
235    Chiihou,
236    Daisangen,
237    Suuankou,
238    SuuankouTanki,
239    Tsuuiisou,
240    Ryuuiisou,
241    Chinroutou,
242    Tyuurenpoutou,
243    Tyuurenpoutou9,
244    Kokushimusou,
245    Kokushimusou13,
246    Daisuushii,
247    Syousuushii,
248    Suukantsu,
249    Dora,
250    UraDora,
251    AkaDora,
252}
253
254/// Corresponds to the AGARI tag.
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct ActionAGARI {
257    /// Bonus points for consecutive draws or dealer wins.
258    pub honba: u8,
259
260    /// Deposit points for unresolved Riichi bets.
261    pub kyoutaku: u8,
262
263    /// The hand at the time of winning. Melds are not included, but the winning tile is included.
264    pub hai: Vec<Hai>,
265
266    /// Vector representing call (meld) information.
267    pub m: Vec<Meld>,
268
269    /// Winning tiles at the time of completion.
270    pub machi: Hai,
271
272    /// Hand value points in mahjong scoring, e.g. 20, 30.
273    pub fu: u8,
274
275    /// Total winning points.
276    pub net_score: u32,
277
278    /// Detailed information on winning points.
279    pub score_rank: ScoreRank,
280
281    /// List of winning Yakus (hand combinations).
282    ///
283    /// ```yaku``` is valid for normal winning hands, in which case ```yakuman``` will be empty.
284    pub yaku: Vec<(Yaku, u8)>,
285
286    /// List of Yakuman.
287    ///
288    /// ```yakuman``` is valid when achieving a Yakuman hand, in which case ```yaku``` will be empty.
289    pub yakuman: Vec<Yaku>,
290
291    /// Dora indicator.
292    pub dora_hai: Vec<Hai>,
293
294    /// Ura-dora indicator.
295    ///
296    /// The ura-dora is only specified when a riichi declaration has been made.
297    /// In the XML, if its size is 0 the field itself is not output;
298    /// however, since this essentially conveys the same meaning, it is not treated as an Option.
299    pub dora_hai_ura: Vec<Hai>,
300
301    /// Winning player number.
302    pub who: Player,
303
304    /// Player number from whom the win was claimed.
305    ///
306    /// If won by Tsumo, it will be the player's own number.
307    pub from_who: Player,
308
309    /// Player number responsible for the payment (if applicable).
310    ///
311    /// If there is no player responsible for the payment, the winning player's number will be used.
312    pub pao_who: Option<Player>,
313
314    /// Each player's score before the win occurred.
315    pub before_points: Vec<GamePoint>,
316
317    /// Point changes due to the win, including the effects of Kyotaku and Honba.
318    pub delta_points: Vec<GamePoint>,
319
320    /// Final results at the end of the game.
321    ///
322    /// If there are remaining rounds, this will be ```None```.
323    pub owari: Option<(Vec<GamePoint>, Vec<f64>)>,
324}
325
326/// Corresponds to the RYUUKYOKU tag.
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct ActionRYUUKYOKU {
329    /// Bonus points for consecutive draws or dealer wins.
330    pub honba: u8,
331
332    /// Deposit points for unresolved Riichi bets.
333    pub kyoutaku: u8,
334
335    /// Each player's score before the win occurred.
336    pub before_points: Vec<GamePoint>,
337
338    /// Point changes due to the win, including the effects of Kyotaku and Honba.
339    pub delta_points: Vec<GamePoint>,
340
341    /// Hands at the time of a drawn game.
342    pub hai0: Option<Vec<Hai>>,
343
344    /// Hands at the time of a drawn game.
345    pub hai1: Option<Vec<Hai>>,
346
347    /// Hands at the time of a drawn game.
348    pub hai2: Option<Vec<Hai>>,
349
350    /// Hands at the time of a drawn game.
351    pub hai3: Option<Vec<Hai>>,
352
353    /// Represents special draw conditions.
354    ///
355    /// In the case of a normal draw, this will be ```None```.
356    pub reason: Option<ExtraRyuukyokuReason>,
357
358    /// Final results at the end of the game.
359    ///
360    /// If there are remaining rounds, this will be ```None```.
361    pub owari: Option<(Vec<GamePoint>, Vec<f64>)>,
362}
363
364/// Corresponds to the SHUFFLE tag.
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct ActionSHUFFLE {
367    pub seed: String,
368}
369
370/// Corresponds to the GO tag.
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct ActionGO {
373    /// In the original XML, this is named "type", but it has been chaned to avoid conflicts with Rust reserved keywords.
374    pub settings: GameSettings,
375    pub lobby: u32,
376}
377
378/// Corresponds to initial state of the UN tag.
379///
380/// In the original XML, the initial state and reconnection share the UN tag.
381/// However, since user utilize them differently, they are intentionally separated into two.
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct ActionUN1 {
384    pub names: Vec<String>,
385    pub dan: Vec<TenhouRank>,
386    pub rate: Vec<f64>,
387    pub sx: Vec<String>,
388}
389
390/// Corresponds to the UN tag in the case of reconnection.
391///
392/// In the original XML, it is expressed as options from n0 to n3, but since that is confusing, it has been reorganized.
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct ActionUN2 {
395    pub who: Player,
396    pub name: String,
397}
398
399/// Corresponds to the BYE tag.
400#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct ActionBYE {
402    pub who: Player,
403}
404
405/// Corresponds to the TAIKYOKU tag.
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct ActionTAIKYOKU {
408    pub oya: Player,
409}
410
411/// Corresponds to the INIT tag.
412#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct ActionINIT {
414    pub seed: InitSeed,
415    pub ten: Vec<GamePoint>,
416    pub oya: Player,
417    pub hai: Vec<Vec<Hai>>,
418}
419
420/// Corresponds to the REACH tag in case of declaration (step 1).
421///
422/// For REACH, although there is a single tag covering both step 1 and step 2,
423/// we split the enum into two since they are usually handled separately.
424/// At step 1, a riichi declaration is made.
425/// Afterwards, a tile is discarded, and if no ron occurs, step is set to 2.
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct ActionREACH1 {
428    pub who: Player,
429}
430
431/// Corresponds to the REACH tag after a tile is discarded (step 2).
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct ActionREACH2 {
434    pub who: Player,
435    pub ten: Vec<GamePoint>,
436}
437
438/// Corresponds to the N tag, represents a call (meld).
439#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct ActionN {
441    pub who: Player,
442    pub m: Meld,
443}
444
445/// Corresponds to the DORA tag, represents a new Dora indicator.
446#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct ActionDORA {
448    pub hai: Hai,
449}
450
451/// Corresponds to the T, U, V, and W tag.
452///
453/// Tsumo actions are represented by the T, U, V, and W tags,
454/// but since they share common properties, they are unified into a single structure.
455#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct ActionDRAW {
457    pub who: Player,
458    pub hai: Hai,
459}
460
461/// Corresponds to the D, E, F, and G tag.
462///
463/// Discard actions are represented by the D, E, F, and G tags,
464/// but since they share common properties, they are unified into a single structure.
465#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct ActionDISCARD {
467    pub who: Player,
468    pub hai: Hai,
469}
470
471/// Corresponds to each tag within ```mgloggm```.
472#[derive(Debug, Clone, Serialize, Deserialize)]
473pub enum Action {
474    SHUFFLE(ActionSHUFFLE),
475    GO(ActionGO),
476    UN1(ActionUN1),
477    UN2(ActionUN2),
478    BYE(ActionBYE),
479    TAIKYOKU(ActionTAIKYOKU),
480    INIT(ActionINIT),
481    REACH1(ActionREACH1),
482    REACH2(ActionREACH2),
483    N(ActionN),
484    DORA(ActionDORA),
485    AGARI(ActionAGARI),
486    RYUUKYOKU(ActionRYUUKYOKU),
487    DRAW(ActionDRAW),
488    DISCARD(ActionDISCARD),
489}
490
491/// Corresponds to the entire mjloggm tag.
492#[derive(Debug, Clone, Serialize, Deserialize, Default)]
493pub struct Mjlog {
494    pub ver: f64,
495    pub actions: Vec<Action>,
496}
497
498impl Hai {
499    pub fn new(x: u8) -> Hai {
500        Hai(x)
501    }
502
503    pub fn to_u8(&self) -> u8 {
504        self.0
505    }
506
507    pub fn is_number5(&self) -> bool {
508        // 123456789m123456789p123456789s1234567z
509        let pict_index = self.0 / 4;
510        // mpsz
511        let pict_type = pict_index / 9;
512        let number = (pict_index % 9) + 1;
513        pict_type <= 2 && number == 5
514    }
515}
516
517impl Player {
518    pub fn new(x: u8) -> Self {
519        Player(x)
520    }
521
522    pub fn to_u8(&self) -> u8 {
523        self.0
524    }
525}
526
527impl ActionAGARI {
528    pub fn is_tsumo(&self) -> bool {
529        self.who == self.from_who
530    }
531}
532
533impl Action {
534    pub fn as_shuffle(&self) -> Option<&ActionSHUFFLE> {
535        match self {
536            Action::SHUFFLE(x) => Some(x),
537            _ => None,
538        }
539    }
540
541    pub fn as_go(&self) -> Option<&ActionGO> {
542        match self {
543            Action::GO(x) => Some(x),
544            _ => None,
545        }
546    }
547
548    pub fn as_un1(&self) -> Option<&ActionUN1> {
549        match self {
550            Action::UN1(x) => Some(x),
551            _ => None,
552        }
553    }
554
555    pub fn as_un2(&self) -> Option<&ActionUN2> {
556        match self {
557            Action::UN2(x) => Some(x),
558            _ => None,
559        }
560    }
561
562    pub fn as_bye(&self) -> Option<&ActionBYE> {
563        match self {
564            Action::BYE(x) => Some(x),
565            _ => None,
566        }
567    }
568
569    pub fn as_taikyoku(&self) -> Option<&ActionTAIKYOKU> {
570        match self {
571            Action::TAIKYOKU(x) => Some(x),
572            _ => None,
573        }
574    }
575
576    pub fn as_init(&self) -> Option<&ActionINIT> {
577        match self {
578            Action::INIT(x) => Some(x),
579            _ => None,
580        }
581    }
582
583    pub fn as_reach1(&self) -> Option<&ActionREACH1> {
584        match self {
585            Action::REACH1(x) => Some(x),
586            _ => None,
587        }
588    }
589
590    pub fn as_reach2(&self) -> Option<&ActionREACH2> {
591        match self {
592            Action::REACH2(x) => Some(x),
593            _ => None,
594        }
595    }
596
597    pub fn as_n(&self) -> Option<&ActionN> {
598        match self {
599            Action::N(x) => Some(x),
600            _ => None,
601        }
602    }
603
604    pub fn as_dora(&self) -> Option<&ActionDORA> {
605        match self {
606            Action::DORA(x) => Some(x),
607            _ => None,
608        }
609    }
610
611    pub fn as_agari(&self) -> Option<&ActionAGARI> {
612        match self {
613            Action::AGARI(x) => Some(x),
614            _ => None,
615        }
616    }
617
618    pub fn as_ryuukyoku(&self) -> Option<&ActionRYUUKYOKU> {
619        match self {
620            Action::RYUUKYOKU(x) => Some(x),
621            _ => None,
622        }
623    }
624
625    pub fn as_draw(&self) -> Option<&ActionDRAW> {
626        match self {
627            Action::DRAW(x) => Some(x),
628            _ => None,
629        }
630    }
631
632    pub fn as_discard(&self) -> Option<&ActionDISCARD> {
633        match self {
634            Action::DISCARD(x) => Some(x),
635            _ => None,
636        }
637    }
638
639    pub fn is_shuffle(&self) -> bool {
640        self.as_shuffle().is_some()
641    }
642
643    pub fn is_go(&self) -> bool {
644        self.as_go().is_some()
645    }
646
647    pub fn is_un1(&self) -> bool {
648        self.as_un1().is_some()
649    }
650
651    pub fn is_un2(&self) -> bool {
652        self.as_un2().is_some()
653    }
654
655    pub fn is_bye(&self) -> bool {
656        self.as_bye().is_some()
657    }
658
659    pub fn is_taikyoku(&self) -> bool {
660        self.as_taikyoku().is_some()
661    }
662
663    pub fn is_init(&self) -> bool {
664        self.as_init().is_some()
665    }
666
667    pub fn is_reach1(&self) -> bool {
668        self.as_reach1().is_some()
669    }
670
671    pub fn is_reach2(&self) -> bool {
672        self.as_reach2().is_some()
673    }
674
675    pub fn is_n(&self) -> bool {
676        self.as_n().is_some()
677    }
678
679    pub fn is_dora(&self) -> bool {
680        self.as_dora().is_some()
681    }
682
683    pub fn is_agari(&self) -> bool {
684        self.as_agari().is_some()
685    }
686
687    pub fn is_ryuukyoku(&self) -> bool {
688        self.as_ryuukyoku().is_some()
689    }
690
691    pub fn is_draw(&self) -> bool {
692        self.as_draw().is_some()
693    }
694
695    pub fn is_discard(&self) -> bool {
696        self.as_discard().is_some()
697    }
698}
699
700impl std::str::FromStr for Hai {
701    type Err = ParseError;
702
703    fn from_str(s: &str) -> Result<Self, Self::Err> {
704        s.parse::<u8>().map(Hai).map_err(|_| ParseError::InvalidHaiNumber)
705    }
706}
707
708impl std::str::FromStr for Player {
709    type Err = ParseError;
710
711    fn from_str(s: &str) -> Result<Self, Self::Err> {
712        s.parse::<u8>().map(Player).map_err(|_| ParseError::InvalidPlayerNumber)
713    }
714}
715
716impl std::str::FromStr for TenhouRank {
717    type Err = ParseError;
718
719    fn from_str(s: &str) -> Result<Self, Self::Err> {
720        let rank = s.parse::<u8>().map_err(|_| ParseError::InvalidTenhouRank)?;
721        TenhouRank::from_u8(rank).ok_or(ParseError::InvalidTenhouRank)
722    }
723}
724
725impl std::str::FromStr for ExtraRyuukyokuReason {
726    type Err = ParseError;
727
728    fn from_str(s: &str) -> Result<Self, Self::Err> {
729        Ok(match s {
730            "yao9" => ExtraRyuukyokuReason::KyuusyuKyuuhai,
731            "reach4" => ExtraRyuukyokuReason::SuuchaRiichi,
732            "ron3" => ExtraRyuukyokuReason::SanchaHoura,
733            "kan4" => ExtraRyuukyokuReason::SuukanSanra,
734            "kaze4" => ExtraRyuukyokuReason::SuufuuRenda,
735            "nm" => ExtraRyuukyokuReason::NagashiMangan,
736            _ => return Err(ParseError::InvalidExtraRyuukyokuReason),
737        })
738    }
739}
740
741impl Default for Meld {
742    fn default() -> Self {
743        Meld::Ankan { hai: Hai::default() }
744    }
745}