use rand::prelude::*;
use rand::rngs::StdRng;
use sha2::{Digest, Sha256};
use crate::types::{TILES_4P, is_sanma_excluded_tile};
#[derive(Debug, Clone)]
pub struct WallState3P {
pub tiles: Vec<u8>,
pub dora_indicators: Vec<u8>,
pub dora_indicator_tiles: [u8; 5],
pub ura_indicator_tiles: [u8; 5],
pub rinshan_draw_count: u8,
pub pending_kan_dora_count: u8,
pub drawable_count: u8,
pub wall_digest: String,
pub salt: String,
pub seed: Option<u64>,
pub hand_index: u64,
}
impl WallState3P {
pub fn new(seed: Option<u64>) -> Self {
Self {
tiles: Vec::new(),
dora_indicators: Vec::new(),
dora_indicator_tiles: [0; 5],
ura_indicator_tiles: [0; 5],
rinshan_draw_count: 0,
pending_kan_dora_count: 0,
drawable_count: 0,
wall_digest: String::new(),
salt: String::new(),
seed,
hand_index: 0,
}
}
pub fn shuffle(&mut self) {
let mut w: Vec<u8> = (0..TILES_4P as u8)
.filter(|&t| !is_sanma_excluded_tile(t))
.collect();
let mut rng = if let Some(episode_seed) = self.seed {
let hand_seed = splitmix64(episode_seed.wrapping_add(self.hand_index));
self.hand_index = self.hand_index.wrapping_add(1);
StdRng::seed_from_u64(hand_seed)
} else {
self.hand_index = self.hand_index.wrapping_add(1);
StdRng::from_rng(&mut rand::rng())
};
w.shuffle(&mut rng);
self.salt = format!("{:016x}", rng.next_u64());
let mut hasher = Sha256::new();
hasher.update(self.salt.as_bytes());
for &t in &w {
hasher.update([t]);
}
self.wall_digest = format!("{:x}", hasher.finalize());
w.reverse();
self.tiles = w;
for i in 0..5 {
self.dora_indicator_tiles[i] = self.tiles[8 + 2 * i];
self.ura_indicator_tiles[i] = self.tiles[9 + 2 * i];
}
self.dora_indicators.clear();
self.dora_indicators.push(self.dora_indicator_tiles[0]);
self.rinshan_draw_count = 0;
self.pending_kan_dora_count = 0;
self.drawable_count = 0;
}
pub fn draw_rinshan_tile(&mut self) -> Option<u8> {
if self.tiles.is_empty() {
return None;
}
let tile = self.tiles.remove(0);
self.drawable_count = self.drawable_count.saturating_sub(1);
Some(tile)
}
pub fn load_wall(&mut self, tiles: Vec<u8>) {
let mut t = tiles;
if t.len() == 108 {
self.dora_indicator_tiles = [t[99], t[97], t[95], t[93], t[91]];
self.ura_indicator_tiles = [t[98], t[96], t[94], t[92], t[90]];
}
t.reverse();
self.tiles = t;
self.dora_indicators.clear();
self.dora_indicators.push(self.dora_indicator_tiles[0]);
self.rinshan_draw_count = 0;
self.pending_kan_dora_count = 0;
self.drawable_count = 0;
}
}
fn splitmix64(x: u64) -> u64 {
let mut z = x.wrapping_add(0x9E3779B97F4A7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^ (z >> 31)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rinshan_draw_does_not_consume_dora_indicators() {
for seed in 0..1000 {
let mut wall = WallState3P::new(Some(seed));
wall.shuffle();
let dora_tiles: Vec<u8> = wall.dora_indicator_tiles.to_vec();
let ura_tiles: Vec<u8> = wall.ura_indicator_tiles.to_vec();
let indicator_set: std::collections::HashSet<u8> =
dora_tiles.iter().chain(ura_tiles.iter()).copied().collect();
for _ in 0..40 {
wall.tiles.pop();
}
wall.drawable_count = (wall.tiles.len() as u8).saturating_sub(14);
for draw_num in 0..8 {
if wall.drawable_count == 0 {
break;
}
let t = wall
.draw_rinshan_tile()
.expect("draw_rinshan_tile should succeed");
wall.rinshan_draw_count += 1;
assert!(
!indicator_set.contains(&t),
"seed={seed}, draw #{draw_num}: rinshan drew tile {t} \
which is a dora/ura indicator! dora={dora_tiles:?}, ura={ura_tiles:?}"
);
}
}
}
#[test]
fn test_dead_wall_layout_has_8_rinshan_slots() {
let mut wall = WallState3P::new(Some(42));
wall.shuffle();
assert_eq!(wall.tiles.len(), 108);
for i in 0..5 {
assert_eq!(wall.dora_indicator_tiles[i], wall.tiles[8 + 2 * i]);
assert_eq!(wall.ura_indicator_tiles[i], wall.tiles[9 + 2 * i]);
}
}
#[test]
fn test_old_layout_remove0_would_consume_dora_indicators() {
let mut found_collision = false;
for seed in 0..100 {
let mut wall = WallState3P::new(Some(seed));
wall.shuffle();
let mut old_indicators = std::collections::HashSet::new();
for i in 0..5 {
old_indicators.insert(wall.tiles[4 + 2 * i]);
old_indicators.insert(wall.tiles[5 + 2 * i]);
}
for _ in 0..40 {
wall.tiles.pop();
}
for draw_num in 0..8 {
if wall.tiles.is_empty() {
break;
}
let t = wall.tiles.remove(0);
if draw_num >= 4 && old_indicators.contains(&t) {
found_collision = true;
break;
}
}
if found_collision {
break;
}
}
assert!(
found_collision,
"Expected the old remove(0) approach with 4 rinshan slots to \
consume dora indicators on the 5th+ draw"
);
}
}