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
156    Ok(Rule {
157        disp: format!("{}{}喰赤", room_str, hanchan_str),
158        aka53: true,
159        aka52: true,
160        aka51: true,
161    })
162}
163
164fn conv_round_setting(actions: &[Action]) -> ConvResult<RoundSettings> {
165    let start_action = &actions[0];
166    assert!(start_action.is_init());
167
168    let init = start_action.as_init().unwrap();
169    let end_actions: Vec<&Action> = actions.iter().filter(|x| x.is_agari() || x.is_ryuukyoku()).collect();
170
171    if end_actions.is_empty() {
172        return Err(ConvError::NotFoundTerminalAction);
173    }
174
175    Ok(RoundSettings {
176        kyoku: init.seed.kyoku,
177        honba: init.seed.honba,
178        kyoutaku: init.seed.kyoutaku,
179        points: init.ten.iter().map(|x| x * 100).collect(),
180        dora: get_dora_vec(init.seed.dora_hyouji, actions)?,
181        ura_dora: get_ura_dora_vec(&end_actions)?,
182    })
183}
184
185fn conv_yaku(x: mjlog::model::Yaku) -> tenhou_json::model::Yaku {
186    match x {
187        mjlog::model::Yaku::MenzenTsumo => tenhou_json::model::Yaku::MenzenTsumo,
188        mjlog::model::Yaku::Riichi => tenhou_json::model::Yaku::Riichi,
189        mjlog::model::Yaku::Ippatsu => tenhou_json::model::Yaku::Ippatsu,
190        mjlog::model::Yaku::Chankan => tenhou_json::model::Yaku::Chankan,
191        mjlog::model::Yaku::Rinshankaihou => tenhou_json::model::Yaku::Rinshankaihou,
192        mjlog::model::Yaku::HaiteiTsumo => tenhou_json::model::Yaku::HaiteiTsumo,
193        mjlog::model::Yaku::HouteiRon => tenhou_json::model::Yaku::HouteiRon,
194        mjlog::model::Yaku::Pinfu => tenhou_json::model::Yaku::Pinfu,
195        mjlog::model::Yaku::Tanyao => tenhou_json::model::Yaku::Tanyao,
196        mjlog::model::Yaku::Iipeikou => tenhou_json::model::Yaku::Iipeikou,
197        mjlog::model::Yaku::PlayerWindTon => tenhou_json::model::Yaku::PlayerWindTon,
198        mjlog::model::Yaku::PlayerWindNan => tenhou_json::model::Yaku::PlayerWindNan,
199        mjlog::model::Yaku::PlayerWindSha => tenhou_json::model::Yaku::PlayerWindSha,
200        mjlog::model::Yaku::PlayerWindPei => tenhou_json::model::Yaku::PlayerWindPei,
201        mjlog::model::Yaku::FieldWindTon => tenhou_json::model::Yaku::FieldWindTon,
202        mjlog::model::Yaku::FieldWindNan => tenhou_json::model::Yaku::FieldWindNan,
203        mjlog::model::Yaku::FieldWindSha => tenhou_json::model::Yaku::FieldWindSha,
204        mjlog::model::Yaku::FieldWindPei => tenhou_json::model::Yaku::FieldWindPei,
205        mjlog::model::Yaku::YakuhaiHaku => tenhou_json::model::Yaku::YakuhaiHaku,
206        mjlog::model::Yaku::YakuhaiHatsu => tenhou_json::model::Yaku::YakuhaiHatsu,
207        mjlog::model::Yaku::YakuhaiChun => tenhou_json::model::Yaku::YakuhaiChun,
208        mjlog::model::Yaku::DoubleRiichi => tenhou_json::model::Yaku::DoubleRiichi,
209        mjlog::model::Yaku::Chiitoitsu => tenhou_json::model::Yaku::Chiitoitsu,
210        mjlog::model::Yaku::Chanta => tenhou_json::model::Yaku::Chanta,
211        mjlog::model::Yaku::Ikkitsuukan => tenhou_json::model::Yaku::Ikkitsuukan,
212        mjlog::model::Yaku::SansyokuDoujun => tenhou_json::model::Yaku::SansyokuDoujun,
213        mjlog::model::Yaku::SanshokuDoukou => tenhou_json::model::Yaku::SanshokuDoukou,
214        mjlog::model::Yaku::Sankantsu => tenhou_json::model::Yaku::Sankantsu,
215        mjlog::model::Yaku::Toitoi => tenhou_json::model::Yaku::Toitoi,
216        mjlog::model::Yaku::Sanannkou => tenhou_json::model::Yaku::Sanannkou,
217        mjlog::model::Yaku::Shousangen => tenhou_json::model::Yaku::Shousangen,
218        mjlog::model::Yaku::Honroutou => tenhou_json::model::Yaku::Honroutou,
219        mjlog::model::Yaku::Ryanpeikou => tenhou_json::model::Yaku::Ryanpeikou,
220        mjlog::model::Yaku::Junchan => tenhou_json::model::Yaku::Junchan,
221        mjlog::model::Yaku::Honiisou => tenhou_json::model::Yaku::Honiisou,
222        mjlog::model::Yaku::Chiniisou => tenhou_json::model::Yaku::Chiniisou,
223        mjlog::model::Yaku::Renhou => tenhou_json::model::Yaku::Renhou,
224        mjlog::model::Yaku::Tenhou => tenhou_json::model::Yaku::Tenhou,
225        mjlog::model::Yaku::Chiihou => tenhou_json::model::Yaku::Chiihou,
226        mjlog::model::Yaku::Daisangen => tenhou_json::model::Yaku::Daisangen,
227        mjlog::model::Yaku::Suuankou => tenhou_json::model::Yaku::Suuankou,
228        mjlog::model::Yaku::SuuankouTanki => tenhou_json::model::Yaku::SuuankouTanki,
229        mjlog::model::Yaku::Tsuuiisou => tenhou_json::model::Yaku::Tsuuiisou,
230        mjlog::model::Yaku::Ryuuiisou => tenhou_json::model::Yaku::Ryuuiisou,
231        mjlog::model::Yaku::Chinroutou => tenhou_json::model::Yaku::Chinroutou,
232        mjlog::model::Yaku::Tyuurenpoutou => tenhou_json::model::Yaku::Tyuurenpoutou,
233        mjlog::model::Yaku::Tyuurenpoutou9 => tenhou_json::model::Yaku::Tyuurenpoutou9,
234        mjlog::model::Yaku::Kokushimusou => tenhou_json::model::Yaku::Kokushimusou,
235        mjlog::model::Yaku::Kokushimusou13 => tenhou_json::model::Yaku::Kokushimusou13,
236        mjlog::model::Yaku::Daisuushii => tenhou_json::model::Yaku::Daisuushii,
237        mjlog::model::Yaku::Syousuushii => tenhou_json::model::Yaku::Syousuushii,
238        mjlog::model::Yaku::Suukantsu => tenhou_json::model::Yaku::Suukantsu,
239        mjlog::model::Yaku::Dora => tenhou_json::model::Yaku::Dora,
240        mjlog::model::Yaku::UraDora => tenhou_json::model::Yaku::UraDora,
241        mjlog::model::Yaku::AkaDora => tenhou_json::model::Yaku::AkaDora,
242    }
243}
244
245fn conv_extra_ryuukyoku_reason(x: &Option<mjlog::model::ExtraRyuukyokuReason>) -> tenhou_json::model::ExtraRyuukyokuReason {
246    match x {
247        Some(mjlog::model::ExtraRyuukyokuReason::KyuusyuKyuuhai) => tenhou_json::model::ExtraRyuukyokuReason::KyuusyuKyuuhai,
248        Some(mjlog::model::ExtraRyuukyokuReason::SuuchaRiichi) => tenhou_json::model::ExtraRyuukyokuReason::SuuchaRiichi,
249        Some(mjlog::model::ExtraRyuukyokuReason::SanchaHoura) => tenhou_json::model::ExtraRyuukyokuReason::SanchaHoura,
250        Some(mjlog::model::ExtraRyuukyokuReason::SuukanSanra) => tenhou_json::model::ExtraRyuukyokuReason::SuukanSanra,
251        Some(mjlog::model::ExtraRyuukyokuReason::SuufuuRenda) => tenhou_json::model::ExtraRyuukyokuReason::SuufuuRenda,
252        Some(mjlog::model::ExtraRyuukyokuReason::NagashiMangan) => tenhou_json::model::ExtraRyuukyokuReason::NagashiMangan,
253        None => tenhou_json::model::ExtraRyuukyokuReason::Ryuukyoku,
254    }
255}
256
257fn is_not_ura_zero(x: &YakuPair) -> bool {
258    !matches!(
259        x,
260        YakuPair {
261            yaku: tenhou_json::model::Yaku::UraDora,
262            level: YakuLevel::Normal(0)
263        }
264    )
265}
266
267fn conv_ranked_score_normal(v: &ActionAGARI, han: u8, oya: Player) -> RankedScore {
268    match (v.is_tsumo(), v.who == oya) {
269        (true, true) => get_oya_tsumo(v.fu, han),
270        (true, false) => get_ko_tsumo(v.fu, han),
271        (false, true) => get_oya_ron(v.fu, han),
272        (false, false) => get_ko_ron(v.fu, han),
273    }
274}
275
276fn conv_ranked_score_yakuman(v: &ActionAGARI, num: u8, oya: Player) -> RankedScore {
277    match (v.is_tsumo(), v.who == oya) {
278        (true, true) => get_oya_tsumo_yakuman(num),
279        (true, false) => get_ko_tsumo_yakuman(num),
280        (false, true) => get_oya_ron_yakuman(num),
281        (false, false) => get_ko_ron_yakuman(num),
282    }
283}
284
285fn conv_yaku_vec(vs: &[(mjlog::model::Yaku, u8)]) -> Vec<YakuPair> {
286    vs.iter()
287        .map(|&(yaku, han)| YakuPair {
288            yaku: conv_yaku(yaku),
289            level: YakuLevel::Normal(han),
290        })
291        .filter(is_not_ura_zero)
292        .collect()
293}
294
295fn conv_yakuman_vec(vs: &[mjlog::model::Yaku]) -> Vec<YakuPair> {
296    vs.iter()
297        .map(|&yaku| YakuPair {
298            yaku: conv_yaku(yaku),
299            level: YakuLevel::Yakuman(1),
300        })
301        .collect()
302}
303
304fn conv_agari(v: &ActionAGARI, oya: Player) -> ConvResult<Agari> {
305    let delta_points = v.delta_points.iter().map(|&x| x * 100).collect();
306    let who = v.who.to_u8();
307    let from_who = v.from_who.to_u8();
308    let pao_who = if let Some(w) = v.pao_who { w.to_u8() } else { v.who.to_u8() };
309
310    let (yaku, ranked_score) = if !v.yaku.is_empty() {
311        let yaku = conv_yaku_vec(&v.yaku);
312        let han = yaku.iter().fold(0, |sum, YakuPair { level, .. }| sum + level.get_number());
313        (yaku, conv_ranked_score_normal(v, han, oya))
314    } else if !v.yakuman.is_empty() {
315        let yaku = conv_yakuman_vec(&v.yakuman);
316        let num = yaku.iter().fold(0, |sum, YakuPair { level, .. }| sum + level.get_number());
317        (yaku, conv_ranked_score_yakuman(v, num, oya))
318    } else {
319        panic!("unexpected");
320    };
321
322    Ok(Agari {
323        delta_points,
324        who,
325        from_who,
326        pao_who,
327        ranked_score,
328        yaku,
329    })
330}
331
332fn conv_agari_vec(vs: &[&ActionAGARI], oya: Player) -> ConvResult<Vec<Agari>> {
333    vs.iter().map(|x| conv_agari(x, oya)).collect()
334}
335
336fn conv_round_result_from_agari(vs: &[&ActionAGARI], oya: Player) -> ConvResult<RoundResult> {
337    Ok(RoundResult::Agari { agari_vec: conv_agari_vec(vs, oya)? })
338}
339
340fn conv_delta_points_ryuukyoku(v: &ActionRYUUKYOKU) -> Vec<i32> {
341    if v.delta_points.iter().any(|&x| x != 0) {
342        v.delta_points.iter().map(|&x| x * 100).collect()
343    } else {
344        Vec::new()
345    }
346}
347
348fn conv_round_result_from_ryuukyoku(v: &ActionRYUUKYOKU) -> ConvResult<RoundResult> {
349    let reason = match conv_extra_ryuukyoku_reason(&v.reason) {
350        tenhou_json::model::ExtraRyuukyokuReason::Ryuukyoku => match (v.hai0.is_some(), v.hai1.is_some(), v.hai2.is_some(), v.hai3.is_some()) {
351            (true, true, true, true) => tenhou_json::model::ExtraRyuukyokuReason::TenpaiEverybody,
352            (false, false, false, false) => tenhou_json::model::ExtraRyuukyokuReason::TenpaiNobody,
353            _ => tenhou_json::model::ExtraRyuukyokuReason::Ryuukyoku,
354        },
355        x => x,
356    };
357
358    Ok(RoundResult::Ryuukyoku {
359        reason,
360        delta_points: conv_delta_points_ryuukyoku(v),
361    })
362}
363
364fn conv_round_result(actions: &[Action]) -> ConvResult<RoundResult> {
365    let init_action = actions[0].as_init().unwrap();
366
367    let ryuukyoku_actions: Vec<&ActionRYUUKYOKU> = actions.iter().filter_map(|x| x.as_ryuukyoku()).collect();
368    if ryuukyoku_actions.len() == 1 {
369        return conv_round_result_from_ryuukyoku(ryuukyoku_actions[0]);
370    }
371
372    // Note: Consider double ron
373    let agari_actions: Vec<&ActionAGARI> = actions.iter().filter_map(|x| x.as_agari()).collect();
374    if !agari_actions.is_empty() {
375        return conv_round_result_from_agari(&agari_actions, init_action.oya);
376    }
377
378    // not found terminal action, or there are multi ryuukyoku tags
379    Err(ConvError::InvalidRoundFormat)
380}
381
382fn conv_tiles(xs: &[Hai]) -> ConvResult<Vec<Tile>> {
383    xs.iter().map(|&x| conv_hai_to_tile(x, true)).collect()
384}
385
386// tenhou json's initial hand order:
387// normal 5 -> red 5 -> normal 6
388fn get_initial_hand_order(t: &Tile) -> u32 {
389    match t.to_u8() {
390        51 => 151,
391        52 => 251,
392        53 => 351,
393        x => x as u32 * 10,
394    }
395}
396
397fn is_valid_player_action(action: &Action, target_player: Player) -> bool {
398    match action {
399        Action::DRAW(ActionDRAW { who, .. }) => *who == target_player,
400        Action::DISCARD(ActionDISCARD { who, .. }) => *who == target_player,
401        Action::REACH1(ActionREACH1 { who, .. }) => *who == target_player,
402        Action::N(ActionN { who, .. }) => *who == target_player,
403        _ => false,
404    }
405}
406
407fn conv_dir(d: mjlog::model::Direction) -> tenhou_json::model::Direction {
408    match d {
409        mjlog::model::Direction::SelfSeat => tenhou_json::model::Direction::SelfSeat,
410        mjlog::model::Direction::Shimocha => tenhou_json::model::Direction::Shimocha,
411        mjlog::model::Direction::Kamicha => tenhou_json::model::Direction::Kamicha,
412        mjlog::model::Direction::Toimen => tenhou_json::model::Direction::Toimen,
413    }
414}
415
416fn replay_actions(actions: &[&Action]) -> ConvResult<(Vec<IncomingTile>, Vec<OutgoingTile>)> {
417    let mut incoming = vec![];
418    let mut outgoing = vec![];
419    let mut reach_declared = false;
420    let mut last_draw = None;
421
422    for a in actions {
423        match a {
424            Action::DRAW(x) => {
425                let tile = conv_hai_to_tile(x.hai, true)?;
426                incoming.push(IncomingTile::Tsumo(tile));
427                last_draw = Some(x.hai);
428            }
429            Action::DISCARD(x) => {
430                match last_draw {
431                    Some(h) if h == x.hai => {
432                        if reach_declared {
433                            outgoing.push(OutgoingTile::TsumogiriRiichi)
434                        } else {
435                            outgoing.push(OutgoingTile::Tsumogiri)
436                        }
437                    }
438                    _ => {
439                        let tile = conv_hai_to_tile(x.hai, true)?;
440                        if reach_declared {
441                            outgoing.push(OutgoingTile::Riichi(tile))
442                        } else {
443                            outgoing.push(OutgoingTile::Discard(tile))
444                        }
445                    }
446                }
447                reach_declared = false;
448                last_draw = None;
449            }
450            Action::REACH1(_) => {
451                reach_declared = true;
452            }
453            Action::N(x) => {
454                match x.m {
455                    Meld::Chii { combination, called_position } => {
456                        // mjlog: sorted in ascending order.
457                        // tenhou json: the placement order on the board.
458                        let orders = match called_position {
459                            0 => combination,
460                            1 => (combination.1, combination.0, combination.2),
461                            2 => (combination.2, combination.0, combination.1),
462                            _ => panic!("unexpected called position"),
463                        };
464
465                        let incoming_tile = IncomingTile::Chii {
466                            combination: (conv_hai_to_tile(orders.0, true)?, conv_hai_to_tile(orders.1, true)?, conv_hai_to_tile(orders.2, true)?),
467                        };
468                        incoming.push(incoming_tile);
469                    }
470                    Meld::Pon { dir: src_dir, called, unused, .. } => {
471                        let dir = conv_dir(src_dir);
472                        // mjlog: sorted in ascending order.
473                        // tenhou json: the placement order on the board.
474                        if called.is_number5() {
475                            let called_tile = conv_hai_to_tile(called, true)?;
476                            let unused_tile = conv_hai_to_tile(unused, true)?;
477                            let tile = called_tile.to_black();
478
479                            if unused_tile.is_red() {
480                                incoming.push(IncomingTile::Pon { dir, combination: (tile, tile, tile) })
481                            } else if called_tile.is_red() {
482                                let combination = match dir {
483                                    tenhou_json::model::Direction::Kamicha => (called_tile, tile, tile),
484                                    tenhou_json::model::Direction::Toimen => (tile, called_tile, tile),
485                                    tenhou_json::model::Direction::Shimocha => (tile, tile, called_tile),
486                                    _ => panic!("unexpected"),
487                                };
488                                incoming.push(IncomingTile::Pon { dir, combination });
489                            } else {
490                                let combination = match dir {
491                                    tenhou_json::model::Direction::Shimocha => (tile, tile.to_red(), tile),
492                                    _ => (tile, tile, tile.to_red()),
493                                };
494                                incoming.push(IncomingTile::Pon { dir, combination });
495                            }
496                        } else {
497                            // combination, called, unused, all the same
498                            let tile = conv_hai_to_tile(called, true)?;
499                            incoming.push(IncomingTile::Pon { dir, combination: (tile, tile, tile) })
500                        }
501                    }
502                    Meld::Kakan { dir: src_dir, called, added, .. } => {
503                        let dir = conv_dir(src_dir);
504
505                        // mjlog: sorted in ascending order.
506                        // tenhou json: the placement order on the board.
507                        if called.is_number5() {
508                            let called_tile = conv_hai_to_tile(called, true)?;
509                            let added_tile = conv_hai_to_tile(added, true)?;
510                            let tile = called_tile.to_black();
511
512                            if added_tile.is_red() {
513                                outgoing.push(OutgoingTile::Kakan {
514                                    dir,
515                                    combination: (tile, tile, tile),
516                                    added: added_tile,
517                                })
518                            } else if called_tile.is_red() {
519                                let combination = match dir {
520                                    tenhou_json::model::Direction::Kamicha => (called_tile, tile, tile),
521                                    tenhou_json::model::Direction::Toimen => (tile, called_tile, tile),
522                                    tenhou_json::model::Direction::Shimocha => (tile, tile, called_tile),
523                                    _ => panic!("unexpected"),
524                                };
525                                outgoing.push(OutgoingTile::Kakan { dir, combination, added: added_tile });
526                            } else {
527                                let combination = match dir {
528                                    tenhou_json::model::Direction::Shimocha => (tile, tile.to_red(), tile),
529                                    _ => (tile, tile, tile.to_red()),
530                                };
531                                outgoing.push(OutgoingTile::Kakan { dir, combination, added: added_tile });
532                            }
533                        } else {
534                            // combination, called, added, all the same
535                            let tile = conv_hai_to_tile(called, true)?;
536                            outgoing.push(OutgoingTile::Kakan {
537                                dir,
538                                combination: (tile, tile, tile),
539                                added: tile,
540                            })
541                        }
542                    }
543                    Meld::Daiminkan { dir: src_dir, hai } => {
544                        let dir = conv_dir(src_dir);
545                        if hai.is_number5() {
546                            let called_tile = conv_hai_to_tile(hai, true)?;
547                            let tile = called_tile.to_black();
548
549                            if called_tile.is_red() {
550                                let combination = match dir {
551                                    tenhou_json::model::Direction::Kamicha => (called_tile, tile, tile, tile),
552                                    tenhou_json::model::Direction::Toimen => (tile, called_tile, tile, tile),
553                                    tenhou_json::model::Direction::Shimocha => (tile, tile, tile, called_tile),
554                                    _ => panic!("unexpected"),
555                                };
556                                incoming.push(IncomingTile::Daiminkan { combination, dir });
557                            } else {
558                                let combination = match dir {
559                                    tenhou_json::model::Direction::Shimocha => (tile, tile, tile.to_red(), tile),
560                                    _ => (tile, tile, tile, tile.to_red()),
561                                };
562                                incoming.push(IncomingTile::Daiminkan { combination, dir });
563                            }
564                        } else {
565                            let tile = conv_hai_to_tile(hai, true)?;
566                            incoming.push(IncomingTile::Daiminkan { combination: (tile, tile, tile, tile), dir });
567                        }
568                        outgoing.push(OutgoingTile::Dummy)
569                    }
570                    Meld::Ankan { hai } => {
571                        // NOT CLEAR
572                        // I think the red 5 is always recorded when ankan of 5.
573                        outgoing.push(OutgoingTile::Ankan(conv_hai_to_tile(hai, true)?.to_red()))
574                    }
575                }
576            }
577            _ => panic!("unexpected"),
578        }
579    }
580
581    // The last dummy is invalid and should be removed.
582    while outgoing.last() == Some(&OutgoingTile::Dummy) {
583        outgoing.pop();
584    }
585
586    Ok((incoming, outgoing))
587}
588
589fn conv_round_players(actions: &[Action]) -> ConvResult<Vec<RoundPlayer>> {
590    let init_action = actions[0].as_init().unwrap();
591
592    let mut players = vec![];
593    for (i, h) in init_action.hai.iter().enumerate() {
594        let mut hand = conv_tiles(h)?;
595        hand.sort_by_key(get_initial_hand_order);
596
597        let player_actions: Vec<&Action> = actions.iter().filter(|x| is_valid_player_action(x, Player::new(i as u8))).collect();
598        let (incoming, outgoing) = replay_actions(&player_actions)?;
599
600        players.push(RoundPlayer { hand, incoming, outgoing });
601    }
602    Ok(players)
603}
604
605fn conv_round(actions: &[Action]) -> ConvResult<Round> {
606    Ok(Round {
607        settings: conv_round_setting(actions)?,
608        players: conv_round_players(actions)?,
609        result: conv_round_result(actions)?,
610    })
611}
612
613fn conv_rounds(actions: &[Action], indices: &[(usize, usize)]) -> ConvResult<Vec<Round>> {
614    let mut rounds = vec![];
615
616    for &(start, end) in indices {
617        rounds.push(conv_round(&actions[start..end])?);
618    }
619
620    Ok(rounds)
621}
622
623fn conv_connections(actions: &[Action], indices: &[(usize, usize)]) -> ConvResult<Vec<Connection>> {
624    let mut connections = vec![];
625
626    // before first INIT
627    for a in &actions[0..indices[0].0] {
628        match a {
629            Action::BYE(bye) => connections.push(Connection {
630                what: 0,
631                log: -1,
632                who: bye.who.to_u8(),
633                step: 0,
634            }),
635            Action::UN2(un2) => connections.push(Connection {
636                what: 1,
637                log: -1,
638                who: un2.who.to_u8(),
639                step: 0,
640            }),
641            _ => {}
642        }
643    }
644
645    // rounds
646    for (log_index, &(start, end)) in indices.iter().enumerate() {
647        let mut step = 0;
648
649        for a in &actions[start..end] {
650            match a {
651                Action::BYE(bye) => connections.push(Connection {
652                    what: 0,
653                    log: log_index as i8,
654                    who: bye.who.to_u8(),
655                    step: step as u32,
656                }),
657                Action::UN2(un2) => connections.push(Connection {
658                    what: 1,
659                    log: log_index as i8,
660                    who: un2.who.to_u8(),
661                    step: step as u32,
662                }),
663                Action::INIT(_) => {}
664                Action::TAIKYOKU(_) => {}
665                Action::SHUFFLE(_) => {}
666                Action::GO(_) => {}
667                Action::UN1(_) => {}
668                Action::AGARI(_) => {}
669                Action::RYUUKYOKU(_) => {}
670                Action::DORA(_) => {}
671                Action::REACH1(_) => {}
672                Action::REACH2(_) => {}
673                Action::N(_) => step += 1,
674                Action::DRAW(_) => step += 1,
675                Action::DISCARD(_) => step += 1,
676            }
677        }
678    }
679
680    Ok(connections)
681}
682
683pub fn conv_to_tenhou_json(mjlog: &Mjlog) -> ConvResult<TenhouJson> {
684    let action_go = if let Some(Action::GO(x)) = mjlog.actions.iter().find(|x| x.is_go()) { Ok(x) } else { Err(ConvError::NotFoundActionGO) }?;
685    let action_un1 = if let Some(Action::UN1(x)) = mjlog.actions.iter().find(|x| x.is_un1()) { Ok(x) } else { Err(ConvError::NotFoundActionUN1) }?;
686    let round_indices = extract_round_indices(&mjlog.actions);
687    if round_indices.is_empty() {
688        return Err(ConvError::NotFoundRound);
689    }
690
691    let (final_points_raw, final_results_raw): (Vec<i32>, Vec<f64>) = find_final_result(&mjlog.actions)?;
692    let final_points = final_points_raw.iter().map(|x| x * 100).collect();
693    let final_results = final_results_raw.clone();
694
695    Ok(TenhouJson {
696        ver: 2.3, // Using this conversion system
697        reference: String::new(),
698        rounds: conv_rounds(&mjlog.actions, &round_indices)?,
699        connections: conv_connections(&mjlog.actions, &round_indices)?,
700        ratingc: "PF4".to_string(), // What does this mean?
701        rule: conv_rule(&action_go.settings)?,
702        lobby: action_go.lobby,
703        dan: action_un1.dan.iter().map(conv_dan).collect(),
704        rate: action_un1.rate.clone(),
705        sx: action_un1.sx.clone(),
706        final_points,
707        final_results,
708        names: action_un1.names.clone(),
709    })
710}