Coverage Report

Created: 2025-09-03 03:49

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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