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#[derive(Debug, Default, PartialEq, Clone, Copy)]
24pub struct Tile(u8);
25
26#[derive(Debug, Default, PartialEq, Clone, Copy)]
28pub enum Direction {
29 #[default]
30 SelfSeat,
31 Kamicha,
32 Toimen,
33 Shimocha,
34}
35
36#[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#[derive(Debug, PartialEq)]
47pub enum OutgoingTile {
48 Discard(Tile),
50
51 Riichi(Tile),
53
54 Ankan(Tile),
59
60 Kakan { combination: (Tile, Tile, Tile), dir: Direction, added: Tile },
62
63 Tsumogiri,
65
66 TsumogiriRiichi,
68
69 Dummy,
73}
74
75#[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#[derive(Debug, PartialEq)]
88pub enum YakuLevel {
89 Normal(u8),
90 Yakuman(u8),
91}
92
93#[derive(Debug, PartialEq)]
95pub struct YakuPair {
96 pub yaku: Yaku,
97 pub level: YakuLevel,
98}
99
100#[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#[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 "門前清自摸和",
129 "立直",
130 "一発",
131 "槍槓",
132 "嶺上開花",
133 "海底摸月",
134 "河底撈魚",
135 "平和",
136 "断幺九",
137 "一盃口",
138 "自風 東",
139 "自風 南",
140 "自風 西",
141 "自風 北",
142 "場風 東",
143 "場風 南",
144 "場風 西",
145 "場風 北",
146 "役牌 白",
147 "役牌 發",
148 "役牌 中",
149 "両立直",
151 "七対子",
152 "混全帯幺九",
153 "一気通貫",
154 "三色同順",
155 "三色同刻",
156 "三槓子",
157 "対々和",
158 "三暗刻",
159 "小三元",
160 "混老頭",
161 "二盃口",
163 "純全帯幺九",
164 "混一色",
165 "清一色",
167 "人和",
169 "天和",
171 "地和",
172 "大三元",
173 "四暗刻",
174 "四暗刻単騎",
175 "字一色",
176 "緑一色",
177 "清老頭",
178 "九蓮宝燈",
179 "純正九蓮宝燈",
180 "国士無双",
181 "国士無双13面",
182 "大四喜",
183 "小四喜",
184 "四槓子",
185 "ドラ",
187 "裏ドラ",
188 "赤ドラ",
189];
190
191#[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#[derive(Debug, PartialEq)]
255pub enum RoundResult {
256 Agari { agari_vec: Vec<Agari> },
257 Ryuukyoku { reason: ExtraRyuukyokuReason, delta_points: Vec<GamePoint> },
258}
259
260#[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#[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#[derive(Debug, Default, PartialEq)]
279pub struct Round {
280 pub settings: RoundSettings,
281 pub players: Vec<RoundPlayer>,
282 pub result: RoundResult,
283}
284
285#[derive(Debug, Default, PartialEq)]
287pub struct Connection {
288 pub what: u8,
289
290 pub log: i8,
294
295 pub who: u8,
296 pub step: u32,
297}
298
299#[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, x if (21..=29).contains(&x) => true, x if (31..=39).contains(&x) => true, x if (41..=47).contains(&x) => true, x if (51..=53).contains(&x) => true, _ => false,
479 }
480}