riichi_elements/wall.rs
1//! The wall of tiles (牌山/山/壁牌) and utils.
2//!
3//! ## How we represent the wall
4//!
5//! ```ascii_art
6//! _______________ ______________
7//! <--- TAIL (CCW) | / HEAD (CW) --->
8//! self draws | dora indicators |rinshan| | initial deal | self draws
9//! 118 120|122 124 126 128 130|132 134| | 0 2 4 6 8 10 48 50 |52 54
10//! ... +---+---*---+---+---+---+###*---+---+ +---+---+---+---+---+---+ ... +---+---*---+---+ ...
11//! |#66|#68| D4| D3| D2| D1| D0|RS2|RS0| |E0 |E2 |S0 |S2 |W0 |W2 | |E12|W12|#00|#02| TOP
12//! ... +===+===*===+===+===+===+===*===+===+ +===+===+===+===+===+===+ ... +===+===*===+===+ ...
13//! |#67|#69|UD4|UD3|UD2|UD1|UD0|RS3|RS1| |E1 |E3 |S1 |S3 |W1 |W3 | |S12|N12|#01|#03| BOTTOM
14//! ... +---+---*---+---+---+---+---*---+---+ +---+---+---+---+---+---+ ... +---+---*---+---+ ...
15//! 119 121|123 125 127 129 131|133 135| | 1 3 5 7 9 11 49 51 |53 55
16//! self draws |ura-dora indicators|rinshan| | initial deal | self draws
17//! ```
18//!
19//! In a physical game, the following procedure is used to prepare the wall:
20//!
21//! 1. Shuffle: 136 tiles => 4 sides x 17 stacks x 2 tiles per stack, treated as a ring.
22//!
23//! 2. Randomly decide the splitting point on this ring (rules vary on this).
24//!
25//! 3. From the splitting point: clockwise => head, counterclockwise => tail.
26//! Now the ring can be treated as a linear 68 x 2 array of tiles.
27//!
28//! 4. Reveal the top tile of the 3rd stack from tail; this is the first Dora Indicator.
29//! (figure: `###`)
30//!
31//! 5. Initial deal: Players take turns (E->S->W->N->E->...) to draw 2 stacks (= 4 tiles) from the
32//! head until everyone has 12. Each player then draws one more tile.
33//! (figure: `E0`~`E3`, `S0`~`S3`, (...), `W8`~`W11`, `N8`~`N11`; `E12`, `S12`, `W12`, `N12`)
34//!
35//! 6. The button player takes his first self-draw and the round starts.
36//! (figure: `#00`)
37//!
38//! 7. Additional draw after each Kan is taken from the tail.
39//! (figure: `RS0`, `RS1`, `RS2`, `RS3`)
40//!
41//! 8. Additional Dora Indicators are flipped further from tail since the initial one.
42//! (figure: `D1`, `D2`, `D3`, `D4`)
43//!
44//! In this crate, we assign an index to each of the 136 tiles in the "linear" wall after splitting.
45//! The split wall is indexed head-to-tail (major), top-to-bottom (minor). In the figure, this index
46//! is annotated as numbers next to the boxes (above/below).
47//!
48//! ## Ref
49//!
50//! - <https://ja.wikipedia.org/wiki/配牌>
51//! - <https://ja.wikipedia.org/wiki/壁牌>
52//! - <https://riichi.wiki/Yama>
53
54use core::fmt::{Display, Formatter};
55
56use crate::{
57 tile::Tile,
58 tile_set::*,
59 player::*,
60};
61
62/// The wall of tiles.
63/// See [mod-level docs](self).
64pub type Wall = [Tile; 136];
65
66/// Wall with some tiles unknown.
67pub type PartialWall = [Option<Tile>; 136];
68
69/// Constructor for an obviously invalid wall. Useful for mutating it later.
70pub const fn make_dummy_wall() -> Wall { [Tile::MIN; 136] }
71
72/// Make a sorted wall of the standard 136-tile set, including specified number of red-5's for each
73/// (numeral) suit.
74pub fn make_sorted_wall(num_reds: [u8; 3]) -> Wall {
75 let mut wall = [Tile::MIN; 136];
76 for encoding in 0u8..34u8 {
77 let tile = Tile::from_encoding(encoding).unwrap();
78 let suit = tile.suit();
79 let num = tile.num();
80 if num == 5 && suit <= 2 {
81 for i in 0..num_reds[suit as usize] {
82 wall[(encoding * 4 + i) as usize] = tile.to_red();
83 }
84 for i in num_reds[suit as usize]..4 {
85 wall[(encoding * 4 + i) as usize] = tile;
86 }
87 } else {
88 for i in 0..4 {
89 wall[(encoding * 4 + i) as usize] = tile;
90 }
91 }
92 }
93 wall
94}
95
96/// Make sure that a wall is valid --- 34 kinds x 4 each = 136
97pub fn is_valid_wall(wall: Wall) -> bool {
98 TileSet34::from_iter(wall).into_iter().all(|n| n == 4)
99}
100
101/// For each player starting from the button player, which wall tiles to take as the initial draw?
102pub const DEAL_INDEX: [[usize; 13]; 4] = [
103 [0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13, 0x20, 0x21, 0x22, 0x23, 0x30],
104 [0x04, 0x05, 0x06, 0x07, 0x14, 0x15, 0x16, 0x17, 0x24, 0x25, 0x26, 0x27, 0x31],
105 [0x08, 0x09, 0x0a, 0x0b, 0x18, 0x19, 0x1a, 0x1b, 0x28, 0x29, 0x2a, 0x2b, 0x32],
106 [0x0c, 0x0d, 0x0e, 0x0f, 0x1c, 0x1d, 0x1e, 0x1f, 0x2c, 0x2d, 0x2e, 0x2f, 0x33],
107];
108/// Index of dora indicators in the wall, by their order of revealing first-to-last.
109pub const DORA_INDICATOR_INDEX: [usize; 5] = [130, 128, 126, 124, 122];
110/// Index of ura-dora indicators in the wall; order corresponding to dora indicators.
111pub const URA_DORA_INDICATOR_INDEX: [usize; 5] = [131, 129, 127, 125, 123];
112/// Index of kan draws in the wall, first-to-last.
113pub const KAN_DRAW_INDEX: [usize; 4] = [134, 135, 132, 133];
114
115/// Total number of draws (front + back) cannot exceed this.
116pub const MAX_NUM_DRAWS: u8 = 136 - 14;
117
118/// Draws the initial 13 tiles for each of the 4 players, according to standard rules.
119/// See [module-level docs](self).
120pub fn deal(wall: &Wall, button: Player) -> [TileSet37; 4] {
121 let mut hists = [
122 TileSet37::default(),
123 TileSet37::default(),
124 TileSet37::default(),
125 TileSet37::default(),
126 ];
127 for i in 0..4 {
128 for wall_index in DEAL_INDEX[i] {
129 let p = button.add(Player::new(i as u8));
130 hists[p.to_usize()][wall[wall_index].encoding() as usize] += 1;
131 }
132 }
133 hists
134}
135
136/// Returns the indexed (0..=4) dora indicator.
137pub fn dora_indicator(wall: &Wall, i: usize) -> Tile {
138 wall[DORA_INDICATOR_INDEX[i]]
139}
140
141/// Returns the indexed (0..=4) ura-dora indicator.
142pub fn ura_dora_indicator(wall: &Wall, i: usize) -> Tile {
143 wall[URA_DORA_INDICATOR_INDEX[i]]
144}
145
146/// Returns the entire dora indicator section of the wall as an array.
147/// Note that this does not handle the gradual revealing of dora indicators.
148pub fn dora_indicators(wall: &Wall) -> [Tile; 5] {
149 DORA_INDICATOR_INDEX.map(|i| wall[i])
150}
151
152/// Returns the entire ura-dora indicator section of the wall as an array.
153/// Note that this does not handle the final revealing of ura-dora indicators.
154pub fn ura_dora_indicators(wall: &Wall) -> [Tile; 5] {
155 URA_DORA_INDICATOR_INDEX.map(|i| wall[i])
156}
157
158/// Returns the indexed (0..=3) Kan draw.
159pub fn kan_draw(wall: &Wall, i: usize) -> Tile {
160 wall[KAN_DRAW_INDEX[i]]
161}
162
163/// Deduces the set of unknown tiles from the given partially-known wall, and the known total number
164/// of red tiles.
165///
166/// Panics when the partially-known wall is inconsistent with the assumed complete set of tiles.
167pub fn get_missing_tiles_in_partial_wall(partial_wall: &PartialWall, num_reds: [u8; 3]) -> TileSet37 {
168 let mut missing = TileSet37::complete_set(num_reds);
169 for tile_or_hole in partial_wall {
170 if let &Some(tile) = tile_or_hole {
171 if missing[tile] == 0 {
172 panic!("More {} in the partial wall than expected.", tile)
173 }
174 missing[tile] -= 1;
175 }
176 }
177 missing
178}
179
180/// Combine the partially-known wall and (reordered) unknown tiles to form a fully-known wall.
181///
182/// Panics when there are not enough tiles in `missing_tiles` to fill the "holes" in `partial_wall`.
183///
184/// Does not check validity of the resulting wall.
185pub fn fill_missing_tiles_in_partial_wall(
186 partial_wall: &PartialWall, missing_tiles: impl IntoIterator<Item=Tile>) -> Wall {
187 let mut missing_iter = missing_tiles.into_iter();
188 partial_wall.map(|tile_or_hole|
189 tile_or_hole.or_else(|| missing_iter.next()).unwrap())
190}
191
192// Hack to impl `Display` for both `Wall` and `PartialWall`
193
194pub struct WallDisplay<'a>(&'a Wall);
195pub trait WallDisplayMethod {
196 fn display(&self) -> WallDisplay;
197}
198impl WallDisplayMethod for Wall {
199 fn display(&self) -> WallDisplay { WallDisplay(self) }
200}
201impl<'a> Display for WallDisplay<'a> {
202 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
203 for (i, tile) in self.0.iter().enumerate() {
204 write!(f, "{} ", tile)?;
205 if i % 8 == 7 {
206 writeln!(f)?;
207 }
208 }
209 writeln!(f)
210 }
211}
212
213pub struct PartialWallDisplay<'a>(&'a PartialWall);
214pub trait PartialWallDisplayMethod {
215 fn display(&self) -> PartialWallDisplay;
216}
217impl PartialWallDisplayMethod for PartialWall {
218 fn display(&self) -> PartialWallDisplay { PartialWallDisplay(self) }
219}
220impl<'a> Display for PartialWallDisplay<'a> {
221 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
222 for (i, maybe_tile) in self.0.iter().enumerate() {
223 if let Some(tile) = maybe_tile {
224 write!(f, "{} ", tile)?;
225 } else {
226 write!(f, "?? ")?;
227 }
228 if i % 8 == 7 {
229 writeln!(f)?;
230 }
231 }
232 Ok(())
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 extern crate std;
239
240 use super::*;
241 use crate::tile::tiles_from_str;
242
243 #[test]
244 fn sorted_wall_is_correct() {
245 let ans = concat!(
246 "111122223333444405556666777788889999m",
247 "111122223333444400556666777788889999p",
248 "111122223333444455556666777788889999s",
249 "1111222233334444555566667777z");
250 let wall = make_sorted_wall([1, 2, 0]);
251 itertools::assert_equal(wall, tiles_from_str(ans));
252 assert!(is_valid_wall(wall));
253 }
254
255 #[test]
256 fn sorted_wall_deals_correctly() {
257 let wall = make_sorted_wall([1, 1, 1]);
258 assert_eq!(deal(&wall, P1), [
259 TileSet37::new([
260 0, 0, 0, 4, 0, 0, 0, 4, 0,
261 0, 0, 4, 1, 0, 0, 0, 0, 0,
262 0, 0, 0, 0, 0, 0, 0, 0, 0,
263 0, 0, 0, 0, 0, 0, 0,
264 0, 0, 0,
265 ]), // N: 4444m 8888m 3333p 4p
266 TileSet37::new([
267 4, 0, 0, 0, 3, 0, 0, 0, 4,
268 0, 0, 0, 1, 0, 0, 0, 0, 0,
269 0, 0, 0, 0, 0, 0, 0, 0, 0,
270 0, 0, 0, 0, 0, 0, 0,
271 1, 0, 0,
272 ]), // E: 1111m 0555m 9999m 4p
273 TileSet37::new([
274 0, 4, 0, 0, 0, 4, 0, 0, 0,
275 4, 0, 0, 1, 0, 0, 0, 0, 0,
276 0, 0, 0, 0, 0, 0, 0, 0, 0,
277 0, 0, 0, 0, 0, 0, 0,
278 0, 0, 0,
279 ]), // S: 2222m 6666m 1111p 4p
280 TileSet37::new([
281 0, 0, 4, 0, 0, 0, 4, 0, 0,
282 0, 4, 0, 1, 0, 0, 0, 0, 0,
283 0, 0, 0, 0, 0, 0, 0, 0, 0,
284 0, 0, 0, 0, 0, 0, 0,
285 0, 0, 0,
286 ]), // W: 3333m 7777m 2222p 4p
287 ]);
288 }
289}