1use itertools::Itertools;
2use log::log_enabled;
3
4use riichi_decomp::{Decomposer, RegularWait, WaitSet};
5use riichi_elements::prelude::*;
6
7use crate::{
8 model::*,
9 rules::Ruleset,
10};
11
12pub fn terminal_kinds(h: &TileSet37) -> u8 {
15 pure_terminal_kinds(h) + honor_kinds(h)
16}
17
18pub fn terminal_count(h: &TileSet37) -> u8 {
19 pure_terminal_count(h) + honor_count(h)
20}
21
22#[rustfmt::skip]
23pub fn pure_terminal_kinds(h: &TileSet37) -> u8 {
24 0u8 + (h[0] > 0) as u8 + (h[8] > 0) as u8
25 + (h[9] > 0) as u8 + (h[17] > 0) as u8
26 + (h[18] > 0) as u8 + (h[26] > 0) as u8
27}
28
29pub fn pure_terminal_count(h: &TileSet37) -> u8 {
30 h[0] + h[8] + h[9] + h[17] + h[18] + h[26]
31}
32
33#[rustfmt::skip]
34pub fn honor_kinds(h: &TileSet37) -> u8 {
35 0u8 + (h[27] > 0) as u8 + (h[28] > 0) as u8
36 + (h[29] > 0) as u8 + (h[30] > 0) as u8
37 + (h[31] > 0) as u8 + (h[32] > 0) as u8
38 + (h[33] > 0) as u8
39}
40
41pub fn honor_count(h: &TileSet37) -> u8 {
42 h[27] + h[28] + h[29] + h[30] + h[31] + h[32] + h[33]
43}
44
45pub fn green_count(h: &TileSet37) -> u8 {
46 h[19] + h[20] + h[21] + h[23] + h[25] + h[32]
47}
48
49pub fn m_count(h: &TileSet37) -> u8 {
50 (&h.0[0..9]).iter().sum::<u8>() + h[34]
51}
52pub fn p_count(h: &TileSet37) -> u8 {
53 (&h.0[9..18]).iter().sum::<u8>() + h[35]
54}
55pub fn s_count(h: &TileSet37) -> u8 {
56 (&h.0[18..27]).iter().sum::<u8>() + h[36]
57}
58pub fn z_count(h: &TileSet37) -> u8 { honor_count(h) }
60
61
62pub fn chuuren_agari(x: u32) -> Option<u8> {
69 if (x + 0o133333331) & 0o444444444 != 0o444444444 { return None; }
71 let r = x - 0o311111113;
73 if !r.is_power_of_two() { return None; }
75 Some(r.trailing_zeros() as u8 / 3)
76}
77
78pub fn chuuren_wait(h: &[u8]) -> Option<(u8, u8)> {
88 const TARGET: [i8; 9] = [3, 1, 1, 1, 1, 1, 1, 1, 3];
89 let mut lack = 100;
90 let mut over = 100;
91 for (i, (a, b)) in itertools::zip_eq(h, TARGET).enumerate() {
92 let x = *a as i8 - b;
93 match x {
94 -1 => {
95 if lack < 9 { return None; }
96 lack = i;
97 }
98 0 => {}
99 1 => {
100 if over < 9 { return None; }
101 over = i;
102 }
103 _ => return None,
104 }
105 }
106 if lack > 9 && over > 9 {
107 Some((0, 0))
108 } else if lack < 9 && over < 9 {
109 Some((lack as u8, over as u8))
110 } else {
111 None
112 }
113}
114
115pub fn is_forbidden_swap_call(ruleset: &Ruleset, meld: Meld, discard: Tile) -> bool {
124 let discard = discard.to_normal();
125 let (allow_same, allow_other) = (ruleset.swap_call_allow_same, ruleset.swap_call_allow_other);
126 match meld {
127 Meld::Chii(chii) => {
128 (!allow_same && chii.called.to_normal() == discard) ||
129 (!allow_other && chii.dir() == 0 && Some(discard) == chii.own[1].succ()) ||
130 (!allow_other && chii.dir() == 2 && Some(discard) == chii.min.pred())
131 }
132 Meld::Pon(pon) => {
133 !allow_same && pon.called.to_normal() == discard
134 }
135 _ => false,
136 }
137}
138
139pub fn is_ankan_ok_under_riichi(
141 ruleset: &Ruleset,
142 decomposer: &mut Decomposer,
143 hand: &TileSet37,
144 wait_set: &WaitSet,
145 draw: Tile,
146 ankan: Tile,
147) -> bool {
148 let draw = draw.to_normal();
149 let ankan = ankan.to_normal();
150 if ruleset.riichi_ankan_strict_mode {
151 is_ankan_ok_under_riichi_strict(hand, &wait_set.regular, draw, ankan)
152 } else {
153 is_ankan_ok_under_riichi_relaxed(hand, decomposer, wait_set, ankan)
154 }
155}
156
157pub fn is_ankan_ok_under_riichi_strict(
158 hand: &TileSet37,
159 regulars: &[RegularWait],
160 draw: Tile,
161 ankan: Tile,
162) -> bool {
163 if draw != ankan { return false; }
165
166 if !regulars.iter().all(|regular|
168 regular.groups().any(|group| group == HandGroup::Koutsu(ankan))) {
169 return false;
170 }
171
172 if ankan.suit() == 3 { return true }
174 let i = (ankan.suit() * 9) as usize;
175 let mut hand = TileSet34::from(hand);
176 hand[ankan] -= 1;
177 !chuuren_wait(&hand.0[i..(i + 9)]).is_some()
178}
179
180pub fn is_ankan_ok_under_riichi_relaxed(
181 hand: &TileSet37,
182 decomposer: &mut Decomposer,
183 wait_set: &WaitSet,
184 ankan: Tile,
185) -> bool {
186 let mut hand = hand.clone();
187 hand[ankan] -= 1;
188 let new_wait_set = WaitSet::from_keys(decomposer, &hand.packed_34());
189 wait_set.waiting_tiles == new_wait_set.waiting_tiles
190}
191
192pub fn num_active_riichi(state: &State) -> usize {
195 state.core.riichi.into_iter().flatten().count()
196}
197
198pub fn num_draws(state: &State) -> u8 {
199 state.core.num_drawn_head + state.core.num_drawn_tail
200}
201
202pub fn is_last_draw(state: &State) -> bool {
204 debug_assert!(num_draws(state) <= wall::MAX_NUM_DRAWS);
205 num_draws(state) == wall::MAX_NUM_DRAWS
206}
207
208pub fn is_first_chance(state: &State) -> bool {
214 state.core.seq <= 3 && state.melds.iter().all(|melds| melds.is_empty())
215}
216
217pub fn is_nagashi_mangan(state: &State, player: Player) -> bool {
221 state.discards[player.to_usize()].iter().all(|discard|
222 discard.tile.is_terminal() && discard.called_by == player)
223}
224
225pub fn is_any_player_nagashi_mangan(state: &State) -> bool {
229 ALL_PLAYERS.into_iter().any(|player| is_nagashi_mangan(state, player))
230}
231
232pub fn is_aborted_four_wind(state: &State, action: Action) -> bool {
234 if let Action::Discard(discard) = action {
235 return is_first_chance(state) &&
236 state.core.seq == 3 &&
237 discard.tile.is_wind() &&
238 other_players_after(state.core.actor).iter()
239 .map(|actor| &state.discards[actor.to_usize()])
240 .all(|discards|
241 discards.len() == 1 && discards[0].tile == discard.tile)
242 }
243 false
244}
245
246pub fn is_aborted_four_kan(state: &State, action: Action, reaction: Option<Reaction>) -> bool {
248 let actor_i = state.core.actor.to_usize();
249
250 if matches!(action, Action::Kakan(_)) ||
251 matches!(action, Action::Ankan(_)) ||
252 matches!(reaction, Some(Reaction::Daiminkan)) {
253 let kan_players =
255 state.melds.iter().enumerate().flat_map(|(player, melds_p)|
256 melds_p.iter().filter_map(move |meld|
257 if meld.is_kan() { Some(player) } else { None })).collect_vec();
258 if kan_players.len() == 4 ||
261 kan_players.len() == 3 && !kan_players.iter().all(|&player| player == actor_i) {
262 return true;
263 }
264 }
265 false
266}
267
268pub fn is_aborted_four_riichi(state: &State, action: Action) -> bool {
270 matches!(action, Action::Discard(Discard{declares_riichi: true, ..})) &&
271 num_active_riichi(state) == 3 }
273
274pub fn calc_wall_exhausted_delta(waiting: [u8; 4]) -> [GamePoints; 4] {
278 const NO_WAIT_PENALTY_TOTAL: GamePoints = 3000;
280 let no_wait = NO_WAIT_PENALTY_TOTAL;
281
282 let num_waiting = waiting.into_iter().sum();
283 let (down, up) = match num_waiting {
284 1 => (-no_wait / 3, no_wait / 1),
285 2 => (-no_wait / 2, no_wait / 2),
286 3 => (-no_wait / 1, no_wait / 3),
287 _ => (0, 0),
288 };
289 waiting.map(|w| if w > 0 { up } else { down })
290}
291
292pub fn calc_nagashi_mangan_delta(state: &State, button: Player) -> [GamePoints; 4] {
295 let mut delta = [0; 4];
298 for player in ALL_PLAYERS {
299 if is_nagashi_mangan(state, player) {
300 if player == button {
301 delta[player.to_usize()] += 12000 + 4000;
302 for qq in 0..4 { delta[qq] -= 4000; }
303 } else {
304 delta[player.to_usize()] += 8000 + 2000;
305 delta[button.to_usize()] -= 2000;
306 for qq in 0..4 { delta[qq] -= 2000; }
307 }
308 }
309 }
310 delta
311}
312
313pub fn calc_pot_delta(riichi: &[Option<Riichi>; 4]) -> [GamePoints; 4] {
315 riichi.map(|r| if r.is_some() { -super::RIICHI_POT } else { 0 })
316}
317
318pub fn get_all_tiles(
323 closed_hand: &TileSet37,
324 winning_tile: Tile,
325 melds: &[Meld],
326) -> TileSet37 {
327 let mut all_tiles = closed_hand.clone();
328 log::debug!("closed_hand: {}", all_tiles);
329 all_tiles[winning_tile] += 1;
330 log::debug!("+winning : {}", all_tiles);
331 for meld in melds {
332 match meld {
333 Meld::Chii(chii) => {
334 for own in chii.own { all_tiles[own] += 1 }
335 all_tiles[chii.called] += 1;
336 }
337 Meld::Pon(pon) => {
338 for own in pon.own { all_tiles[own] += 1 }
339 all_tiles[pon.called] += 1;
340 }
341 Meld::Kakan(kakan) => {
342 for own in kakan.pon.own { all_tiles[own] += 1 }
343 all_tiles[kakan.pon.called] += 1;
344 all_tiles[kakan.added] += 1;
345 }
346 Meld::Daiminkan(daiminkan) => {
347 for own in daiminkan.own { all_tiles[own] += 1 }
348 all_tiles[daiminkan.called] += 1;
349 }
350 Meld::Ankan(ankan) => {
351 for own in ankan.own { all_tiles[own] += 1; }
352 }
353 }
354 }
355 log::debug!("+meld : {}", all_tiles);
356 all_tiles
357}
358
359pub fn count_doras(
360 ruleset: &Ruleset,
361 all_tiles: &TileSet37,
362 num_dora_indicators: u8,
363 wall: &Wall,
364 is_riichi: bool,
365) -> DoraHits {
366 let all_tiles_normal = TileSet34::from(all_tiles);
367
368 let n = if ruleset.dora_allow_kan { num_dora_indicators as usize } else { 1 };
369 let n_ura = if ruleset.dora_allow_kan_ura { n } else { 1 };
370
371 if log_enabled!(log::Level::Debug) {
372 log::debug!("count doras: n={} n_ura={} di={} udi={}, all_tiles={}",
373 n,
374 n_ura,
375 wall::dora_indicators(wall).iter().map(|t| t.as_str()).join(","),
376 wall::ura_dora_indicators(wall).iter().map(|t| t.as_str()).join(","),
377 all_tiles,
378 );
379 }
380
381 DoraHits {
382 dora:
383 (&wall::dora_indicators(wall)[0..n])
384 .iter()
385 .map(|t| all_tiles_normal[t.indicated_dora()])
386 .sum(),
387
388 ura_dora:
389 if is_riichi && ruleset.dora_allow_ura {
390 (&wall::ura_dora_indicators(wall)[0..n_ura])
391 .iter()
392 .map(|t| all_tiles_normal[t.indicated_dora()])
393 .sum()
394 } else { 0 },
395
396 aka_dora: all_tiles[34] + all_tiles[35] + all_tiles[36],
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use rustc_hash::FxHashSet as HashSet;
403 use super::*;
404
405 #[test]
406 fn chuuren_wait_exhaustive() {
407 let mut s: HashSet<[u8; 9]> = HashSet::default();
408
409 let target = [3, 1, 1, 1, 1, 1, 1, 1, 3];
410 s.insert(target);
411 assert_eq!(chuuren_wait(&target[..]), Some((0, 0)));
412
413 for lack in 0..9 {
414 for over in 0..9 {
415 if lack == over { continue; }
416 let mut x = target;
417 x[lack] -= 1;
418 x[over] += 1;
419 s.insert(x);
420 assert_eq!(chuuren_wait(&x[..]), Some((lack as u8, over as u8)));
421 }
422 }
423
424 for x in itertools::repeat_n(0..4, 9).multi_cartesian_product() {
425 let x: [u8; 9] = x.try_into().unwrap();
426 if !s.contains(&x) {
427 assert_eq!(chuuren_wait(&x), None);
428 }
429 }
430 }
431}