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 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 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 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
389fn 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 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 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 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 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 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 outgoing.push(OutgoingTile::Ankan(conv_hai_to_tile(hai, true)?.to_red()))
577 }
578 }
579 }
580 _ => panic!("unexpected"),
581 }
582 }
583
584 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 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 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, reference: String::new(),
701 rounds: conv_rounds(&mjlog.actions, &round_indices)?,
702 connections: conv_connections(&mjlog.actions, &round_indices)?,
703 ratingc: "PF4".to_string(), 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}