@@ -58,6 +58,7 @@ pub enum Yaku {
5858 Chinroutou , // All terminals
5959 Ryuuiisou , // All green
6060 ChuurenPoutou , // Nine gates
61+ SuuKantsu , // Four kans (quads)
6162
6263 // === Double Yakuman ===
6364 Kokushi13Wait , // Kokushi with 13-sided wait
@@ -116,6 +117,7 @@ impl Yaku {
116117 Yaku :: Chinroutou => 13 ,
117118 Yaku :: Ryuuiisou => 13 ,
118119 Yaku :: ChuurenPoutou => 13 ,
120+ Yaku :: SuuKantsu => 13 ,
119121
120122 // Double Yakuman (26 han equivalent)
121123 Yaku :: Kokushi13Wait => 26 ,
@@ -174,6 +176,7 @@ impl Yaku {
174176 Yaku :: Tsuuiisou => Some ( 13 ) ,
175177 Yaku :: Chinroutou => Some ( 13 ) ,
176178 Yaku :: Ryuuiisou => Some ( 13 ) ,
179+ Yaku :: SuuKantsu => Some ( 13 ) ,
177180 }
178181 }
179182
@@ -200,6 +203,7 @@ impl Yaku {
200203 | Yaku :: Ryuuiisou
201204 | Yaku :: ChuurenPoutou
202205 | Yaku :: JunseiChuurenPoutou
206+ | Yaku :: SuuKantsu
203207 )
204208 }
205209}
@@ -314,6 +318,17 @@ pub fn detect_yaku_with_context(
314318 if !is_open && let Some ( yaku) = check_chuuren_poutou ( counts, context) {
315319 yaku_list. push ( yaku) ;
316320 }
321+
322+ // Suu Kantsu (Four Kans)
323+ {
324+ let kan_count = melds
325+ . iter ( )
326+ . filter ( |m| matches ! ( m, Meld :: Kan ( _, _) ) )
327+ . count ( ) ;
328+ if kan_count == 4 {
329+ yaku_list. push ( Yaku :: SuuKantsu ) ;
330+ }
331+ }
317332 }
318333 }
319334
@@ -1712,4 +1727,81 @@ mod tests {
17121727 // Sanankou should NOT be awarded - only 2 concealed triplets remain
17131728 assert ! ( !has_yaku( & results_ron, Yaku :: SanAnkou ) ) ;
17141729 }
1730+
1731+ #[ test]
1732+ fn test_suu_kantsu ( ) {
1733+ use crate :: hand:: decompose_hand_with_melds;
1734+ use crate :: parse:: parse_hand_with_aka;
1735+
1736+ // Suu Kantsu (Four Kans) - yakuman
1737+ // Hand with four closed kans: [1111m] [2222m] [3333m] [4444m] 55m
1738+ // Total tiles: 4*4 + 2 = 18 tiles
1739+ let parsed = parse_hand_with_aka ( "[1111m][2222m][3333m][4444m]55m" ) . unwrap ( ) ;
1740+ let counts = to_counts ( & parsed. tiles ) ;
1741+ let called_melds: Vec < _ > = parsed
1742+ . called_melds
1743+ . iter ( )
1744+ . map ( |cm| cm. meld . clone ( ) )
1745+ . collect ( ) ;
1746+
1747+ let structures = decompose_hand_with_melds ( & counts, & called_melds) ;
1748+ assert ! ( !structures. is_empty( ) ) ;
1749+
1750+ let context = GameContext :: new ( WinType :: Tsumo , Honor :: East , Honor :: East ) ;
1751+ let result = detect_yaku_with_context ( & structures[ 0 ] , & counts, & context) ;
1752+
1753+ assert ! ( result. yaku_list. contains( & Yaku :: SuuKantsu ) ) ;
1754+ assert ! ( result. is_yakuman) ;
1755+ }
1756+
1757+ #[ test]
1758+ fn test_suu_kantsu_open ( ) {
1759+ use crate :: hand:: decompose_hand_with_melds;
1760+ use crate :: parse:: parse_hand_with_aka;
1761+
1762+ // Suu Kantsu can be achieved with open kans
1763+ // Hand with mixed open/closed kans: (1111m) (2222p) [3333s] [4444s] 55z
1764+ let parsed = parse_hand_with_aka ( "(1111m)(2222p)[3333s][4444s]55z" ) . unwrap ( ) ;
1765+ let counts = to_counts ( & parsed. tiles ) ;
1766+ let called_melds: Vec < _ > = parsed
1767+ . called_melds
1768+ . iter ( )
1769+ . map ( |cm| cm. meld . clone ( ) )
1770+ . collect ( ) ;
1771+
1772+ let structures = decompose_hand_with_melds ( & counts, & called_melds) ;
1773+ assert ! ( !structures. is_empty( ) ) ;
1774+
1775+ let context = GameContext :: new ( WinType :: Tsumo , Honor :: East , Honor :: East ) . open ( ) ;
1776+ let result = detect_yaku_with_context ( & structures[ 0 ] , & counts, & context) ;
1777+
1778+ assert ! ( result. yaku_list. contains( & Yaku :: SuuKantsu ) ) ;
1779+ assert ! ( result. is_yakuman) ;
1780+ }
1781+
1782+ #[ test]
1783+ fn test_san_kantsu_not_suu_kantsu ( ) {
1784+ use crate :: hand:: decompose_hand_with_melds;
1785+ use crate :: parse:: parse_hand_with_aka;
1786+
1787+ // Three kans should give San Kantsu (2 han), not Suu Kantsu (yakuman)
1788+ // Hand: [1111m] [2222m] [3333m] 456s 77z
1789+ let parsed = parse_hand_with_aka ( "[1111m][2222m][3333m]456s77z" ) . unwrap ( ) ;
1790+ let counts = to_counts ( & parsed. tiles ) ;
1791+ let called_melds: Vec < _ > = parsed
1792+ . called_melds
1793+ . iter ( )
1794+ . map ( |cm| cm. meld . clone ( ) )
1795+ . collect ( ) ;
1796+
1797+ let structures = decompose_hand_with_melds ( & counts, & called_melds) ;
1798+ assert ! ( !structures. is_empty( ) ) ;
1799+
1800+ let context = GameContext :: new ( WinType :: Tsumo , Honor :: East , Honor :: East ) ;
1801+ let result = detect_yaku_with_context ( & structures[ 0 ] , & counts, & context) ;
1802+
1803+ assert ! ( result. yaku_list. contains( & Yaku :: SanKantsu ) ) ;
1804+ assert ! ( !result. yaku_list. contains( & Yaku :: SuuKantsu ) ) ;
1805+ assert ! ( !result. is_yakuman) ;
1806+ }
17151807}
0 commit comments