use serde::{Deserialize, Serialize};
use crate::context::{GameContext, WinType, count_dora_detailed};
use crate::hand::{HandStructure, Meld};
use crate::parse::TileCounts;
use crate::tile::{Honor, Suit, Tile};
use crate::wait::is_pinfu;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Yaku {
Riichi, Ippatsu, MenzenTsumo, Tanyao, Pinfu, Iipeikou, Yakuhai(Honor), RinshanKaihou, Chankan, HaiteiRaoyue, HouteiRaoyui,
DoubleRiichi, Toitoi, SanshokuDoujun, SanshokuDoukou, Ittsu, Chiitoitsu, Chanta, SanAnkou, SanKantsu, Honroutou, Shousangen,
Honitsu, Junchan, Ryanpeikou,
Chinitsu,
Tenhou, Chiihou, KokushiMusou, Suuankou, Daisangen, Shousuushii, Daisuushii, Tsuuiisou, Chinroutou, Ryuuiisou, ChuurenPoutou, SuuKantsu,
Kokushi13Wait, SuuankouTanki, JunseiChuurenPoutou, }
impl Yaku {
pub fn han(&self) -> u8 {
match self {
Yaku::Riichi => 1,
Yaku::Ippatsu => 1,
Yaku::MenzenTsumo => 1,
Yaku::Tanyao => 1,
Yaku::Pinfu => 1,
Yaku::Iipeikou => 1,
Yaku::Yakuhai(_) => 1,
Yaku::RinshanKaihou => 1,
Yaku::Chankan => 1,
Yaku::HaiteiRaoyue => 1,
Yaku::HouteiRaoyui => 1,
Yaku::DoubleRiichi => 2,
Yaku::Toitoi => 2,
Yaku::SanshokuDoujun => 2,
Yaku::SanshokuDoukou => 2,
Yaku::Ittsu => 2,
Yaku::Chiitoitsu => 2,
Yaku::Chanta => 2,
Yaku::SanAnkou => 2,
Yaku::SanKantsu => 2,
Yaku::Honroutou => 2,
Yaku::Shousangen => 2,
Yaku::Honitsu => 3,
Yaku::Junchan => 3,
Yaku::Ryanpeikou => 3,
Yaku::Chinitsu => 6,
Yaku::Tenhou => 13,
Yaku::Chiihou => 13,
Yaku::KokushiMusou => 13,
Yaku::Suuankou => 13,
Yaku::Daisangen => 13,
Yaku::Shousuushii => 13,
Yaku::Daisuushii => 13,
Yaku::Tsuuiisou => 13,
Yaku::Chinroutou => 13,
Yaku::Ryuuiisou => 13,
Yaku::ChuurenPoutou => 13,
Yaku::SuuKantsu => 13,
Yaku::Kokushi13Wait => 26,
Yaku::SuuankouTanki => 26,
Yaku::JunseiChuurenPoutou => 26,
}
}
pub fn han_open(&self) -> Option<u8> {
match self {
Yaku::Riichi => None,
Yaku::DoubleRiichi => None,
Yaku::Ippatsu => None,
Yaku::MenzenTsumo => None,
Yaku::Pinfu => None,
Yaku::Iipeikou => None,
Yaku::Ryanpeikou => None,
Yaku::Tenhou => None,
Yaku::Chiihou => None,
Yaku::Suuankou => None,
Yaku::SuuankouTanki => None,
Yaku::ChuurenPoutou => None,
Yaku::JunseiChuurenPoutou => None,
Yaku::KokushiMusou => None,
Yaku::Kokushi13Wait => None,
Yaku::SanshokuDoujun => Some(1),
Yaku::Ittsu => Some(1),
Yaku::Chanta => Some(1),
Yaku::Honitsu => Some(2),
Yaku::Junchan => Some(2),
Yaku::Chinitsu => Some(5),
Yaku::Tanyao => Some(1),
Yaku::Yakuhai(_) => Some(1),
Yaku::RinshanKaihou => Some(1),
Yaku::Chankan => Some(1),
Yaku::HaiteiRaoyue => Some(1),
Yaku::HouteiRaoyui => Some(1),
Yaku::Toitoi => Some(2),
Yaku::SanshokuDoukou => Some(2),
Yaku::Chiitoitsu => Some(2), Yaku::SanAnkou => Some(2),
Yaku::SanKantsu => Some(2),
Yaku::Honroutou => Some(2),
Yaku::Shousangen => Some(2),
Yaku::Daisangen => Some(13),
Yaku::Shousuushii => Some(13),
Yaku::Daisuushii => Some(13),
Yaku::Tsuuiisou => Some(13),
Yaku::Chinroutou => Some(13),
Yaku::Ryuuiisou => Some(13),
Yaku::SuuKantsu => Some(13),
}
}
pub fn valid_when_open(&self) -> bool {
self.han_open().is_some()
}
pub fn is_yakuman(&self) -> bool {
matches!(
self,
Yaku::Tenhou
| Yaku::Chiihou
| Yaku::KokushiMusou
| Yaku::Kokushi13Wait
| Yaku::Suuankou
| Yaku::SuuankouTanki
| Yaku::Daisangen
| Yaku::Shousuushii
| Yaku::Daisuushii
| Yaku::Tsuuiisou
| Yaku::Chinroutou
| Yaku::Ryuuiisou
| Yaku::ChuurenPoutou
| Yaku::JunseiChuurenPoutou
| Yaku::SuuKantsu
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct YakuResult {
pub yaku_list: Vec<Yaku>,
pub total_han: u8,
pub dora_count: u8,
pub regular_dora: u8,
pub ura_dora: u8,
pub aka_dora: u8,
pub is_yakuman: bool,
}
impl YakuResult {
pub fn total_han_with_dora(&self) -> u8 {
if self.is_yakuman {
self.total_han
} else {
self.total_han + self.dora_count
}
}
}
pub fn detect_yaku_with_context(
structure: &HandStructure,
counts: &TileCounts,
context: &GameContext,
) -> YakuResult {
let mut yaku_list = Vec::new();
let is_open = context.is_open;
if context.is_tenhou && context.win_type == WinType::Tsumo && !is_open && context.is_dealer() {
yaku_list.push(Yaku::Tenhou);
}
if context.is_chiihou && context.win_type == WinType::Tsumo && !is_open && !context.is_dealer()
{
yaku_list.push(Yaku::Chiihou);
}
match structure {
HandStructure::Kokushi { pair } => {
if let Some(winning_tile) = context.winning_tile {
if winning_tile == *pair {
yaku_list.push(Yaku::Kokushi13Wait);
} else {
yaku_list.push(Yaku::KokushiMusou);
}
} else {
yaku_list.push(Yaku::KokushiMusou);
}
}
HandStructure::Chiitoitsu { pairs } => {
if pairs.iter().all(|t| t.is_honor()) {
yaku_list.push(Yaku::Tsuuiisou);
}
else if pairs.iter().all(|t| t.is_terminal()) {
yaku_list.push(Yaku::Chinroutou);
}
}
HandStructure::Standard { melds, pair } => {
if let Some(yaku) = check_suuankou(melds, *pair, context) {
yaku_list.push(yaku);
}
if check_daisangen(melds) {
yaku_list.push(Yaku::Daisangen);
}
if let Some(yaku) = check_four_winds(melds, *pair) {
yaku_list.push(yaku);
}
if check_tsuuiisou(melds, *pair) {
yaku_list.push(Yaku::Tsuuiisou);
}
if check_chinroutou(melds, *pair) {
yaku_list.push(Yaku::Chinroutou);
}
if check_ryuuiisou(melds, *pair) {
yaku_list.push(Yaku::Ryuuiisou);
}
if !is_open && let Some(yaku) = check_chuuren_poutou(counts, context) {
yaku_list.push(yaku);
}
{
let kan_count = melds
.iter()
.filter(|m| matches!(m, Meld::Kan(_, _)))
.count();
if kan_count == 4 {
yaku_list.push(Yaku::SuuKantsu);
}
}
}
}
let has_yakuman = yaku_list.iter().any(|y| y.is_yakuman());
if !has_yakuman {
if context.is_riichi && !is_open {
if context.is_double_riichi {
yaku_list.push(Yaku::DoubleRiichi);
} else {
yaku_list.push(Yaku::Riichi);
}
if context.is_ippatsu {
yaku_list.push(Yaku::Ippatsu);
}
}
if context.win_type == WinType::Tsumo && !is_open {
yaku_list.push(Yaku::MenzenTsumo);
}
if context.is_rinshan && context.win_type == WinType::Tsumo {
yaku_list.push(Yaku::RinshanKaihou);
}
if context.is_chankan && context.win_type == WinType::Ron {
yaku_list.push(Yaku::Chankan);
}
if context.is_last_tile && context.win_type == WinType::Tsumo {
yaku_list.push(Yaku::HaiteiRaoyue);
}
if context.is_last_tile && context.win_type == WinType::Ron {
yaku_list.push(Yaku::HouteiRaoyui);
}
match structure {
HandStructure::Kokushi { .. } => {
}
HandStructure::Chiitoitsu { pairs } => {
yaku_list.push(Yaku::Chiitoitsu);
if pairs.iter().all(|t| t.is_simple()) {
yaku_list.push(Yaku::Tanyao);
}
if pairs.iter().all(|t| t.is_terminal_or_honor()) {
yaku_list.push(Yaku::Honroutou);
}
if let Some(flush) = check_flush_tiles(pairs) {
yaku_list.push(flush);
}
}
HandStructure::Standard { melds, pair } => {
let all_tiles = collect_all_tiles(melds, *pair);
if all_tiles.iter().all(|t| t.is_simple()) {
yaku_list.push(Yaku::Tanyao);
}
if let Some(winning_tile) = context.winning_tile
&& is_pinfu(structure, winning_tile, context)
{
yaku_list.push(Yaku::Pinfu);
}
if !is_open && let Some(peikou) = check_peikou(melds) {
yaku_list.push(peikou);
}
for yaku in check_yakuhai(melds, context) {
yaku_list.push(yaku);
}
if melds.iter().all(|m| m.is_triplet_or_kan()) {
yaku_list.push(Yaku::Toitoi);
}
if check_sanshoku(melds) {
yaku_list.push(Yaku::SanshokuDoujun);
}
if check_sanshoku_doukou(melds) {
yaku_list.push(Yaku::SanshokuDoukou);
}
if check_ittsu(melds) {
yaku_list.push(Yaku::Ittsu);
}
if check_chanta(melds, *pair) && !check_junchan(melds, *pair) {
yaku_list.push(Yaku::Chanta);
}
{
let winning_tile_completes_sequence = if let Some(wt) = context.winning_tile {
melds.iter().any(|m| {
if let Meld::Shuntsu(start, _) = m {
if let (
Tile::Suited {
suit: ws,
value: wv,
},
Tile::Suited {
suit: ss,
value: sv,
},
) = (wt, start)
{
ws == *ss && wv >= *sv && wv <= sv + 2
} else {
false
}
} else {
false
}
})
} else {
false
};
let mut concealed_triplets = 0;
for meld in melds {
match meld {
Meld::Koutsu(tile, is_open_meld)
if !is_open_meld =>
{
if context.win_type == WinType::Tsumo {
concealed_triplets += 1;
} else if let Some(wt) = context.winning_tile
&& (*tile != wt || winning_tile_completes_sequence)
{
concealed_triplets += 1;
}
}
Meld::Kan(_, kan_type) if !kan_type.is_open() => {
concealed_triplets += 1;
}
_ => {}
}
}
if concealed_triplets == 3 {
yaku_list.push(Yaku::SanAnkou);
}
}
{
let kan_count = melds
.iter()
.filter(|m| matches!(m, Meld::Kan(_, _)))
.count();
if kan_count == 3 {
yaku_list.push(Yaku::SanKantsu);
}
}
if check_honroutou(melds, *pair) {
yaku_list.push(Yaku::Honroutou);
}
if check_shousangen(melds, *pair) {
yaku_list.push(Yaku::Shousangen);
}
if check_junchan(melds, *pair) {
yaku_list.push(Yaku::Junchan);
}
if let Some(flush) = check_flush_tiles(&all_tiles) {
yaku_list.push(flush);
}
}
}
}
let is_yakuman = yaku_list.iter().any(|y| y.is_yakuman());
let total_han: u8 = if is_open {
yaku_list.retain(|y| y.valid_when_open());
yaku_list.iter().filter_map(|y| y.han_open()).sum()
} else {
yaku_list.iter().map(|y| y.han()).sum()
};
let dora = count_dora_detailed(counts, context);
YakuResult {
yaku_list,
total_han,
dora_count: dora.total(),
regular_dora: dora.regular,
ura_dora: dora.ura,
aka_dora: dora.aka,
is_yakuman,
}
}
pub fn detect_yaku(structure: &HandStructure) -> YakuResult {
let dummy_context = GameContext::new(WinType::Ron, Honor::East, Honor::East);
let empty_counts = TileCounts::new();
detect_yaku_with_context(structure, &empty_counts, &dummy_context)
}
fn collect_all_tiles(melds: &[Meld], pair: Tile) -> Vec<Tile> {
let mut tiles = vec![pair, pair];
for meld in melds {
match meld {
Meld::Koutsu(t, _) => {
tiles.push(*t);
tiles.push(*t);
tiles.push(*t);
}
Meld::Shuntsu(t, _) => {
tiles.push(*t);
if let Tile::Suited { suit, value } = t {
tiles.push(Tile::suited(*suit, value + 1));
tiles.push(Tile::suited(*suit, value + 2));
}
}
Meld::Kan(t, _) => {
tiles.push(*t);
tiles.push(*t);
tiles.push(*t);
tiles.push(*t);
}
}
}
tiles
}
fn check_peikou(melds: &[Meld]) -> Option<Yaku> {
let sequences: Vec<_> = melds
.iter()
.filter_map(|m| match m {
Meld::Shuntsu(t, _) => Some(*t),
_ => None,
})
.collect();
if sequences.len() < 2 {
return None;
}
let mut seq_counts: HashMap<Tile, u8> = HashMap::new();
for t in &sequences {
*seq_counts.entry(*t).or_insert(0) += 1;
}
let pairs_of_sequences = seq_counts.values().filter(|&&c| c >= 2).count();
if pairs_of_sequences >= 2 {
Some(Yaku::Ryanpeikou)
} else if pairs_of_sequences == 1 {
Some(Yaku::Iipeikou)
} else {
None
}
}
fn check_yakuhai(melds: &[Meld], context: &GameContext) -> Vec<Yaku> {
let mut result = Vec::new();
for meld in melds {
let honor = match meld {
Meld::Koutsu(Tile::Honor(h), _) => Some(h),
Meld::Kan(Tile::Honor(h), _) => Some(h),
_ => None,
};
if let Some(honor) = honor {
match honor {
Honor::White | Honor::Green | Honor::Red => {
result.push(Yaku::Yakuhai(*honor));
}
Honor::East | Honor::South | Honor::West | Honor::North => {
if *honor == context.round_wind {
result.push(Yaku::Yakuhai(*honor));
}
if *honor == context.seat_wind {
result.push(Yaku::Yakuhai(*honor));
}
}
}
}
}
result
}
fn check_sanshoku(melds: &[Meld]) -> bool {
let sequences: Vec<(Suit, u8)> = melds
.iter()
.filter_map(|m| match m {
Meld::Shuntsu(Tile::Suited { suit, value }, _) => Some((*suit, *value)),
_ => None,
})
.collect();
let mut by_value: HashMap<u8, Vec<Suit>> = HashMap::new();
for (suit, value) in sequences {
by_value.entry(value).or_default().push(suit);
}
for suits in by_value.values() {
let has_man = suits.contains(&Suit::Man);
let has_pin = suits.contains(&Suit::Pin);
let has_sou = suits.contains(&Suit::Sou);
if has_man && has_pin && has_sou {
return true;
}
}
false
}
fn check_ittsu(melds: &[Meld]) -> bool {
let sequences: Vec<(Suit, u8)> = melds
.iter()
.filter_map(|m| match m {
Meld::Shuntsu(Tile::Suited { suit, value }, _) => Some((*suit, *value)),
_ => None,
})
.collect();
let mut by_suit: HashMap<Suit, Vec<u8>> = HashMap::new();
for (suit, value) in sequences {
by_suit.entry(suit).or_default().push(value);
}
for values in by_suit.values() {
if values.contains(&1) && values.contains(&4) && values.contains(&7) {
return true;
}
}
false
}
fn check_chanta(melds: &[Meld], pair: Tile) -> bool {
if !pair.is_terminal_or_honor() {
return false;
}
let has_sequence = melds.iter().any(|m| matches!(m, Meld::Shuntsu(_, _)));
if !has_sequence {
return false;
}
for meld in melds {
let has_terminal_or_honor = match meld {
Meld::Koutsu(t, _) | Meld::Kan(t, _) => t.is_terminal_or_honor(),
Meld::Shuntsu(Tile::Suited { value, .. }, _) => {
*value == 1 || *value == 7
}
Meld::Shuntsu(Tile::Honor(_), _) => true, };
if !has_terminal_or_honor {
return false;
}
}
true
}
fn check_junchan(melds: &[Meld], pair: Tile) -> bool {
let pair_ok = match pair {
Tile::Suited { value, .. } => value == 1 || value == 9,
Tile::Honor(_) => false,
};
if !pair_ok {
return false;
}
let has_sequence = melds.iter().any(|m| matches!(m, Meld::Shuntsu(_, _)));
if !has_sequence {
return false;
}
for meld in melds {
let has_terminal = match meld {
Meld::Koutsu(Tile::Suited { value, .. }, _)
| Meld::Kan(Tile::Suited { value, .. }, _) => *value == 1 || *value == 9,
Meld::Koutsu(Tile::Honor(_), _) | Meld::Kan(Tile::Honor(_), _) => false,
Meld::Shuntsu(Tile::Suited { value, .. }, _) => *value == 1 || *value == 7,
Meld::Shuntsu(Tile::Honor(_), _) => false,
};
if !has_terminal {
return false;
}
}
true
}
fn check_flush_tiles(tiles: &[Tile]) -> Option<Yaku> {
let mut found_suit: Option<Suit> = None;
let mut has_honors = false;
for tile in tiles {
match tile {
Tile::Suited { suit, .. } => {
match found_suit {
None => found_suit = Some(*suit),
Some(s) if s != *suit => return None, _ => {}
}
}
Tile::Honor(_) => has_honors = true,
}
}
found_suit?;
if has_honors {
Some(Yaku::Honitsu)
} else {
Some(Yaku::Chinitsu)
}
}
fn check_suuankou(melds: &[Meld], _pair: Tile, context: &GameContext) -> Option<Yaku> {
if context.win_type == WinType::Ron && context.winning_tile.is_none() {
return None;
}
let mut concealed_triplet_count = 0;
for meld in melds {
match meld {
Meld::Koutsu(tile, is_open) => {
if !is_open {
if context.win_type == WinType::Ron
&& let Some(wt) = context.winning_tile
&& *tile == wt
{
continue;
}
concealed_triplet_count += 1;
}
}
Meld::Kan(_, kan_type) => {
if !kan_type.is_open() {
concealed_triplet_count += 1;
}
}
Meld::Shuntsu(_, _) => {
return None;
}
}
}
if concealed_triplet_count != 4 {
return None;
}
Some(Yaku::Suuankou)
}
fn check_daisangen(melds: &[Meld]) -> bool {
let dragon_triplets = melds
.iter()
.filter(|m| match m {
Meld::Koutsu(t, _) | Meld::Kan(t, _) => t.is_dragon(),
_ => false,
})
.count();
dragon_triplets == 3
}
fn check_four_winds(melds: &[Meld], pair: Tile) -> Option<Yaku> {
let wind_triplets: Vec<Honor> = melds
.iter()
.filter_map(|m| {
let honor = match m {
Meld::Koutsu(Tile::Honor(h), _) | Meld::Kan(Tile::Honor(h), _) => Some(h),
_ => None,
};
if let Some(honor) = honor
&& matches!(
honor,
Honor::East | Honor::South | Honor::West | Honor::North
)
{
return Some(*honor);
}
None
})
.collect();
if wind_triplets.len() == 4 {
return Some(Yaku::Daisuushii);
}
if wind_triplets.len() == 3 {
if let Tile::Honor(honor) = pair
&& matches!(
honor,
Honor::East | Honor::South | Honor::West | Honor::North
)
&& !wind_triplets.contains(&honor)
{
return Some(Yaku::Shousuushii);
}
}
None
}
fn check_tsuuiisou(melds: &[Meld], pair: Tile) -> bool {
if !pair.is_honor() {
return false;
}
melds.iter().all(|m| match m {
Meld::Koutsu(t, _) | Meld::Kan(t, _) => t.is_honor(),
Meld::Shuntsu(_, _) => false, })
}
fn check_chinroutou(melds: &[Meld], pair: Tile) -> bool {
if !pair.is_terminal() {
return false;
}
melds.iter().all(|m| match m {
Meld::Koutsu(t, _) | Meld::Kan(t, _) => t.is_terminal(),
Meld::Shuntsu(_, _) => false, })
}
fn check_ryuuiisou(melds: &[Meld], pair: Tile) -> bool {
if !pair.is_green() {
return false;
}
melds.iter().all(|m| match m {
Meld::Koutsu(t, _) | Meld::Kan(t, _) => t.is_green(),
Meld::Shuntsu(start, _) => {
matches!(
start,
Tile::Suited {
suit: Suit::Sou,
value: 2
}
)
}
})
}
fn check_chuuren_poutou(counts: &TileCounts, context: &GameContext) -> Option<Yaku> {
if context.is_open {
return None;
}
let mut suit: Option<Suit> = None;
for tile in counts.keys() {
match tile {
Tile::Suited { suit: s, .. } => {
if suit.is_none() {
suit = Some(*s);
} else if suit != Some(*s) {
return None; }
}
Tile::Honor(_) => return None, }
}
let suit = suit?;
let mut total = 0u8;
let mut pattern_match = true;
let mut extra_tile: Option<u8> = None;
for value in 1..=9 {
let tile = Tile::suited(suit, value);
let count = counts.get(&tile).copied().unwrap_or(0);
total += count;
let required = if value == 1 || value == 9 { 3 } else { 1 };
if count < required {
pattern_match = false;
break;
}
if count > required {
if extra_tile.is_some() && count > required + 1 {
pattern_match = false;
break;
}
extra_tile = Some(value);
}
}
if !pattern_match || total != 14 {
return None;
}
if let Some(winning_tile) = context.winning_tile
&& let Tile::Suited {
value: wv,
suit: ws,
} = winning_tile
&& ws == suit
{
if extra_tile == Some(wv) {
return Some(Yaku::JunseiChuurenPoutou);
}
}
Some(Yaku::ChuurenPoutou)
}
fn check_sanshoku_doukou(melds: &[Meld]) -> bool {
let triplets: Vec<(Suit, u8)> = melds
.iter()
.filter_map(|m| match m {
Meld::Koutsu(Tile::Suited { suit, value }, _)
| Meld::Kan(Tile::Suited { suit, value }, _) => Some((*suit, *value)),
_ => None,
})
.collect();
for value in 1..=9 {
let mut has_man = false;
let mut has_pin = false;
let mut has_sou = false;
for (suit, v) in &triplets {
if *v == value {
match suit {
Suit::Man => has_man = true,
Suit::Pin => has_pin = true,
Suit::Sou => has_sou = true,
}
}
}
if has_man && has_pin && has_sou {
return true;
}
}
false
}
fn check_honroutou(melds: &[Meld], pair: Tile) -> bool {
if !pair.is_terminal_or_honor() {
return false;
}
melds.iter().all(|m| match m {
Meld::Koutsu(t, _) | Meld::Kan(t, _) => t.is_terminal_or_honor(),
Meld::Shuntsu(_, _) => false, })
}
fn check_shousangen(melds: &[Meld], pair: Tile) -> bool {
let dragon_triplets = melds
.iter()
.filter(|m| match m {
Meld::Koutsu(t, _) | Meld::Kan(t, _) => t.is_dragon(),
_ => false,
})
.count();
let pair_is_dragon = pair.is_dragon();
dragon_triplets == 2 && pair_is_dragon
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::{GameContext, WinType};
use crate::hand::decompose_hand;
use crate::parse::{parse_hand, to_counts};
fn get_yaku(hand: &str) -> Vec<YakuResult> {
let tiles = parse_hand(hand).unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
structures.iter().map(detect_yaku).collect()
}
fn get_yaku_with_context(hand: &str, context: &GameContext) -> Vec<YakuResult> {
let tiles = parse_hand(hand).unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
structures
.iter()
.map(|s| detect_yaku_with_context(s, &counts, context))
.collect()
}
fn has_yaku(results: &[YakuResult], yaku: Yaku) -> bool {
results.iter().any(|r| r.yaku_list.contains(&yaku))
}
#[test]
fn test_tanyao() {
let results = get_yaku("234m345p456567s88p");
assert!(has_yaku(&results, Yaku::Tanyao));
}
#[test]
fn test_no_tanyao_with_terminals() {
let results = get_yaku("123m456p789s11122z");
assert!(!has_yaku(&results, Yaku::Tanyao));
}
#[test]
fn test_toitoi() {
let results = get_yaku("111m222p333s44455z");
assert!(has_yaku(&results, Yaku::Toitoi));
}
#[test]
fn test_iipeikou() {
let results = get_yaku("112233m456p789s55z");
assert!(has_yaku(&results, Yaku::Iipeikou));
}
#[test]
fn test_ryanpeikou() {
let results = get_yaku("112233m112233p55s");
assert!(has_yaku(&results, Yaku::Ryanpeikou));
assert!(!has_yaku(&results, Yaku::Iipeikou));
}
#[test]
fn test_yakuhai_dragon() {
let results = get_yaku("123m456p789s55566z");
assert!(has_yaku(&results, Yaku::Yakuhai(Honor::White)));
}
#[test]
fn test_sanshoku() {
let results = get_yaku("123m123p123s11122z");
assert!(has_yaku(&results, Yaku::SanshokuDoujun));
}
#[test]
fn test_ittsu() {
let results = get_yaku("123456789m111p22z");
assert!(has_yaku(&results, Yaku::Ittsu));
}
#[test]
fn test_chiitoitsu() {
let results = get_yaku("1122m3344p5566s77z");
assert!(has_yaku(&results, Yaku::Chiitoitsu));
}
#[test]
fn test_honitsu() {
let results = get_yaku("123456789m11177z");
assert!(has_yaku(&results, Yaku::Honitsu));
assert!(!has_yaku(&results, Yaku::Chinitsu));
}
#[test]
fn test_chinitsu() {
let results = get_yaku("11123456789999m");
assert!(has_yaku(&results, Yaku::Chinitsu));
assert!(!has_yaku(&results, Yaku::Honitsu));
}
#[test]
fn test_chanta() {
let results = get_yaku("123m789p999s11177z");
assert!(has_yaku(&results, Yaku::Chanta));
}
#[test]
fn test_junchan() {
let results = get_yaku("123789m111p99999s");
assert!(has_yaku(&results, Yaku::Junchan));
assert!(!has_yaku(&results, Yaku::Chanta));
}
#[test]
fn test_multiple_yaku() {
let results = get_yaku("223344m567p678s55p");
assert!(has_yaku(&results, Yaku::Tanyao));
assert!(has_yaku(&results, Yaku::Iipeikou));
let best = results.iter().max_by_key(|r| r.total_han).unwrap();
assert!(best.total_han >= 2);
}
#[test]
fn test_complex_hand_best_interpretation() {
let results = get_yaku("111222333m11155z");
assert!(has_yaku(&results, Yaku::Toitoi));
}
#[test]
fn test_riichi() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::East).riichi();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(has_yaku(&results, Yaku::Riichi));
}
#[test]
fn test_double_riichi() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::East).double_riichi();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(has_yaku(&results, Yaku::DoubleRiichi));
assert!(!has_yaku(&results, Yaku::Riichi));
}
#[test]
fn test_ippatsu() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::East)
.riichi()
.ippatsu();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(has_yaku(&results, Yaku::Riichi));
assert!(has_yaku(&results, Yaku::Ippatsu));
}
#[test]
fn test_menzen_tsumo() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East);
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(has_yaku(&results, Yaku::MenzenTsumo));
}
#[test]
fn test_no_menzen_tsumo_when_open() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East).open();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(!has_yaku(&results, Yaku::MenzenTsumo));
}
#[test]
fn test_wind_yakuhai_round_wind() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::South);
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(has_yaku(&results, Yaku::Yakuhai(Honor::East)));
}
#[test]
fn test_wind_yakuhai_seat_wind() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::South);
let results = get_yaku_with_context("123m456p789s22233z", &context);
assert!(has_yaku(&results, Yaku::Yakuhai(Honor::South)));
}
#[test]
fn test_double_wind_yakuhai() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::East);
let results = get_yaku_with_context("123m456p789s11122z", &context);
let east_yakuhai_count = results
.iter()
.flat_map(|r| &r.yaku_list)
.filter(|y| **y == Yaku::Yakuhai(Honor::East))
.count();
assert!(east_yakuhai_count >= 2);
}
#[test]
fn test_non_value_wind_no_yakuhai() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::South);
let results = get_yaku_with_context("123m456p789s33344z", &context);
assert!(!has_yaku(&results, Yaku::Yakuhai(Honor::West)));
}
#[test]
fn test_no_iipeikou_when_open() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::East).open();
let results = get_yaku_with_context("112233m456p789s55z", &context);
assert!(!has_yaku(&results, Yaku::Iipeikou));
}
#[test]
fn test_open_hand_han_reduction() {
let context_closed = GameContext::new(WinType::Ron, Honor::East, Honor::East);
let context_open = GameContext::new(WinType::Ron, Honor::East, Honor::East).open();
let results_closed = get_yaku_with_context("123456789m11177z", &context_closed);
let results_open = get_yaku_with_context("123456789m11177z", &context_open);
let closed_han = results_closed
.iter()
.map(|r| r.total_han)
.max()
.unwrap_or(0);
let open_han = results_open.iter().map(|r| r.total_han).max().unwrap_or(0);
assert!(
closed_han > open_han,
"Closed should have more han than open"
);
}
#[test]
fn test_dora_counting() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::East)
.with_dora(vec![Tile::suited(Suit::Man, 1)]);
let tiles = parse_hand("222m456p789s11122z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let result = detect_yaku_with_context(&structures[0], &counts, &context);
assert_eq!(result.dora_count, 3);
}
#[test]
fn test_akadora_counting() {
let parsed = crate::parse::parse_hand_with_aka("123m406p789s11122z").unwrap();
let counts = to_counts(&parsed.tiles);
let structures = decompose_hand(&counts);
let context =
GameContext::new(WinType::Ron, Honor::East, Honor::East).with_aka(parsed.aka_count);
let result = detect_yaku_with_context(&structures[0], &counts, &context);
assert_eq!(result.dora_count, 1);
}
#[test]
fn test_total_han_with_dora() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::East)
.with_dora(vec![Tile::suited(Suit::Man, 1)]);
let tiles = parse_hand("222m345p456567s88p").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let result = detect_yaku_with_context(&structures[0], &counts, &context);
assert!(has_yaku(std::slice::from_ref(&result), Yaku::Tanyao));
assert_eq!(result.dora_count, 3);
assert_eq!(result.total_han_with_dora(), result.total_han + 3);
}
#[test]
fn test_rinshan_kaihou() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East).rinshan();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(has_yaku(&results, Yaku::RinshanKaihou));
assert!(has_yaku(&results, Yaku::MenzenTsumo)); }
#[test]
fn test_rinshan_requires_tsumo() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::East).rinshan();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(!has_yaku(&results, Yaku::RinshanKaihou));
}
#[test]
fn test_chankan() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::South).chankan();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(has_yaku(&results, Yaku::Chankan));
let yaku_result = &results[0];
let chankan_han = yaku_result
.yaku_list
.iter()
.find(|y| **y == Yaku::Chankan)
.map(|y| y.han())
.unwrap_or(0);
assert_eq!(chankan_han, 1);
}
#[test]
fn test_chankan_requires_ron() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South).chankan();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(!has_yaku(&results, Yaku::Chankan));
}
#[test]
fn test_haitei_raoyue() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East).last_tile();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(has_yaku(&results, Yaku::HaiteiRaoyue));
assert!(!has_yaku(&results, Yaku::HouteiRaoyui));
}
#[test]
fn test_houtei_raoyui() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::East).last_tile();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(has_yaku(&results, Yaku::HouteiRaoyui));
assert!(!has_yaku(&results, Yaku::HaiteiRaoyue));
}
#[test]
fn test_tenhou() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East).tenhou();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(has_yaku(&results, Yaku::Tenhou));
assert!(results[0].is_yakuman);
assert_eq!(results[0].total_han, 13); }
#[test]
fn test_tenhou_requires_dealer() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South).tenhou();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(!has_yaku(&results, Yaku::Tenhou));
}
#[test]
fn test_tenhou_requires_tsumo() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::East).tenhou();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(!has_yaku(&results, Yaku::Tenhou));
}
#[test]
fn test_chiihou() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South).chiihou();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(has_yaku(&results, Yaku::Chiihou));
assert!(results[0].is_yakuman);
}
#[test]
fn test_chiihou_not_for_dealer() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East).chiihou();
let results = get_yaku_with_context("123m456p789s11122z", &context);
assert!(!has_yaku(&results, Yaku::Chiihou));
}
#[test]
fn test_yakuman_overrides_regular_yaku() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East)
.tenhou()
.riichi(); let results = get_yaku_with_context("234m345p456567s88p", &context);
assert!(has_yaku(&results, Yaku::Tenhou));
assert!(!has_yaku(&results, Yaku::Tanyao)); assert!(!has_yaku(&results, Yaku::Riichi)); assert!(!has_yaku(&results, Yaku::MenzenTsumo)); }
#[test]
fn test_yakuman_dora_ignored() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East)
.tenhou()
.with_dora(vec![Tile::suited(Suit::Man, 1)]);
let tiles = parse_hand("222m456p789s11122z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let result = detect_yaku_with_context(&structures[0], &counts, &context);
assert!(result.is_yakuman);
assert_eq!(result.total_han, 13);
assert_eq!(result.total_han_with_dora(), 13); }
#[test]
fn test_pinfu_basic() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 4));
let results = get_yaku_with_context("123456m789p234s55p", &context);
assert!(has_yaku(&results, Yaku::Pinfu), "Should have pinfu");
assert!(
has_yaku(&results, Yaku::MenzenTsumo),
"Should have menzen tsumo"
);
}
#[test]
fn test_pinfu_with_tanyao() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 5));
let results = get_yaku_with_context("234567m234p345s66p", &context);
assert!(has_yaku(&results, Yaku::Pinfu));
assert!(has_yaku(&results, Yaku::Tanyao));
}
#[test]
fn test_pinfu_fails_with_triplet() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::honor(Honor::White));
let results = get_yaku_with_context("123m456p789s11155z", &context);
assert!(!has_yaku(&results, Yaku::Pinfu));
}
#[test]
fn test_pinfu_fails_with_dragon_pair() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 4));
let results = get_yaku_with_context("123m456m789p234s55z", &context);
assert!(
!has_yaku(&results, Yaku::Pinfu),
"Dragon pair means no pinfu"
);
}
#[test]
fn test_pinfu_fails_with_value_wind_pair() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 4));
let results = get_yaku_with_context("123m456m789p234s22z", &context);
assert!(
!has_yaku(&results, Yaku::Pinfu),
"Seat wind pair means no pinfu"
);
}
#[test]
fn test_pinfu_ok_with_non_value_wind_pair() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 4));
let results = get_yaku_with_context("123m456m789p234s33z", &context);
assert!(
has_yaku(&results, Yaku::Pinfu),
"Non-value wind pair allows pinfu"
);
}
#[test]
fn test_pinfu_fails_with_kanchan_wait() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 3));
let results = get_yaku_with_context("123m456m789p234s55p", &context);
assert!(
!has_yaku(&results, Yaku::Pinfu),
"Kanchan wait means no pinfu"
);
}
#[test]
fn test_pinfu_fails_when_open() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.open()
.with_winning_tile(Tile::suited(Suit::Sou, 4));
let results = get_yaku_with_context("123m456m789p234s55p", &context);
assert!(!has_yaku(&results, Yaku::Pinfu), "Open hand can't be pinfu");
}
#[test]
fn test_pinfu_no_winning_tile_no_pinfu() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South);
let results = get_yaku_with_context("123m456m789p234s55p", &context);
assert!(
!has_yaku(&results, Yaku::Pinfu),
"No winning tile = no pinfu detection"
);
}
#[test]
fn test_san_ankou_ron_invalidation() {
let hand_str = "111m222p333s456s77z";
let winning_tile = Tile::suited(Suit::Sou, 3);
let context_tsumo = GameContext::new(WinType::Tsumo, Honor::East, Honor::East)
.with_winning_tile(winning_tile);
let results_tsumo = get_yaku_with_context(hand_str, &context_tsumo);
assert!(has_yaku(&results_tsumo, Yaku::SanAnkou));
let context_ron = GameContext::new(WinType::Ron, Honor::East, Honor::East)
.with_winning_tile(winning_tile);
let results_ron = get_yaku_with_context(hand_str, &context_ron);
assert!(!has_yaku(&results_ron, Yaku::SanAnkou));
let pair_tile = Tile::honor(Honor::Red);
let context_tanki =
GameContext::new(WinType::Ron, Honor::East, Honor::East).with_winning_tile(pair_tile);
let results_tanki = get_yaku_with_context(hand_str, &context_tanki);
assert!(has_yaku(&results_tanki, Yaku::SanAnkou));
}
#[test]
fn test_san_ankou_ron_with_sequence_alternative() {
let hand_str = "111m222p333345s77z";
let winning_tile = Tile::suited(Suit::Sou, 3);
let context_ron = GameContext::new(WinType::Ron, Honor::East, Honor::East)
.with_winning_tile(winning_tile);
let results_ron = get_yaku_with_context(hand_str, &context_ron);
assert!(has_yaku(&results_ron, Yaku::SanAnkou));
}
#[test]
fn test_san_ankou_ron_no_sequence_alternative() {
let hand_str = "111m222p333s678s77z";
let winning_tile = Tile::suited(Suit::Sou, 3);
let context_ron = GameContext::new(WinType::Ron, Honor::East, Honor::East)
.with_winning_tile(winning_tile);
let results_ron = get_yaku_with_context(hand_str, &context_ron);
assert!(!has_yaku(&results_ron, Yaku::SanAnkou));
}
#[test]
fn test_suu_kantsu() {
use crate::hand::decompose_hand_with_melds;
use crate::parse::parse_hand_with_aka;
let parsed = parse_hand_with_aka("[1111m][2222m][3333m][4444m]55m").unwrap();
let counts = to_counts(&parsed.tiles);
let called_melds: Vec<_> = parsed
.called_melds
.iter()
.map(|cm| cm.meld.clone())
.collect();
let structures = decompose_hand_with_melds(&counts, &called_melds);
assert!(!structures.is_empty());
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East);
let result = detect_yaku_with_context(&structures[0], &counts, &context);
assert!(result.yaku_list.contains(&Yaku::SuuKantsu));
assert!(result.is_yakuman);
}
#[test]
fn test_suu_kantsu_open() {
use crate::hand::decompose_hand_with_melds;
use crate::parse::parse_hand_with_aka;
let parsed = parse_hand_with_aka("(1111m)(2222p)[3333s][4444s]55z").unwrap();
let counts = to_counts(&parsed.tiles);
let called_melds: Vec<_> = parsed
.called_melds
.iter()
.map(|cm| cm.meld.clone())
.collect();
let structures = decompose_hand_with_melds(&counts, &called_melds);
assert!(!structures.is_empty());
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East).open();
let result = detect_yaku_with_context(&structures[0], &counts, &context);
assert!(result.yaku_list.contains(&Yaku::SuuKantsu));
assert!(result.is_yakuman);
}
#[test]
fn test_san_kantsu_not_suu_kantsu() {
use crate::hand::decompose_hand_with_melds;
use crate::parse::parse_hand_with_aka;
let parsed = parse_hand_with_aka("[1111m][2222m][3333m]456s77z").unwrap();
let counts = to_counts(&parsed.tiles);
let called_melds: Vec<_> = parsed
.called_melds
.iter()
.map(|cm| cm.meld.clone())
.collect();
let structures = decompose_hand_with_melds(&counts, &called_melds);
assert!(!structures.is_empty());
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East);
let result = detect_yaku_with_context(&structures[0], &counts, &context);
assert!(result.yaku_list.contains(&Yaku::SanKantsu));
assert!(!result.yaku_list.contains(&Yaku::SuuKantsu));
assert!(!result.is_yakuman);
}
}