Skip to content

Commit ea80b3f

Browse files
authored
Merge pull request #14 from ryblogs/fix-suukantsu
Add Suu Kantsu (Four Kans) yakuman
2 parents 0558c9d + 072eb06 commit ea80b3f

File tree

5 files changed

+95
-0
lines changed

5 files changed

+95
-0
lines changed

crates/agari-core/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,6 +1423,7 @@ fn yaku_name(yaku: &Yaku) -> &'static str {
14231423
Yaku::Chinroutou => "Chinroutou (All Terminals)",
14241424
Yaku::Ryuuiisou => "Ryuuiisou (All Green)",
14251425
Yaku::ChuurenPoutou => "Chuuren Poutou (Nine Gates)",
1426+
Yaku::SuuKantsu => "Suu Kantsu (Four Kans)",
14261427

14271428
// Double Yakuman
14281429
Yaku::Kokushi13Wait => "Kokushi Juusanmen (Kokushi Musou 13-wait)",

crates/agari-core/src/yaku.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

crates/agari-wasm/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,7 @@ fn yaku_name(yaku: &Yaku) -> String {
631631
Yaku::Chinroutou => "Chinroutou".to_string(),
632632
Yaku::Ryuuiisou => "Ryuuiisou".to_string(),
633633
Yaku::ChuurenPoutou => "Chuuren Poutou".to_string(),
634+
Yaku::SuuKantsu => "Suu Kantsu".to_string(),
634635
Yaku::Kokushi13Wait => "Kokushi 13-Wait".to_string(),
635636
Yaku::SuuankouTanki => "Suuankou Tanki".to_string(),
636637
Yaku::JunseiChuurenPoutou => "Junsei Chuuren Poutou".to_string(),

web/src/lib/i18n/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const yakuNameMap: Record<string, keyof Translations> = {
5555
Chinroutou: "yakuChinroutou",
5656
Ryuuiisou: "yakuRyuuiisou",
5757
"Chuuren Poutou": "yakuChuurenPoutou",
58+
"Suu Kantsu": "yakuSuuKantsu",
5859
"Kokushi 13-Wait": "yakuKokushi13Wait",
5960
"Suuankou Tanki": "yakuSuuankouTanki",
6061
"Junsei Chuuren Poutou": "yakuJunseiChuurenPoutou",
753 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)