mjlog2json_core/
conv.rs

1use mjlog::model::*;
2use mjlog::parser::MjlogError;
3use std::iter::once;
4use tenhou_json::calc::*;
5use tenhou_json::model::*;
6use tenhou_json::parser::*;
7use tenhou_json::score::*;
8use thiserror::Error;
9
10#[derive(Debug, Error)]
11pub enum ConvError {
12    #[error(transparent)]
13    MjlogError(#[from] MjlogError),
14    #[error(transparent)]
15    TenhouJsonError(#[from] TenhouJsonError),
16    #[error("Action GO is not found")]
17    NotFoundActionGO,
18    #[error("Action UN1 is not found")]
19    NotFoundActionUN1,
20    #[error("Terminal action is not found")]
21    NotFoundTerminalAction,
22    #[error("Round is not found")]
23    NotFoundRound,
24    #[error("FinalResult is not found")]
25    NotFoundFinalResult,
26    #[error("Invalid round format")]
27    InvalidRoundFormat,
28    #[error("Invalid tile format")]
29    InvalidTileFormat,
30}
31
32type ConvResult<T> = Result<T, ConvError>;
33
34fn extract_round_indices(actions: &[Action]) -> Vec<(usize, usize)> {
35    let mut indices: Vec<(usize, usize)> = Vec::new();
36    let mut start = None;
37
38    for (i, a) in actions.iter().enumerate() {
39        if a.is_init() {
40            if let Some(start_index) = start {
41                indices.push((start_index, i));
42            }
43            start = Some(i);
44        }
45    }
46
47    if let Some(start_index) = start {
48        indices.push((start_index, actions.len()));
49    }
50
51    indices
52}
53
54// NOT CLEAR: When double ron
55fn find_final_result(actions: &[Action]) -> ConvResult<(Vec<i32>, Vec<f64>)> {
56    // find from last
57    for a in actions.iter().rev() {
58        match a {
59            Action::AGARI(ActionAGARI { owari, .. }) => {
60                if let Some(x) = owari {
61                    return Ok(x.clone());
62                } else {
63                    return Err(ConvError::InvalidRoundFormat);
64                }
65            }
66            Action::RYUUKYOKU(ActionRYUUKYOKU { owari, .. }) => {
67                if let Some(x) = owari {
68                    return Ok(x.clone());
69                } else {
70                    return Err(ConvError::InvalidRoundFormat);
71                }
72            }
73            _ => {}
74        }
75    }
76    Err(ConvError::NotFoundFinalResult)
77}
78
79const DAN_NAME: [&str; 21] = [
80    "新人", "9級", "8級", "7級", "6級", "5級", "4級", "3級", "2級", "1級", "初段", "二段", "三段", "四段", "五段", "六段", "七段", "八段", "九段", "十段", "天鳳",
81];
82
83fn conv_dan(dan: &TenhouRank) -> String {
84    DAN_NAME[*dan as usize].to_string()
85}
86
87fn conv_tile_from_u8(x: u8) -> ConvResult<Tile> {
88    Tile::from_u8(x).map_err(|_| ConvError::InvalidTileFormat)
89}
90
91fn conv_hai_to_tile(hai: Hai, red_enable: bool) -> ConvResult<Tile> {
92    let hai_number = hai.to_u8();
93
94    if red_enable {
95        match hai_number {
96            16 => return conv_tile_from_u8(51),
97            52 => return conv_tile_from_u8(52),
98            88 => return conv_tile_from_u8(53),
99            _ => {}
100        }
101    }
102
103    // pict_order
104    // 123456789m123456789p123456789s1234567z
105    let pict_order = hai_number / 4;
106
107    // 1 == m
108    // 2 == p
109    // 3 == s
110    // 4 == z
111    let pict_type = (pict_order / 9) + 1;
112
113    // 1..9mps or 1..7z
114    let pict_num = (pict_order % 9) + 1;
115
116    conv_tile_from_u8(pict_type * 10 + pict_num)
117}
118
119fn get_dora_vec(dora_hyouji: Hai, mid_actions: &[Action]) -> ConvResult<Vec<Tile>> {
120    let dora_hais: Vec<Hai> = once(dora_hyouji).chain(mid_actions.iter().filter_map(|x| x.as_dora()).map(|x| x.hai)).collect();
121    dora_hais.iter().map(|x| conv_hai_to_tile(*x, true)).collect()
122}
123
124fn get_ura_dora(end_action: &Action) -> ConvResult<Vec<Tile>> {
125    match end_action {
126        Action::AGARI(ActionAGARI { dora_hai_ura, .. }) => dora_hai_ura.iter().map(|x| conv_hai_to_tile(*x, true)).collect(),
127        Action::RYUUKYOKU(_) => Ok(vec![]),
128        _ => panic!("unexpected end action"),
129    }
130}
131
132/// Note:
133/// The ura-dora is only recorded in the winning information of the riichi declarer.
134/// Therefore, in the case of multiple ron, the ura-dora must be retrieved from each winner.
135/// However, if it is found for one player, it will be the same for all winners.
136fn get_ura_dora_vec(end_actions: &[&Action]) -> ConvResult<Vec<Tile>> {
137    for a in end_actions {
138        let ura_dora = get_ura_dora(a)?;
139        if !ura_dora.is_empty() {
140            return Ok(ura_dora);
141        }
142    }
143    Ok(Vec::new())
144}
145
146fn conv_rule(settings: &GameSettings) -> ConvResult<Rule> {
147    let room_str = match settings.room {
148        TenhouRoom::Ippan => "般",
149        TenhouRoom::Joukyu => "上",
150        TenhouRoom::Tokujou => "特",
151        TenhouRoom::Houou => "鳳",
152    };
153
154    let hanchan_str = if settings.hanchan { "南" } else { "東" };
155    let aka_str = if settings.no_red { "" } else { "赤" };
156    let kuitan_str = if settings.no_kuitan { "" } else { "喰" };
157    let soku_str = if settings.soku { "速" } else { "" };
158
159    Ok(Rule {
160        disp: format!("{}{}{}{}{}", room_str, hanchan_str, kuitan_str, aka_str, soku_str),
161        aka53: !settings.no_red,
162        aka52: !settings.no_red,
163        aka51: !settings.no_red,
164    })
165}
166
167fn conv_round_setting(actions: &[Action]) -> ConvResult<RoundSettings> {
168    let start_action = &actions[0];
169    assert!(start_action.is_init());
170
171    let init = start_action.as_init().unwrap();
172    let end_actions: Vec<&Action> = actions.iter().filter(|x| x.is_agari() || x.is_ryuukyoku()).collect();
173
174    if end_actions.is_empty() {
175        return Err(ConvError::NotFoundTerminalAction);
176    }
177
178    Ok(RoundSettings {
179        kyoku: init.seed.kyoku,
180        honba: init.seed.honba,
181        kyoutaku: init.seed.kyoutaku,
182        points: init.ten.iter().map(|x| x * 100).collect(),
183        dora: get_dora_vec(init.seed.dora_hyouji, actions)?,
184        ura_dora: get_ura_dora_vec(&end_actions)?,
185    })
186}
187
188fn conv_yaku(x: mjlog::model::Yaku) -> tenhou_json::model::Yaku {
189    match x {
190        mjlog::model::Yaku::MenzenTsumo => tenhou_json::model::Yaku::MenzenTsumo,
191        mjlog::model::Yaku::Riichi => tenhou_json::model::Yaku::Riichi,
192        mjlog::model::Yaku::Ippatsu => tenhou_json::model::Yaku::Ippatsu,
193        mjlog::model::Yaku::Chankan => tenhou_json::model::Yaku::Chankan,
194        mjlog::model::Yaku::Rinshankaihou => tenhou_json::model::Yaku::Rinshankaihou,
195        mjlog::model::Yaku::HaiteiTsumo => tenhou_json::model::Yaku::HaiteiTsumo,
196        mjlog::model::Yaku::HouteiRon => tenhou_json::model::Yaku::HouteiRon,
197        mjlog::model::Yaku::Pinfu => tenhou_json::model::Yaku::Pinfu,
198        mjlog::model::Yaku::Tanyao => tenhou_json::model::Yaku::Tanyao,
199        mjlog::model::Yaku::Iipeikou => tenhou_json::model::Yaku::Iipeikou,
200        mjlog::model::Yaku::PlayerWindTon => tenhou_json::model::Yaku::PlayerWindTon,
201        mjlog::model::Yaku::PlayerWindNan => tenhou_json::model::Yaku::PlayerWindNan,
202        mjlog::model::Yaku::PlayerWindSha => tenhou_json::model::Yaku::PlayerWindSha,
203        mjlog::model::Yaku::PlayerWindPei => tenhou_json::model::Yaku::PlayerWindPei,
204        mjlog::model::Yaku::FieldWindTon => tenhou_json::model::Yaku::FieldWindTon,
205        mjlog::model::Yaku::FieldWindNan => tenhou_json::model::Yaku::FieldWindNan,
206        mjlog::model::Yaku::FieldWindSha => tenhou_json::model::Yaku::FieldWindSha,
207        mjlog::model::Yaku::FieldWindPei => tenhou_json::model::Yaku::FieldWindPei,
208        mjlog::model::Yaku::YakuhaiHaku => tenhou_json::model::Yaku::YakuhaiHaku,
209        mjlog::model::Yaku::YakuhaiHatsu => tenhou_json::model::Yaku::YakuhaiHatsu,
210        mjlog::model::Yaku::YakuhaiChun => tenhou_json::model::Yaku::YakuhaiChun,
211        mjlog::model::Yaku::DoubleRiichi => tenhou_json::model::Yaku::DoubleRiichi,
212        mjlog::model::Yaku::Chiitoitsu => tenhou_json::model::Yaku::Chiitoitsu,
213        mjlog::model::Yaku::Chanta => tenhou_json::model::Yaku::Chanta,
214        mjlog::model::Yaku::Ikkitsuukan => tenhou_json::model::Yaku::Ikkitsuukan,
215        mjlog::model::Yaku::SansyokuDoujun => tenhou_json::model::Yaku::SansyokuDoujun,
216        mjlog::model::Yaku::SanshokuDoukou => tenhou_json::model::Yaku::SanshokuDoukou,
217        mjlog::model::Yaku::Sankantsu => tenhou_json::model::Yaku::Sankantsu,
218        mjlog::model::Yaku::Toitoi => tenhou_json::model::Yaku::Toitoi,
219        mjlog::model::Yaku::Sanannkou => tenhou_json::model::Yaku::Sanannkou,
220        mjlog::model::Yaku::Shousangen => tenhou_json::model::Yaku::Shousangen,
221        mjlog::model::Yaku::Honroutou => tenhou_json::model::Yaku::Honroutou,
222        mjlog::model::Yaku::Ryanpeikou => tenhou_json::model::Yaku::Ryanpeikou,
223        mjlog::model::Yaku::Junchan => tenhou_json::model::Yaku::Junchan,
224        mjlog::model::Yaku::Honiisou => tenhou_json::model::Yaku::Honiisou,
225        mjlog::model::Yaku::Chiniisou => tenhou_json::model::Yaku::Chiniisou,
226        mjlog::model::Yaku::Renhou => tenhou_json::model::Yaku::Renhou,
227        mjlog::model::Yaku::Tenhou => tenhou_json::model::Yaku::Tenhou,
228        mjlog::model::Yaku::Chiihou => tenhou_json::model::Yaku::Chiihou,
229        mjlog::model::Yaku::Daisangen => tenhou_json::model::Yaku::Daisangen,
230        mjlog::model::Yaku::Suuankou => tenhou_json::model::Yaku::Suuankou,
231        mjlog::model::Yaku::SuuankouTanki => tenhou_json::model::Yaku::SuuankouTanki,
232        mjlog::model::Yaku::Tsuuiisou => tenhou_json::model::Yaku::Tsuuiisou,
233        mjlog::model::Yaku::Ryuuiisou => tenhou_json::model::Yaku::Ryuuiisou,
234        mjlog::model::Yaku::Chinroutou => tenhou_json::model::Yaku::Chinroutou,
235        mjlog::model::Yaku::Tyuurenpoutou => tenhou_json::model::Yaku::Tyuurenpoutou,
236        mjlog::model::Yaku::Tyuurenpoutou9 => tenhou_json::model::Yaku::Tyuurenpoutou9,
237        mjlog::model::Yaku::Kokushimusou => tenhou_json::model::Yaku::Kokushimusou,
238        mjlog::model::Yaku::Kokushimusou13 => tenhou_json::model::Yaku::Kokushimusou13,
239        mjlog::model::Yaku::Daisuushii => tenhou_json::model::Yaku::Daisuushii,
240        mjlog::model::Yaku::Syousuushii => tenhou_json::model::Yaku::Syousuushii,
241        mjlog::model::Yaku::Suukantsu => tenhou_json::model::Yaku::Suukantsu,
242        mjlog::model::Yaku::Dora => tenhou_json::model::Yaku::Dora,
243        mjlog::model::Yaku::UraDora => tenhou_json::model::Yaku::UraDora,
244        mjlog::model::Yaku::AkaDora => tenhou_json::model::Yaku::AkaDora,
245    }
246}
247
248fn conv_extra_ryuukyoku_reason(x: &Option<mjlog::model::ExtraRyuukyokuReason>) -> tenhou_json::model::ExtraRyuukyokuReason {
249    match x {
250        Some(mjlog::model::ExtraRyuukyokuReason::KyuusyuKyuuhai) => tenhou_json::model::ExtraRyuukyokuReason::KyuusyuKyuuhai,
251        Some(mjlog::model::ExtraRyuukyokuReason::SuuchaRiichi) => tenhou_json::model::ExtraRyuukyokuReason::SuuchaRiichi,
252        Some(mjlog::model::ExtraRyuukyokuReason::SanchaHoura) => tenhou_json::model::ExtraRyuukyokuReason::SanchaHoura,
253        Some(mjlog::model::ExtraRyuukyokuReason::SuukanSanra) => tenhou_json::model::ExtraRyuukyokuReason::SuukanSanra,
254        Some(mjlog::model::ExtraRyuukyokuReason::SuufuuRenda) => tenhou_json::model::ExtraRyuukyokuReason::SuufuuRenda,
255        Some(mjlog::model::ExtraRyuukyokuReason::NagashiMangan) => tenhou_json::model::ExtraRyuukyokuReason::NagashiMangan,
256        None => tenhou_json::model::ExtraRyuukyokuReason::Ryuukyoku,
257    }
258}
259
260fn is_not_ura_zero(x: &YakuPair) -> bool {
261    !matches!(
262        x,
263        YakuPair {
264            yaku: tenhou_json::model::Yaku::UraDora,
265            level: YakuLevel::Normal(0)
266        }
267    )
268}
269
270fn conv_ranked_score_normal(v: &ActionAGARI, han: u8, oya: Player) -> RankedScore {
271    match (v.is_tsumo(), v.who == oya) {
272        (true, true) => get_oya_tsumo(v.fu, han),
273        (true, false) => get_ko_tsumo(v.fu, han),
274        (false, true) => get_oya_ron(v.fu, han),
275        (false, false) => get_ko_ron(v.fu, han),
276    }
277}
278
279fn conv_ranked_score_yakuman(v: &ActionAGARI, num: u8, oya: Player) -> RankedScore {
280    match (v.is_tsumo(), v.who == oya) {
281        (true, true) => get_oya_tsumo_yakuman(num),
282        (true, false) => get_ko_tsumo_yakuman(num),
283        (false, true) => get_oya_ron_yakuman(num),
284        (false, false) => get_ko_ron_yakuman(num),
285    }
286}
287
288fn conv_yaku_vec(vs: &[(mjlog::model::Yaku, u8)]) -> Vec<YakuPair> {
289    vs.iter()
290        .map(|&(yaku, han)| YakuPair {
291            yaku: conv_yaku(yaku),
292            level: YakuLevel::Normal(han),
293        })
294        .filter(is_not_ura_zero)
295        .collect()
296}
297
298fn conv_yakuman_vec(vs: &[mjlog::model::Yaku]) -> Vec<YakuPair> {
299    vs.iter()
300        .map(|&yaku| YakuPair {
301            yaku: conv_yaku(yaku),
302            level: YakuLevel::Yakuman(1),
303        })
304        .collect()
305}
306
307fn conv_agari(v: &ActionAGARI, oya: Player) -> ConvResult<Agari> {
308    let delta_points = v.delta_points.iter().map(|&x| x * 100).collect();
309    let who = v.who.to_u8();
310    let from_who = v.from_who.to_u8();
311    let pao_who = if let Some(w) = v.pao_who { w.to_u8() } else { v.who.to_u8() };
312
313    let (yaku, ranked_score) = if !v.yaku.is_empty() {
314        let yaku = conv_yaku_vec(&v.yaku);
315        let han = yaku.iter().fold(0, |sum, YakuPair { level, .. }| sum + level.get_number());
316        (yaku, conv_ranked_score_normal(v, han, oya))
317    } else if !v.yakuman.is_empty() {
318        let yaku = conv_yakuman_vec(&v.yakuman);
319        let num = yaku.iter().fold(0, |sum, YakuPair { level, .. }| sum + level.get_number());
320        (yaku, conv_ranked_score_yakuman(v, num, oya))
321    } else {
322        panic!("unexpected");
323    };
324
325    Ok(Agari {
326        delta_points,
327        who,
328        from_who,
329        pao_who,
330        ranked_score,
331        yaku,
332    })
333}
334
335fn conv_agari_vec(vs: &[&ActionAGARI], oya: Player) -> ConvResult<Vec<Agari>> {
336    vs.iter().map(|x| conv_agari(x, oya)).collect()
337}
338
339fn conv_round_result_from_agari(vs: &[&ActionAGARI], oya: Player) -> ConvResult<RoundResult> {
340    Ok(RoundResult::Agari { agari_vec: conv_agari_vec(vs, oya)? })
341}
342
343fn conv_delta_points_ryuukyoku(v: &ActionRYUUKYOKU) -> Vec<i32> {
344    if v.delta_points.iter().any(|&x| x != 0) {
345        v.delta_points.iter().map(|&x| x * 100).collect()
346    } else {
347        Vec::new()
348    }
349}
350
351fn conv_round_result_from_ryuukyoku(v: &ActionRYUUKYOKU) -> ConvResult<RoundResult> {
352    let reason = match conv_extra_ryuukyoku_reason(&v.reason) {
353        tenhou_json::model::ExtraRyuukyokuReason::Ryuukyoku => match (v.hai0.is_some(), v.hai1.is_some(), v.hai2.is_some(), v.hai3.is_some()) {
354            (true, true, true, true) => tenhou_json::model::ExtraRyuukyokuReason::TenpaiEverybody,
355            (false, false, false, false) => tenhou_json::model::ExtraRyuukyokuReason::TenpaiNobody,
356            _ => tenhou_json::model::ExtraRyuukyokuReason::Ryuukyoku,
357        },
358        x => x,
359    };
360
361    Ok(RoundResult::Ryuukyoku {
362        reason,
363        delta_points: conv_delta_points_ryuukyoku(v),
364    })
365}
366
367fn conv_round_result(actions: &[Action]) -> ConvResult<RoundResult> {
368    let init_action = actions[0].as_init().unwrap();
369
370    let ryuukyoku_actions: Vec<&ActionRYUUKYOKU> = actions.iter().filter_map(|x| x.as_ryuukyoku()).collect();
371    if ryuukyoku_actions.len() == 1 {
372        return conv_round_result_from_ryuukyoku(ryuukyoku_actions[0]);
373    }
374
375    // Note: Consider double ron
376    let agari_actions: Vec<&ActionAGARI> = actions.iter().filter_map(|x| x.as_agari()).collect();
377    if !agari_actions.is_empty() {
378        return conv_round_result_from_agari(&agari_actions, init_action.oya);
379    }
380
381    // not found terminal action, or there are multi ryuukyoku tags
382    Err(ConvError::InvalidRoundFormat)
383}
384
385fn conv_tiles(xs: &[Hai]) -> ConvResult<Vec<Tile>> {
386    xs.iter().map(|&x| conv_hai_to_tile(x, true)).collect()
387}
388
389// tenhou json's initial hand order:
390// normal 5 -> red 5 -> normal 6
391fn get_initial_hand_order(t: &Tile) -> u32 {
392    match t.to_u8() {
393        51 => 151,
394        52 => 251,
395        53 => 351,
396        x => x as u32 * 10,
397    }
398}
399
400fn is_valid_player_action(action: &Action, target_player: Player) -> bool {
401    match action {
402        Action::DRAW(ActionDRAW { who, .. }) => *who == target_player,
403        Action::DISCARD(ActionDISCARD { who, .. }) => *who == target_player,
404        Action::REACH1(ActionREACH1 { who, .. }) => *who == target_player,
405        Action::N(ActionN { who, .. }) => *who == target_player,
406        _ => false,
407    }
408}
409
410fn conv_dir(d: mjlog::model::Direction) -> tenhou_json::model::Direction {
411    match d {
412        mjlog::model::Direction::SelfSeat => tenhou_json::model::Direction::SelfSeat,
413        mjlog::model::Direction::Shimocha => tenhou_json::model::Direction::Shimocha,
414        mjlog::model::Direction::Kamicha => tenhou_json::model::Direction::Kamicha,
415        mjlog::model::Direction::Toimen => tenhou_json::model::Direction::Toimen,
416    }
417}
418
419fn replay_actions(actions: &[&Action]) -> ConvResult<(Vec<IncomingTile>, Vec<OutgoingTile>)> {
420    let mut incoming = vec![];
421    let mut outgoing = vec![];
422    let mut reach_declared = false;
423    let mut last_draw = None;
424
425    for a in actions {
426        match a {
427            Action::DRAW(x) => {
428                let tile = conv_hai_to_tile(x.hai, true)?;
429                incoming.push(IncomingTile::Tsumo(tile));
430                last_draw = Some(x.hai);
431            }
432            Action::DISCARD(x) => {
433                match last_draw {
434                    Some(h) if h == x.hai => {
435                        if reach_declared {
436                            outgoing.push(OutgoingTile::TsumogiriRiichi)
437                        } else {
438                            outgoing.push(OutgoingTile::Tsumogiri)
439                        }
440                    }
441                    _ => {
442                        let tile = conv_hai_to_tile(x.hai, true)?;
443                        if reach_declared {
444                            outgoing.push(OutgoingTile::Riichi(tile))
445                        } else {
446                            outgoing.push(OutgoingTile::Discard(tile))
447                        }
448                    }
449                }
450                reach_declared = false;
451                last_draw = None;
452            }
453            Action::REACH1(_) => {
454                reach_declared = true;
455            }
456            Action::N(x) => {
457                match x.m {
458                    Meld::Chii { combination, called_position } => {
459                        // mjlog: sorted in ascending order.
460                        // tenhou json: the placement order on the board.
461                        let orders = match called_position {
462                            0 => combination,
463                            1 => (combination.1, combination.0, combination.2),
464                            2 => (combination.2, combination.0, combination.1),
465                            _ => panic!("unexpected called position"),
466                        };
467
468                        let incoming_tile = IncomingTile::Chii {
469                            combination: (conv_hai_to_tile(orders.0, true)?, conv_hai_to_tile(orders.1, true)?, conv_hai_to_tile(orders.2, true)?),
470                        };
471                        incoming.push(incoming_tile);
472                    }
473                    Meld::Pon { dir: src_dir, called, unused, .. } => {
474                        let dir = conv_dir(src_dir);
475                        // mjlog: sorted in ascending order.
476                        // tenhou json: the placement order on the board.
477                        if called.is_number5() {
478                            let called_tile = conv_hai_to_tile(called, true)?;
479                            let unused_tile = conv_hai_to_tile(unused, true)?;
480                            let tile = called_tile.to_black();
481
482                            if unused_tile.is_red() {
483                                incoming.push(IncomingTile::Pon { dir, combination: (tile, tile, tile) })
484                            } else if called_tile.is_red() {
485                                let combination = match dir {
486                                    tenhou_json::model::Direction::Kamicha => (called_tile, tile, tile),
487                                    tenhou_json::model::Direction::Toimen => (tile, called_tile, tile),
488                                    tenhou_json::model::Direction::Shimocha => (tile, tile, called_tile),
489                                    _ => panic!("unexpected"),
490                                };
491                                incoming.push(IncomingTile::Pon { dir, combination });
492                            } else {
493                                let combination = match dir {
494                                    tenhou_json::model::Direction::Shimocha => (tile, tile.to_red(), tile),
495                                    _ => (tile, tile, tile.to_red()),
496                                };
497                                incoming.push(IncomingTile::Pon { dir, combination });
498                            }
499                        } else {
500                            // combination, called, unused, all the same
501                            let tile = conv_hai_to_tile(called, true)?;
502                            incoming.push(IncomingTile::Pon { dir, combination: (tile, tile, tile) })
503                        }
504                    }
505                    Meld::Kakan { dir: src_dir, called, added, .. } => {
506                        let dir = conv_dir(src_dir);
507
508                        // mjlog: sorted in ascending order.
509                        // tenhou json: the placement order on the board.
510                        if called.is_number5() {
511                            let called_tile = conv_hai_to_tile(called, true)?;
512                            let added_tile = conv_hai_to_tile(added, true)?;
513                            let tile = called_tile.to_black();
514
515                            if added_tile.is_red() {
516                                outgoing.push(OutgoingTile::Kakan {
517                                    dir,
518                                    combination: (tile, tile, tile),
519                                    added: added_tile,
520                                })
521                            } else if called_tile.is_red() {
522                                let combination = match dir {
523                                    tenhou_json::model::Direction::Kamicha => (called_tile, tile, tile),
524                                    tenhou_json::model::Direction::Toimen => (tile, called_tile, tile),
525                                    tenhou_json::model::Direction::Shimocha => (tile, tile, called_tile),
526                                    _ => panic!("unexpected"),
527                                };
528                                outgoing.push(OutgoingTile::Kakan { dir, combination, added: added_tile });
529                            } else {
530                                let combination = match dir {
531                                    tenhou_json::model::Direction::Shimocha => (tile, tile.to_red(), tile),
532                                    _ => (tile, tile, tile.to_red()),
533                                };
534                                outgoing.push(OutgoingTile::Kakan { dir, combination, added: added_tile });
535                            }
536                        } else {
537                            // combination, called, added, all the same
538                            let tile = conv_hai_to_tile(called, true)?;
539                            outgoing.push(OutgoingTile::Kakan {
540                                dir,
541                                combination: (tile, tile, tile),
542                                added: tile,
543                            })
544                        }
545                    }
546                    Meld::Daiminkan { dir: src_dir, hai } => {
547                        let dir = conv_dir(src_dir);
548                        if hai.is_number5() {
549                            let called_tile = conv_hai_to_tile(hai, true)?;
550                            let tile = called_tile.to_black();
551
552                            if called_tile.is_red() {
553                                let combination = match dir {
554                                    tenhou_json::model::Direction::Kamicha => (called_tile, tile, tile, tile),
555                                    tenhou_json::model::Direction::Toimen => (tile, called_tile, tile, tile),
556                                    tenhou_json::model::Direction::Shimocha => (tile, tile, tile, called_tile),
557                                    _ => panic!("unexpected"),
558                                };
559                                incoming.push(IncomingTile::Daiminkan { combination, dir });
560                            } else {
561                                let combination = match dir {
562                                    tenhou_json::model::Direction::Shimocha => (tile, tile, tile.to_red(), tile),
563                                    _ => (tile, tile, tile, tile.to_red()),
564                                };
565                                incoming.push(IncomingTile::Daiminkan { combination, dir });
566                            }
567                        } else {
568                            let tile = conv_hai_to_tile(hai, true)?;
569                            incoming.push(IncomingTile::Daiminkan { combination: (tile, tile, tile, tile), dir });
570                        }
571                        outgoing.push(OutgoingTile::Dummy)
572                    }
573                    Meld::Ankan { hai } => {
574                        // NOT CLEAR
575                        // I think the red 5 is always recorded when ankan of 5.
576                        outgoing.push(OutgoingTile::Ankan(conv_hai_to_tile(hai, true)?.to_red()))
577                    }
578                }
579            }
580            _ => panic!("unexpected"),
581        }
582    }
583
584    // The last dummy is invalid and should be removed.
585    while outgoing.last() == Some(&OutgoingTile::Dummy) {
586        outgoing.pop();
587    }
588
589    Ok((incoming, outgoing))
590}
591
592fn conv_round_players(actions: &[Action]) -> ConvResult<Vec<RoundPlayer>> {
593    let init_action = actions[0].as_init().unwrap();
594
595    let mut players = vec![];
596    for (i, h) in init_action.hai.iter().enumerate() {
597        let mut hand = conv_tiles(h)?;
598        hand.sort_by_key(get_initial_hand_order);
599
600        let player_actions: Vec<&Action> = actions.iter().filter(|x| is_valid_player_action(x, Player::new(i as u8))).collect();
601        let (incoming, outgoing) = replay_actions(&player_actions)?;
602
603        players.push(RoundPlayer { hand, incoming, outgoing });
604    }
605    Ok(players)
606}
607
608fn conv_round(actions: &[Action]) -> ConvResult<Round> {
609    Ok(Round {
610        settings: conv_round_setting(actions)?,
611        players: conv_round_players(actions)?,
612        result: conv_round_result(actions)?,
613    })
614}
615
616fn conv_rounds(actions: &[Action], indices: &[(usize, usize)]) -> ConvResult<Vec<Round>> {
617    let mut rounds = vec![];
618
619    for &(start, end) in indices {
620        rounds.push(conv_round(&actions[start..end])?);
621    }
622
623    Ok(rounds)
624}
625
626fn conv_connections(actions: &[Action], indices: &[(usize, usize)]) -> ConvResult<Vec<Connection>> {
627    let mut connections = vec![];
628
629    // before first INIT
630    for a in &actions[0..indices[0].0] {
631        match a {
632            Action::BYE(bye) => connections.push(Connection {
633                what: 0,
634                log: -1,
635                who: bye.who.to_u8(),
636                step: 0,
637            }),
638            Action::UN2(un2) => connections.push(Connection {
639                what: 1,
640                log: -1,
641                who: un2.who.to_u8(),
642                step: 0,
643            }),
644            _ => {}
645        }
646    }
647
648    // rounds
649    for (log_index, &(start, end)) in indices.iter().enumerate() {
650        let mut step = 0;
651
652        for a in &actions[start..end] {
653            match a {
654                Action::BYE(bye) => connections.push(Connection {
655                    what: 0,
656                    log: log_index as i8,
657                    who: bye.who.to_u8(),
658                    step: step as u32,
659                }),
660                Action::UN2(un2) => connections.push(Connection {
661                    what: 1,
662                    log: log_index as i8,
663                    who: un2.who.to_u8(),
664                    step: step as u32,
665                }),
666                Action::INIT(_) => {}
667                Action::TAIKYOKU(_) => {}
668                Action::SHUFFLE(_) => {}
669                Action::GO(_) => {}
670                Action::UN1(_) => {}
671                Action::AGARI(_) => {}
672                Action::RYUUKYOKU(_) => {}
673                Action::DORA(_) => {}
674                Action::REACH1(_) => {}
675                Action::REACH2(_) => {}
676                Action::N(_) => step += 1,
677                Action::DRAW(_) => step += 1,
678                Action::DISCARD(_) => step += 1,
679            }
680        }
681    }
682
683    Ok(connections)
684}
685
686pub fn conv_to_tenhou_json(mjlog: &Mjlog) -> ConvResult<TenhouJson> {
687    let action_go = if let Some(Action::GO(x)) = mjlog.actions.iter().find(|x| x.is_go()) { Ok(x) } else { Err(ConvError::NotFoundActionGO) }?;
688    let action_un1 = if let Some(Action::UN1(x)) = mjlog.actions.iter().find(|x| x.is_un1()) { Ok(x) } else { Err(ConvError::NotFoundActionUN1) }?;
689    let round_indices = extract_round_indices(&mjlog.actions);
690    if round_indices.is_empty() {
691        return Err(ConvError::NotFoundRound);
692    }
693
694    let (final_points_raw, final_results_raw): (Vec<i32>, Vec<f64>) = find_final_result(&mjlog.actions)?;
695    let final_points = final_points_raw.iter().map(|x| x * 100).collect();
696    let final_results = final_results_raw.clone();
697
698    Ok(TenhouJson {
699        ver: 2.3, // Using this conversion system
700        reference: String::new(),
701        rounds: conv_rounds(&mjlog.actions, &round_indices)?,
702        connections: conv_connections(&mjlog.actions, &round_indices)?,
703        ratingc: "PF4".to_string(), // What does this mean?
704        rule: conv_rule(&action_go.settings)?,
705        lobby: action_go.lobby,
706        dan: action_un1.dan.iter().map(conv_dan).collect(),
707        rate: action_un1.rate.clone(),
708        sx: action_un1.sx.clone(),
709        final_points,
710        final_results,
711        names: action_un1.names.clone(),
712    })
713}