riichi_decomp/
irregular.rs

1use std::fmt::{Display, Formatter};
2use std::iter::zip;
3
4use riichi_elements::prelude::*;
5
6/// Represents one of the irregular waiting hand patterns.
7///
8/// Note that they are mutually exclusive --- one hand can fit at most one of these patterns.
9///
10/// ## Optional `Serde` support
11///
12/// `{type, wait?}` (adjacently tagged, in serde terms).
13/// Examples: `{"type": "SevenPairs", "wait": "9s"}`, `{"type": "ThirteenOrphansAll"}`
14///
15#[derive(Copy, Clone, Debug, Eq, PartialEq)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[cfg_attr(feature = "serde", serde(tag = "type", content = "wait"))]
18pub enum IrregularWait {
19    /// Seven Pairs (七対子)
20    ///
21    /// The associated tile is the waiting tile, i.e. the final (7th) incomplete pair.
22    ///
23    /// <https://riichi.wiki/Chiitoitsu>
24    SevenPairs(Tile),
25
26    /// Thirteen Orphans (十三幺), more commonly known as Kokushi-Musou (国士無双).
27    ///
28    /// The associated tile is the waiting tile, i.e. the "missing"
29    /// [terminal tile](Tile::is_terminal).
30    ///
31    /// <https://riichi.wiki/Kokushi_musou>
32    ThirteenOrphans(Tile),
33
34    /// 13-way waiting version of Thirteen Orphans (国士無双13面待ち).
35    ///
36    /// Any [terminal tile](Tile::is_terminal) will complete this hand.
37    ThirteenOrphansAll,
38}
39
40impl IrregularWait {
41    pub fn to_waiting_set(self) -> TileMask34 {
42        match self {
43            IrregularWait::SevenPairs(t) | IrregularWait::ThirteenOrphans(t)
44                => TileMask34(1u64 << (t.encoding() as u64)),
45            IrregularWait::ThirteenOrphansAll
46                => TileMask34(0b1111111_100000001_100000001_100000001),
47        }
48    }
49}
50
51// This is necessary to pretty-print the wrapped tile.
52impl Display for IrregularWait {
53    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
54        match self {
55            IrregularWait::SevenPairs(t) => write!(f, "SevenPairs({})", t),
56            IrregularWait::ThirteenOrphans(t) => write!(f, "ThirteenOrphans({})", t),
57            IrregularWait::ThirteenOrphansAll => write!(f, "ThirteenOrphansAll"),
58        }
59    }
60}
61
62/// Detect which irregular waiting patterns match the supplied hand (octal-[packed]).
63///
64/// [packed]: TileSet34::packed_34
65pub fn detect_irregular_wait(keys: [u32; 4]) -> Option<IrregularWait> {
66    if let Some(tile) = detect_seven_pairs(keys) {
67        Some(IrregularWait::SevenPairs(tile))
68    } else {
69        detect_thirteen_orphans(keys)
70    }
71}
72
73/// Exactly 6 pairs and 1 single
74fn detect_seven_pairs(keys: [u32; 4]) -> Option<Tile> {
75    let d = keys.map(one_two);
76    let num_twos = d[0].3 + d[1].3 + d[2].3 + d[3].3;
77    if num_twos != 6 { return None; }
78    let num_ones = d[0].1 + d[1].1 + d[2].1 + d[3].1;
79    if num_ones != 1 { return None; }
80    for i in 0..4 {
81        let ones = d[i].0;
82        if ones > 0 {
83            return Tile::from_encoding(ones.trailing_zeros() as u8 / 3 + (i as u8) * 9);
84        }
85    }
86    panic!()
87}
88
89/// - No middle numerals
90/// - No trips/quads
91/// - If 0 pair and 13 singles: 13-wait version
92/// - If 1 pair and 11 singles: 1-wait version; the "hole" is the waiting tile
93fn detect_thirteen_orphans(keys: [u32; 4]) -> Option<IrregularWait> {
94    const MASK: [u32; 4] = [
95        0o700000007,
96        0o700000007,
97        0o700000007,
98        0o7777777,
99    ];
100    if zip(keys, MASK).any(|(key, mask)| key & !mask > 0) {
101        return None;
102    }
103    let d = keys.map(one_two);
104    let num_twos = d[0].3 + d[1].3 + d[2].3 + d[3].3;
105    let num_ones = d[0].1 + d[1].1 + d[2].1 + d[3].1;
106    match (num_ones, num_twos) {
107        (13, 0) => Some(IrregularWait::ThirteenOrphansAll),
108        (11, 1) => {
109            for i in 0..4 {
110                let k = (MASK[i] & 0o111111111) - (d[i].0 | d[i].2);
111                if k > 0 {
112                    return Some(IrregularWait::ThirteenOrphans(Tile::from_encoding(
113                        k.trailing_zeros() as u8 / 3 + (i as u8) * 9
114                    ).unwrap()))
115                }
116            }
117            None
118        }
119        _ => None,
120    }
121}
122
123/// Bit hack to obtain {bit mask, count} of {isolated tiles, pairs} in one octal-packed suit.
124/// If there are any trips / quads, return an absurdly big count for both.
125fn one_two(x: u32) -> (u32, u32, u32, u32) {
126    // detect trips/quads
127    let over = (x + 0o111111111) & 0o444444444;
128    if over > 0 { return (0, 20, 0, 20); }
129    // now everything is 0/1/2; 2 has one more bit.
130    let twos = (x >> 1) & 0o111111111;
131    let num_twos = twos.count_ones();
132    // 0/1/2 with 2 removed becomes 0/1
133    let ones = x - twos * 2;
134    let num_ones = ones.count_ones();
135    (ones, num_ones, twos, num_twos)
136}
137
138#[cfg(test)]
139mod tests {
140    use std::str::FromStr;
141    use super::*;
142
143    #[test]
144    fn just_seven_pairs() {
145        assert_eq!(
146            detect_seven_pairs([0o202020202, 0o000000002, 0o000000000, 0o0100000]),
147            Some(Tile::from_str("6z").unwrap()));
148        assert_eq!(
149            detect_seven_pairs([0o201020202, 0o000000002, 0o000000000, 0o0200000]),
150            Some(Tile::from_str("7m").unwrap()));
151        assert_eq!(
152            detect_seven_pairs([0o200020202, 0o000000002, 0o000020001, 0o0000000]),
153            Some(Tile::from_str("1s").unwrap()));
154        assert_eq!(
155            detect_seven_pairs([0o202000202, 0o000000002, 0o000000000, 0o0100000]),
156            None);
157        assert_eq!(
158            detect_seven_pairs([0o202040202, 0o000000001, 0o000000000, 0o0000000]),
159            None);
160        assert_eq!(
161            detect_seven_pairs([0o202020202, 0o000000002, 0o000000000, 0o0000110]),
162            None);
163        assert_eq!(
164            detect_seven_pairs([0o202020202, 0o001000000, 0o000001000, 0o0000100]),
165            None);
166    }
167
168    #[test]
169    fn just_thirteen_orphans() {
170        assert_eq!(
171            detect_thirteen_orphans([0o100000001, 0o100000001, 0o100000001, 0o1111111]),
172            Some(IrregularWait::ThirteenOrphansAll));
173        assert_eq!(
174            detect_thirteen_orphans([0o100000000, 0o100000001, 0o100000001, 0o1111112]),
175            Some(IrregularWait::ThirteenOrphans(Tile::from_str("1m").unwrap())));
176        assert_eq!(
177            detect_thirteen_orphans([0o000000002, 0o100000001, 0o100000001, 0o1111111]),
178            Some(IrregularWait::ThirteenOrphans(Tile::from_str("9m").unwrap())));
179        assert_eq!(
180            detect_thirteen_orphans([0o100000001, 0o100000000, 0o100000001, 0o1211111]),
181            Some(IrregularWait::ThirteenOrphans(Tile::from_str("1p").unwrap())));
182        assert_eq!(
183            detect_thirteen_orphans([0o200000001, 0o000000001, 0o100000001, 0o1111111]),
184            Some(IrregularWait::ThirteenOrphans(Tile::from_str("9p").unwrap())));
185        assert_eq!(
186            detect_thirteen_orphans([0o100000002, 0o100000001, 0o100000000, 0o1111111]),
187            Some(IrregularWait::ThirteenOrphans(Tile::from_str("1s").unwrap())));
188        assert_eq!(
189            detect_thirteen_orphans([0o100000002, 0o100000001, 0o000000001, 0o1111111]),
190            Some(IrregularWait::ThirteenOrphans(Tile::from_str("9s").unwrap())));
191        assert_eq!(
192            detect_thirteen_orphans([0o100000001, 0o100000001, 0o100000001, 0o1102111]),
193            Some(IrregularWait::ThirteenOrphans(Tile::from_str("5z").unwrap())));
194
195        assert_eq!(
196            detect_thirteen_orphans([0o100000010, 0o100000001, 0o100000001, 0o1102111]),
197            None);
198        assert_eq!(
199            detect_thirteen_orphans([0o100000003, 0o100000001, 0o100000001, 0o1102111]),
200            None);
201    }
202}