tenhou_json/
parser.rs

1use crate::model::*;
2use crate::score::*;
3use serde_json::value::Index;
4use serde_json::Value;
5use std::str::FromStr;
6use thiserror::Error;
7
8pub type TenhouJsonResult<T> = Result<T, TenhouJsonError>;
9
10#[derive(Debug, Error)]
11#[error("{kind} at {path}")]
12pub struct TenhouJsonError {
13    pub kind: TenhouJsonErrorKind,
14    pub path: String,
15}
16
17impl TenhouJsonError {
18    pub fn new(kind: TenhouJsonErrorKind) -> Self {
19        TenhouJsonError { path: String::new(), kind }
20    }
21}
22
23#[derive(Debug, Error)]
24pub enum TenhouJsonErrorKind {
25    #[error("Cannot parse json")]
26    JsonParseError,
27    #[error("Missing field")]
28    MissingField,
29    #[error("Missmatch type")]
30    TypeMismatch,
31    #[error("Invalid array length")]
32    InvalidArrayLength,
33    #[error("Invalid meld format")]
34    InvalidMeld,
35    #[error("Invalid riichi format")]
36    InvalidRiichi,
37    #[error("Invalid ankan format")]
38    InvalidAnkan,
39    #[error("Invalid kakan format")]
40    InvalidKakan,
41    #[error("Invalid decoration")]
42    InvalidDecoration,
43    #[error("Invalid tile number")]
44    InvalidTileNumber,
45    #[error("Invalid extra ryuukyoku reason")]
46    InvalidExtraRyuukyokuReason,
47    #[error("Invalid yaku name")]
48    InvalidYakuName,
49    #[error("Invalid yaku level")]
50    InvalidYakuLevel,
51    #[error("Invalid yaku format")]
52    InvalidYakuFormat,
53    #[error("Invalid ranked score")]
54    InvalidRankedScore,
55    #[error("Invalid agari format")]
56    InvalidAgariFormat,
57    #[error("Invalid letter position")]
58    InvalidLetterPosition,
59}
60
61trait WithContext {
62    fn context(self, key: &str) -> Self;
63    fn index_context(self, index: usize) -> Self;
64}
65
66impl<T> WithContext for TenhouJsonResult<T> {
67    fn context(self, key: &str) -> Self {
68        self.map_err(|e| {
69            TenhouJsonError { path: format!("{}.{}", key, e.path), ..e } // TODO array index
70        })
71    }
72
73    fn index_context(self, index: usize) -> Self {
74        self.context(&format!("[{}]", index))
75    }
76}
77
78fn conv_i64(v: &Value) -> TenhouJsonResult<i64> {
79    v.as_i64().ok_or_else(|| TenhouJsonError::new(TenhouJsonErrorKind::TypeMismatch))
80}
81
82fn conv_i32(v: &Value) -> TenhouJsonResult<i32> {
83    Ok(conv_i64(v)? as i32)
84}
85
86fn conv_i8(v: &Value) -> TenhouJsonResult<i8> {
87    Ok(conv_i64(v)? as i8)
88}
89
90fn conv_u32(v: &Value) -> TenhouJsonResult<u32> {
91    Ok(conv_i64(v)? as u32)
92}
93
94fn conv_u8(v: &Value) -> TenhouJsonResult<u8> {
95    Ok(conv_i64(v)? as u8)
96}
97
98fn conv_f64(v: &Value) -> TenhouJsonResult<f64> {
99    v.as_f64().ok_or_else(|| TenhouJsonError::new(TenhouJsonErrorKind::TypeMismatch))
100}
101
102fn conv_array(v: &Value) -> TenhouJsonResult<&Vec<Value>> {
103    v.as_array().ok_or_else(|| TenhouJsonError::new(TenhouJsonErrorKind::TypeMismatch))
104}
105
106fn conv_str(v: &Value) -> TenhouJsonResult<&str> {
107    v.as_str().ok_or_else(|| TenhouJsonError::new(TenhouJsonErrorKind::TypeMismatch))
108}
109
110fn conv_string(v: &Value) -> TenhouJsonResult<String> {
111    Ok(conv_str(v)?.to_string())
112}
113
114fn conv_rule(v: &Value) -> TenhouJsonResult<Rule> {
115    Ok(Rule {
116        disp: get_field_string(v, "disp")?,
117        aka51: get_field_u32(v, "aka51")? != 0,
118        aka52: get_field_u32(v, "aka52")? != 0,
119        aka53: get_field_u32(v, "aka53")? != 0,
120    })
121}
122
123fn conv_tile_from_u8(x: u8) -> TenhouJsonResult<Tile> {
124    Tile::from_u8(x).map_err(|_| TenhouJsonError::new(TenhouJsonErrorKind::InvalidTileNumber))
125}
126
127fn conv_tile_from_ascii(x0: u8, x1: u8) -> TenhouJsonResult<Tile> {
128    let y0 = x0 - b'0';
129    let y1 = x1 - b'0';
130    conv_tile_from_u8(y0 * 10 + y1)
131}
132
133fn conv_tile(v: &Value) -> TenhouJsonResult<Tile> {
134    conv_tile_from_u8(conv_u8(v)?)
135}
136
137fn parse_decorated_tile(s: &str) -> TenhouJsonResult<(Vec<Tile>, u8, usize)> {
138    if !s.chars().all(|c| c.is_ascii_alphanumeric()) {
139        return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidMeld));
140    }
141
142    let xs: Vec<u8> = s.bytes().collect();
143
144    let (letter_pos, letter) = xs.iter().enumerate().find(|(_, c)| c.is_ascii_alphabetic()).ok_or_else(|| TenhouJsonError::new(TenhouJsonErrorKind::InvalidMeld))?;
145    if letter_pos % 2 != 0 {
146        return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidMeld));
147    }
148
149    let numbers: Vec<u8> = xs.iter().enumerate().filter(|(i, _)| *i != letter_pos).map(|(_, c)| *c).collect();
150    if !numbers.iter().all(|c| c.is_ascii_digit()) {
151        return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidMeld));
152    }
153
154    let tiles = numbers.chunks(2).map(|c| conv_tile_from_ascii(c[0], c[1])).collect::<TenhouJsonResult<Vec<_>>>()?;
155
156    Ok((tiles, *letter, letter_pos))
157}
158
159fn conv_incoming_tile(v: &Value) -> TenhouJsonResult<IncomingTile> {
160    if v.is_i64() {
161        // normal tsumo
162        Ok(IncomingTile::Tsumo(conv_tile(v)?))
163    } else {
164        // chii/pon
165        let s = conv_str(v)?;
166        let (tiles, letter, letter_pos) = parse_decorated_tile(s)?;
167        match letter {
168            b'c' => {
169                if letter_pos != 0 {
170                    return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidLetterPosition));
171                }
172                Ok(IncomingTile::Chii { combination: (tiles[0], tiles[1], tiles[2]) })
173            }
174            b'p' => {
175                let dir = match letter_pos {
176                    0 => Direction::Kamicha,
177                    2 => Direction::Toimen,
178                    4 => Direction::Shimocha,
179                    _ => return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidLetterPosition)),
180                };
181                Ok(IncomingTile::Pon {
182                    combination: (tiles[0], tiles[1], tiles[2]),
183                    dir,
184                })
185            }
186            b'm' => {
187                let dir = match letter_pos {
188                    0 => Direction::Kamicha,
189                    2 => Direction::Toimen,
190                    6 => Direction::Shimocha,
191                    _ => return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidLetterPosition)),
192                };
193                Ok(IncomingTile::Daiminkan {
194                    combination: (tiles[0], tiles[1], tiles[2], tiles[3]),
195                    dir,
196                })
197            }
198            _ => Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidMeld))?,
199        }
200    }
201}
202
203fn conv_outgoing_tile(v: &Value) -> TenhouJsonResult<OutgoingTile> {
204    if v.is_i64() {
205        // normal discard
206        let x = conv_u8(v)?;
207        match x {
208            60 => Ok(OutgoingTile::Tsumogiri),
209            0 => Ok(OutgoingTile::Dummy),
210            x => Ok(OutgoingTile::Discard(conv_tile_from_u8(x)?)),
211        }
212    } else {
213        // riichi/ankan/kakan
214        let s = conv_str(v)?;
215
216        // this is special case
217        if s == "r60" {
218            return Ok(OutgoingTile::TsumogiriRiichi);
219        }
220
221        let (tiles, letter, letter_pos) = parse_decorated_tile(s)?;
222        if letter == b'r' {
223            if tiles.len() != 1 {
224                return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidRiichi));
225            }
226            Ok(OutgoingTile::Riichi(tiles[0]))
227        } else if letter == b'a' {
228            if tiles.len() != 4 {
229                return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidAnkan));
230            }
231            Ok(OutgoingTile::Ankan(tiles[3]))
232        } else if letter == b'k' {
233            if tiles.len() != 4 {
234                return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidKakan));
235            }
236
237            let dir = match letter_pos {
238                0 => Direction::Kamicha,
239                2 => Direction::Toimen,
240                4 => Direction::Shimocha,
241                _ => return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidLetterPosition)),
242            };
243
244            // reference:
245            // 2019010215gm-00a9-0000-93e74c9f.json
246            // 35p3553 -> 35k353553
247            // added tile is after 'k'?
248            let added_index = letter_pos / 2;
249            let added = tiles[added_index];
250            let mut comb = tiles.clone();
251            comb.remove(added_index);
252
253            Ok(OutgoingTile::Kakan {
254                combination: (comb[0], comb[1], comb[2]),
255                dir,
256                added,
257            })
258        } else {
259            Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidDecoration))
260        }
261    }
262}
263
264fn conv_tiles(v: &Value) -> TenhouJsonResult<Vec<Tile>> {
265    conv_array(v)?.iter().map(conv_tile).collect()
266}
267
268fn conv_incoming_tiles(v: &Value) -> TenhouJsonResult<Vec<IncomingTile>> {
269    conv_array(v)?.iter().map(conv_incoming_tile).collect()
270}
271
272fn conv_outgoing_tiles(v: &Value) -> TenhouJsonResult<Vec<OutgoingTile>> {
273    conv_array(v)?.iter().map(conv_outgoing_tile).collect()
274}
275
276fn conv_round_setting(vs: &[Value]) -> TenhouJsonResult<RoundSettings> {
277    let h1 = conv_i32_array(&vs[0])?;
278    if h1.len() != 3 {
279        return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidArrayLength));
280    }
281
282    Ok(RoundSettings {
283        kyoku: h1[0] as u8,
284        honba: h1[1] as u8,
285        kyoutaku: h1[2] as u8,
286        points: conv_i32_array(&vs[1])?,
287        dora: conv_tiles(&vs[2])?,
288        ura_dora: conv_tiles(&vs[3])?,
289    })
290}
291
292fn conv_round_player(vs: &[Value]) -> TenhouJsonResult<RoundPlayer> {
293    Ok(RoundPlayer {
294        hand: conv_tiles(&vs[0])?,
295        incoming: conv_incoming_tiles(&vs[1])?,
296        outgoing: conv_outgoing_tiles(&vs[2])?,
297    })
298}
299
300fn conv_round_players(vs: &[Value]) -> TenhouJsonResult<Vec<RoundPlayer>> {
301    vs.chunks(3).map(conv_round_player).collect()
302}
303
304fn conv_extra_ryuukyoku_reason(s: &str) -> TenhouJsonResult<ExtraRyuukyokuReason> {
305    ExtraRyuukyokuReason::from_str(s).map_err(|_| TenhouJsonError::new(TenhouJsonErrorKind::InvalidExtraRyuukyokuReason))
306}
307
308fn conv_ranked_score(v: &Value) -> TenhouJsonResult<RankedScore> {
309    let s = conv_str(v)?;
310    RankedScore::from_str(s).map_err(|_| TenhouJsonError::new(TenhouJsonErrorKind::InvalidRankedScore))
311}
312
313fn conv_yaku_pair(v: &Value) -> TenhouJsonResult<YakuPair> {
314    let s = conv_str(v)?;
315    YakuPair::from_str(s).map_err(|_| TenhouJsonError::new(TenhouJsonErrorKind::InvalidYakuFormat))
316}
317
318fn conv_yaku_pair_array(vs: &[Value]) -> TenhouJsonResult<Vec<YakuPair>> {
319    vs.iter().map(conv_yaku_pair).collect()
320}
321
322fn conv_agari(chunk0: &Value, chunk1: &Value) -> TenhouJsonResult<Agari> {
323    let xs = conv_array(chunk1)?;
324    if xs.len() <= 4 {
325        return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidAgariFormat));
326    }
327
328    Ok(Agari {
329        delta_points: conv_i32_array(chunk0)?,
330        who: conv_u8(&xs[0])?,
331        from_who: conv_u8(&xs[1])?,
332        pao_who: conv_u8(&xs[2])?,
333        ranked_score: conv_ranked_score(&xs[3])?,
334        yaku: conv_yaku_pair_array(&xs[4..])?,
335    })
336}
337
338fn conv_agari_array(vs: &[Value]) -> TenhouJsonResult<Vec<Agari>> {
339    vs.chunks(2).map(|chunk| conv_agari(&chunk[0], &chunk[1])).collect()
340}
341
342fn conv_round_result(v: &Value) -> TenhouJsonResult<RoundResult> {
343    let xs = conv_array(v)?;
344    if xs.is_empty() {
345        return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidArrayLength));
346    }
347
348    match conv_str(&xs[0])? {
349        "和了" => Ok(RoundResult::Agari { agari_vec: conv_agari_array(&xs[1..])? }),
350        x => {
351            // NOT CLEAR:
352            // If the score changes due to double riichi, will the nine tiles affect delta_points?
353            Ok(RoundResult::Ryuukyoku {
354                reason: conv_extra_ryuukyoku_reason(x)?,
355                delta_points: if xs.len() >= 2 { conv_i32_array(&xs[1])? } else { vec![] },
356            })
357        }
358    }
359}
360
361fn conv_round(v: &Value) -> TenhouJsonResult<Round> {
362    let xs = conv_array(v)?;
363
364    // header(4) + players(4*3) + result(1) == 17
365    if xs.len() != 17 {
366        return Err(TenhouJsonError::new(TenhouJsonErrorKind::InvalidArrayLength));
367    }
368
369    Ok(Round {
370        settings: conv_round_setting(&xs[0..4])?,
371        players: conv_round_players(&xs[4..16])?,
372        result: conv_round_result(&xs[16])?,
373    })
374}
375
376fn conv_connection(v: &Value) -> TenhouJsonResult<Connection> {
377    Ok(Connection {
378        what: get_field_u8(v, "what")?,
379        log: get_field_i8(v, "log")?,
380        who: get_field_u8(v, "who")?,
381        step: get_field_u32(v, "step")?,
382    })
383}
384
385fn conv_tenhou_json(v: &Value) -> TenhouJsonResult<TenhouJson> {
386    let sc = get_field(v, "sc")?;
387    let sc_array = conv_array(sc)?;
388    let (even_sc, odd_sc) = get_partition_even_odd(sc_array);
389    let final_points = even_sc.iter().map(conv_i32).collect::<TenhouJsonResult<Vec<i32>>>()?;
390    let final_results = odd_sc.iter().map(conv_f64).collect::<TenhouJsonResult<Vec<f64>>>()?;
391
392    Ok(TenhouJson {
393        ver: get_field_f64(v, "ver")?,
394        reference: get_field_string(v, "ref")?,
395        rounds: get_field_round_array(v, "log")?,
396        connections: get_field_connection_array(v, "connection")?,
397        ratingc: get_field_string(v, "ratingc")?,
398        rule: get_field_rule(v, "rule")?,
399        lobby: get_field_u32(v, "lobby")?,
400        dan: get_field_string_array(v, "dan")?,
401        rate: get_field_f64_array(v, "rate")?,
402        sx: get_field_string_array(v, "sx")?,
403        final_points,
404        final_results,
405        names: get_field_string_array(v, "name")?,
406    })
407}
408
409fn conv_string_array(v: &Value) -> TenhouJsonResult<Vec<String>> {
410    conv_array(v)?.iter().enumerate().map(|(i, x)| conv_string(x).index_context(i)).collect()
411}
412
413fn conv_f64_array(v: &Value) -> TenhouJsonResult<Vec<f64>> {
414    conv_array(v)?.iter().enumerate().map(|(i, x)| conv_f64(x).index_context(i)).collect()
415}
416
417fn conv_i32_array(v: &Value) -> TenhouJsonResult<Vec<i32>> {
418    conv_array(v)?.iter().enumerate().map(|(i, x)| conv_i32(x).index_context(i)).collect()
419}
420
421fn conv_round_array(v: &Value) -> TenhouJsonResult<Vec<Round>> {
422    conv_array(v)?.iter().enumerate().map(|(i, x)| conv_round(x).index_context(i)).collect()
423}
424
425fn conv_connection_array(v: &Value) -> TenhouJsonResult<Vec<Connection>> {
426    conv_array(v)?.iter().enumerate().map(|(i, x)| conv_connection(x).index_context(i)).collect()
427}
428
429fn get_field<I: Index + ToString>(json: &Value, index: I) -> TenhouJsonResult<&Value> {
430    json.get(&index).ok_or_else(|| TenhouJsonError::new(TenhouJsonErrorKind::MissingField))
431}
432
433fn get_field_u8(json: &Value, key: &str) -> TenhouJsonResult<u8> {
434    let v = get_field(json, key)?;
435    conv_u8(v).context(key)
436}
437
438fn get_field_i8(json: &Value, key: &str) -> TenhouJsonResult<i8> {
439    let v = get_field(json, key)?;
440    conv_i8(v).context(key)
441}
442
443fn get_field_u32(json: &Value, key: &str) -> TenhouJsonResult<u32> {
444    let v = get_field(json, key)?;
445    conv_u32(v).context(key)
446}
447
448fn get_field_f64(json: &Value, key: &str) -> TenhouJsonResult<f64> {
449    let v = get_field(json, key)?;
450    conv_f64(v).context(key)
451}
452
453fn get_field_string(json: &Value, key: &str) -> TenhouJsonResult<String> {
454    let v = get_field(json, key)?;
455    conv_string(v).context(key)
456}
457
458fn get_field_string_array(json: &Value, key: &str) -> TenhouJsonResult<Vec<String>> {
459    let v = get_field(json, key)?;
460    conv_string_array(v).context(key)
461}
462
463fn get_field_f64_array(json: &Value, key: &str) -> TenhouJsonResult<Vec<f64>> {
464    let v = get_field(json, key)?;
465    conv_f64_array(v).context(key)
466}
467
468fn get_field_rule(json: &Value, key: &str) -> TenhouJsonResult<Rule> {
469    let v = get_field(json, key)?;
470    conv_rule(v).context(key)
471}
472
473fn get_field_round_array(json: &Value, key: &str) -> TenhouJsonResult<Vec<Round>> {
474    let v = get_field(json, key)?;
475    conv_round_array(v).context(key)
476}
477
478fn get_field_connection_array(json: &Value, key: &str) -> TenhouJsonResult<Vec<Connection>> {
479    if let Some(v) = json.get(key) {
480        conv_connection_array(v).context(key)
481    } else {
482        Ok(vec![])
483    }
484}
485
486fn get_partition_even_odd<T: Clone>(v: &[T]) -> (Vec<T>, Vec<T>) {
487    (v.iter().step_by(2).cloned().collect(), v.iter().skip(1).step_by(2).cloned().collect())
488}
489
490pub fn parse_tenhou_json(text: &str) -> TenhouJsonResult<TenhouJson> {
491    let json: Value = serde_json::from_str(text).map_err(|_| TenhouJsonError::new(TenhouJsonErrorKind::JsonParseError))?;
492    conv_tenhou_json(&json)
493}