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}