use serde::{Deserialize, Serialize};
use crate::hand::KanType;
use crate::parse::TileCounts;
use crate::tile::{Honor, KOKUSHI_TILES, Suit, Tile};
use std::cmp::{max, min};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShantenResult {
pub shanten: i8,
pub best_type: ShantenType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ShantenType {
Standard,
Chiitoitsu,
Kokushi,
}
pub fn calculate_shanten(counts: &TileCounts) -> ShantenResult {
calculate_shanten_with_melds(counts, 0)
}
pub fn calculate_shanten_with_melds(counts: &TileCounts, called_melds: u8) -> ShantenResult {
let standard = calculate_standard_shanten_with_melds(counts, called_melds);
if called_melds > 0 {
return ShantenResult {
shanten: standard,
best_type: ShantenType::Standard,
};
}
let chiitoi = calculate_chiitoitsu_shanten(counts);
let kokushi = calculate_kokushi_shanten(counts);
if standard <= chiitoi && standard <= kokushi {
ShantenResult {
shanten: standard,
best_type: ShantenType::Standard,
}
} else if chiitoi <= kokushi {
ShantenResult {
shanten: chiitoi,
best_type: ShantenType::Chiitoitsu,
}
} else {
ShantenResult {
shanten: kokushi,
best_type: ShantenType::Kokushi,
}
}
}
pub fn calculate_standard_shanten(counts: &TileCounts) -> i8 {
calculate_standard_shanten_with_melds(counts, 0)
}
fn calculate_standard_shanten_with_melds(counts: &TileCounts, called_melds: u8) -> i8 {
let tiles = counts_to_array(counts);
let total_hand_tiles: u8 = tiles.iter().sum();
let min_tenpai_tiles: u8 = if called_melds >= 4 {
1
} else {
13u8.saturating_sub(3 * called_melds)
};
let tile_deficit = min_tenpai_tiles.saturating_sub(total_hand_tiles);
let mut best_shanten = 8i8;
let (melds, taatsu) = count_melds_and_taatsu(&tiles);
let shanten =
calculate_shanten_value_with_called(melds, taatsu, false, called_melds, tile_deficit);
best_shanten = min(best_shanten, shanten);
for i in 0..34 {
if tiles[i] >= 2 {
let mut tiles_copy = tiles;
tiles_copy[i] -= 2;
let (melds, taatsu) = count_melds_and_taatsu(&tiles_copy);
let shanten = calculate_shanten_value_with_called(
melds,
taatsu,
true,
called_melds,
tile_deficit,
);
best_shanten = min(best_shanten, shanten);
}
}
best_shanten
}
pub fn array_to_tilecounts(arr: &[u8; 34]) -> TileCounts {
let mut counts = TileCounts::new();
for (idx, &count) in arr.iter().enumerate() {
if count > 0 {
counts.insert(index_to_tile(idx), count);
}
}
counts
}
pub fn counts_to_array(counts: &TileCounts) -> [u8; 34] {
let mut arr = [0u8; 34];
for (&tile, &count) in counts {
let idx = tile_to_index(tile);
arr[idx] = count;
}
arr
}
pub fn tile_to_index(tile: Tile) -> usize {
match tile {
Tile::Suited { suit, value } => {
let base = match suit {
Suit::Man => 0,
Suit::Pin => 9,
Suit::Sou => 18,
};
base + (value as usize - 1)
}
Tile::Honor(honor) => {
27 + match honor {
Honor::East => 0,
Honor::South => 1,
Honor::West => 2,
Honor::North => 3,
Honor::White => 4,
Honor::Green => 5,
Honor::Red => 6,
}
}
}
}
pub fn index_to_tile(idx: usize) -> Tile {
if idx < 27 {
let suit = match idx / 9 {
0 => Suit::Man,
1 => Suit::Pin,
_ => Suit::Sou,
};
let value = (idx % 9) as u8 + 1;
Tile::suited(suit, value)
} else {
let honor = match idx - 27 {
0 => Honor::East,
1 => Honor::South,
2 => Honor::West,
3 => Honor::North,
4 => Honor::White,
5 => Honor::Green,
_ => Honor::Red,
};
Tile::honor(honor)
}
}
fn count_melds_and_taatsu(tiles: &[u8; 34]) -> (u8, u8) {
let mut tiles = *tiles;
let mut melds = 0u8;
let mut taatsu = 0u8;
for suit_start in [0, 9, 18] {
let (suit_melds, suit_taatsu) = count_suit_melds(&mut tiles, suit_start);
melds += suit_melds;
taatsu += suit_taatsu;
}
for tile_count in tiles.iter_mut().skip(27) {
if *tile_count >= 3 {
melds += 1;
*tile_count -= 3
}
if *tile_count >= 2 {
taatsu += 1;
*tile_count -= 2;
}
}
(melds, taatsu)
}
fn count_suit_melds(tiles: &mut [u8; 34], start: usize) -> (u8, u8) {
let mut melds = 0u8;
let mut taatsu = 0u8;
let (m1, remaining1) = extract_melds_sequences_first(tiles, start);
let (m2, remaining2) = extract_melds_triplets_first(tiles, start);
let (best_melds, mut remaining) = if m1 >= m2 {
(m1, remaining1)
} else {
(m2, remaining2)
};
melds += best_melds;
for count in remaining.iter_mut().skip(start).take(9) {
if *count >= 2 {
taatsu += 1;
*count -= 2;
}
}
for i in start..(start + 8) {
if remaining[i] >= 1 && remaining[i + 1] >= 1 {
taatsu += 1;
remaining[i] -= 1;
remaining[i + 1] -= 1;
}
}
for i in start..(start + 7) {
if remaining[i] >= 1 && remaining[i + 2] >= 1 {
taatsu += 1;
remaining[i] -= 1;
remaining[i + 2] -= 1;
}
}
tiles[start..(start + 9)].copy_from_slice(&remaining[start..(start + 9)]);
(melds, taatsu)
}
fn extract_melds_sequences_first(tiles: &[u8; 34], start: usize) -> (u8, [u8; 34]) {
let mut remaining = *tiles;
let mut melds = 0u8;
for i in start..(start + 7) {
while remaining[i] >= 1 && remaining[i + 1] >= 1 && remaining[i + 2] >= 1 {
melds += 1;
remaining[i] -= 1;
remaining[i + 1] -= 1;
remaining[i + 2] -= 1;
}
}
for count in remaining.iter_mut().skip(start).take(9) {
while *count >= 3 {
melds += 1;
*count -= 3;
}
}
(melds, remaining)
}
fn extract_melds_triplets_first(tiles: &[u8; 34], start: usize) -> (u8, [u8; 34]) {
let mut remaining = *tiles;
let mut melds = 0u8;
for count in remaining.iter_mut().skip(start).take(9) {
while *count >= 3 {
melds += 1;
*count -= 3;
}
}
for i in start..(start + 7) {
while remaining[i] >= 1 && remaining[i + 1] >= 1 && remaining[i + 2] >= 1 {
melds += 1;
remaining[i] -= 1;
remaining[i + 1] -= 1;
remaining[i + 2] -= 1;
}
}
(melds, remaining)
}
fn calculate_shanten_value_with_called(
melds: u8,
taatsu: u8,
has_pair: bool,
called_melds: u8,
tile_deficit: u8,
) -> i8 {
let total_melds = melds + called_melds;
if total_melds >= 4 && has_pair && tile_deficit == 0 {
return -1;
}
let max_useful_taatsu = 4u8.saturating_sub(total_melds);
let useful_taatsu = min(taatsu, max_useful_taatsu);
let mut shanten = 8i8 - (2 * total_melds.min(4) as i8) - (useful_taatsu as i8);
if has_pair {
shanten -= 1;
}
let total_blocks = total_melds.min(4) + useful_taatsu;
if total_blocks > 4 {
shanten += (total_blocks - 4) as i8;
}
if shanten >= 0 {
shanten = max(shanten, tile_deficit as i8);
}
shanten
}
pub fn calculate_chiitoitsu_shanten(counts: &TileCounts) -> i8 {
let mut pairs = 0i8;
let mut unique_tiles = 0i8;
for &count in counts.values() {
if count >= 1 {
unique_tiles += 1;
}
if count >= 2 {
pairs += 1;
}
}
6 - pairs + (7 - unique_tiles).max(0)
}
pub fn calculate_kokushi_shanten(counts: &TileCounts) -> i8 {
let mut unique_terminals = 0i8;
let mut has_pair = false;
for &tile in &KOKUSHI_TILES {
let count = counts.get(&tile).copied().unwrap_or(0);
if count >= 1 {
unique_terminals += 1;
}
if count >= 2 {
has_pair = true;
}
}
13 - unique_terminals - if has_pair { 1 } else { 0 }
}
pub fn calculate_ukeire(counts: &TileCounts) -> UkeireResult {
calculate_ukeire_inner(counts, 0, None)
}
pub fn calculate_ukeire_with_melds(counts: &TileCounts, called_melds: u8) -> UkeireResult {
calculate_ukeire_inner(counts, called_melds, None)
}
pub fn calculate_ukeire_with_visible(
counts: &TileCounts,
visible_counts: &TileCounts,
) -> UkeireResult {
calculate_ukeire_inner(counts, 0, Some(visible_counts))
}
pub fn calculate_ukeire_with_melds_and_visible(
counts: &TileCounts,
called_melds: u8,
visible_counts: &TileCounts,
) -> UkeireResult {
calculate_ukeire_inner(counts, called_melds, Some(visible_counts))
}
fn calculate_ukeire_inner(
counts: &TileCounts,
called_melds: u8,
visible_counts: Option<&TileCounts>,
) -> UkeireResult {
let current = calculate_shanten_with_melds(counts, called_melds);
let mut accepting_tiles = Vec::new();
let mut total_count = 0u8;
for idx in 0..34 {
let tile = index_to_tile(idx);
let hand_count = counts.get(&tile).copied().unwrap_or(0);
let visible_count = visible_counts
.and_then(|vc| vc.get(&tile).copied())
.unwrap_or(0);
if hand_count >= 4 {
continue;
}
let mut test_counts = counts.clone();
*test_counts.entry(tile).or_insert(0) += 1;
let new_shanten = calculate_shanten_with_melds(&test_counts, called_melds);
if new_shanten.shanten < current.shanten {
let available = 4u8.saturating_sub(hand_count + visible_count);
accepting_tiles.push(UkeireTile { tile, available });
total_count += available;
}
}
UkeireResult {
shanten: current.shanten,
tiles: accepting_tiles,
total_count,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UkeireResult {
pub shanten: i8,
pub tiles: Vec<UkeireTile>,
pub total_count: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UkeireTile {
pub tile: Tile,
pub available: u8,
}
pub fn valid_chi_combinations(hand: &TileCounts, discarded_tile: Tile) -> Vec<(Tile, Tile)> {
let arr = counts_to_array(hand);
let d = tile_to_index(discarded_tile);
if d > 26 {
return vec![];
}
let val = d % 9; let mut combos = Vec::new();
if val >= 2 && arr[d - 2] > 0 && arr[d - 1] > 0 {
combos.push((index_to_tile(d - 2), index_to_tile(d - 1)));
}
if (1..=7).contains(&val) && arr[d - 1] > 0 && arr[d + 1] > 0 {
combos.push((index_to_tile(d - 1), index_to_tile(d + 1)));
}
if val <= 6 && arr[d + 1] > 0 && arr[d + 2] > 0 {
combos.push((index_to_tile(d + 1), index_to_tile(d + 2)));
}
combos
}
pub fn shanten_after_chi(hand: &TileCounts, combo: (Tile, Tile), num_melds: u8) -> i8 {
let mut arr = counts_to_array(hand);
let idx0 = tile_to_index(combo.0);
let idx1 = tile_to_index(combo.1);
arr[idx0] = arr[idx0]
.checked_sub(1)
.expect("combo tile not in hand");
arr[idx1] = arr[idx1]
.checked_sub(1)
.expect("combo tile not in hand");
let new_melds = num_melds + 1;
let mut best = i8::MAX;
for i in 0..34 {
if arr[i] > 0 {
arr[i] -= 1;
let counts = array_to_tilecounts(&arr);
let s = calculate_shanten_with_melds(&counts, new_melds).shanten;
if s < best {
best = s;
}
arr[i] += 1;
}
}
best
}
pub fn shanten_after_kan(
hand: &TileCounts,
kan_tile: Tile,
kan_type: KanType,
num_melds: u8,
) -> i8 {
let mut arr = counts_to_array(hand);
let kt = tile_to_index(kan_tile);
let (remove, melds) = match kan_type {
KanType::Open => (3u8, num_melds + 1),
KanType::Closed => (4u8, num_melds + 1),
KanType::Added => (1u8, num_melds),
};
arr[kt] = arr[kt]
.checked_sub(remove)
.expect("not enough tiles in hand for kan");
let counts = array_to_tilecounts(&arr);
calculate_shanten_with_melds(&counts, melds).shanten
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::{parse_hand, to_counts};
fn shanten(hand: &str) -> i8 {
let tiles = parse_hand(hand).unwrap();
let counts = to_counts(&tiles);
calculate_shanten(&counts).shanten
}
fn shanten_type(hand: &str) -> ShantenType {
let tiles = parse_hand(hand).unwrap();
let counts = to_counts(&tiles);
calculate_shanten(&counts).best_type
}
#[test]
fn test_complete_standard_hand() {
assert_eq!(shanten("123m456p789s11122z"), -1);
}
#[test]
fn test_complete_chiitoitsu() {
assert_eq!(shanten("1122m3344p5566s77z"), -1);
}
#[test]
fn test_complete_kokushi() {
assert_eq!(shanten("19m19p19s12345677z"), -1);
}
#[test]
fn test_tenpai_standard() {
assert_eq!(shanten("123m456p789s1112z"), 0);
}
#[test]
fn test_tenpai_chiitoitsu() {
assert_eq!(shanten("1122m3344p5566s7z"), 0);
}
#[test]
fn test_tenpai_kokushi() {
assert_eq!(shanten("19m19p19s1234567z"), 0);
}
#[test]
fn test_iishanten_standard() {
assert_eq!(shanten("123m456p789s112z"), 1);
}
#[test]
fn test_iishanten_chiitoitsu() {
assert_eq!(shanten("1122m3344p5566s"), 1);
}
#[test]
fn test_high_shanten() {
assert!(shanten("1379m1379p1379s1z") >= 4);
}
#[test]
fn test_multi_shanten() {
let s = shanten("123m147p258s12345z");
assert!(
(3..=7).contains(&s),
"Expected shanten between 3 and 7, got {}",
s
);
let s2 = shanten("123m456p789s11234z");
assert!(
s2 <= 3,
"Expected shanten <= 3 for connected hand, got {}",
s2
);
}
#[test]
fn test_best_type_standard() {
assert_eq!(shanten_type("123m456p789s1112z"), ShantenType::Standard);
}
#[test]
fn test_best_type_chiitoitsu() {
assert_eq!(shanten_type("1122m3344p5566s7z"), ShantenType::Chiitoitsu);
}
#[test]
fn test_best_type_kokushi() {
assert_eq!(shanten_type("19m19p19s1234567z"), ShantenType::Kokushi);
}
#[test]
fn test_ukeire_tenpai() {
let tiles = parse_hand("123m456p789s1112z").unwrap();
let counts = to_counts(&tiles);
let ukeire = calculate_ukeire(&counts);
assert_eq!(ukeire.shanten, 0);
assert!(!ukeire.tiles.is_empty());
}
#[test]
fn test_ukeire_iishanten() {
let tiles = parse_hand("123m456p789s112z").unwrap();
let counts = to_counts(&tiles);
let ukeire = calculate_ukeire(&counts);
assert_eq!(ukeire.shanten, 1);
assert!(ukeire.total_count > 0);
}
#[test]
fn test_ukeire_complete_hand() {
let tiles = parse_hand("123m456p789s11122z").unwrap();
let counts = to_counts(&tiles);
let ukeire = calculate_ukeire(&counts);
assert_eq!(ukeire.shanten, -1);
assert!(ukeire.tiles.is_empty());
}
#[test]
fn test_ukeire_with_called_pon_tenpai() {
use crate::parse::parse_hand_with_aka;
let parsed = parse_hand_with_aka("23678p234567s(222z)").unwrap();
let counts = to_counts(&parsed.tiles);
let called_melds = parsed.called_melds.len() as u8;
let ukeire = calculate_ukeire_with_melds(&counts, called_melds);
assert_eq!(ukeire.shanten, 0, "Hand should be tenpai");
assert!(
ukeire.tiles.len() <= 5,
"Tenpai hand should have at most a few waits, got {}",
ukeire.tiles.len()
);
assert!(
ukeire.total_count <= 20,
"Total accepting tiles should be reasonable, got {}",
ukeire.total_count
);
}
#[test]
fn test_ukeire_with_two_called_melds_iishanten() {
use crate::parse::parse_hand_with_aka;
let parsed = parse_hand_with_aka("234568m(789p)(whwhwh)").unwrap();
let counts = to_counts(&parsed.tiles);
let called_melds = parsed.called_melds.len() as u8;
let ukeire = calculate_ukeire_with_melds(&counts, called_melds);
assert_eq!(ukeire.shanten, 1, "Hand should be iishanten");
assert!(
ukeire.tiles.len() <= 10,
"Iishanten hand with 2 called melds should have limited waits, got {}",
ukeire.tiles.len()
);
assert!(
ukeire.total_count <= 40,
"Total accepting tiles should be reasonable, got {}",
ukeire.total_count
);
}
#[test]
fn test_ukeire_without_melds_matches_original() {
let tiles = parse_hand("123m456p789s1112z").unwrap();
let counts = to_counts(&tiles);
let ukeire_original = calculate_ukeire(&counts);
let ukeire_with_melds = calculate_ukeire_with_melds(&counts, 0);
assert_eq!(ukeire_original.shanten, ukeire_with_melds.shanten);
assert_eq!(ukeire_original.tiles.len(), ukeire_with_melds.tiles.len());
assert_eq!(ukeire_original.total_count, ukeire_with_melds.total_count);
}
#[test]
fn test_ukeire_called_melds_vs_no_melds_differ() {
use crate::parse::parse_hand_with_aka;
let parsed = parse_hand_with_aka("23678p234567s(222z)").unwrap();
let counts = to_counts(&parsed.tiles);
let called_melds = parsed.called_melds.len() as u8;
let ukeire_correct = calculate_ukeire_with_melds(&counts, called_melds);
let ukeire_wrong = calculate_ukeire_with_melds(&counts, 0);
assert!(
ukeire_wrong.tiles.len() > ukeire_correct.tiles.len(),
"Ignoring called melds should produce more (incorrect) accepting tiles: wrong={}, correct={}",
ukeire_wrong.tiles.len(),
ukeire_correct.tiles.len()
);
}
#[test]
fn test_ukeire_with_visible_reduces_count() {
let tiles = parse_hand("123m456p789s1112z").unwrap();
let counts = to_counts(&tiles);
let theoretical = calculate_ukeire(&counts);
let mut visible = TileCounts::new();
visible.insert(Tile::honor(Honor::South), 2);
let practical = calculate_ukeire_with_visible(&counts, &visible);
assert_eq!(theoretical.shanten, practical.shanten);
let theo_2z = theoretical
.tiles
.iter()
.find(|t| t.tile == Tile::honor(Honor::South));
let prac_2z = practical
.tiles
.iter()
.find(|t| t.tile == Tile::honor(Honor::South));
assert!(theo_2z.is_some(), "2z should be a theoretical wait");
assert!(prac_2z.is_some(), "2z should still be a practical wait");
assert_eq!(theo_2z.unwrap().available, 3);
assert_eq!(prac_2z.unwrap().available, 1);
assert!(
practical.total_count < theoretical.total_count,
"Practical total ({}) should be less than theoretical ({})",
practical.total_count,
theoretical.total_count
);
}
#[test]
fn test_ukeire_with_visible_shows_zero_available_when_all_copies_seen() {
let tiles = parse_hand("123m456p789s1112z").unwrap();
let counts = to_counts(&tiles);
let mut visible = TileCounts::new();
visible.insert(Tile::honor(Honor::South), 3);
let practical = calculate_ukeire_with_visible(&counts, &visible);
let prac_2z = practical
.tiles
.iter()
.find(|t| t.tile == Tile::honor(Honor::South));
assert!(
prac_2z.is_some(),
"2z should still appear as a wait even with 0 available"
);
assert_eq!(prac_2z.unwrap().available, 0);
}
#[test]
fn test_ukeire_with_visible_no_visible_matches_theoretical() {
let tiles = parse_hand("123m456p789s1112z").unwrap();
let counts = to_counts(&tiles);
let theoretical = calculate_ukeire(&counts);
let practical = calculate_ukeire_with_visible(&counts, &TileCounts::new());
assert_eq!(theoretical.shanten, practical.shanten);
assert_eq!(theoretical.tiles.len(), practical.tiles.len());
assert_eq!(theoretical.total_count, practical.total_count);
}
#[test]
fn test_ukeire_with_melds_and_visible() {
use crate::parse::parse_hand_with_aka;
let parsed = parse_hand_with_aka("23678p234567s(222z)").unwrap();
let counts = to_counts(&parsed.tiles);
let called_melds = parsed.called_melds.len() as u8;
let theoretical = calculate_ukeire_with_melds(&counts, called_melds);
let mut visible = TileCounts::new();
visible.insert(Tile::suited(Suit::Pin, 1), 2);
let practical = calculate_ukeire_with_melds_and_visible(&counts, called_melds, &visible);
assert_eq!(theoretical.shanten, practical.shanten);
assert!(
practical.total_count <= theoretical.total_count,
"Practical total ({}) should be <= theoretical ({})",
practical.total_count,
theoretical.total_count
);
}
#[test]
fn test_tile_index_roundtrip() {
for idx in 0..34 {
let tile = index_to_tile(idx);
let back = tile_to_index(tile);
assert_eq!(
idx, back,
"Tile {:?} at index {} converted back to {}",
tile, idx, back
);
}
}
#[test]
fn test_specific_tile_indices() {
assert_eq!(tile_to_index(Tile::suited(Suit::Man, 1)), 0);
assert_eq!(tile_to_index(Tile::suited(Suit::Man, 9)), 8);
assert_eq!(tile_to_index(Tile::suited(Suit::Pin, 1)), 9);
assert_eq!(tile_to_index(Tile::suited(Suit::Sou, 1)), 18);
assert_eq!(tile_to_index(Tile::honor(Honor::East)), 27);
assert_eq!(tile_to_index(Tile::honor(Honor::Red)), 33);
}
#[test]
fn test_sequences_first_then_triplet_extraction() {
assert_eq!(
shanten("233344455666m1p"),
0,
"Hand 233344455666m1p should be tenpai (shanten=0), not iishanten"
);
}
#[test]
fn test_extract_melds_sequences_first_with_remaining_triplet() {
let mut tiles = [0u8; 34];
tiles[1] = 1; tiles[2] = 3; tiles[3] = 3; tiles[4] = 2; tiles[5] = 3;
let (melds, remaining) = extract_melds_sequences_first(&tiles, 0);
assert_eq!(
melds, 4,
"Should extract 4 melds (3 sequences + 1 triplet), got {}",
melds
);
assert_eq!(
remaining[5], 0,
"All 6m tiles should be extracted as triplet, but {} remain",
remaining[5]
);
}
fn counts_from_arr(arr: [u8; 34]) -> TileCounts {
array_to_tilecounts(&arr)
}
#[test]
fn test_chi_no_combos() {
let mut arr = [0u8; 34];
arr[5] = 1; let combos = valid_chi_combinations(&counts_from_arr(arr), index_to_tile(0));
assert!(combos.is_empty());
}
#[test]
fn test_chi_low_end() {
let mut arr = [0u8; 34];
arr[1] = 1; arr[2] = 1; let combos = valid_chi_combinations(&counts_from_arr(arr), index_to_tile(0));
assert_eq!(combos, vec![(index_to_tile(1), index_to_tile(2))]);
}
#[test]
fn test_chi_high_end() {
let mut arr = [0u8; 34];
arr[6] = 1; arr[7] = 1; let combos = valid_chi_combinations(&counts_from_arr(arr), index_to_tile(8));
assert_eq!(combos, vec![(index_to_tile(6), index_to_tile(7))]);
}
#[test]
fn test_chi_two_combos() {
let mut arr = [0u8; 34];
arr[0] = 1; arr[1] = 1; arr[3] = 1; let combos = valid_chi_combinations(&counts_from_arr(arr), index_to_tile(2));
assert_eq!(
combos,
vec![
(index_to_tile(0), index_to_tile(1)),
(index_to_tile(1), index_to_tile(3)),
]
);
}
#[test]
fn test_chi_three_combos() {
let mut arr = [0u8; 34];
arr[2] = 1; arr[3] = 1; arr[5] = 1; arr[6] = 1; let combos = valid_chi_combinations(&counts_from_arr(arr), index_to_tile(4));
assert_eq!(
combos,
vec![
(index_to_tile(2), index_to_tile(3)),
(index_to_tile(3), index_to_tile(5)),
(index_to_tile(5), index_to_tile(6)),
]
);
}
#[test]
fn test_chi_honor_tile() {
let mut arr = [0u8; 34];
arr[27] = 3; let combos = valid_chi_combinations(&counts_from_arr(arr), index_to_tile(27));
assert!(combos.is_empty());
}
#[test]
fn test_shanten_after_chi_good() {
let mut arr = [0u8; 34];
arr[..9].fill(1); arr[9] = 1; arr[18] = 1; arr[19] = 1; arr[20] = 1; let hand = counts_from_arr(arr);
let before = calculate_shanten(&hand).shanten;
assert_eq!(before, 0);
let after = shanten_after_chi(&hand, (index_to_tile(19), index_to_tile(20)), 0);
assert!(after <= before, "Expected shanten <= {before} after good chi, got {after}");
}
#[test]
fn test_shanten_after_chi_bad() {
let mut arr = [0u8; 34];
for &i in &[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24] {
arr[i] = 1;
}
let hand = counts_from_arr(arr);
let before = calculate_shanten(&hand).shanten;
let after = shanten_after_chi(&hand, (index_to_tile(0), index_to_tile(2)), 0);
assert!(
after >= before - 1,
"Unexpectedly good shanten after bad chi: {before} -> {after}"
);
}
#[test]
fn test_shanten_after_kan_open() {
let mut arr = [0u8; 34];
arr[..9].fill(1); arr[27] = 3; arr[9] = 1; let hand = counts_from_arr(arr);
let before = calculate_shanten(&hand).shanten;
assert_eq!(before, 0);
let after = shanten_after_kan(&hand, index_to_tile(27), KanType::Open, 0);
assert_eq!(after, 0, "Expected 0 after open kan, got {after}");
}
#[test]
fn test_shanten_after_kan_closed() {
let mut arr = [0u8; 34];
arr[..6].fill(1); arr[27] = 4; arr[9] = 1; let hand = counts_from_arr(arr);
let after = shanten_after_kan(&hand, index_to_tile(27), KanType::Closed, 0);
assert!(after >= 0, "Shanten should be non-negative after closed kan: {after}");
}
#[test]
fn test_shanten_after_kan_added() {
let mut arr = [0u8; 34];
arr[..9].fill(1); arr[27] = 1; let hand = counts_from_arr(arr);
let before = calculate_shanten_with_melds(&hand, 1).shanten;
let after = shanten_after_kan(&hand, index_to_tile(27), KanType::Added, 1);
assert!((-1..=2).contains(&after),
"Unexpected shanten after added kan: {before} -> {after}");
}
}