Skip to content

Commit 8590afe

Browse files
Agony5757claude
andcommitted
refactor: clean up Python API surface and fix 3 critical bugs
- Fix get_scores() binding missing in pybind11 (was causing AttributeError in web/game_manager.py and web/server.py) - Fix seed attribute: use set_seed() instead of direct member assignment - Fix all to_string methods returning bytes instead of str (py::bytes -> py::str) - Add return_value_policy::reference_internal to Table methods for safe STL container lifetime management - Establish clean public __all__ API surface: explicit imports, no wildcard pollution, 36 public symbols documented - Add __version__ from setuptools-scm build-time injection - Remove deprecated env_mahjong.py (superseded by env_pymahjong.py) - Add __all__ to all Python submodules - Rename paipu_replay_1 -> paipu_replay_summary - Move test() to internal module (not part of public API) - Add C++ API documentation (docs/cpp_api/) - Add web UI for game replay and AI opponent (web/) - Add ScoreTable/YakuDetector/FuCalculator C++ modules - Add state machine documentation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5a22ec6 commit 8590afe

33 files changed

Lines changed: 6692 additions & 1234 deletions

Mahjong/FuCalculator.cpp

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
#include "FuCalculator.h"
2+
#include <algorithm>
3+
4+
namespace_mahjong
5+
namespace fu_calculator {
6+
7+
// ─── Helper predicates ─────────────────────────────────────────────────────────
8+
9+
// Returns true if a tgs contains at least one terminal or honor tile.
10+
// Used to distinguish yaochu from chunchan melds.
11+
static bool has_yaochu_or_z(const std::string& s) {
12+
if (s.size() < 2) return false;
13+
if (s[1] == 'z') return true;
14+
if (s[0] == '1' || s[0] == '9') return true;
15+
return false;
16+
}
17+
18+
// Returns the number of cases for a yakuhai toitsu (役牌对子).
19+
// 1 if seat wind, 1 if game wind, 1 if dragon (total up to 3).
20+
static int yakuhai_toitsu_cases(const std::string& s, Wind self_wind, Wind game_wind) {
21+
if (s.size() < 3 || s[2] != ':' || s[1] != 'z') return 0;
22+
int n = s[0] - '1'; // 0-3: winds; 4-6: dragons
23+
int cases = 0;
24+
if (n == self_wind) cases++;
25+
if (n == game_wind) cases++;
26+
if (n >= 4) cases++; // dragons always count
27+
return cases;
28+
}
29+
30+
// ─── Wait shape helpers ───────────────────────────────────────────────────────
31+
32+
bool has_single_wait(const std::vector<std::string>& tgs) {
33+
for (const auto& s : tgs) {
34+
if (s.size() == 4 && s[2] == ':') return true;
35+
}
36+
return false;
37+
}
38+
39+
bool has_kanchan_wait(const std::vector<std::string>& tgs) {
40+
// Kanchan: waiting on the middle tile of a shuntsu.
41+
// Marked as '@' (tsumo 2nd) or '%' (ron 2nd).
42+
for (const auto& s : tgs) {
43+
if (s.size() == 4 && s[2] == 'S') {
44+
if (s[3] == '@' || s[3] == '%') return true;
45+
}
46+
}
47+
return false;
48+
}
49+
50+
bool has_penchan_wait(const std::vector<std::string>& tgs) {
51+
// Penchan: waiting on 1 or 7 in 1-2-3 or 7-8-9 sequence.
52+
// Marked as: '#', '^' (tsumo/ron 3rd for 123) or '!', '$' (1st for 123/789).
53+
for (const auto& s : tgs) {
54+
if (s.size() == 4 && s[2] == 'S') {
55+
if (s[0] == '1' && (s[3] == '#' || s[3] == '^' ||
56+
s[3] == '!' || s[3] == '$')) return true;
57+
if (s[0] == '7' && (s[3] == '!' || s[3] == '$')) return true;
58+
}
59+
}
60+
return false;
61+
}
62+
63+
// ─── Sub-step: wait shape fu ─────────────────────────────────────────────────
64+
65+
int fu_from_wait_shape(const std::vector<std::string>& tgs) {
66+
int fu = 0;
67+
if (has_single_wait(tgs)) fu += 2; // tanki (单骑)
68+
if (has_kanchan_wait(tgs)) fu += 2; // kanchan (坎张)
69+
if (has_penchan_wait(tgs)) fu += 2; // penchan (边张)
70+
return fu;
71+
}
72+
73+
// ─── Sub-step: winning method fu ──────────────────────────────────────────────
74+
75+
bool is_tsumo_wait(const std::vector<std::string>& tgs) {
76+
// Any tgs has a tsumo position mark ('!', '@', '#')
77+
for (const auto& s : tgs) {
78+
if (s.size() == 4) {
79+
if (s[3] == '!' || s[3] == '@' || s[3] == '#') return true;
80+
}
81+
}
82+
return false;
83+
}
84+
85+
bool is_ron_wait(const std::vector<std::string>& tgs) {
86+
// Any tgs has a ron position mark ('$', '%', '^')
87+
for (const auto& s : tgs) {
88+
if (s.size() == 4) {
89+
if (s[3] == '$' || s[3] == '%' || s[3] == '^') return true;
90+
}
91+
}
92+
return false;
93+
}
94+
95+
int fu_from_winning_method(const std::vector<std::string>& tgs,
96+
bool menzen, bool is_pinfu) {
97+
int fu = 0;
98+
if (is_tsumo_wait(tgs) && !is_pinfu) {
99+
fu += 2; // tsumo (non-pinfu)
100+
}
101+
if (is_ron_wait(tgs) && menzen) {
102+
fu += 10; // ron in closed hand
103+
}
104+
return fu;
105+
}
106+
107+
// ─── Sub-step: head fu ───────────────────────────────────────────────────────
108+
109+
int fu_from_head(const std::vector<std::string>& tgs,
110+
Wind self_wind, Wind game_wind) {
111+
int fu = 0;
112+
for (const auto& s : tgs) {
113+
fu += yakuhai_toitsu_cases(s, self_wind, game_wind) * 2;
114+
}
115+
return fu;
116+
}
117+
118+
// ─── Sub-step: meld fu ───────────────────────────────────────────────────────
119+
//
120+
// For koutsu (刻子): yaochu=8fu, chunchan=4fu
121+
// For minkou (明刻/副露碰): yaochu=4fu, chunchan=2fu
122+
// For ankou/kan (暗刻): yaochu=8fu, chunchan=4fu
123+
// For minkan (大明杠): yaochu=16fu, chunchan=8fu
124+
// For ankan (暗杠): yaochu=32fu, chunchan=16fu
125+
//
126+
// A kantsu with '-' is minkan, '+' is ankan.
127+
128+
static int fu_for_koutsu(bool yaochu) { return yaochu ? 8 : 4; }
129+
static int fu_for_minkou(bool yaochu) { return yaochu ? 4 : 2; }
130+
static int fu_for_minkan(bool yaochu) { return yaochu ? 16 : 8; }
131+
static int fu_for_ankan(bool yaochu) { return yaochu ? 32 : 16; }
132+
133+
int fu_from_melds(const std::vector<std::string>& tgs) {
134+
int fu = 0;
135+
for (const auto& s : tgs) {
136+
if (s.size() == 3 && s[2] == 'K') {
137+
// Closed koutsu (暗刻 or 明刻)
138+
fu += fu_for_koutsu(has_yaochu_or_z(s));
139+
}
140+
else if (s.size() == 4) {
141+
switch (s[2]) {
142+
case 'S':
143+
// Shuntsu: no fu
144+
break;
145+
case 'K':
146+
// Pon: either tsumo pon (!@#) or fuuro pon (-)
147+
if (s[3] == '!' || s[3] == '@' || s[3] == '#') {
148+
// Tsumo pon — counted as closed koutsu (ankou)
149+
fu += fu_for_koutsu(has_yaochu_or_z(s));
150+
}
151+
else if (s[3] == '$' || s[3] == '%' || s[3] == '^' || s[3] == '-') {
152+
// Ron pon (including fuuro pon '-')
153+
fu += fu_for_minkou(has_yaochu_or_z(s));
154+
}
155+
break;
156+
case '|':
157+
if (s[3] == '-') {
158+
// Minkan (大明杠)
159+
fu += fu_for_minkan(has_yaochu_or_z(s));
160+
}
161+
else if (s[3] == '+') {
162+
// Ankan (暗杠)
163+
fu += fu_for_ankan(has_yaochu_or_z(s));
164+
}
165+
break;
166+
}
167+
}
168+
}
169+
return fu;
170+
}
171+
172+
// ─── Round up ─────────────────────────────────────────────────────────────────
173+
174+
int round_up_fu(int fu) {
175+
if (fu == 25) return fu; // chiitoitsu: fixed 25 fu, no rounding
176+
if (fu % 10 == 0) return fu;
177+
return (fu / 10 + 1) * 10;
178+
}
179+
180+
// ─── Special hand type ────────────────────────────────────────────────────────
181+
182+
bool is_chiitoitsu(const std::vector<std::string>& tgs) {
183+
return tgs.size() == 7;
184+
}
185+
186+
// ─── Main entry point ────────────────────────────────────────────────────────
187+
188+
int calculate_fu(const std::vector<std::string>& tgs,
189+
Wind self_wind, Wind game_wind,
190+
bool menzen, bool is_pinfu) {
191+
int fu = 20; // base fu
192+
193+
fu += fu_from_wait_shape(tgs);
194+
fu += fu_from_winning_method(tgs, menzen, is_pinfu);
195+
fu += fu_from_head(tgs, self_wind, game_wind);
196+
fu += fu_from_melds(tgs);
197+
198+
// Fuuro pinfu: if ron in non-menzen with fu==20, bump to 30
199+
// (This is the "extra fu" for ron in a hand that would otherwise be 20-fu pinfu)
200+
if (is_ron_wait(tgs) && !menzen && fu == 20) {
201+
fu = 30;
202+
}
203+
204+
// Pinfu tsumo: always 20 fu
205+
if (is_pinfu && is_tsumo_wait(tgs)) {
206+
fu = 20;
207+
}
208+
209+
// Chiitoitsu: fixed 25 fu
210+
if (is_chiitoitsu(tgs)) {
211+
return 25;
212+
}
213+
214+
return round_up_fu(fu);
215+
}
216+
217+
} // namespace fu_calculator
218+
namespace_mahjong_end

Mahjong/FuCalculator.h

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#ifndef FU_CALCULATOR_H
2+
#define FU_CALCULATOR_H
3+
4+
#include <vector>
5+
#include "macro.h"
6+
7+
namespace_mahjong
8+
9+
/**
10+
* FuCalculator — pure-function fu (符) calculator.
11+
*
12+
* Extracted from get_hand_yakus() to isolate the fu calculation logic.
13+
* All functions are pure (no side effects).
14+
*
15+
* Fu breakdown (standard Riichi Mahjong):
16+
* Base fu: 20 (menzen) or 20 (fuuro) — both start at 20
17+
* Wait type: tanki (+2), penchan (+2), kanchan (+2)
18+
* Win method: tsumo (+2, non-pinfu), ron (+10, fuuro)
19+
* Head: yakuhai toitsu (+2 per)
20+
* Melds: koutsu yaochu(+8) / others(+4)
21+
* minkou yaochu(+4) / others(+2)
22+
* ankou yaochu(+8) / others(+4) -- tsumo pon
23+
* minkan yaochu(+16)/ others(+8)
24+
* ankan yaochu(+32)/ others(+16)
25+
* Round up: fu = ((fu + 9) / 10) * 10 (not applied to chiitoitsu)
26+
*
27+
* Reference: https://en.wikipedia.org/wiki/Fu_(mahjong)
28+
*/
29+
30+
namespace fu_calculator {
31+
32+
/**
33+
* Calculate fu for a completed hand.
34+
*
35+
* @param tgs tile group strings (with position marks)
36+
* @param self_wind player's seat wind
37+
* @param game_wind current game wind
38+
* @param menzen true if hand is closed (no fuuro calls)
39+
* @param is_pinfu true if pinfu yaku was detected (affects tsumo fu)
40+
* @return fu value, already rounded to nearest 10
41+
*/
42+
int calculate_fu(
43+
const std::vector<std::string>& tgs,
44+
Wind self_wind, Wind game_wind,
45+
bool menzen, bool is_pinfu
46+
);
47+
48+
/**
49+
* Fu from agari shape (tanki / penchan / kanchan).
50+
*/
51+
int fu_from_wait_shape(const std::vector<std::string>& tgs);
52+
53+
/**
54+
* Fu from winning method (tsumo / ron).
55+
* @param tgs tile group strings
56+
* @param menzen hand is closed
57+
* @param is_pinfu pinfu detected (tsumo gives no fu if pinfu)
58+
*/
59+
int fu_from_winning_method(
60+
const std::vector<std::string>& tgs,
61+
bool menzen, bool is_pinfu
62+
);
63+
64+
/**
65+
* Fu from head (toitsu) — yakuhai toitsu gives +2 fu each.
66+
*/
67+
int fu_from_head(
68+
const std::vector<std::string>& tgs,
69+
Wind self_wind, Wind game_wind
70+
);
71+
72+
/**
73+
* Fu from melds (koutsu / shuntsu / kantsu).
74+
* Handles both closed (ankou) and called (minkou/minkan) melds.
75+
*/
76+
int fu_from_melds(const std::vector<std::string>& tgs);
77+
78+
/**
79+
* Round fu up to the nearest 10.
80+
* Returns fu unchanged if already a multiple of 10.
81+
* NOTE: chiitoitsu (7 toitsu) uses fixed 25 fu, not rounded.
82+
*/
83+
int round_up_fu(int fu);
84+
85+
/**
86+
* Returns true if the hand is chiitoitsu (七对子, 7 toitsu).
87+
*/
88+
bool is_chiitoitsu(const std::vector<std::string>& tgs);
89+
90+
/**
91+
* Returns true if the hand has a kanchan wait (隔 Loret).
92+
* A kanchan is a middle wait on a shuntsu: e.g. waiting on 5 in 3-4-5.
93+
* Detected by: shuntsu with mark '@' (tsumo 2nd) or '%' (ron 2nd).
94+
*/
95+
bool has_kanchan_wait(const std::vector<std::string>& tgs);
96+
97+
/**
98+
* Returns true if the hand has a penchan wait (边张).
99+
* A penchan is a wait on 1-2-3 or 7-8-9: waiting on 1 or 3 in 1-2-3.
100+
* Detected by: shuntsu starting with 1 or 7 with mark '#', '^', '!', '$'.
101+
*/
102+
bool has_penchan_wait(const std::vector<std::string>& tgs);
103+
104+
} // namespace fu_calculator
105+
106+
namespace_mahjong_end
107+
108+
#endif // FU_CALCULATOR_H

0 commit comments

Comments
 (0)