/nix/store/i1aar97n7b4yf8rk94p66if25brfvvdx-gqvzl8a5pvrg3xj44q0msrndcripi8dk-source/src/scoring/scoring.cc
Line | Count | Source |
1 | | #include "scoring/scoring.h" |
2 | | #include <algorithm> |
3 | | #include <vector> |
4 | | |
5 | | #include "analysis/analysis.h" |
6 | | #include "analysis/util.h" |
7 | | #include "scoring/yakus.h" |
8 | | #include "scoring/yakus/pinfu.h" |
9 | | #include "scoring/yakus/sevenpairs.h" |
10 | | #include "scoring/yakus/thirteenorphans.h" |
11 | | #include "types/gamestate.h" |
12 | | #include "types/pieces.h" |
13 | | #include "types/piecetype.h" |
14 | | #include "types/score.h" |
15 | | #include "types/sets.h" |
16 | | |
17 | | namespace mahjong { |
18 | | |
19 | | namespace { |
20 | | const int kLowHanBasicPoints = 2000; |
21 | | const int kManganBasicPoints = 2000; |
22 | | const int kManganMinHan = 5; |
23 | | const int kHanemanBasicPoints = 3000; |
24 | | const int kHanemanMinHan = 6; |
25 | | const int kBaimanBasicPoints = 4000; |
26 | | const int kBaimanMinHan = 8; |
27 | | const int kSanbaimanBasicPoints = 6000; |
28 | | const int kSanbaimanMinHan = 11; |
29 | | const int kYakumanBasicPoints = 8000; |
30 | | |
31 | | const int kPinfuDiscard = 30; |
32 | | const int kPinfuSelfdraw = 20; |
33 | | const int kConcealedDiscard = 25; |
34 | | const int kOpenOrSelfdraw = 20; |
35 | | |
36 | | const int kSimplepon = 2; |
37 | | const int kTermHonorpon = 4; |
38 | | const int kSimplekan = 8; |
39 | | const int kTermHonorkan = 16; |
40 | | |
41 | | const int kConcealedMul = 2; |
42 | | const int kCsimplepon = kSimplepon * kConcealedMul; |
43 | | const int kCtermHonorpon = kTermHonorpon * kConcealedMul; |
44 | | const int kCsimplekan = kSimplekan * kConcealedMul; |
45 | | const int kCtermHonorkan = kTermHonorkan * kConcealedMul; |
46 | | |
47 | | const int kDragonseatprevalentwind = 2; |
48 | | const int kEdgeclosedpairwait = 2; |
49 | | const int kSelfdraw = 2; |
50 | | const int kOpenpinfu = 2; |
51 | | |
52 | | const int kLowedgewait = 3; |
53 | | const int kHighedgewait = 7; |
54 | | |
55 | | const int kFuRounding = 10; |
56 | | |
57 | | } // namespace |
58 | | |
59 | 0 | Score scoreHand(const GameState& state, int player) { |
60 | 0 | auto root = breakdownHand(state.hands.at(player).live); |
61 | 0 | Score s; |
62 | 0 | s.han = 0; |
63 | 0 | s.yakuman = 0; |
64 | 0 | s.fu = 0; |
65 | 0 | if (!root->IsComplete() && !yaku::isThirteenOrphans(state, player)) { |
66 | 0 | return s; |
67 | 0 | } |
68 | 0 | for (const auto& branch : Node::AsBranchVectors(root.get())) { |
69 | 0 | Score branchscore; |
70 | 0 | if (std::ranges::any_of(branch, [](const auto* node) { |
71 | 0 | return node->type() == SetType::kSingle; |
72 | 0 | })) { |
73 | 0 | continue; |
74 | 0 | } |
75 | | |
76 | 0 | for (const auto& yaku : Yakus::Instance().GetYakus()) { |
77 | 0 | if (yaku.type == Yaku::kClosed && state.hands[player].open) { |
78 | 0 | continue; |
79 | 0 | } |
80 | 0 | if (!yaku.is_yaku_func(state, player, branch)) { |
81 | 0 | continue; |
82 | 0 | } |
83 | 0 | switch (yaku.type) { |
84 | 0 | case Yaku::kOpen: |
85 | 0 | case Yaku::kClosed: |
86 | 0 | branchscore.han += yaku.value; |
87 | 0 | break; |
88 | 0 | case Yaku::kBonusWhenClosed: |
89 | 0 | branchscore.han += yaku.value + (state.hands[player].open ? 0 : 1); |
90 | 0 | break; |
91 | 0 | case Yaku::kYakuman: |
92 | 0 | branchscore.yakuman++; |
93 | 0 | break; |
94 | 0 | } |
95 | 0 | } |
96 | | |
97 | 0 | for (const auto& dora : state.walls.GetDoras()) { |
98 | 0 | for (const auto& p : state.hands.at(player).live) { |
99 | 0 | if (p == dora) { |
100 | 0 | branchscore.han++; |
101 | 0 | } |
102 | 0 | } |
103 | 0 | for (const auto& meld : state.hands.at(player).melds) { |
104 | 0 | if (meld.start == dora) { |
105 | 0 | branchscore.han++; |
106 | 0 | } |
107 | 0 | if (meld.type == SetType::kChi) { |
108 | 0 | if (meld.start + 1 == dora) { |
109 | 0 | branchscore.han++; |
110 | 0 | } |
111 | 0 | if (meld.start + 2 == dora) { |
112 | 0 | branchscore.han++; |
113 | 0 | } |
114 | 0 | } |
115 | 0 | } |
116 | 0 | } |
117 | 0 | branchscore.fu = getFu(state, player, branch); |
118 | 0 | if (getBasicPoints(branchscore) > getBasicPoints(s)) { |
119 | 0 | s = branchscore; |
120 | 0 | } |
121 | 0 | } |
122 | | |
123 | 0 | return s; |
124 | 0 | } |
125 | | |
126 | 0 | int getBasicPoints(Score s) { |
127 | 0 | if (s.yakuman > 0) { |
128 | 0 | return s.yakuman * kYakumanBasicPoints; |
129 | 0 | } |
130 | 0 | if (s.han >= kManganMinHan) { |
131 | 0 | if (s.han >= kSanbaimanMinHan) { |
132 | 0 | return kSanbaimanBasicPoints; |
133 | 0 | } |
134 | 0 | if (s.han >= kBaimanMinHan) { |
135 | 0 | return kBaimanBasicPoints; |
136 | 0 | } |
137 | 0 | if (s.han >= kHanemanMinHan) { |
138 | 0 | return kHanemanBasicPoints; |
139 | 0 | } |
140 | 0 | if (s.han >= kSanbaimanMinHan) { |
141 | 0 | return kSanbaimanBasicPoints; |
142 | 0 | } |
143 | 0 | if (s.han >= kManganMinHan) { |
144 | 0 | return kManganBasicPoints; |
145 | 0 | } |
146 | 0 | } |
147 | 0 | const int p = s.fu * (2 << (1 + s.han)); |
148 | 0 | if (p > kLowHanBasicPoints) { |
149 | 0 | return kLowHanBasicPoints; |
150 | 0 | } |
151 | 0 | return p; |
152 | 0 | } |
153 | | |
154 | | const int kSevenpairs = 25; |
155 | | |
156 | | int getFu(const GameState& state, int player, |
157 | 0 | const std::vector<const mahjong::Node*>& branch) { |
158 | 0 | if (yaku::isSevenPairs(state, player, branch)) { |
159 | 0 | return kSevenpairs; |
160 | 0 | } |
161 | 0 | if (yaku::isPinfu(state, player, branch)) { |
162 | 0 | if (state.hasRonned.at(player)) { |
163 | 0 | return kPinfuDiscard; |
164 | 0 | } |
165 | 0 | return kPinfuSelfdraw; |
166 | 0 | } |
167 | 0 | int fu = 0; |
168 | 0 | if (!state.hands.at(player).open && state.hasRonned.at(player)) { |
169 | 0 | fu = kConcealedDiscard; |
170 | 0 | } else { |
171 | 0 | fu = kOpenOrSelfdraw; |
172 | 0 | } |
173 | 0 | const bool open = state.hands.at(player).open; |
174 | 0 | if (isOpenPinfu(state, player, branch)) { |
175 | 0 | fu += kOpenpinfu; |
176 | 0 | } else if (!state.hasRonned.at(player)) { |
177 | 0 | fu += kSelfdraw; |
178 | 0 | } |
179 | 0 | for (const auto& meld : state.hands.at(player).melds) { |
180 | 0 | if (meld.type == SetType::kKan) { |
181 | 0 | if (!meld.start.isHonor() && !meld.start.isTerminal()) { |
182 | 0 | fu += open ? kSimplekan : kCsimplekan; |
183 | 0 | } else { |
184 | 0 | fu += open ? kTermHonorkan : kCtermHonorkan; |
185 | 0 | } |
186 | 0 | } |
187 | 0 | } |
188 | 0 | for (const auto* node : branch) { |
189 | 0 | if (node->type() == SetType::kPon) { |
190 | 0 | if (!node->start().isHonor() && !node->start().isTerminal()) { |
191 | 0 | fu += open ? kSimplepon : kCsimplepon; |
192 | 0 | } else { |
193 | 0 | fu += open ? kTermHonorpon : kCtermHonorpon; |
194 | 0 | } |
195 | 0 | } |
196 | 0 | if (open) { |
197 | 0 | continue; |
198 | 0 | } |
199 | 0 | if (node->type() == SetType::kPair) { |
200 | 0 | if (node->start().isHonor()) { |
201 | 0 | if (node->start() == kGreenDragon || node->start() == kRedDragon || |
202 | 0 | node->start() == kWhiteDragon || |
203 | 0 | node->start() == (state.roundNum > 3 ? kSouthWind : kEastWind)) { |
204 | 0 | fu += kDragonseatprevalentwind; |
205 | 0 | } |
206 | 0 | } |
207 | 0 | if (node->start() == state.pendingPiece) { |
208 | 0 | fu += kEdgeclosedpairwait; |
209 | 0 | } |
210 | 0 | } |
211 | 0 | if (node->type() == SetType::kChi) { |
212 | 0 | if (node->start() + 1 == state.pendingPiece) { |
213 | 0 | fu += kEdgeclosedpairwait; |
214 | 0 | } |
215 | 0 | if (node->start().getPieceNum() == 1 && |
216 | 0 | state.pendingPiece.getPieceNum() == kLowedgewait && |
217 | 0 | node->start().getSuit() == state.pendingPiece.getSuit()) { |
218 | 0 | fu += kEdgeclosedpairwait; |
219 | 0 | } |
220 | 0 | if (node->start().getPieceNum() == kHighedgewait && |
221 | 0 | state.pendingPiece == node->start()) { |
222 | 0 | fu += kEdgeclosedpairwait; |
223 | 0 | } |
224 | 0 | } |
225 | 0 | } |
226 | 0 | if ((fu % kFuRounding) != 0) { |
227 | 0 | return fu; |
228 | 0 | } |
229 | 0 | return fu + |
230 | 0 | (kFuRounding - (fu % kFuRounding)); // rounding up to multiple of ten |
231 | 0 | } |
232 | | |
233 | | bool isOpenPinfu(const GameState& state, int player, |
234 | 0 | const std::vector<const mahjong::Node*>& branch) { |
235 | 0 | for (const auto* node : branch) { |
236 | 0 | if (node->type() == SetType::kPon) { |
237 | 0 | return false; |
238 | 0 | } |
239 | 0 | if (node->type() == SetType::kPair) { |
240 | 0 | if (node->start() == kRedDragon || node->start() == kWhiteDragon || |
241 | 0 | node->start() == kGreenDragon) { |
242 | 0 | return false; |
243 | 0 | } |
244 | 0 | if (node->start() == kSouthWind && state.roundNum > 3) { |
245 | 0 | return false; |
246 | 0 | } |
247 | 0 | if (node->start() == kEastWind && state.roundNum < 4) { |
248 | 0 | return false; |
249 | 0 | } |
250 | 0 | } |
251 | 0 | } |
252 | 0 | for (const auto& meld : state.hands.at(player).melds) { |
253 | 0 | if (meld.type > SetType::kChi) { |
254 | 0 | return false; |
255 | 0 | } |
256 | 0 | } |
257 | 0 | auto waits = isInTenpai(state.hands.at(player).live, /*allWaits=*/true); |
258 | 0 | return waits.size() != 1; |
259 | 0 | } |
260 | | |
261 | 44 | bool isComplete(const GameState& state, int player) { |
262 | 44 | auto root = breakdownHand(state.hands.at(player).live); |
263 | 44 | if (!root->IsComplete() && !yaku::isThirteenOrphans(state, player)) { |
264 | 43 | return false; |
265 | 43 | } |
266 | 1 | for (const auto& branch : Node::AsBranchVectors(root.get())) { |
267 | 5 | if (std::any_of(branch.begin(), branch.end(), [](const auto* node) { |
268 | 5 | return node->type() == SetType::kSingle; |
269 | 5 | })) { |
270 | 0 | continue; |
271 | 0 | } |
272 | 1 | if (std::ranges::any_of(Yakus::Instance().GetYakus(), |
273 | 5 | [&state, player, &branch](const auto& yaku) { |
274 | 5 | return yaku.is_yaku_func(state, player, branch); |
275 | 5 | })) { |
276 | 1 | return true; |
277 | 1 | } |
278 | 1 | } |
279 | | |
280 | 0 | return false; |
281 | 1 | } |
282 | | } // namespace mahjong |