tenhou_json/
model.rs

1use crate::score::*;
2use num_derive::FromPrimitive;
3use num_traits::FromPrimitive;
4use std::fmt;
5
6pub type GamePoint = i32;
7
8pub struct InvalidTileNumberError;
9pub struct InvalidYakuFormatError;
10pub struct InvalidExtraRyuukyokuReasonError;
11
12/// Represents a tile.
13///
14/// ```
15/// 11...19 萬子
16/// 21...29 筒子
17/// 31...39 索子
18/// 41...47 東南西北白発中
19/// 51      赤5萬
20/// 52      赤5筒
21/// 53      赤5索
22/// ```
23#[derive(Debug, Default, PartialEq, Clone, Copy)]
24pub struct Tile(u8);
25
26/// Represents the relative direction of a player based on the current player’s perspective.
27#[derive(Debug, Default, PartialEq, Clone, Copy)]
28pub enum Direction {
29    #[default]
30    SelfSeat,
31    Kamicha,
32    Toimen,
33    Shimocha,
34}
35
36/// Represents a tile obtained by Tsumo or a call (meld).
37#[derive(Debug, PartialEq)]
38pub enum IncomingTile {
39    Tsumo(Tile),
40    Chii { combination: (Tile, Tile, Tile) },
41    Pon { combination: (Tile, Tile, Tile), dir: Direction },
42    Daiminkan { combination: (Tile, Tile, Tile, Tile), dir: Direction },
43}
44
45/// Represents a tile discarded or used in an Ankan (closed Kan) or Kakan (added Kan).
46#[derive(Debug, PartialEq)]
47pub enum OutgoingTile {
48    /// The discarded from the hand.
49    Discard(Tile),
50
51    /// Riichi declared with a tile discarded from the hand.
52    Riichi(Tile),
53
54    /// Represents an Ankan (closed Kan).
55    ///
56    /// It is likely that when an Ankan (closed Kan) is made with a 5,
57    /// the red 5 is always specified (though this is not certain).
58    Ankan(Tile),
59
60    /// Represents and Kakan (added Kan).
61    Kakan { combination: (Tile, Tile, Tile), dir: Direction, added: Tile },
62
63    /// Discarding the drawn tile.
64    Tsumogiri,
65
66    /// Declaring Riichi while discarding the drawn tile.
67    TsumogiriRiichi,
68
69    /// Dummy tile.
70    ///
71    /// When daiminkan, add dummy(Tile(0)) to align the index.
72    Dummy,
73}
74
75/// Represents the initial settings for each round.
76#[derive(Debug, Default, PartialEq)]
77pub struct RoundSettings {
78    pub kyoku: u8,
79    pub honba: u8,
80    pub kyoutaku: u8,
81    pub points: Vec<GamePoint>,
82    pub dora: Vec<Tile>,
83    pub ura_dora: Vec<Tile>,
84}
85
86/// Represents the number of Han for Yakus or the count of Yakuman.
87#[derive(Debug, PartialEq)]
88pub enum YakuLevel {
89    Normal(u8),
90    Yakuman(u8),
91}
92
93/// Pair of Yaku and its Han value.
94#[derive(Debug, PartialEq)]
95pub struct YakuPair {
96    pub yaku: Yaku,
97    pub level: YakuLevel,
98}
99
100/// Represents the winning information of a single player.
101#[derive(Debug, Default, PartialEq)]
102pub struct Agari {
103    pub delta_points: Vec<GamePoint>,
104    pub who: u8,
105    pub from_who: u8,
106    pub pao_who: u8,
107    pub ranked_score: RankedScore,
108    pub yaku: Vec<YakuPair>,
109}
110
111/// Represents the reason for a drawn game.
112#[derive(Debug, Default, PartialEq)]
113pub enum ExtraRyuukyokuReason {
114    #[default]
115    Ryuukyoku,
116    KyuusyuKyuuhai,
117    SuuchaRiichi,
118    SanchaHoura,
119    SuukanSanra,
120    SuufuuRenda,
121    NagashiMangan,
122    TenpaiEverybody,
123    TenpaiNobody,
124}
125
126const YAKU_NAME: [&str; 55] = [
127    // 一飜
128    "門前清自摸和",
129    "立直",
130    "一発",
131    "槍槓",
132    "嶺上開花",
133    "海底摸月",
134    "河底撈魚",
135    "平和",
136    "断幺九",
137    "一盃口",
138    "自風 東",
139    "自風 南",
140    "自風 西",
141    "自風 北",
142    "場風 東",
143    "場風 南",
144    "場風 西",
145    "場風 北",
146    "役牌 白",
147    "役牌 發",
148    "役牌 中",
149    // 二飜
150    "両立直",
151    "七対子",
152    "混全帯幺九",
153    "一気通貫",
154    "三色同順",
155    "三色同刻",
156    "三槓子",
157    "対々和",
158    "三暗刻",
159    "小三元",
160    "混老頭",
161    // 三飜
162    "二盃口",
163    "純全帯幺九",
164    "混一色",
165    // 六飜
166    "清一色",
167    // 満貫
168    "人和",
169    // 役満
170    "天和",
171    "地和",
172    "大三元",
173    "四暗刻",
174    "四暗刻単騎",
175    "字一色",
176    "緑一色",
177    "清老頭",
178    "九蓮宝燈",
179    "純正九蓮宝燈",
180    "国士無双",
181    "国士無双13面",
182    "大四喜",
183    "小四喜",
184    "四槓子",
185    // ドラ
186    "ドラ",
187    "裏ドラ",
188    "赤ドラ",
189];
190
191/// Represents a Yaku (winning hand combination).
192#[repr(u8)]
193#[derive(Debug, Default, PartialEq, Clone, Copy, FromPrimitive)]
194pub enum Yaku {
195    #[default]
196    MenzenTsumo,
197    Riichi,
198    Ippatsu,
199    Chankan,
200    Rinshankaihou,
201    HaiteiTsumo,
202    HouteiRon,
203    Pinfu,
204    Tanyao,
205    Iipeikou,
206    PlayerWindTon,
207    PlayerWindNan,
208    PlayerWindSha,
209    PlayerWindPei,
210    FieldWindTon,
211    FieldWindNan,
212    FieldWindSha,
213    FieldWindPei,
214    YakuhaiHaku,
215    YakuhaiHatsu,
216    YakuhaiChun,
217    DoubleRiichi,
218    Chiitoitsu,
219    Chanta,
220    Ikkitsuukan,
221    SansyokuDoujun,
222    SanshokuDoukou,
223    Sankantsu,
224    Toitoi,
225    Sanannkou,
226    Shousangen,
227    Honroutou,
228    Ryanpeikou,
229    Junchan,
230    Honiisou,
231    Chiniisou,
232    Renhou,
233    Tenhou,
234    Chiihou,
235    Daisangen,
236    Suuankou,
237    SuuankouTanki,
238    Tsuuiisou,
239    Ryuuiisou,
240    Chinroutou,
241    Tyuurenpoutou,
242    Tyuurenpoutou9,
243    Kokushimusou,
244    Kokushimusou13,
245    Daisuushii,
246    Syousuushii,
247    Suukantsu,
248    Dora,
249    UraDora,
250    AkaDora,
251}
252
253/// Represents information at the end of a round.
254#[derive(Debug, PartialEq)]
255pub enum RoundResult {
256    Agari { agari_vec: Vec<Agari> },
257    Ryuukyoku { reason: ExtraRyuukyokuReason, delta_points: Vec<GamePoint> },
258}
259
260/// Represents the rules for the entire match.
261#[derive(Debug, Default, PartialEq)]
262pub struct Rule {
263    pub disp: String,
264    pub aka53: bool,
265    pub aka52: bool,
266    pub aka51: bool,
267}
268
269/// Information for each player.
270#[derive(Debug, Default, PartialEq)]
271pub struct RoundPlayer {
272    pub hand: Vec<Tile>,
273    pub incoming: Vec<IncomingTile>,
274    pub outgoing: Vec<OutgoingTile>,
275}
276
277/// Round information.
278#[derive(Debug, Default, PartialEq)]
279pub struct Round {
280    pub settings: RoundSettings,
281    pub players: Vec<RoundPlayer>,
282    pub result: RoundResult,
283}
284
285/// Reconnection and disconnection information.
286#[derive(Debug, Default, PartialEq)]
287pub struct Connection {
288    pub what: u8,
289
290    /// round number.
291    ///
292    /// -1 if before first INIT
293    pub log: i8,
294
295    pub who: u8,
296    pub step: u32,
297}
298
299/// Represents tenhou-json.
300#[derive(Debug, Default, PartialEq)]
301pub struct TenhouJson {
302    pub ver: f64,
303    pub reference: String,
304    pub rounds: Vec<Round>,
305    pub connections: Vec<Connection>,
306    pub ratingc: String,
307    pub rule: Rule,
308    pub lobby: u32,
309    pub dan: Vec<String>,
310    pub rate: Vec<f64>,
311    pub sx: Vec<String>,
312    pub final_points: Vec<GamePoint>,
313    pub final_results: Vec<f64>,
314    pub names: Vec<String>,
315}
316
317impl Tile {
318    pub fn from_u8(x: u8) -> Result<Self, InvalidTileNumberError> {
319        if is_valid_tile(x) {
320            Ok(Tile(x))
321        } else {
322            Err(InvalidTileNumberError)
323        }
324    }
325
326    pub fn to_u8(&self) -> u8 {
327        self.0
328    }
329
330    pub fn is_red(&self) -> bool {
331        self.0 == 51 || self.0 == 52 || self.0 == 53
332    }
333
334    pub fn to_black(&self) -> Tile {
335        match self.0 {
336            51 => Tile(15),
337            52 => Tile(25),
338            53 => Tile(35),
339            _ => *self,
340        }
341    }
342
343    pub fn to_red(&self) -> Tile {
344        match self.0 {
345            15 => Tile(51),
346            25 => Tile(52),
347            35 => Tile(53),
348            _ => *self,
349        }
350    }
351}
352
353impl YakuLevel {
354    pub fn get_number(&self) -> u8 {
355        match self {
356            YakuLevel::Normal(x) => *x,
357            YakuLevel::Yakuman(x) => *x,
358        }
359    }
360}
361
362impl Yaku {
363    pub fn to_str(&self) -> &str {
364        YAKU_NAME[*self as usize]
365    }
366}
367
368impl ExtraRyuukyokuReason {
369    pub fn to_str(&self) -> &str {
370        match self {
371            ExtraRyuukyokuReason::Ryuukyoku => "流局",
372            ExtraRyuukyokuReason::KyuusyuKyuuhai => "九種九牌",
373            ExtraRyuukyokuReason::SuuchaRiichi => "四家立直",
374            ExtraRyuukyokuReason::SanchaHoura => "三家和了",
375            ExtraRyuukyokuReason::SuukanSanra => "四槓散了",
376            ExtraRyuukyokuReason::SuufuuRenda => "四風連打",
377            ExtraRyuukyokuReason::NagashiMangan => "流し満貫",
378            ExtraRyuukyokuReason::TenpaiEverybody => "全員聴牌",
379            ExtraRyuukyokuReason::TenpaiNobody => "全員不聴",
380        }
381    }
382}
383
384impl fmt::Display for Yaku {
385    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
386        f.write_str(self.to_str())
387    }
388}
389
390impl fmt::Display for YakuLevel {
391    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
392        match self {
393            YakuLevel::Normal(x) => write!(f, "{}飜", x),
394            YakuLevel::Yakuman(_) => write!(f, "役満"),
395        }
396    }
397}
398
399impl fmt::Display for YakuPair {
400    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
401        write!(f, "{}({})", self.yaku, self.level)
402    }
403}
404
405impl std::str::FromStr for Yaku {
406    type Err = InvalidYakuFormatError;
407
408    fn from_str(s: &str) -> Result<Self, Self::Err> {
409        if let Some(pos) = YAKU_NAME.iter().position(|name| *name == s) {
410            Ok(Yaku::from_u8(pos as u8).unwrap())
411        } else {
412            Err(InvalidYakuFormatError)
413        }
414    }
415}
416
417impl std::str::FromStr for YakuLevel {
418    type Err = InvalidYakuFormatError;
419
420    fn from_str(s: &str) -> Result<Self, Self::Err> {
421        if s == "役満" {
422            Ok(YakuLevel::Yakuman(1))
423        } else if let Ok(n) = s.trim_end_matches('飜').parse() {
424            Ok(YakuLevel::Normal(n))
425        } else {
426            Err(InvalidYakuFormatError)
427        }
428    }
429}
430
431impl std::str::FromStr for YakuPair {
432    type Err = InvalidYakuFormatError;
433
434    fn from_str(s: &str) -> Result<Self, Self::Err> {
435        let start = s.find('(').ok_or(InvalidYakuFormatError)?;
436        let end = s.find(')').ok_or(InvalidYakuFormatError)?;
437        let yaku = Yaku::from_str(&s[..start])?;
438        let level = YakuLevel::from_str(&s[start + 1..end])?;
439        Ok(YakuPair { yaku, level })
440    }
441}
442
443impl std::str::FromStr for ExtraRyuukyokuReason {
444    type Err = InvalidExtraRyuukyokuReasonError;
445
446    fn from_str(s: &str) -> Result<Self, Self::Err> {
447        match s {
448            "流局" => Ok(ExtraRyuukyokuReason::Ryuukyoku),
449            "九種九牌" => Ok(ExtraRyuukyokuReason::KyuusyuKyuuhai),
450            "四家立直" => Ok(ExtraRyuukyokuReason::SuuchaRiichi),
451            "三家和了" => Ok(ExtraRyuukyokuReason::SanchaHoura),
452            "四槓散了" => Ok(ExtraRyuukyokuReason::SuukanSanra),
453            "四風連打" => Ok(ExtraRyuukyokuReason::SuufuuRenda),
454            "流し満貫" => Ok(ExtraRyuukyokuReason::NagashiMangan),
455            "全員聴牌" => Ok(ExtraRyuukyokuReason::TenpaiEverybody),
456            "全員不聴" => Ok(ExtraRyuukyokuReason::TenpaiNobody),
457            _ => Err(InvalidExtraRyuukyokuReasonError),
458        }
459    }
460}
461
462impl Default for RoundResult {
463    fn default() -> Self {
464        RoundResult::Ryuukyoku {
465            reason: ExtraRyuukyokuReason::Ryuukyoku,
466            delta_points: Vec::new(),
467        }
468    }
469}
470
471fn is_valid_tile(x: u8) -> bool {
472    match x {
473        x if (11..=19).contains(&x) => true, // m
474        x if (21..=29).contains(&x) => true, // p
475        x if (31..=39).contains(&x) => true, // s
476        x if (41..=47).contains(&x) => true, // z
477        x if (51..=53).contains(&x) => true, // red 5
478        _ => false,
479    }
480}