feat(计分): 实现血战计分V1核心逻辑

- 新增血战计分服务,支持七对、清一色等基础番型及杠上花等特殊加番
- 扩展结算结果结构,包含番型明细与支付分数计算
- 新增PostGangContext记录杠后补摸窗口,用于判断杠上花/杠上炮
- 完善胡牌判定器,新增七对和对对胡识别方法
- 更新开发计划文档,补充注释规范要求
- 添加计分相关单元测试,确保核心逻辑正确性
This commit is contained in:
hujun
2026-03-20 14:50:19 +08:00
parent d038a8732d
commit 34809fd0f3
17 changed files with 804 additions and 29 deletions

View File

@@ -17,6 +17,8 @@ public class GameSession {
private final GameTable table;
private final List<GameEvent> events;
private ResponseActionWindow pendingResponseActionWindow;
// 记录最近一次杠后补摸窗口,用于判断杠上花与杠上炮。
private PostGangContext postGangContext;
private final Map<Integer, ActionType> responseActionSelections;
public GameSession(String roomId, GameTable table) {
@@ -56,6 +58,14 @@ public class GameSession {
this.pendingResponseActionWindow = pendingResponseActionWindow;
}
public PostGangContext getPostGangContext() {
return postGangContext;
}
public void setPostGangContext(PostGangContext postGangContext) {
this.postGangContext = postGangContext;
}
public Map<Integer, ActionType> getResponseActionSelections() {
return responseActionSelections;
}
@@ -63,4 +73,8 @@ public class GameSession {
public void clearResponseActionSelections() {
responseActionSelections.clear();
}
public void clearPostGangContext() {
this.postGangContext = null;
}
}

View File

@@ -0,0 +1,7 @@
package com.xuezhanmaster.game.domain;
public record PostGangContext(
int seatNo,
String tileDisplayName
) {
}

View File

@@ -0,0 +1,11 @@
package com.xuezhanmaster.game.domain;
import java.util.List;
public record SettlementDetail(
int baseScore,
int totalFan,
int paymentScore,
List<SettlementFan> fans
) {
}

View File

@@ -0,0 +1,8 @@
package com.xuezhanmaster.game.domain;
public record SettlementFan(
String code,
String label,
int fan
) {
}

View File

@@ -8,6 +8,7 @@ public record SettlementResult(
int actorSeatNo,
int sourceSeatNo,
String triggerTile,
SettlementDetail settlementDetail,
List<ScoreChange> scoreChanges
) {
}

View File

@@ -1,6 +1,8 @@
package com.xuezhanmaster.game.event;
import com.xuezhanmaster.game.domain.ScoreChange;
import com.xuezhanmaster.game.domain.SettlementDetail;
import com.xuezhanmaster.game.domain.SettlementFan;
import com.xuezhanmaster.game.domain.SettlementResult;
import java.time.Instant;
@@ -92,6 +94,7 @@ public record GameEvent(
payload.put("actorSeatNo", settlementResult.actorSeatNo());
payload.put("sourceSeatNo", settlementResult.sourceSeatNo());
payload.put("triggerTile", settlementResult.triggerTile());
payload.put("settlementDetail", toSettlementDetailPayload(settlementResult.settlementDetail()));
payload.put("scoreChanges", toScoreChangePayload(settlementResult.scoreChanges()));
return of(gameId, GameEventType.SETTLEMENT_APPLIED, settlementResult.actorSeatNo(), payload);
}
@@ -143,4 +146,21 @@ public record GameEvent(
}
return payload;
}
private static Map<String, Object> toSettlementDetailPayload(SettlementDetail settlementDetail) {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("baseScore", settlementDetail.baseScore());
payload.put("totalFan", settlementDetail.totalFan());
payload.put("paymentScore", settlementDetail.paymentScore());
List<Map<String, Object>> fans = new ArrayList<>();
for (SettlementFan settlementFan : settlementDetail.fans()) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("code", settlementFan.code());
item.put("label", settlementFan.label());
item.put("fan", settlementFan.fan());
fans.add(item);
}
payload.put("fans", fans);
return payload;
}
}

View File

@@ -0,0 +1,167 @@
package com.xuezhanmaster.game.service;
import com.xuezhanmaster.game.domain.GameSeat;
import com.xuezhanmaster.game.domain.MeldGroup;
import com.xuezhanmaster.game.domain.MeldType;
import com.xuezhanmaster.game.domain.SettlementDetail;
import com.xuezhanmaster.game.domain.SettlementFan;
import com.xuezhanmaster.game.domain.Tile;
import com.xuezhanmaster.game.domain.TileSuit;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Component
public class BloodBattleScoringService {
private static final int BASE_SCORE = 1;
private static final int EXPOSED_GANG_SCORE = 2;
private static final int SUPPLEMENTAL_GANG_SCORE = 1;
private static final int CONCEALED_GANG_SCORE = 2;
private final HuEvaluator huEvaluator;
public BloodBattleScoringService(HuEvaluator huEvaluator) {
this.huEvaluator = huEvaluator;
}
public SettlementDetail buildDiscardHuDetail(
GameSeat winnerSeat,
boolean gangShangPao,
boolean haiDiPao
) {
return buildHuDetail(winnerSeat, false, false, gangShangPao, false, haiDiPao);
}
public SettlementDetail buildSelfDrawHuDetail(
GameSeat winnerSeat,
boolean gangShangHua,
boolean haiDiLaoYue
) {
// V1 先采用统一基础口径,不叠加地方“自摸加底/加番”变体。
return buildHuDetail(winnerSeat, false, gangShangHua, false, haiDiLaoYue, false);
}
public SettlementDetail buildRobbingGangHuDetail(GameSeat winnerSeat) {
return buildHuDetail(winnerSeat, true, false, false, false, false);
}
public SettlementDetail buildExposedGangDetail() {
return new SettlementDetail(BASE_SCORE, 0, EXPOSED_GANG_SCORE, List.of());
}
public SettlementDetail buildSupplementalGangDetail() {
return new SettlementDetail(BASE_SCORE, 0, SUPPLEMENTAL_GANG_SCORE, List.of());
}
public SettlementDetail buildConcealedGangDetail() {
return new SettlementDetail(BASE_SCORE, 0, CONCEALED_GANG_SCORE, List.of());
}
private SettlementDetail buildHuDetail(
GameSeat winnerSeat,
boolean robbingGang,
boolean gangShangHua,
boolean gangShangPao,
boolean haiDiLaoYue,
boolean haiDiPao
) {
List<SettlementFan> fans = new ArrayList<>();
if (huEvaluator.isSevenPairs(winnerSeat.getHandTiles())) {
fans.add(new SettlementFan("QI_DUI", "七对", 2));
} else {
if (huEvaluator.isPengPengHu(winnerSeat.getHandTiles())) {
fans.add(new SettlementFan("DUI_DUI_HU", "对对胡", 1));
}
if (isJinGouDiao(winnerSeat)) {
fans.add(new SettlementFan("JIN_GOU_DIAO", "金钩钓", 1));
}
}
if (isQingYiSe(winnerSeat)) {
fans.add(new SettlementFan("QING_YI_SE", "清一色", 2));
}
int genCount = countGen(winnerSeat);
for (int i = 0; i < genCount; i++) {
fans.add(new SettlementFan("GEN", "", 1));
}
if (gangShangHua) {
fans.add(new SettlementFan("GANG_SHANG_HUA", "杠上花", 1));
}
if (gangShangPao) {
fans.add(new SettlementFan("GANG_SHANG_PAO", "杠上炮", 1));
}
if (haiDiLaoYue) {
fans.add(new SettlementFan("HAI_DI_LAO_YUE", "海底捞月", 1));
}
if (haiDiPao) {
fans.add(new SettlementFan("HAI_DI_PAO", "海底炮", 1));
}
if (robbingGang) {
fans.add(new SettlementFan("QIANG_GANG_HU", "抢杠胡", 1));
}
int totalFan = fans.stream()
.mapToInt(SettlementFan::fan)
.sum();
int paymentScore = BASE_SCORE << totalFan;
return new SettlementDetail(BASE_SCORE, totalFan, paymentScore, List.copyOf(fans));
}
private boolean isJinGouDiao(GameSeat winnerSeat) {
return winnerSeat.getMeldGroups().size() == 4
&& winnerSeat.getHandTiles().size() == 2
&& winnerSeat.getHandTiles().get(0).equals(winnerSeat.getHandTiles().get(1));
}
private boolean isQingYiSe(GameSeat winnerSeat) {
TileSuit firstSuit = null;
for (Tile tile : winnerSeat.getHandTiles()) {
if (firstSuit == null) {
firstSuit = tile.getSuit();
continue;
}
if (tile.getSuit() != firstSuit) {
return false;
}
}
for (MeldGroup meldGroup : winnerSeat.getMeldGroups()) {
Tile meldTile = meldGroup.tile();
if (firstSuit == null) {
firstSuit = meldTile.getSuit();
continue;
}
if (meldTile.getSuit() != firstSuit) {
return false;
}
}
return firstSuit != null;
}
private int countGen(GameSeat winnerSeat) {
Map<Tile, Integer> tileCounts = new LinkedHashMap<>();
for (Tile tile : winnerSeat.getHandTiles()) {
tileCounts.merge(tile, 1, Integer::sum);
}
for (MeldGroup meldGroup : winnerSeat.getMeldGroups()) {
tileCounts.merge(meldGroup.tile(), tileCountForMeld(meldGroup.type()), Integer::sum);
}
int genCount = 0;
for (Integer count : tileCounts.values()) {
if (count == 4) {
genCount++;
}
}
return genCount;
}
private int tileCountForMeld(MeldType meldType) {
return switch (meldType) {
case PENG -> 3;
case MING_GANG, BU_GANG, AN_GANG -> 4;
};
}
}

View File

@@ -6,6 +6,7 @@ import com.xuezhanmaster.game.domain.GamePhase;
import com.xuezhanmaster.game.domain.GameSeat;
import com.xuezhanmaster.game.domain.GameSession;
import com.xuezhanmaster.game.domain.GameTable;
import com.xuezhanmaster.game.domain.PostGangContext;
import com.xuezhanmaster.game.domain.ResponseActionResolution;
import com.xuezhanmaster.game.domain.ResponseActionSeatCandidate;
import com.xuezhanmaster.game.domain.ResponseActionWindow;
@@ -495,6 +496,7 @@ public class GameSessionService {
}
private void continueAfterDiscardWithoutResponse(GameSession session) {
session.clearPostGangContext();
moveToNextSeat(session.getTable(), session.getGameId());
autoPlayBots(session);
notifyActionIfHumanTurn(session);
@@ -526,6 +528,7 @@ public class GameSessionService {
}
private void executePeng(GameSession session, ResponseActionResolution resolution) {
session.clearPostGangContext();
Tile claimedTile = parseTile(resolution.triggerTile());
GameTable table = session.getTable();
GameSeat winnerSeat = table.getSeats().get(resolution.winnerSeatNo());
@@ -548,6 +551,7 @@ public class GameSessionService {
}
private void executeGang(GameSession session, ResponseActionResolution resolution) {
session.clearPostGangContext();
Tile claimedTile = parseTile(resolution.triggerTile());
GameTable table = session.getTable();
GameSeat winnerSeat = table.getSeats().get(resolution.winnerSeatNo());
@@ -580,6 +584,7 @@ public class GameSessionService {
Tile drawnTile = table.getWallTiles().remove(0);
winnerSeat.receiveTile(drawnTile);
markPostGangContext(session, winnerSeat.getSeatNo(), claimedTile.getDisplayName());
appendAndPublish(session, GameEvent.tileDrawn(session.getGameId(), winnerSeat.getSeatNo(), table.getWallTiles().size()));
appendAndPublish(session, GameEvent.turnSwitched(session.getGameId(), winnerSeat.getSeatNo()));
continueFromResolvedActionTurn(session, winnerSeat);
@@ -619,6 +624,11 @@ public class GameSessionService {
sourceSeat.getSeatNo(),
claimedTile.getDisplayName()
));
// 牌墙已空时,这次点炮对应“最后一张牌后的弃牌胡”,按海底炮加番。
boolean gangShangPao = !isSupplementalGangWindow(responseActionWindow)
&& isPostGangDiscard(session, sourceSeat.getSeatNo());
boolean haiDiPao = !isSupplementalGangWindow(responseActionWindow)
&& table.getWallTiles().isEmpty();
if (isSupplementalGangWindow(responseActionWindow)) {
appendSettlementEvents(session, settlementService.settleRobbingGangHu(
table,
@@ -628,12 +638,15 @@ public class GameSessionService {
));
} else {
appendSettlementEvents(session, settlementService.settleDiscardHu(
table,
winnerSeat.getSeatNo(),
sourceSeat.getSeatNo(),
claimedTile.getDisplayName()
table,
winnerSeat.getSeatNo(),
sourceSeat.getSeatNo(),
claimedTile.getDisplayName(),
gangShangPao,
haiDiPao
));
}
session.clearPostGangContext();
if (shouldFinishTable(table)) {
table.setPhase(GamePhase.FINISHED);
@@ -647,6 +660,9 @@ public class GameSessionService {
private void executeSelfDrawHu(GameSession session, String userId) {
GameTable table = session.getTable();
GameSeat winnerSeat = findSeatByUserId(table, userId);
// 自摸胡时牌墙已空,说明当前这张就是最后一张牌,按海底捞月加番。
boolean gangShangHua = isPostGangDraw(session, winnerSeat.getSeatNo());
boolean haiDiLaoYue = table.getWallTiles().isEmpty();
winnerSeat.declareHu();
appendAndPublish(session, GameEvent.responseActionDeclared(
@@ -659,8 +675,11 @@ public class GameSessionService {
appendSettlementEvents(session, settlementService.settleSelfDrawHu(
table,
winnerSeat.getSeatNo(),
null
null,
gangShangHua,
haiDiLaoYue
));
session.clearPostGangContext();
if (shouldFinishTable(table)) {
table.setPhase(GamePhase.FINISHED);
@@ -703,6 +722,7 @@ public class GameSessionService {
Tile drawnTile = table.getWallTiles().remove(0);
winnerSeat.receiveTile(drawnTile);
markPostGangContext(session, winnerSeat.getSeatNo(), gangTile.getDisplayName());
appendAndPublish(session, GameEvent.tileDrawn(session.getGameId(), winnerSeat.getSeatNo(), table.getWallTiles().size()));
appendAndPublish(session, GameEvent.turnSwitched(session.getGameId(), winnerSeat.getSeatNo()));
continueFromResolvedActionTurn(session, winnerSeat);
@@ -764,6 +784,7 @@ public class GameSessionService {
Tile drawnTile = table.getWallTiles().remove(0);
winnerSeat.receiveTile(drawnTile);
markPostGangContext(session, winnerSeat.getSeatNo(), gangTile.getDisplayName());
appendAndPublish(session, GameEvent.tileDrawn(session.getGameId(), winnerSeat.getSeatNo(), table.getWallTiles().size()));
appendAndPublish(session, GameEvent.turnSwitched(session.getGameId(), winnerSeat.getSeatNo()));
continueFromResolvedActionTurn(session, winnerSeat);
@@ -815,6 +836,21 @@ public class GameSessionService {
return huEvaluator.canHu(seat.getHandTiles());
}
private void markPostGangContext(GameSession session, int seatNo, String tileDisplayName) {
// 杠后补摸只对当前这名玩家的下一次自摸/弃牌裁决有效。
session.setPostGangContext(new PostGangContext(seatNo, tileDisplayName));
}
private boolean isPostGangDraw(GameSession session, int seatNo) {
PostGangContext postGangContext = session.getPostGangContext();
return postGangContext != null && postGangContext.seatNo() == seatNo;
}
private boolean isPostGangDiscard(GameSession session, int sourceSeatNo) {
PostGangContext postGangContext = session.getPostGangContext();
return postGangContext != null && postGangContext.seatNo() == sourceSeatNo;
}
private boolean isSupplementalGangWindow(ResponseActionWindow responseActionWindow) {
return "SUPPLEMENTAL_GANG_DECLARED".equals(responseActionWindow.triggerEventType());
}

View File

@@ -14,6 +14,9 @@ public class HuEvaluator {
if (tiles.size() % 3 != 2) {
return false;
}
if (isSevenPairs(tiles)) {
return true;
}
int[] counts = buildCounts(tiles);
for (int i = 0; i < counts.length; i++) {
@@ -29,6 +32,45 @@ public class HuEvaluator {
return false;
}
public boolean isSevenPairs(List<Tile> tiles) {
if (tiles.size() != 14) {
return false;
}
int[] counts = buildCounts(tiles);
int pairCount = 0;
for (int count : counts) {
if (count == 0) {
continue;
}
if (count != 2 && count != 4) {
return false;
}
pairCount += count / 2;
}
return pairCount == 7;
}
public boolean isPengPengHu(List<Tile> tiles) {
if (tiles.size() % 3 != 2) {
return false;
}
int[] counts = buildCounts(tiles);
for (int i = 0; i < counts.length; i++) {
if (counts[i] < 2) {
continue;
}
counts[i] -= 2;
if (canFormTripletsOnly(counts)) {
counts[i] += 2;
return true;
}
counts[i] += 2;
}
return false;
}
public boolean canHuWithClaimedTile(List<Tile> handTiles, Tile claimedTile) {
List<Tile> tiles = new ArrayList<>(handTiles);
tiles.add(claimedTile);
@@ -73,6 +115,20 @@ public class HuEvaluator {
return false;
}
private boolean canFormTripletsOnly(int[] counts) {
int firstIndex = firstNonZeroIndex(counts);
if (firstIndex == -1) {
return true;
}
if (counts[firstIndex] < 3) {
return false;
}
counts[firstIndex] -= 3;
boolean result = canFormTripletsOnly(counts);
counts[firstIndex] += 3;
return result;
}
private int firstNonZeroIndex(int[] counts) {
for (int i = 0; i < counts.length; i++) {
if (counts[i] > 0) {

View File

@@ -4,6 +4,7 @@ import com.xuezhanmaster.game.domain.ActionType;
import com.xuezhanmaster.game.domain.GameSeat;
import com.xuezhanmaster.game.domain.GameTable;
import com.xuezhanmaster.game.domain.ScoreChange;
import com.xuezhanmaster.game.domain.SettlementDetail;
import com.xuezhanmaster.game.domain.SettlementResult;
import com.xuezhanmaster.game.domain.SettlementType;
import org.springframework.stereotype.Service;
@@ -16,18 +17,25 @@ import java.util.Map;
@Service
public class SettlementService {
private static final int DIAN_PAO_HU_SCORE = 1;
private static final int ZI_MO_HU_SCORE = 1;
private static final int BU_GANG_SCORE = 1;
private static final int MING_GANG_SCORE = 1;
private static final int AN_GANG_SCORE = 2;
private final BloodBattleScoringService scoringService;
public SettlementService(BloodBattleScoringService scoringService) {
this.scoringService = scoringService;
}
public SettlementResult settleDiscardHu(
GameTable table,
int winnerSeatNo,
int sourceSeatNo,
String triggerTile
String triggerTile,
boolean gangShangPao,
boolean haiDiPao
) {
SettlementDetail settlementDetail = scoringService.buildDiscardHuDetail(
table.getSeats().get(winnerSeatNo),
gangShangPao,
haiDiPao
);
return applySettlement(
table,
SettlementType.DIAN_PAO_HU,
@@ -35,7 +43,13 @@ public class SettlementService {
winnerSeatNo,
sourceSeatNo,
triggerTile,
orderedDeltas(winnerSeatNo, DIAN_PAO_HU_SCORE, sourceSeatNo, -DIAN_PAO_HU_SCORE)
settlementDetail,
orderedDeltas(
winnerSeatNo,
settlementDetail.paymentScore(),
sourceSeatNo,
-settlementDetail.paymentScore()
)
);
}
@@ -45,6 +59,7 @@ public class SettlementService {
int sourceSeatNo,
String triggerTile
) {
SettlementDetail settlementDetail = scoringService.buildExposedGangDetail();
return applySettlement(
table,
SettlementType.MING_GANG,
@@ -52,23 +67,36 @@ public class SettlementService {
winnerSeatNo,
sourceSeatNo,
triggerTile,
orderedDeltas(winnerSeatNo, MING_GANG_SCORE, sourceSeatNo, -MING_GANG_SCORE)
settlementDetail,
orderedDeltas(
winnerSeatNo,
settlementDetail.paymentScore(),
sourceSeatNo,
-settlementDetail.paymentScore()
)
);
}
public SettlementResult settleSelfDrawHu(
GameTable table,
int winnerSeatNo,
String triggerTile
String triggerTile,
boolean gangShangHua,
boolean haiDiLaoYue
) {
SettlementDetail settlementDetail = scoringService.buildSelfDrawHuDetail(
table.getSeats().get(winnerSeatNo),
gangShangHua,
haiDiLaoYue
);
Map<Integer, Integer> scoreDeltas = new LinkedHashMap<>();
int totalWinScore = 0;
for (GameSeat seat : table.getSeats()) {
if (seat.getSeatNo() == winnerSeatNo || seat.isWon()) {
continue;
}
scoreDeltas.put(seat.getSeatNo(), -ZI_MO_HU_SCORE);
totalWinScore += ZI_MO_HU_SCORE;
scoreDeltas.put(seat.getSeatNo(), -settlementDetail.paymentScore());
totalWinScore += settlementDetail.paymentScore();
}
scoreDeltas.put(winnerSeatNo, totalWinScore);
return applySettlement(
@@ -78,6 +106,7 @@ public class SettlementService {
winnerSeatNo,
winnerSeatNo,
triggerTile,
settlementDetail,
scoreDeltas
);
}
@@ -88,6 +117,7 @@ public class SettlementService {
int sourceSeatNo,
String triggerTile
) {
SettlementDetail settlementDetail = scoringService.buildRobbingGangHuDetail(table.getSeats().get(winnerSeatNo));
return applySettlement(
table,
SettlementType.QIANG_GANG_HU,
@@ -95,7 +125,13 @@ public class SettlementService {
winnerSeatNo,
sourceSeatNo,
triggerTile,
orderedDeltas(winnerSeatNo, DIAN_PAO_HU_SCORE, sourceSeatNo, -DIAN_PAO_HU_SCORE)
settlementDetail,
orderedDeltas(
winnerSeatNo,
settlementDetail.paymentScore(),
sourceSeatNo,
-settlementDetail.paymentScore()
)
);
}
@@ -104,14 +140,15 @@ public class SettlementService {
int winnerSeatNo,
String triggerTile
) {
SettlementDetail settlementDetail = scoringService.buildSupplementalGangDetail();
Map<Integer, Integer> scoreDeltas = new LinkedHashMap<>();
int totalWinScore = 0;
for (GameSeat seat : table.getSeats()) {
if (seat.getSeatNo() == winnerSeatNo || seat.isWon()) {
continue;
}
scoreDeltas.put(seat.getSeatNo(), -BU_GANG_SCORE);
totalWinScore += BU_GANG_SCORE;
scoreDeltas.put(seat.getSeatNo(), -settlementDetail.paymentScore());
totalWinScore += settlementDetail.paymentScore();
}
scoreDeltas.put(winnerSeatNo, totalWinScore);
return applySettlement(
@@ -121,6 +158,7 @@ public class SettlementService {
winnerSeatNo,
winnerSeatNo,
triggerTile,
settlementDetail,
scoreDeltas
);
}
@@ -130,14 +168,15 @@ public class SettlementService {
int winnerSeatNo,
String triggerTile
) {
SettlementDetail settlementDetail = scoringService.buildConcealedGangDetail();
Map<Integer, Integer> scoreDeltas = new LinkedHashMap<>();
int totalWinScore = 0;
for (GameSeat seat : table.getSeats()) {
if (seat.getSeatNo() == winnerSeatNo || seat.isWon()) {
continue;
}
scoreDeltas.put(seat.getSeatNo(), -AN_GANG_SCORE);
totalWinScore += AN_GANG_SCORE;
scoreDeltas.put(seat.getSeatNo(), -settlementDetail.paymentScore());
totalWinScore += settlementDetail.paymentScore();
}
scoreDeltas.put(winnerSeatNo, totalWinScore);
return applySettlement(
@@ -147,6 +186,7 @@ public class SettlementService {
winnerSeatNo,
winnerSeatNo,
triggerTile,
settlementDetail,
scoreDeltas
);
}
@@ -158,6 +198,7 @@ public class SettlementService {
int actorSeatNo,
int sourceSeatNo,
String triggerTile,
SettlementDetail settlementDetail,
Map<Integer, Integer> scoreDeltas
) {
List<ScoreChange> scoreChanges = new ArrayList<>();
@@ -176,6 +217,7 @@ public class SettlementService {
actorSeatNo,
sourceSeatNo,
triggerTile,
settlementDetail,
List.copyOf(scoreChanges)
);
}

View File

@@ -2,6 +2,8 @@ package com.xuezhanmaster.game.event;
import com.xuezhanmaster.game.domain.ActionType;
import com.xuezhanmaster.game.domain.ScoreChange;
import com.xuezhanmaster.game.domain.SettlementDetail;
import com.xuezhanmaster.game.domain.SettlementFan;
import com.xuezhanmaster.game.domain.SettlementResult;
import com.xuezhanmaster.game.domain.SettlementType;
import org.junit.jupiter.api.Test;
@@ -54,6 +56,12 @@ class GameEventTest {
2,
0,
"9筒",
new SettlementDetail(
1,
2,
4,
List.of(new SettlementFan("QING_YI_SE", "清一色", 2))
),
List.of(
new ScoreChange(2, 1, 3),
new ScoreChange(0, -1, -2)
@@ -69,7 +77,12 @@ class GameEventTest {
.containsEntry("actionType", "HU")
.containsEntry("sourceSeatNo", 0)
.containsEntry("triggerTile", "9筒");
assertThat(settlementApplied.payload()).containsKey("settlementDetail");
assertThat(settlementApplied.payload()).containsKey("scoreChanges");
assertThat((java.util.Map<String, Object>) settlementApplied.payload().get("settlementDetail"))
.containsEntry("baseScore", 1)
.containsEntry("totalFan", 2)
.containsEntry("paymentScore", 4);
assertThat(scoreChanged.eventType()).isEqualTo(GameEventType.SCORE_CHANGED);
assertThat(scoreChanged.payload())

View File

@@ -0,0 +1,128 @@
package com.xuezhanmaster.game.service;
import com.xuezhanmaster.game.domain.GameSeat;
import com.xuezhanmaster.game.domain.SettlementDetail;
import com.xuezhanmaster.game.domain.Tile;
import com.xuezhanmaster.game.domain.TileSuit;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class BloodBattleScoringServiceTest {
private final HuEvaluator huEvaluator = new HuEvaluator();
private final BloodBattleScoringService scoringService = new BloodBattleScoringService(huEvaluator);
@Test
void shouldEvaluateQingQiDuiWithGen() {
GameSeat winnerSeat = new GameSeat(0, false, "user-1", "玩家一");
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 1));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 1));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 1));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 1));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 2));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 2));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 3));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 3));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 4));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 4));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 5));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 5));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 6));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 6));
SettlementDetail settlementDetail = scoringService.buildSelfDrawHuDetail(winnerSeat, false, false);
assertThat(settlementDetail.totalFan()).isEqualTo(5);
assertThat(settlementDetail.paymentScore()).isEqualTo(32);
assertThat(settlementDetail.fans())
.extracting(fan -> fan.code())
.containsExactly("QI_DUI", "QING_YI_SE", "GEN");
}
@Test
void shouldEvaluateJinGouDiaoAndRobbingGangHu() {
GameSeat winnerSeat = new GameSeat(0, false, "user-1", "玩家一");
winnerSeat.addPengMeld(new Tile(TileSuit.WAN, 1), 1);
winnerSeat.addPengMeld(new Tile(TileSuit.WAN, 2), 1);
winnerSeat.addMingGangMeld(new Tile(TileSuit.WAN, 3), 1);
winnerSeat.addAnGangMeld(new Tile(TileSuit.WAN, 4));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 9));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 9));
SettlementDetail settlementDetail = scoringService.buildRobbingGangHuDetail(winnerSeat);
assertThat(settlementDetail.totalFan()).isEqualTo(7);
assertThat(settlementDetail.paymentScore()).isEqualTo(128);
assertThat(settlementDetail.fans())
.extracting(fan -> fan.code())
.containsExactly("DUI_DUI_HU", "JIN_GOU_DIAO", "QING_YI_SE", "GEN", "GEN", "QIANG_GANG_HU");
}
@Test
void shouldExposeFixedGangScores() {
assertThat(scoringService.buildExposedGangDetail().paymentScore()).isEqualTo(2);
assertThat(scoringService.buildSupplementalGangDetail().paymentScore()).isEqualTo(1);
assertThat(scoringService.buildConcealedGangDetail().paymentScore()).isEqualTo(2);
}
@Test
void shouldAddGangShangHuaAndGangShangPaoFans() {
GameSeat winnerSeat = new GameSeat(0, false, "user-1", "玩家一");
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 1));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 1));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 1));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 2));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 3));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 4));
winnerSeat.receiveTile(new Tile(TileSuit.TONG, 2));
winnerSeat.receiveTile(new Tile(TileSuit.TONG, 3));
winnerSeat.receiveTile(new Tile(TileSuit.TONG, 4));
winnerSeat.receiveTile(new Tile(TileSuit.TIAO, 5));
winnerSeat.receiveTile(new Tile(TileSuit.TIAO, 6));
winnerSeat.receiveTile(new Tile(TileSuit.TIAO, 7));
winnerSeat.receiveTile(new Tile(TileSuit.TONG, 9));
winnerSeat.receiveTile(new Tile(TileSuit.TONG, 9));
SettlementDetail gangShangHua = scoringService.buildSelfDrawHuDetail(winnerSeat, true, false);
SettlementDetail gangShangPao = scoringService.buildDiscardHuDetail(winnerSeat, true, false);
assertThat(gangShangHua.totalFan()).isEqualTo(1);
assertThat(gangShangHua.paymentScore()).isEqualTo(2);
assertThat(gangShangHua.fans()).extracting(fan -> fan.code()).containsExactly("GANG_SHANG_HUA");
assertThat(gangShangPao.totalFan()).isEqualTo(1);
assertThat(gangShangPao.paymentScore()).isEqualTo(2);
assertThat(gangShangPao.fans()).extracting(fan -> fan.code()).containsExactly("GANG_SHANG_PAO");
}
@Test
void shouldAddHaiDiFans() {
GameSeat winnerSeat = new GameSeat(0, false, "user-1", "玩家一");
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 1));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 1));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 1));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 2));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 3));
winnerSeat.receiveTile(new Tile(TileSuit.WAN, 4));
winnerSeat.receiveTile(new Tile(TileSuit.TONG, 2));
winnerSeat.receiveTile(new Tile(TileSuit.TONG, 3));
winnerSeat.receiveTile(new Tile(TileSuit.TONG, 4));
winnerSeat.receiveTile(new Tile(TileSuit.TIAO, 5));
winnerSeat.receiveTile(new Tile(TileSuit.TIAO, 6));
winnerSeat.receiveTile(new Tile(TileSuit.TIAO, 7));
winnerSeat.receiveTile(new Tile(TileSuit.TONG, 9));
winnerSeat.receiveTile(new Tile(TileSuit.TONG, 9));
SettlementDetail haiDiLaoYue = scoringService.buildSelfDrawHuDetail(winnerSeat, false, true);
SettlementDetail haiDiPao = scoringService.buildDiscardHuDetail(winnerSeat, false, true);
assertThat(haiDiLaoYue.totalFan()).isEqualTo(1);
assertThat(haiDiLaoYue.paymentScore()).isEqualTo(2);
assertThat(haiDiLaoYue.fans()).extracting(fan -> fan.code()).containsExactly("HAI_DI_LAO_YUE");
assertThat(haiDiPao.totalFan()).isEqualTo(1);
assertThat(haiDiPao.paymentScore()).isEqualTo(2);
assertThat(haiDiPao.fans()).extracting(fan -> fan.code()).containsExactly("HAI_DI_PAO");
}
}

View File

@@ -34,13 +34,14 @@ class GameSessionServiceTest {
private final RoomService roomService = new RoomService();
private final HuEvaluator huEvaluator = new HuEvaluator();
private final BloodBattleScoringService bloodBattleScoringService = new BloodBattleScoringService(huEvaluator);
private final GameSessionService gameSessionService = new GameSessionService(
roomService,
new GameEngine(new DeckFactory()),
new GameActionProcessor(huEvaluator),
new ResponseActionWindowBuilder(huEvaluator),
new ResponseActionResolver(),
new SettlementService(),
new SettlementService(bloodBattleScoringService),
huEvaluator,
new StrategyService(),
new PlayerVisibilityService(),
@@ -255,8 +256,8 @@ class GameSessionServiceTest {
);
assertThat(afterGang.currentSeatNo()).isEqualTo(1);
assertThat(afterGang.selfSeat().score()).isEqualTo(1);
assertThat(afterGang.seats().get(0).score()).isEqualTo(-1);
assertThat(afterGang.selfSeat().score()).isEqualTo(2);
assertThat(afterGang.seats().get(0).score()).isEqualTo(-2);
assertThat(afterGang.selfSeat().melds()).containsExactly("明杠:" + discardTile);
assertThat(session.getEvents())
.extracting(event -> event.eventType())
@@ -420,17 +421,56 @@ class GameSessionServiceTest {
assertThat(afterGang.selfSeat().handTiles()).hasSize(11);
}
@Test
void shouldScoreGangShangHuaAfterReplacementDrawHu() {
RoomSummaryResponse room = roomService.createRoom(new CreateRoomRequest("host-1", true));
roomService.joinRoom(room.roomId(), new com.xuezhanmaster.room.dto.JoinRoomRequest("player-2", "玩家二"));
roomService.joinRoom(room.roomId(), new com.xuezhanmaster.room.dto.JoinRoomRequest("player-3", "玩家三"));
roomService.joinRoom(room.roomId(), new com.xuezhanmaster.room.dto.JoinRoomRequest("player-4", "玩家四"));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("host-1", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("player-2", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("player-3", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("player-4", true));
GameStateResponse started = gameSessionService.startGame(room.roomId(), "host-1");
gameSessionService.selectLackSuit(started.gameId(), "host-1", TileSuit.WAN.name());
gameSessionService.selectLackSuit(started.gameId(), "player-2", TileSuit.TONG.name());
gameSessionService.selectLackSuit(started.gameId(), "player-3", TileSuit.TIAO.name());
gameSessionService.selectLackSuit(started.gameId(), "player-4", TileSuit.WAN.name());
GameSession session = getSession(started.gameId());
prepareGangShangHuaHand(session.getTable().getSeats().get(0), "3万");
setNextWallTile(session, "9筒");
GameStateResponse afterGang = gameSessionService.performAction(
started.gameId(),
new GameActionRequest("host-1", "GANG", "3万", null)
);
GameStateResponse afterHu = gameSessionService.performAction(
started.gameId(),
new GameActionRequest("host-1", "HU", null, null)
);
assertThat(afterGang.currentSeatNo()).isEqualTo(0);
assertThat(afterHu.selfSeat().won()).isTrue();
assertThat(afterHu.selfSeat().score()).isEqualTo(12);
assertThat(afterHu.seats()).extracting(seat -> seat.score()).contains(12, -4, -4, -4);
assertThat(latestSettlementFanCodes(session)).contains("GANG_SHANG_HUA");
}
@Test
void shouldRejectSelfConcealedGangWithoutFourMatchingTiles() {
RoomSummaryResponse room = roomService.createRoom(new CreateRoomRequest("host-1", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("host-1", true));
GameStateResponse started = gameSessionService.startGame(room.roomId(), "host-1");
gameSessionService.selectLackSuit(started.gameId(), "host-1", TileSuit.WAN.name());
GameStateResponse afterLack = gameSessionService.selectLackSuit(started.gameId(), "host-1", TileSuit.WAN.name());
GameSession session = getSession(started.gameId());
String invalidGangTile = findTileWithLessThanFourCopies(session.getTable().getSeats().get(0));
assertThatThrownBy(() -> gameSessionService.performAction(
started.gameId(),
new GameActionRequest("host-1", "GANG", "3万", null)
afterLack.gameId(),
new GameActionRequest("host-1", "GANG", invalidGangTile, null)
))
.isInstanceOf(BusinessException.class)
.extracting(throwable -> ((BusinessException) throwable).getCode())
@@ -504,11 +544,106 @@ class GameSessionServiceTest {
assertThat(afterHu.phase()).isEqualTo("PLAYING");
assertThat(afterHu.currentSeatNo()).isEqualTo(2);
assertThat(afterHu.selfSeat().won()).isTrue();
assertThat(afterHu.selfSeat().score()).isEqualTo(1);
assertThat(afterHu.seats().get(0).score()).isEqualTo(-1);
assertThat(afterHu.selfSeat().score()).isEqualTo(2);
assertThat(afterHu.seats().get(0).score()).isEqualTo(-2);
assertThat(afterHu.seats().get(0).melds()).containsExactly("碰:9筒");
}
@Test
void shouldScoreGangShangPaoOnDiscardAfterReplacementDraw() {
RoomSummaryResponse room = roomService.createRoom(new CreateRoomRequest("host-1", true));
roomService.joinRoom(room.roomId(), new com.xuezhanmaster.room.dto.JoinRoomRequest("player-2", "玩家二"));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("host-1", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("player-2", true));
GameStateResponse started = gameSessionService.startGame(room.roomId(), "host-1");
gameSessionService.selectLackSuit(started.gameId(), "host-1", TileSuit.WAN.name());
gameSessionService.selectLackSuit(started.gameId(), "player-2", TileSuit.TONG.name());
GameSession session = getSession(started.gameId());
prepareGangShangPaoHand(session.getTable().getSeats().get(0), "3万");
prepareWinningHand(session.getTable().getSeats().get(1));
removeMatchingTilesFromOtherSeats(session, "9筒", 0, 1);
setNextWallTile(session, "8条");
GameStateResponse afterGang = gameSessionService.performAction(
started.gameId(),
new GameActionRequest("host-1", "GANG", "3万", null)
);
GameStateResponse afterHu = gameSessionService.discardTile(started.gameId(), "host-1", "9筒");
afterHu = gameSessionService.performAction(
started.gameId(),
new GameActionRequest("player-2", "HU", "9筒", 0)
);
assertThat(afterGang.currentSeatNo()).isEqualTo(0);
assertThat(afterHu.selfSeat().won()).isTrue();
assertThat(afterHu.selfSeat().score()).isEqualTo(0);
assertThat(afterHu.seats().get(0).score()).isEqualTo(4);
assertThat(latestSettlementFanCodes(session)).contains("GANG_SHANG_PAO");
}
@Test
void shouldScoreHaiDiLaoYueOnLastTileSelfDraw() {
RoomSummaryResponse room = roomService.createRoom(new CreateRoomRequest("host-1", true));
roomService.joinRoom(room.roomId(), new com.xuezhanmaster.room.dto.JoinRoomRequest("player-2", "玩家二"));
roomService.joinRoom(room.roomId(), new com.xuezhanmaster.room.dto.JoinRoomRequest("player-3", "玩家三"));
roomService.joinRoom(room.roomId(), new com.xuezhanmaster.room.dto.JoinRoomRequest("player-4", "玩家四"));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("host-1", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("player-2", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("player-3", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("player-4", true));
GameStateResponse started = gameSessionService.startGame(room.roomId(), "host-1");
gameSessionService.selectLackSuit(started.gameId(), "host-1", TileSuit.WAN.name());
gameSessionService.selectLackSuit(started.gameId(), "player-2", TileSuit.TONG.name());
gameSessionService.selectLackSuit(started.gameId(), "player-3", TileSuit.TIAO.name());
gameSessionService.selectLackSuit(started.gameId(), "player-4", TileSuit.WAN.name());
GameSession session = getSession(started.gameId());
prepareSelfDrawWinningHand(session.getTable().getSeats().get(0));
session.getTable().getWallTiles().clear();
GameStateResponse afterHu = gameSessionService.performAction(
started.gameId(),
new GameActionRequest("host-1", "HU", null, null)
);
assertThat(afterHu.selfSeat().won()).isTrue();
assertThat(afterHu.selfSeat().score()).isEqualTo(6);
assertThat(afterHu.seats()).extracting(seat -> seat.score()).contains(6, -2, -2, -2);
assertThat(latestSettlementFanCodes(session)).contains("HAI_DI_LAO_YUE");
}
@Test
void shouldScoreHaiDiPaoOnLastTileDiscardHu() {
RoomSummaryResponse room = roomService.createRoom(new CreateRoomRequest("host-1", true));
roomService.joinRoom(room.roomId(), new com.xuezhanmaster.room.dto.JoinRoomRequest("player-2", "玩家二"));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("host-1", true));
roomService.toggleReady(room.roomId(), new ToggleReadyRequest("player-2", true));
GameStateResponse started = gameSessionService.startGame(room.roomId(), "host-1");
gameSessionService.selectLackSuit(started.gameId(), "host-1", TileSuit.WAN.name());
gameSessionService.selectLackSuit(started.gameId(), "player-2", TileSuit.TONG.name());
GameSession session = getSession(started.gameId());
prepareWinningHand(session.getTable().getSeats().get(1));
ensureSeatHasMatchingTiles(session.getTable().getSeats().get(0), "9筒", 1);
removeMatchingTilesFromOtherSeats(session, "9筒", 0, 1);
session.getTable().getWallTiles().clear();
gameSessionService.discardTile(started.gameId(), "host-1", "9筒");
GameStateResponse afterHu = gameSessionService.performAction(
started.gameId(),
new GameActionRequest("player-2", "HU", "9筒", 0)
);
assertThat(afterHu.selfSeat().won()).isTrue();
assertThat(afterHu.selfSeat().score()).isEqualTo(2);
assertThat(afterHu.seats().get(0).score()).isEqualTo(-2);
assertThat(latestSettlementFanCodes(session)).contains("HAI_DI_PAO");
}
@Test
void shouldRejectSelfDrawHuWhenHandIsNotWinning() {
RoomSummaryResponse room = roomService.createRoom(new CreateRoomRequest("host-1", true));
@@ -575,6 +710,21 @@ class GameSessionServiceTest {
};
}
private String findTileWithLessThanFourCopies(GameSeat seat) {
for (Tile tile : seat.getHandTiles()) {
int matchCount = 0;
for (Tile handTile : seat.getHandTiles()) {
if (handTile.equals(tile)) {
matchCount++;
}
}
if (matchCount < 4) {
return tile.getDisplayName();
}
}
throw new IllegalStateException("测试手牌意外全部为四张同牌");
}
private void prepareWinningHand(GameSeat seat) {
seat.getHandTiles().clear();
seat.receiveTile(new Tile(TileSuit.WAN, 1));
@@ -628,6 +778,42 @@ class GameSessionServiceTest {
seat.receiveTile(new Tile(TileSuit.WAN, 9));
}
private void prepareGangShangHuaHand(GameSeat seat, String gangTileDisplayName) {
seat.getHandTiles().clear();
Tile gangTile = parseTile(gangTileDisplayName);
for (int i = 0; i < 4; i++) {
seat.receiveTile(gangTile);
}
seat.receiveTile(new Tile(TileSuit.WAN, 1));
seat.receiveTile(new Tile(TileSuit.WAN, 1));
seat.receiveTile(new Tile(TileSuit.WAN, 1));
seat.receiveTile(new Tile(TileSuit.WAN, 2));
seat.receiveTile(new Tile(TileSuit.WAN, 3));
seat.receiveTile(new Tile(TileSuit.WAN, 4));
seat.receiveTile(new Tile(TileSuit.TONG, 2));
seat.receiveTile(new Tile(TileSuit.TONG, 3));
seat.receiveTile(new Tile(TileSuit.TONG, 4));
seat.receiveTile(new Tile(TileSuit.TONG, 9));
}
private void prepareGangShangPaoHand(GameSeat seat, String gangTileDisplayName) {
seat.getHandTiles().clear();
Tile gangTile = parseTile(gangTileDisplayName);
for (int i = 0; i < 4; i++) {
seat.receiveTile(gangTile);
}
seat.receiveTile(new Tile(TileSuit.WAN, 1));
seat.receiveTile(new Tile(TileSuit.WAN, 1));
seat.receiveTile(new Tile(TileSuit.WAN, 1));
seat.receiveTile(new Tile(TileSuit.WAN, 2));
seat.receiveTile(new Tile(TileSuit.WAN, 3));
seat.receiveTile(new Tile(TileSuit.WAN, 4));
seat.receiveTile(new Tile(TileSuit.TONG, 2));
seat.receiveTile(new Tile(TileSuit.TONG, 3));
seat.receiveTile(new Tile(TileSuit.TONG, 4));
seat.receiveTile(new Tile(TileSuit.TONG, 9));
}
private void prepareSupplementalGangHand(GameSeat seat, String gangTileDisplayName) {
seat.getHandTiles().clear();
seat.getMeldGroups().clear();
@@ -645,4 +831,21 @@ class GameSessionServiceTest {
seat.receiveTile(new Tile(TileSuit.TIAO, 9));
seat.receiveTile(new Tile(TileSuit.WAN, 9));
}
private void setNextWallTile(GameSession session, String tileDisplayName) {
session.getTable().getWallTiles().set(0, parseTile(tileDisplayName));
}
@SuppressWarnings("unchecked")
private java.util.List<String> latestSettlementFanCodes(GameSession session) {
return session.getEvents().stream()
.filter(event -> event.eventType() == GameEventType.SETTLEMENT_APPLIED)
.reduce((first, second) -> second)
.map(event -> (Map<String, Object>) event.payload().get("settlementDetail"))
.map(detail -> (java.util.List<Map<String, Object>>) detail.get("fans"))
.map(fans -> fans.stream()
.map(item -> String.valueOf(item.get("code")))
.toList())
.orElseGet(java.util.List::of);
}
}

View File

@@ -59,4 +59,26 @@ class HuEvaluatorTest {
assertThat(result).isFalse();
}
@Test
void shouldRecognizeSevenPairsWinningHand() {
boolean result = huEvaluator.canHu(List.of(
new Tile(TileSuit.WAN, 1),
new Tile(TileSuit.WAN, 1),
new Tile(TileSuit.WAN, 2),
new Tile(TileSuit.WAN, 2),
new Tile(TileSuit.WAN, 3),
new Tile(TileSuit.WAN, 3),
new Tile(TileSuit.TONG, 4),
new Tile(TileSuit.TONG, 4),
new Tile(TileSuit.TONG, 5),
new Tile(TileSuit.TONG, 5),
new Tile(TileSuit.TIAO, 6),
new Tile(TileSuit.TIAO, 6),
new Tile(TileSuit.TIAO, 9),
new Tile(TileSuit.TIAO, 9)
));
assertThat(result).isTrue();
}
}