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}