use serde::{Deserialize, Serialize};
use crate::context::{GameContext, WinType};
use crate::hand::{HandStructure, Meld};
use crate::tile::{Honor, Tile};
use crate::wait::{best_wait_type_for_scoring, is_pinfu};
use crate::yaku::YakuResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ScoreLevel {
Normal,
Mangan,
Haneman,
Baiman,
Sanbaiman,
Yakuman,
DoubleYakuman,
}
impl ScoreLevel {
pub fn basic_points(&self) -> u32 {
match self {
ScoreLevel::Normal => 0, ScoreLevel::Mangan => 2000,
ScoreLevel::Haneman => 3000,
ScoreLevel::Baiman => 4000,
ScoreLevel::Sanbaiman => 6000,
ScoreLevel::Yakuman => 8000,
ScoreLevel::DoubleYakuman => 16000,
}
}
pub fn name(&self) -> &'static str {
match self {
ScoreLevel::Normal => "",
ScoreLevel::Mangan => "Mangan",
ScoreLevel::Haneman => "Haneman",
ScoreLevel::Baiman => "Baiman",
ScoreLevel::Sanbaiman => "Sanbaiman",
ScoreLevel::Yakuman => "Yakuman",
ScoreLevel::DoubleYakuman => "Double Yakuman",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FuResult {
pub total: u8,
pub breakdown: FuBreakdown,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FuBreakdown {
pub base: u8, pub menzen_ron: u8, pub tsumo: u8, pub melds: u8, pub pair: u8, pub wait: u8, pub raw_total: u8, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Payment {
pub total: u32,
pub from_non_dealer: Option<u32>,
pub from_dealer: Option<u32>,
pub from_discarder: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoringResult {
pub fu: FuResult,
pub han: u8,
pub score_level: ScoreLevel,
pub basic_points: u32,
pub payment: Payment,
pub is_dealer: bool,
pub is_counted_yakuman: bool,
}
pub fn calculate_fu(structure: &HandStructure, context: &GameContext) -> FuResult {
match structure {
HandStructure::Chiitoitsu { .. } => {
FuResult {
total: 25,
breakdown: FuBreakdown {
base: 25,
..Default::default()
},
}
}
HandStructure::Kokushi { .. } => {
FuResult {
total: 30,
breakdown: FuBreakdown {
base: 30,
..Default::default()
},
}
}
HandStructure::Standard { melds, pair } => calculate_standard_fu(melds, *pair, context),
}
}
fn calculate_standard_fu(melds: &[Meld], pair: Tile, context: &GameContext) -> FuResult {
let mut breakdown = FuBreakdown {
base: 20,
..Default::default()
};
let winning_tile = context.winning_tile;
let is_pinfu_hand = winning_tile
.map(|wt| {
is_pinfu(
&HandStructure::Standard {
melds: melds.to_vec(),
pair,
},
wt,
context,
)
})
.unwrap_or(false);
if is_pinfu_hand && context.win_type == WinType::Tsumo {
return FuResult {
total: 20,
breakdown: FuBreakdown {
base: 20,
..Default::default()
},
};
}
if !context.is_open && context.win_type == WinType::Ron {
breakdown.menzen_ron = 10;
}
if context.win_type == WinType::Tsumo && !is_pinfu_hand {
breakdown.tsumo = 2;
}
for meld in melds {
breakdown.melds += meld_fu_with_context(meld, melds, context);
}
breakdown.pair = pair_fu(pair, context);
if let Some(wt) = winning_tile {
if is_pinfu_hand {
breakdown.wait = 0;
} else if let Some(wait_type) = best_wait_type_for_scoring(
&HandStructure::Standard {
melds: melds.to_vec(),
pair,
},
wt,
) {
breakdown.wait = wait_type.fu();
}
}
breakdown.raw_total = breakdown.base
+ breakdown.menzen_ron
+ breakdown.tsumo
+ breakdown.melds
+ breakdown.pair
+ breakdown.wait;
let total = round_up_to_10(breakdown.raw_total);
let total = if context.is_open && total < 30 {
30
} else {
total
};
FuResult { total, breakdown }
}
fn meld_fu_with_context(meld: &Meld, all_melds: &[Meld], context: &GameContext) -> u8 {
match meld {
Meld::Shuntsu(_, _) => 0,
Meld::Koutsu(tile, is_meld_open) => {
let is_terminal_or_honor = tile.is_terminal_or_honor();
let base = if is_terminal_or_honor { 4 } else { 2 };
let is_ron_on_this_tile =
context.win_type == WinType::Ron && context.winning_tile == Some(*tile);
let is_true_shanpon =
is_ron_on_this_tile && !winning_tile_in_closed_sequence(*tile, all_melds);
if *is_meld_open || is_true_shanpon {
base } else {
base * 2 }
}
Meld::Kan(tile, kan_type) => {
let is_terminal_or_honor = tile.is_terminal_or_honor();
let base = if is_terminal_or_honor { 16 } else { 8 };
if kan_type.is_open() { base } else { base * 2 }
}
}
}
fn winning_tile_in_closed_sequence(tile: Tile, melds: &[Meld]) -> bool {
let (suit, value) = match tile {
Tile::Suited { suit, value } => (suit, value),
Tile::Honor(_) => return false,
};
for meld in melds {
if let Meld::Shuntsu(start_tile, is_open) = meld {
if *is_open {
continue; }
if let Tile::Suited {
suit: seq_suit,
value: start_val,
} = start_tile
{
if *seq_suit == suit && value >= *start_val && value <= start_val + 2 {
return true;
}
}
}
}
false
}
#[cfg(test)]
fn meld_fu(meld: &Meld) -> u8 {
match meld {
Meld::Shuntsu(_, _) => 0,
Meld::Koutsu(tile, is_meld_open) => {
let is_terminal_or_honor = tile.is_terminal_or_honor();
let base = if is_terminal_or_honor { 4 } else { 2 };
if *is_meld_open { base } else { base * 2 }
}
Meld::Kan(tile, kan_type) => {
let is_terminal_or_honor = tile.is_terminal_or_honor();
let base = if is_terminal_or_honor { 16 } else { 8 };
if kan_type.is_open() { base } else { base * 2 }
}
}
}
fn pair_fu(pair: Tile, context: &GameContext) -> u8 {
match pair {
Tile::Honor(honor) => {
match honor {
Honor::White | Honor::Green | Honor::Red => 2,
wind => {
let mut fu = 0;
if wind == context.round_wind {
fu += 2;
}
if wind == context.seat_wind {
fu += 2;
}
fu
}
}
}
Tile::Suited { .. } => 0, }
}
fn round_up_to_10(value: u8) -> u8 {
value.div_ceil(10) * 10
}
pub fn determine_score_level(han: u8, fu: u8, is_yakuman: bool) -> ScoreLevel {
if is_yakuman {
if han >= 26 {
ScoreLevel::DoubleYakuman
} else {
ScoreLevel::Yakuman
}
} else if han >= 13 {
ScoreLevel::Yakuman } else if han >= 11 {
ScoreLevel::Sanbaiman
} else if han >= 8 {
ScoreLevel::Baiman
} else if han >= 6 {
ScoreLevel::Haneman
} else if han >= 5 || (han == 4 && fu >= 40) || (han == 3 && fu >= 70) {
ScoreLevel::Mangan
} else {
ScoreLevel::Normal
}
}
pub fn calculate_basic_points(han: u8, fu: u8, is_yakuman: bool) -> u32 {
let level = determine_score_level(han, fu, is_yakuman);
if level != ScoreLevel::Normal {
return level.basic_points();
}
let basic = (fu as u32) * 2u32.pow((han + 2) as u32);
basic.min(2000)
}
pub fn calculate_payment(basic_points: u32, is_dealer: bool, win_type: WinType) -> Payment {
match win_type {
WinType::Tsumo => {
if is_dealer {
let from_each = round_up_to_100(basic_points * 2);
Payment {
total: from_each * 3,
from_non_dealer: Some(from_each),
from_dealer: None, from_discarder: None,
}
} else {
let from_dealer = round_up_to_100(basic_points * 2);
let from_non_dealer = round_up_to_100(basic_points);
Payment {
total: from_dealer + (from_non_dealer * 2),
from_non_dealer: Some(from_non_dealer),
from_dealer: Some(from_dealer),
from_discarder: None,
}
}
}
WinType::Ron => {
let multiplier = if is_dealer { 6 } else { 4 };
let from_discarder = round_up_to_100(basic_points * multiplier);
Payment {
total: from_discarder,
from_non_dealer: None,
from_dealer: None,
from_discarder: Some(from_discarder),
}
}
}
}
fn round_up_to_100(value: u32) -> u32 {
value.div_ceil(100) * 100
}
pub fn calculate_score(
structure: &HandStructure,
yaku_result: &YakuResult,
context: &GameContext,
) -> ScoringResult {
let fu = calculate_fu(structure, context);
let han = yaku_result.total_han_with_dora();
let score_level = determine_score_level(han, fu.total, yaku_result.is_yakuman);
let basic_points = calculate_basic_points(han, fu.total, yaku_result.is_yakuman);
let is_dealer = context.is_dealer();
let payment = calculate_payment(basic_points, is_dealer, context.win_type);
let is_counted_yakuman = (score_level == ScoreLevel::Yakuman
|| score_level == ScoreLevel::DoubleYakuman)
&& !yaku_result.is_yakuman;
ScoringResult {
fu,
han,
score_level,
basic_points,
payment,
is_dealer,
is_counted_yakuman,
}
}
pub fn format_score(result: &ScoringResult, yaku_result: &YakuResult) -> String {
let mut output = String::new();
output.push_str("Yaku:\n");
for yaku in &yaku_result.yaku_list {
let han = yaku.han();
output.push_str(&format!(" • {:?} ({} han)\n", yaku, han));
}
if yaku_result.regular_dora > 0 {
output.push_str(&format!(" • Dora ({} han)\n", yaku_result.regular_dora));
}
if yaku_result.ura_dora > 0 {
output.push_str(&format!(" • Ura Dora ({} han)\n", yaku_result.ura_dora));
}
if yaku_result.aka_dora > 0 {
output.push_str(&format!(
" • Red Fives (Akadora) ({} han)\n",
yaku_result.aka_dora
));
}
output.push_str(&format!("\n{} han / {} fu\n", result.han, result.fu.total));
if result.score_level != ScoreLevel::Normal {
output.push_str(&format!("{}\n", result.score_level.name()));
}
output.push_str(&format!("\nTotal: {} points\n", result.payment.total));
if let Some(from_discarder) = result.payment.from_discarder {
output.push_str(&format!("Ron: {} from discarder\n", from_discarder));
} else if result.is_dealer {
if let Some(from_each) = result.payment.from_non_dealer {
output.push_str(&format!("Tsumo: {} all\n", from_each));
}
} else if let (Some(from_dealer), Some(from_non_dealer)) =
(result.payment.from_dealer, result.payment.from_non_dealer)
{
output.push_str(&format!("Tsumo: {}/{}\n", from_dealer, from_non_dealer));
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hand::decompose_hand;
use crate::parse::{parse_hand, to_counts};
use crate::tile::Suit;
use crate::yaku::detect_yaku_with_context;
fn score_hand(hand: &str, context: &GameContext) -> Vec<ScoringResult> {
let tiles = parse_hand(hand).unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
structures
.iter()
.map(|s| {
let yaku_result = detect_yaku_with_context(s, &counts, context);
calculate_score(s, &yaku_result, context)
})
.collect()
}
fn best_score(results: &[ScoringResult]) -> &ScoringResult {
results
.iter()
.max_by(|a, b| {
a.payment
.total
.cmp(&b.payment.total)
.then_with(|| a.han.cmp(&b.han))
.then_with(|| b.fu.total.cmp(&a.fu.total))
})
.unwrap()
}
#[test]
fn test_fu_chiitoitsu() {
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 { .. }))
.unwrap();
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South);
let fu = calculate_fu(chiitoi, &context);
assert_eq!(fu.total, 25);
}
#[test]
fn test_fu_pinfu_tsumo() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 4));
let tiles = parse_hand("123456m789p234s55p").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let fu_results: Vec<_> = structures
.iter()
.map(|s| calculate_fu(s, &context))
.collect();
assert!(fu_results.iter().any(|f| f.total == 20));
}
#[test]
fn test_fu_menzen_ron() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 4));
let tiles = parse_hand("234m456p789s11122z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let fu = calculate_fu(&structures[0], &context);
assert!(fu.total >= 30);
assert_eq!(fu.breakdown.menzen_ron, 10);
}
#[test]
fn test_fu_tsumo_bonus() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::honor(Honor::East));
let tiles = parse_hand("123m456p789s11122z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let fu = calculate_fu(&structures[0], &context);
assert_eq!(fu.breakdown.tsumo, 2);
}
#[test]
fn test_fu_triplet_simple_open() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::South)
.open()
.with_winning_tile(Tile::suited(Suit::Man, 5));
let tiles = parse_hand("555m456p789s11122z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let fu = calculate_fu(&structures[0], &context);
assert!(fu.breakdown.melds >= 2);
}
#[test]
fn test_fu_triplet_terminal_closed() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 2));
let tiles = parse_hand("111234m456p789s22z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let fu = calculate_fu(&structures[0], &context);
assert!(fu.breakdown.melds >= 8);
}
#[test]
fn test_fu_yakuhai_pair() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 4));
let tiles = parse_hand("234m456p789s11155z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
assert!(!structures.is_empty(), "Should have valid decomposition");
let fu = calculate_fu(&structures[0], &context);
assert_eq!(fu.breakdown.pair, 2);
}
#[test]
fn test_fu_double_wind_pair() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::East)
.with_winning_tile(Tile::suited(Suit::Man, 4));
let tiles = parse_hand("234m456p789s22211z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
assert!(!structures.is_empty(), "Should have valid decomposition");
let fu = calculate_fu(&structures[0], &context);
assert_eq!(fu.breakdown.pair, 4);
}
#[test]
fn test_fu_wait_kanchan() {
let context = GameContext::new(WinType::Ron, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 3));
let tiles = parse_hand("234m456p789s11122z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let fu = calculate_fu(&structures[0], &context);
assert_eq!(fu.breakdown.wait, 2);
}
#[test]
fn test_fu_ron_completed_triplet_simple() {
let context = GameContext::new(WinType::Ron, Honor::West, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 2));
let tiles = parse_hand("222678m444666p11z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let fu = calculate_fu(&structures[0], &context);
assert_eq!(fu.breakdown.melds, 10); assert_eq!(fu.breakdown.pair, 0);
assert_eq!(fu.total, 40);
}
#[test]
fn test_fu_ron_completed_triplet_honor() {
use crate::hand::decompose_hand_with_melds;
use crate::parse::parse_hand_with_aka;
let parsed = parse_hand_with_aka("66p456s444z(222s)(555z)").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);
let context = GameContext::new(WinType::Ron, Honor::South, Honor::South)
.open()
.with_winning_tile(Tile::honor(Honor::North));
let fu = calculate_fu(&structures[0], &context);
assert_eq!(fu.breakdown.melds, 10); assert_eq!(fu.total, 30);
}
#[test]
fn test_fu_tsumo_triplet_stays_closed() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 2));
let tiles = parse_hand("222678m444666p11z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let fu = calculate_fu(&structures[0], &context);
assert_eq!(fu.breakdown.melds, 12); assert_eq!(fu.total, 40);
}
#[test]
fn test_fu_nobetan_triplet_stays_closed() {
use crate::hand::decompose_hand_with_melds;
use crate::parse::parse_hand_with_aka;
let parsed = parse_hand_with_aka("99m111123p789s(222z)").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);
let context = GameContext::new(WinType::Ron, Honor::South, Honor::North)
.open()
.with_winning_tile(Tile::suited(Suit::Pin, 1));
let fu = calculate_fu(&structures[0], &context);
assert_eq!(fu.breakdown.melds, 12); assert_eq!(fu.total, 40);
}
#[test]
fn test_fu_nobetan_open_sequence_does_not_count() {
use crate::hand::decompose_hand_with_melds;
use crate::parse::parse_hand_with_aka;
let parsed = parse_hand_with_aka("99m111p456s789s(123p)").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);
let context = GameContext::new(WinType::Ron, Honor::East, Honor::East)
.open()
.with_winning_tile(Tile::suited(Suit::Pin, 1));
let fu = calculate_fu(&structures[0], &context);
assert_eq!(fu.breakdown.melds, 4); assert_eq!(fu.total, 30);
}
#[test]
fn test_fu_rounding() {
assert_eq!(round_up_to_10(22), 30);
assert_eq!(round_up_to_10(30), 30);
assert_eq!(round_up_to_10(31), 40);
assert_eq!(round_up_to_10(25), 30); }
#[test]
fn test_fu_kan_simple_open() {
use crate::hand::{KanType, Meld};
let kan = Meld::kan(Tile::suited(Suit::Man, 5), KanType::Open);
assert_eq!(meld_fu(&kan), 8);
let added_kan = Meld::kan(Tile::suited(Suit::Pin, 3), KanType::Added);
assert_eq!(meld_fu(&added_kan), 8);
}
#[test]
fn test_fu_kan_simple_closed() {
use crate::hand::{KanType, Meld};
let kan = Meld::kan(Tile::suited(Suit::Sou, 7), KanType::Closed);
assert_eq!(meld_fu(&kan), 16);
}
#[test]
fn test_fu_kan_terminal_open() {
use crate::hand::{KanType, Meld};
let kan = Meld::kan(Tile::suited(Suit::Man, 1), KanType::Open);
assert_eq!(meld_fu(&kan), 16);
let kan_9 = Meld::kan(Tile::suited(Suit::Pin, 9), KanType::Added);
assert_eq!(meld_fu(&kan_9), 16);
}
#[test]
fn test_fu_kan_terminal_closed() {
use crate::hand::{KanType, Meld};
let kan = Meld::kan(Tile::suited(Suit::Sou, 1), KanType::Closed);
assert_eq!(meld_fu(&kan), 32);
}
#[test]
fn test_fu_kan_honor_open() {
use crate::hand::{KanType, Meld};
use crate::tile::Honor;
let kan = Meld::kan(Tile::honor(Honor::East), KanType::Open);
assert_eq!(meld_fu(&kan), 16);
let dragon_kan = Meld::kan(Tile::honor(Honor::White), KanType::Added);
assert_eq!(meld_fu(&dragon_kan), 16);
}
#[test]
fn test_fu_kan_honor_closed() {
use crate::hand::{KanType, Meld};
use crate::tile::Honor;
let kan = Meld::kan(Tile::honor(Honor::Red), KanType::Closed);
assert_eq!(meld_fu(&kan), 32);
let wind_kan = Meld::kan(Tile::honor(Honor::North), KanType::Closed);
assert_eq!(meld_fu(&wind_kan), 32);
}
#[test]
fn test_fu_comparison_triplet_vs_kan() {
use crate::hand::{KanType, Meld};
let simple_tile = Tile::suited(Suit::Man, 5);
let terminal_tile = Tile::suited(Suit::Pin, 1);
let triplet_open = Meld::Koutsu(simple_tile, true);
let kan_open = Meld::kan(simple_tile, KanType::Open);
assert_eq!(meld_fu(&kan_open), meld_fu(&triplet_open) * 4);
let triplet_closed = Meld::koutsu(simple_tile);
let kan_closed = Meld::kan(simple_tile, KanType::Closed);
assert_eq!(meld_fu(&kan_closed), meld_fu(&triplet_closed) * 4);
let triplet_term_open = Meld::Koutsu(terminal_tile, true);
let kan_term_open = Meld::kan(terminal_tile, KanType::Open);
assert_eq!(meld_fu(&kan_term_open), meld_fu(&triplet_term_open) * 4);
let triplet_term_closed = Meld::koutsu(terminal_tile);
let kan_term_closed = Meld::kan(terminal_tile, KanType::Closed);
assert_eq!(meld_fu(&kan_term_closed), meld_fu(&triplet_term_closed) * 4);
}
#[test]
fn test_score_level_mangan() {
assert_eq!(determine_score_level(5, 30, false), ScoreLevel::Mangan);
assert_eq!(determine_score_level(4, 40, false), ScoreLevel::Mangan);
assert_eq!(determine_score_level(3, 70, false), ScoreLevel::Mangan);
}
#[test]
fn test_score_level_haneman() {
assert_eq!(determine_score_level(6, 30, false), ScoreLevel::Haneman);
assert_eq!(determine_score_level(7, 30, false), ScoreLevel::Haneman);
}
#[test]
fn test_score_level_baiman() {
assert_eq!(determine_score_level(8, 30, false), ScoreLevel::Baiman);
assert_eq!(determine_score_level(10, 30, false), ScoreLevel::Baiman);
}
#[test]
fn test_score_level_sanbaiman() {
assert_eq!(determine_score_level(11, 30, false), ScoreLevel::Sanbaiman);
assert_eq!(determine_score_level(12, 30, false), ScoreLevel::Sanbaiman);
}
#[test]
fn test_score_level_yakuman() {
assert_eq!(determine_score_level(13, 30, false), ScoreLevel::Yakuman);
assert_eq!(determine_score_level(13, 30, true), ScoreLevel::Yakuman);
}
#[test]
fn test_basic_points_simple() {
assert_eq!(calculate_basic_points(1, 30, false), 240);
assert_eq!(calculate_basic_points(2, 30, false), 480);
assert_eq!(calculate_basic_points(3, 30, false), 960);
assert_eq!(calculate_basic_points(4, 30, false), 1920);
}
#[test]
fn test_basic_points_mangan_cap() {
assert_eq!(calculate_basic_points(4, 40, false), 2000);
assert_eq!(calculate_basic_points(5, 30, false), 2000);
}
#[test]
fn test_basic_points_limits() {
assert_eq!(calculate_basic_points(6, 30, false), 3000); assert_eq!(calculate_basic_points(8, 30, false), 4000); assert_eq!(calculate_basic_points(11, 30, false), 6000); assert_eq!(calculate_basic_points(13, 30, false), 8000); }
#[test]
fn test_payment_dealer_tsumo() {
let payment = calculate_payment(2000, true, WinType::Tsumo);
assert_eq!(payment.from_non_dealer, Some(4000));
assert_eq!(payment.from_dealer, None);
assert_eq!(payment.total, 12000);
}
#[test]
fn test_payment_non_dealer_tsumo() {
let payment = calculate_payment(2000, false, WinType::Tsumo);
assert_eq!(payment.from_dealer, Some(4000));
assert_eq!(payment.from_non_dealer, Some(2000));
assert_eq!(payment.total, 8000);
}
#[test]
fn test_payment_dealer_ron() {
let payment = calculate_payment(2000, true, WinType::Ron);
assert_eq!(payment.from_discarder, Some(12000));
assert_eq!(payment.total, 12000);
}
#[test]
fn test_payment_non_dealer_ron() {
let payment = calculate_payment(2000, false, WinType::Ron);
assert_eq!(payment.from_discarder, Some(8000));
assert_eq!(payment.total, 8000);
}
#[test]
fn test_payment_rounding() {
let payment = calculate_payment(240, false, WinType::Ron);
assert_eq!(payment.from_discarder, Some(1000));
}
#[test]
fn test_complete_score_riichi_tsumo() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.riichi()
.with_winning_tile(Tile::suited(Suit::Man, 4));
let results = score_hand("234m456p789s11122z", &context);
let best = best_score(&results);
assert!(best.han >= 2);
assert!(best.payment.total > 0);
}
#[test]
fn test_complete_score_pinfu_tsumo() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 4));
let results = score_hand("123456m789p234s55p", &context);
let best = best_score(&results);
assert_eq!(best.fu.total, 20);
assert_eq!(best.han, 2);
}
#[test]
fn test_complete_score_haneman() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 5));
let results = score_hand("111234345555m99m", &context);
let best = best_score(&results);
assert_eq!(best.score_level, ScoreLevel::Haneman);
assert_eq!(best.basic_points, 3000);
}
#[test]
fn test_complete_score_yakuman() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East)
.tenhou()
.with_winning_tile(Tile::suited(Suit::Man, 4));
let results = score_hand("234m456p789s11122z", &context);
let best = best_score(&results);
assert_eq!(best.score_level, ScoreLevel::Yakuman);
assert_eq!(best.han, 13);
assert_eq!(best.payment.total, 48000);
}
#[test]
fn test_common_scores() {
let payment = calculate_payment(calculate_basic_points(1, 30, false), false, WinType::Ron);
assert_eq!(payment.total, 1000);
let payment = calculate_payment(calculate_basic_points(2, 30, false), false, WinType::Ron);
assert_eq!(payment.total, 2000);
let payment = calculate_payment(calculate_basic_points(3, 40, false), false, WinType::Ron);
assert_eq!(payment.total, 5200);
let payment = calculate_payment(calculate_basic_points(4, 30, false), false, WinType::Ron);
assert_eq!(payment.total, 7700);
let payment = calculate_payment(calculate_basic_points(4, 40, false), false, WinType::Ron);
assert_eq!(payment.total, 8000);
}
#[test]
fn test_format_score() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.riichi()
.with_winning_tile(Tile::suited(Suit::Man, 4));
let tiles = parse_hand("234m456p789s11122z").unwrap();
let counts = to_counts(&tiles);
let structures = decompose_hand(&counts);
let yaku_result = detect_yaku_with_context(&structures[0], &counts, &context);
let score_result = calculate_score(&structures[0], &yaku_result, &context);
let formatted = format_score(&score_result, &yaku_result);
assert!(formatted.contains("Riichi"));
assert!(formatted.contains("han"));
assert!(formatted.contains("fu"));
assert!(formatted.contains("Total:"));
}
#[test]
fn test_hand_with_closed_kan_fu() {
use crate::hand::decompose_hand_with_melds;
use crate::parse::parse_hand_with_aka;
let parsed = parse_hand_with_aka("[1111m]222333m555p11z").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)
.with_winning_tile(Tile::suited(Suit::Man, 2));
let fu = calculate_fu(&structures[0], &context);
assert_eq!(fu.total, 70);
}
#[test]
fn test_hand_with_open_kan_fu() {
use crate::hand::decompose_hand_with_melds;
use crate::parse::parse_hand_with_aka;
let parsed = parse_hand_with_aka("(5555m)123456p789s11z").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::Ron, Honor::East, Honor::South)
.open()
.with_winning_tile(Tile::suited(Suit::Pin, 3));
let fu = calculate_fu(&structures[0], &context);
assert_eq!(fu.breakdown.melds, 8);
assert!(fu.total >= 30);
}
#[test]
fn test_hand_with_honor_kan() {
use crate::hand::decompose_hand_with_melds;
use crate::parse::parse_hand_with_aka;
let parsed = parse_hand_with_aka("[5555z]123m456p789s11z").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)
.with_winning_tile(Tile::suited(Suit::Man, 2));
let fu = calculate_fu(&structures[0], &context);
assert_eq!(fu.total, 60);
}
#[test]
fn test_counted_yakuman_chinitsu_ryanpeikou() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East)
.riichi()
.with_winning_tile(Tile::suited(Suit::Sou, 2));
let results = score_hand("22334455667799s", &context);
let best = best_score(&results);
assert!(best.han >= 12);
assert!(!best.is_counted_yakuman); }
#[test]
fn test_counted_yakuman_with_dora() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East)
.riichi()
.ippatsu()
.with_winning_tile(Tile::suited(Suit::Sou, 2))
.with_dora(vec![Tile::suited(Suit::Sou, 1)]);
let results = score_hand("22334455667799s", &context);
let best = best_score(&results);
assert!(best.han >= 13);
assert_eq!(best.score_level, ScoreLevel::Yakuman);
assert!(best.is_counted_yakuman); }
#[test]
fn test_true_yakuman_kokushi() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East)
.with_winning_tile(Tile::suited(Suit::Man, 1));
let results = score_hand("19m19p19s12345677z", &context);
let best = best_score(&results);
assert_eq!(best.score_level, ScoreLevel::Yakuman);
assert!(!best.is_counted_yakuman); }
#[test]
fn test_true_yakuman_suuankou() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East)
.with_winning_tile(Tile::Honor(Honor::White));
let results = score_hand("111222333m444p55z", &context);
let best = best_score(&results);
assert_eq!(best.score_level, ScoreLevel::Yakuman);
assert!(!best.is_counted_yakuman); }
#[test]
fn test_true_yakuman_tenhou() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East)
.tenhou()
.with_winning_tile(Tile::suited(Suit::Man, 4));
let results = score_hand("234m456p789s11122z", &context);
let best = best_score(&results);
assert_eq!(best.score_level, ScoreLevel::Yakuman);
assert!(!best.is_counted_yakuman); }
#[test]
fn test_not_counted_yakuman_below_13_han() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East)
.riichi()
.with_winning_tile(Tile::suited(Suit::Man, 5));
let results = score_hand("123345567789m55m", &context);
let best = best_score(&results);
assert!(best.han < 13);
assert_eq!(best.score_level, ScoreLevel::Baiman);
assert!(!best.is_counted_yakuman);
}
#[test]
fn test_prefer_ryanpeikou_over_chiitoitsu() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::East)
.riichi()
.ippatsu()
.with_winning_tile(Tile::suited(Suit::Sou, 2))
.with_dora(vec![Tile::suited(Suit::Sou, 1)]);
let results = score_hand("22334455667799s", &context);
let best = best_score(&results);
assert!(
best.han >= 15,
"Expected 15+ han for Ryanpeikou, got {}",
best.han
);
assert_eq!(best.score_level, ScoreLevel::Yakuman);
}
#[test]
fn test_chiitoitsu_vs_ryanpeikou_different_scores() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 2));
let results = score_hand("22334455667799s", &context);
let best = best_score(&results);
assert!(
best.han >= 11,
"Expected 11+ han for Ryanpeikou, got {}",
best.han
);
assert_eq!(best.score_level, ScoreLevel::Sanbaiman);
}
#[test]
fn test_winning_tile_affects_pinfu_eligibility() {
let context_ryanmen = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 3));
let results_ryanmen = score_hand("334455m334455p66s", &context_ryanmen);
let best_ryanmen = best_score(&results_ryanmen);
assert_eq!(
best_ryanmen.fu.total, 20,
"Ryanmen wait should give 20 fu (Pinfu)"
);
assert_eq!(best_ryanmen.han, 6, "Should have 6 han with Pinfu");
let context_tanki = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 6));
let results_tanki = score_hand("334455m334455p66s", &context_tanki);
let best_tanki = best_score(&results_tanki);
assert_eq!(
best_tanki.fu.total, 30,
"Tanki wait should give 30 fu (no Pinfu)"
);
assert_eq!(best_tanki.han, 5, "Should have 5 han without Pinfu");
}
#[test]
fn test_winning_tile_affects_payment_with_pinfu() {
let context_ryanmen = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 3)); let results_ryanmen = score_hand("334455m334455p66s", &context_ryanmen);
let best_ryanmen = best_score(&results_ryanmen);
let context_tanki = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 6));
let results_tanki = score_hand("334455m334455p66s", &context_tanki);
let best_tanki = best_score(&results_tanki);
assert!(
best_ryanmen.payment.total > best_tanki.payment.total,
"Ryanmen interpretation ({} points) should pay more than tanki ({} points)",
best_ryanmen.payment.total,
best_tanki.payment.total
);
assert_eq!(best_ryanmen.score_level, ScoreLevel::Haneman);
assert_eq!(best_tanki.score_level, ScoreLevel::Mangan);
}
#[test]
fn test_winning_tile_ryanpeikou_multiple_ryanmen_options() {
let ryanmen_tiles = [
Tile::suited(Suit::Man, 3),
Tile::suited(Suit::Man, 5),
Tile::suited(Suit::Pin, 3),
Tile::suited(Suit::Pin, 5),
];
let mut ryanmen_scores = Vec::new();
for tile in &ryanmen_tiles {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(*tile);
let results = score_hand("334455m334455p66s", &context);
let best = best_score(&results);
ryanmen_scores.push((tile, best.han, best.fu.total, best.payment.total));
}
let first = &ryanmen_scores[0];
for (tile, han, fu, payment) in &ryanmen_scores {
assert_eq!(
*han, first.1,
"Tile {:?} gave {} han, expected {} han",
tile, han, first.1
);
assert_eq!(
*fu, first.2,
"Tile {:?} gave {} fu, expected {} fu",
tile, fu, first.2
);
assert_eq!(
*payment, first.3,
"Tile {:?} gave {} payment, expected {} payment",
tile, payment, first.3
);
}
assert_eq!(first.2, 20, "Ryanmen wait should give 20 fu");
assert_eq!(first.1, 6, "Ryanmen wait should give 6 han");
}
#[test]
fn test_winning_tile_inference_chooses_best_for_mahjong_soul_hand() {
let context_good = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.riichi()
.with_winning_tile(Tile::suited(Suit::Man, 4)); let results_good = score_hand("445566m334455p66s", &context_good);
let best_good = best_score(&results_good);
let context_bad = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.riichi()
.with_winning_tile(Tile::suited(Suit::Sou, 6)); let results_bad = score_hand("445566m334455p66s", &context_bad);
let best_bad = best_score(&results_bad);
assert_eq!(best_good.fu.total, 20);
assert_eq!(best_good.han, 7);
assert_eq!(best_good.score_level, ScoreLevel::Haneman);
assert_eq!(best_bad.fu.total, 30);
assert_eq!(best_bad.han, 6);
assert_eq!(best_bad.score_level, ScoreLevel::Haneman);
assert!(
best_good.han > best_bad.han,
"Good inference ({} han) should have more han than bad ({} han)",
best_good.han,
best_bad.han
);
}
#[test]
fn test_winning_tile_no_inference_needed_for_explicit_tile() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 6));
let results = score_hand("334455m334455p66s", &context);
let best = best_score(&results);
assert_eq!(best.fu.total, 30);
assert_eq!(best.han, 5); }
#[test]
fn test_winning_tile_affects_wait_fu() {
let context_tanki = GameContext::new(WinType::Ron, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 2));
let results_tanki = score_hand("234567m234567p22s", &context_tanki);
let best_tanki = best_score(&results_tanki);
let context_ryanmen = GameContext::new(WinType::Ron, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 7));
let results_ryanmen = score_hand("234567m234567p22s", &context_ryanmen);
let best_ryanmen = best_score(&results_ryanmen);
assert!(
best_ryanmen.han >= best_tanki.han,
"Ryanmen should have >= han ({} vs {})",
best_ryanmen.han,
best_tanki.han
);
}
#[test]
fn test_winning_tile_chinitsu_ryanpeikou_best_inference() {
let context_8s = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 8));
let results_8s = score_hand("22334455667788s", &context_8s);
let best_8s = best_score(&results_8s);
let context_2s = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 2));
let results_2s = score_hand("22334455667788s", &context_2s);
let best_2s = best_score(&results_2s);
assert_eq!(best_8s.fu.total, 20, "8s ryanmen should give 20 fu");
assert_eq!(best_2s.fu.total, 20, "2s ryanmen should give 20 fu");
assert_eq!(best_8s.han, best_2s.han, "Both should have same han");
assert_eq!(best_8s.score_level, ScoreLevel::Sanbaiman);
}
#[test]
fn test_winning_tile_with_no_winning_tile_set() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South);
let results = score_hand("334455m334455p66s", &context);
let best = best_score(&results);
assert_eq!(
best.han, 5,
"Without winning tile, should get 5 han (no Pinfu)"
);
assert_eq!(best.fu.total, 30, "Without winning tile, should get 30 fu");
}
#[test]
fn test_winning_tile_inference_with_dora() {
let context_7m = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 7)) .with_dora(vec![Tile::suited(Suit::Man, 6)]);
let results = score_hand("234567m234567p22s", &context_7m);
let best = best_score(&results);
assert!(
best.han >= 4,
"Should have at least 4 han with dora, got {}",
best.han
);
assert_eq!(best.fu.total, 20, "Should have 20 fu with Pinfu");
}
#[test]
fn test_winning_tile_shanpon_vs_ryanmen_ambiguous() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 1));
let results = score_hand("111222333m456p77s", &context);
let best = best_score(&results);
assert!(
best.fu.total >= 30,
"Should have at least 30 fu with triplets"
);
}
#[test]
fn test_winning_tile_seven_pairs_always_tanki() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 1));
let results = score_hand("1133m2255p4477s11z", &context);
let chiitoitsu_result = results.iter().find(|r| r.fu.total == 25);
assert!(
chiitoitsu_result.is_some(),
"Should have a Chiitoitsu interpretation with 25 fu"
);
let chii = chiitoitsu_result.unwrap();
assert_eq!(chii.fu.total, 25, "Chiitoitsu always has 25 fu");
}
#[test]
fn test_winning_tile_inference_prefers_higher_payment() {
let context_good = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 3)); let results_good = score_hand("334455m334455p66s", &context_good);
let best_good = best_score(&results_good);
let context_bad = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 6)); let results_bad = score_hand("334455m334455p66s", &context_bad);
let best_bad = best_score(&results_bad);
assert!(
best_good.payment.total > best_bad.payment.total,
"Good interpretation ({} points) should pay more than bad ({} points)",
best_good.payment.total,
best_bad.payment.total
);
assert!(
best_good.han > best_bad.han,
"Good interpretation ({} han) should have more han than bad ({} han)",
best_good.han,
best_bad.han
);
}
#[test]
fn test_winning_tile_kanchan_vs_ryanmen() {
let context_kanchan = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 4)); let results_kanchan = score_hand("334455m334455p66s", &context_kanchan);
let best_kanchan = best_score(&results_kanchan);
let context_ryanmen = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 3)); let results_ryanmen = score_hand("334455m334455p66s", &context_ryanmen);
let best_ryanmen = best_score(&results_ryanmen);
assert_eq!(best_kanchan.fu.total, 30, "Kanchan should give 30 fu");
assert_eq!(best_kanchan.han, 5, "Kanchan should give 5 han (no Pinfu)");
assert_eq!(best_ryanmen.fu.total, 20, "Ryanmen should give 20 fu");
assert_eq!(
best_ryanmen.han, 6,
"Ryanmen should give 6 han (with Pinfu)"
);
}
#[test]
fn test_all_wait_types_for_ryanpeikou_hand() {
let hand = "334455m334455p66s";
for tile in [
Tile::suited(Suit::Man, 3),
Tile::suited(Suit::Man, 5),
Tile::suited(Suit::Pin, 3),
Tile::suited(Suit::Pin, 5),
] {
let context =
GameContext::new(WinType::Tsumo, Honor::East, Honor::South).with_winning_tile(tile);
let results = score_hand(hand, &context);
let best = best_score(&results);
assert_eq!(
best.fu.total, 20,
"Tile {:?} should be ryanmen (20 fu), got {} fu",
tile, best.fu.total
);
assert_eq!(
best.han, 6,
"Tile {:?} should give 6 han with Pinfu, got {} han",
tile, best.han
);
}
for tile in [Tile::suited(Suit::Man, 4), Tile::suited(Suit::Pin, 4)] {
let context =
GameContext::new(WinType::Tsumo, Honor::East, Honor::South).with_winning_tile(tile);
let results = score_hand(hand, &context);
let best = best_score(&results);
assert_eq!(
best.fu.total, 30,
"Tile {:?} should be kanchan (30 fu), got {} fu",
tile, best.fu.total
);
assert_eq!(
best.han, 5,
"Tile {:?} should give 5 han without Pinfu, got {} han",
tile, best.han
);
}
let tile = Tile::suited(Suit::Sou, 6);
let context =
GameContext::new(WinType::Tsumo, Honor::East, Honor::South).with_winning_tile(tile);
let results = score_hand(hand, &context);
let best = best_score(&results);
assert_eq!(
best.fu.total, 30,
"Tile {:?} should be tanki (30 fu), got {} fu",
tile, best.fu.total
);
assert_eq!(
best.han, 5,
"Tile {:?} should give 5 han without Pinfu, got {} han",
tile, best.han
);
}
#[test]
fn test_inference_should_maximize_score_not_just_han() {
let context_ryanmen = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 7)); let results_ryanmen = score_hand("234567m234567p22s", &context_ryanmen);
let best_ryanmen = best_score(&results_ryanmen);
let context_tanki = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 2)); let results_tanki = score_hand("234567m234567p22s", &context_tanki);
let best_tanki = best_score(&results_tanki);
assert!(
best_ryanmen.payment.total >= best_tanki.payment.total,
"Ryanmen ({}) should pay >= tanki ({})",
best_ryanmen.payment.total,
best_tanki.payment.total
);
}
#[test]
fn test_inference_handles_hand_with_only_tanki_wait() {
let context = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 1));
let results = score_hand("1133m2255p4477s11z", &context);
let has_chiitoitsu = results.iter().any(|r| r.fu.total == 25);
assert!(has_chiitoitsu, "Should have Chiitoitsu interpretation");
}
#[test]
fn test_inference_with_multiple_suits_and_honors() {
let context_1z = GameContext::new(WinType::Tsumo, Honor::East, Honor::East)
.with_winning_tile(Tile::honor(Honor::East)); let results_1z = score_hand("123m456p789s11122z", &context_1z);
let best_1z = best_score(&results_1z);
assert!(
best_1z.han >= 3,
"Should have at least 3 han with double wind yakuhai"
);
}
#[test]
fn test_inference_ron_vs_tsumo_different_optimal() {
let context_ron = GameContext::new(WinType::Ron, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 7)); let results_ron = score_hand("234567m234567p22s", &context_ron);
let best_ron = best_score(&results_ron);
let context_tsumo = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 7)); let results_tsumo = score_hand("234567m234567p22s", &context_tsumo);
let best_tsumo = best_score(&results_tsumo);
assert_eq!(
best_tsumo.han,
best_ron.han + 1,
"Tsumo ({}) should have 1 more han than ron ({})",
best_tsumo.han,
best_ron.han
);
}
#[test]
fn test_inference_preserves_dora_count() {
let context_dora_win = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Sou, 2))
.with_dora(vec![Tile::suited(Suit::Sou, 1)]); let results_dora = score_hand("234567m234567p22s", &context_dora_win);
let best_dora = best_score(&results_dora);
let context_no_dora_win = GameContext::new(WinType::Tsumo, Honor::East, Honor::South)
.with_winning_tile(Tile::suited(Suit::Man, 7))
.with_dora(vec![Tile::suited(Suit::Sou, 1)]); let results_no_dora = score_hand("234567m234567p22s", &context_no_dora_win);
let best_no_dora = best_score(&results_no_dora);
assert!(
best_no_dora.payment.total >= best_dora.payment.total,
"Ryanmen+Pinfu ({}) should pay >= tanki+dora ({}) due to higher han",
best_no_dora.payment.total,
best_dora.payment.total
);
}
}