riichi_decomp/
regular.rs

1use std::{
2    cmp::Ordering,
3    fmt::{Display, Formatter},
4};
5
6use itertools::Itertools;
7
8use riichi_elements::prelude::*;
9use riichi_decomp_table::{
10    WaitingKind
11};
12
13pub(crate) type RegularWaitGroups = nanovec::NanoDequeBit<u32, u8, 8>;
14
15/// A regular waiting pattern and hand decomposition of a waiting hand.
16///
17/// For a regular `3N+1` hand, this includes:
18///
19/// - Exactly `N` hand groups, including at most one incomplete (waiting) hand group.
20///   The complete hand groups can be iterated using [`RegularWait::groups()`].
21/// - A complete pair (雀頭) or a [Tanki (単騎)][Tanki] waiting pattern (= incomplete pair).
22///
23/// Note that there is exactly one waiting pattern, either a [Tanki] or an incomplete group.
24///
25/// [Tanki]: WaitingKind::Tanki
26///
27/// ## Optional `serde` support
28///
29/// Custom format, serialization only.
30/// Example:
31/// ```json
32/// {
33///     "groups": [
34///         {"type": "Koutsu", "tile": "1m"},
35///         {"type": "Koutsu", "tile": "2m"},
36///         {"type": "Shuntsu", "tile": "7m"}
37///     ],
38///     "pair": "6z",
39///     "kind": "Shanpon",
40///     "pattern": "7z",
41///     "waiting": "7z"
42/// }
43/// ```
44/// 
45#[derive(Copy, Clone, Debug)]
46pub struct RegularWait {
47    /// complete groups in this hand decomposition.
48    pub(crate) raw_groups: RegularWaitGroups,
49
50    /// The complete pair (excluding Tanki).
51    pub pair: Option<Tile>,
52
53    /// The detailed kind of the waiting pattern.
54    pub waiting_kind: WaitingKind,
55
56    /// The smallest tile in the waiting pattern.
57    ///
58    /// Examples:
59    /// - 12m wait 3m => 1m
60    /// - 34m wait 2m => 3m
61    /// - 79p wait 8p => 7p
62    /// - 3s wait 3s => 3s
63    pub pattern_tile: Tile,
64
65    /// The waiting tile (duh).
66    pub waiting_tile: Tile,
67}
68
69impl RegularWait {
70    /// Construct from components. This is only used for testing purposes.
71    #[cfg(test)]
72    pub fn new(groups: &[HandGroup], pair: Option<Tile>,
73               waiting_kind: WaitingKind, pattern_tile: Tile, waiting_tile: Tile) -> Self {
74        Self {
75            raw_groups: groups.iter().map(|g| g.packed()).collect(),
76            pair,
77            waiting_kind,
78            pattern_tile,
79            waiting_tile,
80        }
81    }
82
83    /// Iterate all complete groups in this hand decomposition.
84    pub fn groups(&self) -> impl Iterator<Item = HandGroup> {
85        self.raw_groups.map(|x| HandGroup::from_packed(x).unwrap())
86    }
87
88    /// Since groups are unordered, comparison must be applied to sorted groups.
89    /// Here we don't need to convert back to `HandGroup` --- raw (packed) is sufficient.
90    fn sorted_raw_groups(&self) -> [u8; 4] {
91        let mut result = self.raw_groups.packed().to_le_bytes();
92        sortnet::sortnet4(&mut result);
93        result
94    }
95
96    /// Returns whether this waiting pattern has a pair (complete or incomplete).
97    pub fn has_pair_or_tanki(&self) -> bool {
98        self.pair.is_some() || self.waiting_kind == WaitingKind::Tanki
99    }
100
101    /// Returns the tile of the pair (complete or incomplete).
102    pub fn pair_or_tanki(&self) -> Option<Tile> {
103        self.pair.or_else(||
104            (self.waiting_kind == WaitingKind::Tanki).then_some(self.waiting_tile))
105    }
106
107    /// Returns whether this waiting pattern is part of a double-sided wait, i.e.
108    /// 45m waits 3m or 6m (両面). This mostly affects scoring and the Pinfu Yaku.
109    ///
110    /// The reason this is separate is because we overloaded the "Ryanmen" term in [`WaitingKind`]
111    /// to broaden its scope to any 2 consecutive numerals, which simplified handling.
112    ///
113    /// For game rule purposes, this method should be used.
114    pub fn is_true_ryanmen(&self) -> bool {
115        matches!((self.waiting_kind, self.pattern_tile.normal_num()),
116            (WaitingKind::RyanmenLow, 2..=7) |  // (1)23 ..= (6)78 ; excluding (7)89
117            (WaitingKind::RyanmenHigh, 2..=7) |  // 23(4) ..= 78(9) ; excluding 12(3)
118            (WaitingKind::RyanmenBoth, _)
119        )
120    }
121}
122
123// NOTE: Comparison of two waiting patterns is non-trivial because `groups` is logically an
124// unordered collection. Fortunately we only need to trivially sort.
125
126impl PartialEq<Self> for RegularWait {
127    fn eq(&self, other: &Self) -> bool {
128        self.sorted_raw_groups() == other.sorted_raw_groups()
129            && self.pair == other.pair
130            && self.waiting_kind == other.waiting_kind
131            && self.pattern_tile == other.pattern_tile
132            && self.waiting_tile == other.waiting_tile
133    }
134}
135
136impl Eq for RegularWait {}
137
138impl PartialOrd<Self> for RegularWait {
139    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
140        Some(self.cmp(other))
141    }
142}
143
144impl Ord for RegularWait {
145    fn cmp(&self, other: &Self) -> Ordering {
146        let o =
147            self.sorted_raw_groups().cmp(&other.sorted_raw_groups());
148        if o != Ordering::Equal { return o; }
149        let o = self.pair.cmp(&other.pair);
150        if o != Ordering::Equal { return o; }
151        let o = self.waiting_kind.cmp(&other.waiting_kind);
152        if o != Ordering::Equal { return o; }
153        let o = self.pattern_tile.cmp(&other.pattern_tile);
154        if o != Ordering::Equal { return o; }
155        self.waiting_tile.cmp(&other.waiting_tile)
156    }
157}
158
159impl Display for RegularWait {
160    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
161        use WaitingKind::*;
162        write!(f, "{}", self.groups().sorted().map(|g| g.to_string()).join(" "))?;
163        if let Some(pair) = self.pair {
164            write!(f, " {}{}", pair.num(), pair)?;
165        }
166        let p = self.pattern_tile;
167        let t = self.waiting_tile;
168        match self.waiting_kind {
169            Tanki =>
170                write!(f, " {}+{}", p.num(), t),
171            Shanpon =>
172                write!(f, " {}{}+{}", p.num(), p.num(), t),
173            Kanchan =>
174                write!(f, " {}{}+{}", p.num(), p.succ2().unwrap().num(), t),
175            RyanmenHigh | RyanmenLow | RyanmenBoth =>
176                write!(f, " {}{}+{}", p.num(), p.succ().unwrap().num(), t),
177        }
178    }
179}
180
181// End of test-only comparisons.
182
183#[cfg(feature = "serde")]
184mod regular_wait_serde {
185    use serde::ser::SerializeStruct;
186    use serde::Serializer;
187    use super::*;
188    impl serde::Serialize for RegularWait {
189        fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error> where S: Serializer {
190            let mut st = s.serialize_struct("RegularWait", 5)?;
191            st.serialize_field("groups", &self.groups().sorted().collect_vec())?;
192            st.serialize_field("pair", &self.pair)?;
193            st.serialize_field("kind", self.waiting_kind.as_ref())?;
194            st.serialize_field("pattern", &self.pattern_tile)?;
195            st.serialize_field("waiting", &self.waiting_tile)?;
196            st.end()
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    use HandGroup::{Koutsu, Shuntsu};
206
207    #[allow(unused)]
208    fn k(str: &str) -> HandGroup { Koutsu(t!(str)) }
209    #[allow(unused)]
210    fn s(str: &str) -> HandGroup { Shuntsu(t!(str)) }
211
212    #[cfg(feature = "serde")]
213    mod serde_tests {
214        use assert_json_diff::assert_json_eq;
215        use WaitingKind::*;
216        use RegularWait as W;
217        use super::*;
218
219        #[test]
220        fn serialize_regular_wait() {
221            let w = W::new(&[k("1m"), k("2m"), s("7m")], Some(t!("6z")),
222                           Shanpon, t!("7z"), t!("7z"));
223            let json = serde_json::json!({
224                "groups": [
225                    {"type": "Koutsu", "tile": "1m"},
226                    {"type": "Koutsu", "tile": "2m"},
227                    {"type": "Shuntsu", "tile": "7m"}
228                ],
229                "pair": "6z",
230                "kind": "Shanpon",
231                "pattern": "7z",
232                "waiting": "7z"
233            });
234            let serialized = serde_json::to_value(w).unwrap();
235            assert_json_eq!(serialized, json);
236        }
237    }
238}