use serde::{Deserialize, Serialize};
use crate::context::GameContext;
use crate::hand::{HandStructure, Meld};
use crate::tile::{Honor, Tile};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum WaitType {
Ryanmen,
Kanchan,
Penchan,
Shanpon,
Tanki,
Kokushi13,
}
impl WaitType {
pub fn fu(&self) -> u8 {
match self {
WaitType::Ryanmen => 0,
WaitType::Shanpon => 0,
WaitType::Kanchan => 2,
WaitType::Penchan => 2,
WaitType::Tanki => 2,
WaitType::Kokushi13 => 0, }
}
pub fn is_good_wait(&self) -> bool {
matches!(
self,
WaitType::Ryanmen | WaitType::Shanpon | WaitType::Kokushi13
)
}
}
pub fn detect_wait_types(structure: &HandStructure, winning_tile: Tile) -> Vec<WaitType> {
match structure {
HandStructure::Chiitoitsu { pairs } => {
if pairs.contains(&winning_tile) {
vec![WaitType::Tanki]
} else {
vec![]
}
}
HandStructure::Kokushi { pair } => {
if *pair == winning_tile {
vec![WaitType::Tanki]
} else {
vec![WaitType::Kokushi13]
}
}
HandStructure::Standard { melds, pair } => {
let mut wait_types = Vec::new();
if *pair == winning_tile {
wait_types.push(WaitType::Tanki);
}
for meld in melds {
match meld {
Meld::Koutsu(t, is_open) if *t == winning_tile && !is_open => {
wait_types.push(WaitType::Shanpon);
}
Meld::Shuntsu(start_tile, is_open) if !is_open => {
if let Some(wt) = check_shuntsu_wait(*start_tile, winning_tile) {
wait_types.push(wt);
}
}
_ => {}
}
}
wait_types
}
}
}
fn check_shuntsu_wait(start_tile: Tile, winning_tile: Tile) -> Option<WaitType> {
let (suit, start_val) = match start_tile {
Tile::Suited { suit, value } => (suit, value),
Tile::Honor(_) => return None, };
let (w_suit, w_val) = match winning_tile {
Tile::Suited { suit, value } => (suit, value),
Tile::Honor(_) => return None, };
if suit != w_suit {
return None;
}
if w_val < start_val || w_val > start_val + 2 {
return None;
}
Some(wait_type_for_shuntsu_position(start_val, w_val))
}
fn wait_type_for_shuntsu_position(start_val: u8, winning_val: u8) -> WaitType {
if winning_val == start_val {
if start_val + 2 == 9 {
WaitType::Penchan
} else {
WaitType::Ryanmen
}
} else if winning_val == start_val + 1 {
WaitType::Kanchan
} else {
if start_val == 1 {
WaitType::Penchan
} else {
WaitType::Ryanmen
}
}
}
pub fn is_pinfu(structure: &HandStructure, winning_tile: Tile, context: &GameContext) -> bool {
if context.is_open {
return false;
}
match structure {
HandStructure::Chiitoitsu { .. } => false,
HandStructure::Kokushi { .. } => false,
HandStructure::Standard { melds, pair } => {
let all_sequences = melds.iter().all(|m| m.is_sequence());
if !all_sequences {
return false;
}
if is_yakuhai_pair(*pair, context) {
return false;
}
let wait_types = detect_wait_types(structure, winning_tile);
wait_types.contains(&WaitType::Ryanmen)
}
}
}
fn is_yakuhai_pair(pair: Tile, context: &GameContext) -> bool {
match pair {
Tile::Honor(honor) => {
match honor {
Honor::White | Honor::Green | Honor::Red => true,
wind => wind == context.round_wind || wind == context.seat_wind,
}
}
Tile::Suited { .. } => false,
}
}
pub fn best_wait_type(structure: &HandStructure, winning_tile: Tile) -> Option<WaitType> {
let wait_types = detect_wait_types(structure, winning_tile);
wait_types.into_iter().min_by_key(|wt| {
let priority = match wt {
WaitType::Ryanmen => 0,
WaitType::Shanpon => 1,
WaitType::Kanchan => 2,
WaitType::Penchan => 3,
WaitType::Tanki => 4,
WaitType::Kokushi13 => 5,
};
(wt.fu(), priority)
})
}
pub fn best_wait_type_for_scoring(
structure: &HandStructure,
winning_tile: Tile,
) -> Option<WaitType> {
let wait_types = detect_wait_types(structure, winning_tile);
wait_types.into_iter().max_by_key(|wt| {
let priority = match wt {
WaitType::Tanki => 5, WaitType::Kanchan => 4, WaitType::Penchan => 3, WaitType::Shanpon => 1, WaitType::Ryanmen => 0, WaitType::Kokushi13 => 2, };
(wt.fu(), priority)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::WinType;
use crate::hand::decompose_hand;
use crate::parse::{parse_hand, to_counts};
use crate::tile::Suit;
#[test]
fn test_ryanmen_middle_sequence() {
let wt = wait_type_for_shuntsu_position(2, 2);
assert_eq!(wt, WaitType::Ryanmen);
let wt = wait_type_for_shuntsu_position(2, 4);
assert_eq!(wt, WaitType::Ryanmen);
}
#[test]
fn test_kanchan() {
let wt = wait_type_for_shuntsu_position(2, 3);
assert_eq!(wt, WaitType::Kanchan);
let wt = wait_type_for_shuntsu_position(5, 6);
assert_eq!(wt, WaitType::Kanchan);
}
#[test]
fn test_penchan_low() {
let wt = wait_type_for_shuntsu_position(1, 3);
assert_eq!(wt, WaitType::Penchan);
}
#[test]
fn test_penchan_high() {
let wt = wait_type_for_shuntsu_position(7, 7);
assert_eq!(wt, WaitType::Penchan);
}
#[test]
fn test_ryanmen_at_edges_not_penchan() {
let wt = wait_type_for_shuntsu_position(1, 1);
assert_eq!(wt, WaitType::Ryanmen);
let wt = wait_type_for_shuntsu_position(7, 9);
assert_eq!(wt, WaitType::Ryanmen);
}
#[test]
fn test_detect_tanki_wait() {
let tiles = parse_hand("123m456p789s11177z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let winning_tile = Tile::honor(Honor::Red);
let wait_types = detect_wait_types(&structures[0], winning_tile);
assert!(wait_types.contains(&WaitType::Tanki));
}
#[test]
fn test_detect_shanpon_wait() {
let tiles = parse_hand("123m456p789s11122z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let winning_tile = Tile::honor(Honor::East);
let wait_types = detect_wait_types(&structures[0], winning_tile);
assert!(wait_types.contains(&WaitType::Shanpon));
}
#[test]
fn test_detect_ryanmen_wait() {
let tiles = parse_hand("234m456p789s11122z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let winning_tile = Tile::suited(Suit::Man, 4);
let wait_types = detect_wait_types(&structures[0], winning_tile);
assert!(wait_types.contains(&WaitType::Ryanmen));
}
#[test]
fn test_detect_kanchan_wait() {
let tiles = parse_hand("234m456p789s11122z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let winning_tile = Tile::suited(Suit::Man, 3);
let wait_types = detect_wait_types(&structures[0], winning_tile);
assert!(wait_types.contains(&WaitType::Kanchan));
}
#[test]
fn test_multiple_wait_types() {
let tiles = parse_hand("111123m456p789s22z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let winning_tile = Tile::suited(Suit::Man, 1);
let has_multiple_waits = structures.iter().any(|structure| {
let wait_types = detect_wait_types(structure, winning_tile);
wait_types.len() > 1
});
assert!(!structures.is_empty());
assert!(
has_multiple_waits,
"Should find structure with multiple wait types"
);
}
#[test]
fn test_chiitoitsu_always_tanki() {
let tiles = parse_hand("1122m3344p5566s77z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let chiitoi = structures
.iter()
.find(|s| matches!(s, HandStructure::Chiitoitsu { .. }))
.expect("Should have chiitoitsu structure");
let winning_tile = Tile::honor(Honor::Red); let wait_types = detect_wait_types(chiitoi, winning_tile);
assert_eq!(wait_types, vec![WaitType::Tanki]);
}
#[test]
fn test_pinfu_basic() {
let tiles = parse_hand("123456m789p234s55p").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South);
let winning_tile = Tile::suited(Suit::Sou, 4);
let has_pinfu = structures
.iter()
.any(|s| is_pinfu(s, winning_tile, &context));
assert!(has_pinfu, "Should qualify for pinfu");
}
#[test]
fn test_pinfu_fails_with_triplet() {
let tiles = parse_hand("123m456p789s11155z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South);
let winning_tile = Tile::honor(Honor::White);
let has_pinfu = structures
.iter()
.any(|s| is_pinfu(s, winning_tile, &context));
assert!(!has_pinfu, "Triplet hand can't be pinfu");
}
#[test]
fn test_pinfu_fails_with_yakuhai_pair() {
let tiles = parse_hand("123m456m789p234s55z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South);
let winning_tile = Tile::suited(Suit::Sou, 4);
let has_pinfu = structures
.iter()
.any(|s| is_pinfu(s, winning_tile, &context));
assert!(!has_pinfu, "Yakuhai pair (dragon) means no pinfu");
}
#[test]
fn test_pinfu_fails_with_value_wind_pair() {
let tiles = parse_hand("123m456m789p234s22z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South);
let winning_tile = Tile::suited(Suit::Sou, 4);
let has_pinfu = structures
.iter()
.any(|s| is_pinfu(s, winning_tile, &context));
assert!(!has_pinfu, "Value wind pair means no pinfu");
}
#[test]
fn test_pinfu_ok_with_non_value_wind_pair() {
let tiles = parse_hand("123m456m789p234s33z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South);
let winning_tile = Tile::suited(Suit::Sou, 4);
let has_pinfu = structures
.iter()
.any(|s| is_pinfu(s, winning_tile, &context));
assert!(has_pinfu, "Non-value wind pair allows pinfu");
}
#[test]
fn test_pinfu_fails_with_kanchan_wait() {
let tiles = parse_hand("123m456m789p234s55p").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South);
let winning_tile = Tile::suited(Suit::Sou, 3);
let has_pinfu = structures
.iter()
.any(|s| is_pinfu(s, winning_tile, &context));
assert!(!has_pinfu, "Kanchan wait means no pinfu");
}
#[test]
fn test_pinfu_fails_when_open() {
let tiles = parse_hand("123m456m789p234s55p").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South).open(); let winning_tile = Tile::suited(Suit::Sou, 4);
let has_pinfu = structures
.iter()
.any(|s| is_pinfu(s, winning_tile, &context));
assert!(!has_pinfu, "Open hand can't be pinfu");
}
#[test]
fn test_best_wait_prefers_ryanmen() {
let tiles = parse_hand("111123m456p789s22z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let winning_tile = Tile::suited(Suit::Man, 1);
for structure in &structures {
if let Some(best) = best_wait_type(structure, winning_tile) {
let all_waits = detect_wait_types(structure, winning_tile);
if all_waits.contains(&WaitType::Ryanmen) {
assert_eq!(best, WaitType::Ryanmen);
}
}
}
}
}