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 } })
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 Ok(IncomingTile::Tsumo(conv_tile(v)?))
163 } else {
164 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 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 let s = conv_str(v)?;
215
216 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 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 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 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}