riichi_elements/
hand_group.rs

1//! [Hand Group](HandGroup) = Mentsu (面子) = Koutsu ((暗)刻子) or Shuntsu ((暗)順子)
2//!
3//! ## Ref
4//!
5//! - <https://riichi.wiki/Mentsu>
6//! - <https://ja.wikipedia.org/wiki/面子_(麻雀)>
7
8use core::fmt::{Display, Formatter};
9
10use crate::tile::Tile;
11
12/// A group of 3 tiles within a player's _closed_ hand, a.k.a. Mentsu (面子).
13///
14/// Can be either:
15/// - Koutsu ((暗)刻子): 3 of a kind (ignoring red); e.g. `222z`, `055m`
16/// - Shuntsu ((暗)順子): 3 consecutive numerals (ignoring red); e.g. `789m`, `406s`
17///
18/// These are like [Chii] and [Pon] respectively, except concealed.
19///
20/// It can be encoded as a 6-bit integer (the same size as a [`Tile`]!), with 2 bitfields:
21///
22/// - `[3:0]`: `[111, 123, 222, 234, 333, 345, 444, 456, 555, 567, 666, 678, 777, 789, 888, 999]`.
23///   Basically with `999` shifting 1 place to occupy the encoding for `89A` (invalid).
24///   For suit 3 (honors), `123`, `234`, ..., `789`, `888`, `999` are all invalid.
25///
26/// - `[5:4]`: suit (0/1/2/3 = m/p/s/z)
27///
28/// [Chii]: crate::meld::Chii
29/// [Pon]: crate::meld::Pon
30///
31/// ## Optional `serde` support
32///
33/// `{type, tile}` where `type` is `"Shuntsu"` or `"Koutsu"`.
34///
35#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37#[cfg_attr(feature = "serde", serde(tag = "type", content = "tile"))]
38pub enum HandGroup {
39    /// Koutsu (暗)刻子: 3 of a kind (ignoring red); e.g. `222z`, `055m`.
40    /// The tile argument is the repeated tile.
41    Koutsu(Tile),
42
43    /// Shuntsu (暗)順子: 3 consecutives (ignoring red); e.g. `789m`, `406s`.
44    /// The tile argument is the minimum (normal) tile in the group.
45    Shuntsu(Tile),
46}
47
48impl HandGroup {
49    /// Parse from the 6-bit integer encoding. Higher 2 bits are ignored.
50    pub fn from_packed(packed: u8) -> Option<Self> {
51        let num = ((packed & 0b1111) >> 1) + 1;
52        let suit = (packed >> 4) & 0b11;
53        let tile = Tile::from_num_suit(num, suit)?;
54        if (packed & 1) == 1 {
55            if num == 8 {
56                // What should have encoded [8, 9, 10] is reused to represent [9, 9, 9]
57                Some(HandGroup::Koutsu(tile.succ().unwrap()))
58            } else if suit < 3 {
59                Some(HandGroup::Shuntsu(tile))
60            } else {
61                // Honors cannot form shuntsu
62                None
63            }
64        } else {
65            Some(HandGroup::Koutsu(tile))
66        }
67    }
68
69    /// Encode as a 6-bit integer.
70    pub fn packed(self) -> u8 {
71        match self {
72            HandGroup::Koutsu(tile) => {
73                let n = tile.num() - 1;
74                let s = tile.suit();
75                (s << 4) | ((n << 1) - ((n == 8) as u8))
76            }
77            HandGroup::Shuntsu(tile) => {
78                let n = tile.num() - 1;
79                let s = tile.suit();
80                (s << 4) | ((n << 1) + 1)
81            }
82        }
83    }
84
85    /// Returns the min tile in the group.
86    pub fn min_tile(self) -> Tile {
87        match self {
88            HandGroup::Koutsu(tile) => tile,
89            HandGroup::Shuntsu(tile) => tile,
90        }
91    }
92}
93
94impl Display for HandGroup {
95    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
96        match self {
97            HandGroup::Koutsu(tile) => {
98                let n = tile.normal_num();
99                let s = tile.suit_char();
100                write!(f, "{}{}{}{}", n, n, n, s)
101            },
102            HandGroup::Shuntsu(tile) => {
103                let n = tile.normal_num();
104                let s = tile.suit_char();
105                write!(f, "{}{}{}{}", n, n + 1, n + 2, s)
106            },
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    
115    #[test]
116    fn all_hand_groups_are_correctly_encoded() {
117        let t = |enc| Tile::from_encoding(enc).unwrap();
118        let k = |x| Some(HandGroup::Koutsu(t(x)));
119        let s = |x| Some(HandGroup::Shuntsu(t(x)));
120        let all = [
121            // 111m, 123m, ..., 888m, 999m
122            k(0), s(0), k(1), s(1), k(2), s(2), k(3), s(3),
123            k(4), s(4), k(5), s(5), k(6), s(6), k(7), k(8),
124
125            // 111p, 123p, ..., 888p, 999p
126            k(9), s(9), k(10), s(10), k(11), s(11), k(12), s(12),
127            k(13), s(13), k(14), s(14), k(15), s(15), k(16), k(17),
128
129            // 111s, 123s, ..., 888s, 999s
130            k(18), s(18), k(19), s(19), k(20), s(20), k(21), s(21),
131            k(22), s(22), k(23), s(23), k(24), s(24), k(25), k(26),
132
133            // 111z, x, 222z, x, ...777z, x, x, x
134            k(27), None, k(28), None, k(29), None, k(30), None,
135            k(31), None, k(32), None, k(33), None, None, None,
136        ];
137        for (i, ans) in all.into_iter().enumerate() {
138            let i = i as u8;
139            let unpacked = HandGroup::from_packed(i as u8);
140            assert_eq!(unpacked, ans);
141            if let Some(g) = unpacked {
142                assert_eq!(g.packed(), i);
143            }
144        }
145    }
146}