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
54fn find_final_result(actions: &[Action]) -> ConvResult<(Vec<i32>, Vec<f64>)> {
56 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 let pict_order = hai_number / 4;
106
107 let pict_type = (pict_order / 9) + 1;
112
113 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
132fn 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 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 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
386fn 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 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 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 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 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 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 outgoing.push(OutgoingTile::Ankan(conv_hai_to_tile(hai, true)?.to_red()))
574 }
575 }
576 }
577 _ => panic!("unexpected"),
578 }
579 }
580
581 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 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 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, reference: String::new(),
698 rounds: conv_rounds(&mjlog.actions, &round_indices)?,
699 connections: conv_connections(&mjlog.actions, &round_indices)?,
700 ratingc: "PF4".to_string(), 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}