feat: 实现麻将游戏结算系统与自摸胡功能
新增结算类型枚举和分数变更记录模型 补全响应裁决器与结算服务,支持点炮胡、自摸胡和明杠结算 扩展座位模型,增加已胡状态和分数字段 完善胡牌评估器,支持自摸胡判断 前端原型页增加分数显示和已胡状态 更新SPRINT文档记录当前进度
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
package com.xuezhanmaster.game.event;
|
||||
|
||||
import com.xuezhanmaster.game.domain.ActionType;
|
||||
import com.xuezhanmaster.game.domain.ScoreChange;
|
||||
import com.xuezhanmaster.game.domain.SettlementResult;
|
||||
import com.xuezhanmaster.game.domain.SettlementType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class GameEventTest {
|
||||
@@ -39,4 +45,53 @@ class GameEventTest {
|
||||
assertThat(switched.eventType()).isEqualTo(GameEventType.TURN_SWITCHED);
|
||||
assertThat(switched.payload()).containsEntry("currentSeatNo", 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildSettlementEvents() {
|
||||
SettlementResult settlementResult = new SettlementResult(
|
||||
SettlementType.DIAN_PAO_HU,
|
||||
ActionType.HU,
|
||||
2,
|
||||
0,
|
||||
"9筒",
|
||||
List.of(
|
||||
new ScoreChange(2, 1, 3),
|
||||
new ScoreChange(0, -1, -2)
|
||||
)
|
||||
);
|
||||
|
||||
GameEvent settlementApplied = GameEvent.settlementApplied("game-1", settlementResult);
|
||||
GameEvent scoreChanged = GameEvent.scoreChanged("game-1", 2, SettlementType.DIAN_PAO_HU.name(), 1, 3);
|
||||
|
||||
assertThat(settlementApplied.eventType()).isEqualTo(GameEventType.SETTLEMENT_APPLIED);
|
||||
assertThat(settlementApplied.payload())
|
||||
.containsEntry("settlementType", "DIAN_PAO_HU")
|
||||
.containsEntry("actionType", "HU")
|
||||
.containsEntry("sourceSeatNo", 0)
|
||||
.containsEntry("triggerTile", "9筒");
|
||||
assertThat(settlementApplied.payload()).containsKey("scoreChanges");
|
||||
|
||||
assertThat(scoreChanged.eventType()).isEqualTo(GameEventType.SCORE_CHANGED);
|
||||
assertThat(scoreChanged.payload())
|
||||
.containsEntry("settlementType", "DIAN_PAO_HU")
|
||||
.containsEntry("delta", 1)
|
||||
.containsEntry("score", 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowHuEventWithoutSourceSeatForSelfDraw() {
|
||||
GameEvent selfDrawHu = GameEvent.responseActionDeclared(
|
||||
"game-1",
|
||||
GameEventType.HU_DECLARED,
|
||||
0,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
assertThat(selfDrawHu.eventType()).isEqualTo(GameEventType.HU_DECLARED);
|
||||
assertThat(selfDrawHu.payload())
|
||||
.containsEntry("actionType", "HU")
|
||||
.doesNotContainKey("sourceSeatNo")
|
||||
.doesNotContainKey("tile");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.xuezhanmaster.game.service;
|
||||
|
||||
import com.xuezhanmaster.common.exception.BusinessException;
|
||||
import com.xuezhanmaster.game.domain.GameSeat;
|
||||
import com.xuezhanmaster.game.domain.GameSession;
|
||||
import com.xuezhanmaster.game.domain.Tile;
|
||||
import com.xuezhanmaster.game.dto.GameActionRequest;
|
||||
import com.xuezhanmaster.game.domain.TileSuit;
|
||||
import com.xuezhanmaster.game.dto.GameStateResponse;
|
||||
import com.xuezhanmaster.game.service.GameActionProcessor;
|
||||
import com.xuezhanmaster.game.event.GameEventType;
|
||||
import com.xuezhanmaster.strategy.service.StrategyService;
|
||||
import com.xuezhanmaster.teaching.service.PlayerVisibilityService;
|
||||
import com.xuezhanmaster.teaching.service.TeachingService;
|
||||
@@ -18,6 +22,9 @@ import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
|
||||
import com.xuezhanmaster.ws.service.GameMessagePublisher;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -26,11 +33,15 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
class GameSessionServiceTest {
|
||||
|
||||
private final RoomService roomService = new RoomService();
|
||||
private final HuEvaluator huEvaluator = new HuEvaluator();
|
||||
private final GameSessionService gameSessionService = new GameSessionService(
|
||||
roomService,
|
||||
new GameEngine(new DeckFactory()),
|
||||
new GameActionProcessor(),
|
||||
new ResponseActionWindowBuilder(),
|
||||
new GameActionProcessor(huEvaluator),
|
||||
new ResponseActionWindowBuilder(huEvaluator),
|
||||
new ResponseActionResolver(),
|
||||
new SettlementService(),
|
||||
huEvaluator,
|
||||
new StrategyService(),
|
||||
new PlayerVisibilityService(),
|
||||
new TeachingService(),
|
||||
@@ -65,6 +76,8 @@ class GameSessionServiceTest {
|
||||
GameStateResponse started = gameSessionService.startGame(room.roomId(), "host-1");
|
||||
GameStateResponse playing = gameSessionService.selectLackSuit(started.gameId(), "host-1", TileSuit.WAN.name());
|
||||
String discardTile = playing.selfSeat().handTiles().get(0);
|
||||
GameSession session = getSession(playing.gameId());
|
||||
removeMatchingTilesFromOtherSeats(session, discardTile, 0);
|
||||
|
||||
GameStateResponse afterDiscard = gameSessionService.discardTile(playing.gameId(), "host-1", discardTile);
|
||||
|
||||
@@ -88,7 +101,7 @@ class GameSessionServiceTest {
|
||||
))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.extracting(throwable -> ((BusinessException) throwable).getCode())
|
||||
.isEqualTo("GAME_ACTION_UNSUPPORTED");
|
||||
.isEqualTo("GAME_ACTION_WINDOW_NOT_FOUND");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -156,4 +169,327 @@ class GameSessionServiceTest {
|
||||
.extracting(throwable -> ((BusinessException) throwable).getCode())
|
||||
.isEqualTo("GAME_ACTION_INVALID");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPauseDiscardFlowUntilHumanPassesResponseWindow() {
|
||||
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());
|
||||
GameStateResponse playing = gameSessionService.getState(started.gameId(), "host-1");
|
||||
String discardTile = playing.selfSeat().handTiles().get(0);
|
||||
GameSession session = getSession(playing.gameId());
|
||||
ensureSeatHasMatchingTiles(session.getTable().getSeats().get(1), discardTile, 2);
|
||||
removeMatchingTilesFromOtherSeats(session, discardTile, 0, 1);
|
||||
|
||||
GameStateResponse afterDiscard = gameSessionService.discardTile(playing.gameId(), "host-1", discardTile);
|
||||
|
||||
assertThat(session.getPendingResponseActionWindow()).isNotNull();
|
||||
assertThat(afterDiscard.currentSeatNo()).isEqualTo(0);
|
||||
|
||||
GameStateResponse afterPass = gameSessionService.performAction(
|
||||
playing.gameId(),
|
||||
new GameActionRequest("player-2", "PASS", null, 0)
|
||||
);
|
||||
|
||||
assertThat(session.getPendingResponseActionWindow()).isNull();
|
||||
assertThat(afterPass.currentSeatNo()).isEqualTo(1);
|
||||
assertThat(afterPass.phase()).isEqualTo("PLAYING");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolvePengAndTransferTurnToWinner() {
|
||||
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());
|
||||
|
||||
GameStateResponse hostView = gameSessionService.getState(started.gameId(), "host-1");
|
||||
String discardTile = hostView.selfSeat().handTiles().get(0);
|
||||
GameSession session = getSession(hostView.gameId());
|
||||
ensureSeatHasMatchingTiles(session.getTable().getSeats().get(1), discardTile, 2);
|
||||
removeMatchingTilesFromOtherSeats(session, discardTile, 0, 1);
|
||||
|
||||
gameSessionService.discardTile(hostView.gameId(), "host-1", discardTile);
|
||||
GameStateResponse afterPeng = gameSessionService.performAction(
|
||||
hostView.gameId(),
|
||||
new GameActionRequest("player-2", "PENG", discardTile, 0)
|
||||
);
|
||||
|
||||
assertThat(session.getPendingResponseActionWindow()).isNull();
|
||||
assertThat(afterPeng.currentSeatNo()).isEqualTo(1);
|
||||
assertThat(afterPeng.seats().get(0).discardTiles()).doesNotContain(discardTile);
|
||||
assertThat(afterPeng.selfSeat().handTiles()).hasSize(13);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveGangAndApplySettlementScore() {
|
||||
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());
|
||||
|
||||
GameStateResponse hostView = gameSessionService.getState(started.gameId(), "host-1");
|
||||
String discardTile = hostView.selfSeat().handTiles().get(0);
|
||||
GameSession session = getSession(hostView.gameId());
|
||||
ensureSeatHasMatchingTiles(session.getTable().getSeats().get(1), discardTile, 3);
|
||||
removeMatchingTilesFromOtherSeats(session, discardTile, 0, 1);
|
||||
|
||||
gameSessionService.discardTile(hostView.gameId(), "host-1", discardTile);
|
||||
GameStateResponse afterGang = gameSessionService.performAction(
|
||||
hostView.gameId(),
|
||||
new GameActionRequest("player-2", "GANG", discardTile, 0)
|
||||
);
|
||||
|
||||
assertThat(afterGang.currentSeatNo()).isEqualTo(1);
|
||||
assertThat(afterGang.selfSeat().score()).isEqualTo(1);
|
||||
assertThat(afterGang.seats().get(0).score()).isEqualTo(-1);
|
||||
assertThat(session.getEvents())
|
||||
.extracting(event -> event.eventType())
|
||||
.contains(GameEventType.GANG_DECLARED, GameEventType.SETTLEMENT_APPLIED, GameEventType.SCORE_CHANGED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveHuAndContinueBloodBattleWhenMultipleActiveSeatsRemain() {
|
||||
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());
|
||||
prepareWinningHand(session.getTable().getSeats().get(1));
|
||||
String discardTile = "9筒";
|
||||
ensureSeatHasMatchingTiles(session.getTable().getSeats().get(0), discardTile, 1);
|
||||
removeMatchingTilesFromOtherSeats(session, discardTile, 0, 1);
|
||||
|
||||
gameSessionService.discardTile(started.gameId(), "host-1", discardTile);
|
||||
GameStateResponse afterHu = gameSessionService.performAction(
|
||||
started.gameId(),
|
||||
new GameActionRequest("player-2", "HU", discardTile, 0)
|
||||
);
|
||||
|
||||
assertThat(session.getPendingResponseActionWindow()).isNull();
|
||||
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(session.getEvents())
|
||||
.extracting(event -> event.eventType())
|
||||
.contains(GameEventType.HU_DECLARED, GameEventType.SETTLEMENT_APPLIED, GameEventType.SCORE_CHANGED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFinishBloodBattleWhenOnlyOneActiveSeatRemainsAfterHu() {
|
||||
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));
|
||||
session.getTable().getSeats().get(2).declareHu();
|
||||
session.getTable().getSeats().get(3).declareHu();
|
||||
String discardTile = "9筒";
|
||||
ensureSeatHasMatchingTiles(session.getTable().getSeats().get(0), discardTile, 1);
|
||||
removeMatchingTilesFromOtherSeats(session, discardTile, 0, 1);
|
||||
|
||||
gameSessionService.discardTile(started.gameId(), "host-1", discardTile);
|
||||
GameStateResponse afterHu = gameSessionService.performAction(
|
||||
started.gameId(),
|
||||
new GameActionRequest("player-2", "HU", discardTile, 0)
|
||||
);
|
||||
|
||||
assertThat(session.getPendingResponseActionWindow()).isNull();
|
||||
assertThat(afterHu.phase()).isEqualTo("FINISHED");
|
||||
assertThat(afterHu.selfSeat().won()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSkipWonSeatWhenAdvancingToNextTurn() {
|
||||
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());
|
||||
session.getTable().getSeats().get(1).declareHu();
|
||||
String discardTile = started.selfSeat().handTiles().get(0);
|
||||
removeMatchingTilesFromOtherSeats(session, discardTile, 0);
|
||||
|
||||
GameStateResponse afterDiscard = gameSessionService.discardTile(started.gameId(), "host-1", discardTile);
|
||||
|
||||
assertThat(afterDiscard.currentSeatNo()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveSelfDrawHuAndContinueToNextActiveSeat() {
|
||||
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));
|
||||
GameStateResponse afterHu = gameSessionService.performAction(
|
||||
started.gameId(),
|
||||
new GameActionRequest("host-1", "HU", null, null)
|
||||
);
|
||||
|
||||
assertThat(afterHu.phase()).isEqualTo("PLAYING");
|
||||
assertThat(afterHu.selfSeat().won()).isTrue();
|
||||
assertThat(afterHu.selfSeat().score()).isEqualTo(3);
|
||||
assertThat(afterHu.currentSeatNo()).isEqualTo(1);
|
||||
assertThat(afterHu.seats()).extracting(seat -> seat.score()).contains(3, -1, -1, -1);
|
||||
assertThat(session.getEvents())
|
||||
.extracting(event -> event.eventType())
|
||||
.contains(GameEventType.HU_DECLARED, GameEventType.SETTLEMENT_APPLIED, GameEventType.SCORE_CHANGED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectSelfDrawHuWhenHandIsNotWinning() {
|
||||
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());
|
||||
|
||||
assertThatThrownBy(() -> gameSessionService.performAction(
|
||||
started.gameId(),
|
||||
new GameActionRequest("host-1", "HU", null, null)
|
||||
))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.extracting(throwable -> ((BusinessException) throwable).getCode())
|
||||
.isEqualTo("GAME_ACTION_WINDOW_INVALID");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private GameSession getSession(String gameId) {
|
||||
try {
|
||||
Field field = GameSessionService.class.getDeclaredField("sessions");
|
||||
field.setAccessible(true);
|
||||
Map<String, GameSession> sessions = (Map<String, GameSession>) field.get(gameSessionService);
|
||||
return sessions.get(gameId);
|
||||
} catch (ReflectiveOperationException exception) {
|
||||
throw new IllegalStateException("无法读取测试用对局会话", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureSeatHasMatchingTiles(GameSeat seat, String tileDisplayName, int count) {
|
||||
Tile tile = parseTile(tileDisplayName);
|
||||
for (int i = 0; i < count; i++) {
|
||||
seat.receiveTile(tile);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeMatchingTilesFromOtherSeats(GameSession session, String tileDisplayName, int... keepSeatNos) {
|
||||
for (GameSeat seat : session.getTable().getSeats()) {
|
||||
if (shouldKeepSeat(seat.getSeatNo(), keepSeatNos)) {
|
||||
continue;
|
||||
}
|
||||
seat.getHandTiles().removeIf(tile -> tile.getDisplayName().equals(tileDisplayName));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldKeepSeat(int seatNo, int... keepSeatNos) {
|
||||
for (int keepSeatNo : keepSeatNos) {
|
||||
if (seatNo == keepSeatNo) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Tile parseTile(String tileDisplayName) {
|
||||
String rankPart = tileDisplayName.substring(0, tileDisplayName.length() - 1);
|
||||
String suitLabel = tileDisplayName.substring(tileDisplayName.length() - 1);
|
||||
int rank = Integer.parseInt(rankPart);
|
||||
return switch (suitLabel) {
|
||||
case "万" -> new Tile(TileSuit.WAN, rank);
|
||||
case "筒" -> new Tile(TileSuit.TONG, rank);
|
||||
case "条" -> new Tile(TileSuit.TIAO, rank);
|
||||
default -> throw new IllegalArgumentException("未知花色");
|
||||
};
|
||||
}
|
||||
|
||||
private void prepareWinningHand(GameSeat seat) {
|
||||
seat.getHandTiles().clear();
|
||||
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.TIAO, 5));
|
||||
seat.receiveTile(new Tile(TileSuit.TIAO, 6));
|
||||
seat.receiveTile(new Tile(TileSuit.TIAO, 7));
|
||||
seat.receiveTile(new Tile(TileSuit.TONG, 9));
|
||||
}
|
||||
|
||||
private void prepareSelfDrawWinningHand(GameSeat seat) {
|
||||
seat.getHandTiles().clear();
|
||||
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.TIAO, 5));
|
||||
seat.receiveTile(new Tile(TileSuit.TIAO, 6));
|
||||
seat.receiveTile(new Tile(TileSuit.TIAO, 7));
|
||||
seat.receiveTile(new Tile(TileSuit.TIAO, 9));
|
||||
seat.receiveTile(new Tile(TileSuit.TIAO, 9));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.xuezhanmaster.game.service;
|
||||
|
||||
import com.xuezhanmaster.game.domain.Tile;
|
||||
import com.xuezhanmaster.game.domain.TileSuit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class HuEvaluatorTest {
|
||||
|
||||
private final HuEvaluator huEvaluator = new HuEvaluator();
|
||||
|
||||
@Test
|
||||
void shouldRecognizeStandardWinningHandWithClaimedTile() {
|
||||
boolean result = huEvaluator.canHuWithClaimedTile(
|
||||
List.of(
|
||||
new Tile(TileSuit.WAN, 1),
|
||||
new Tile(TileSuit.WAN, 1),
|
||||
new Tile(TileSuit.WAN, 1),
|
||||
new Tile(TileSuit.WAN, 2),
|
||||
new Tile(TileSuit.WAN, 3),
|
||||
new Tile(TileSuit.WAN, 4),
|
||||
new Tile(TileSuit.TONG, 2),
|
||||
new Tile(TileSuit.TONG, 3),
|
||||
new Tile(TileSuit.TONG, 4),
|
||||
new Tile(TileSuit.TIAO, 5),
|
||||
new Tile(TileSuit.TIAO, 6),
|
||||
new Tile(TileSuit.TIAO, 7),
|
||||
new Tile(TileSuit.TONG, 9)
|
||||
),
|
||||
new Tile(TileSuit.TONG, 9)
|
||||
);
|
||||
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectNonWinningHandWithClaimedTile() {
|
||||
boolean result = huEvaluator.canHuWithClaimedTile(
|
||||
List.of(
|
||||
new Tile(TileSuit.WAN, 1),
|
||||
new Tile(TileSuit.WAN, 2),
|
||||
new Tile(TileSuit.WAN, 3),
|
||||
new Tile(TileSuit.WAN, 4),
|
||||
new Tile(TileSuit.WAN, 5),
|
||||
new Tile(TileSuit.WAN, 6),
|
||||
new Tile(TileSuit.TONG, 1),
|
||||
new Tile(TileSuit.TONG, 2),
|
||||
new Tile(TileSuit.TONG, 3),
|
||||
new Tile(TileSuit.TIAO, 1),
|
||||
new Tile(TileSuit.TIAO, 2),
|
||||
new Tile(TileSuit.TIAO, 4),
|
||||
new Tile(TileSuit.TONG, 9)
|
||||
),
|
||||
new Tile(TileSuit.TIAO, 9)
|
||||
);
|
||||
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ResponseActionWindowBuilderTest {
|
||||
|
||||
private final ResponseActionWindowBuilder builder = new ResponseActionWindowBuilder();
|
||||
private final ResponseActionWindowBuilder builder = new ResponseActionWindowBuilder(new HuEvaluator());
|
||||
|
||||
@Test
|
||||
void shouldBuildResponseCandidatesForDiscardedTile() {
|
||||
@@ -69,6 +69,58 @@ class ResponseActionWindowBuilderTest {
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIncludeHuWhenClaimedTileCompletesWinningHand() {
|
||||
Tile discardedTile = new Tile(TileSuit.TONG, 9);
|
||||
GameSession session = new GameSession("room-1", createPlayingTable(
|
||||
seat(0, "u0"),
|
||||
seat(1, "u1",
|
||||
new Tile(TileSuit.WAN, 1),
|
||||
new Tile(TileSuit.WAN, 1),
|
||||
new Tile(TileSuit.WAN, 1),
|
||||
new Tile(TileSuit.WAN, 2),
|
||||
new Tile(TileSuit.WAN, 3),
|
||||
new Tile(TileSuit.WAN, 4),
|
||||
new Tile(TileSuit.TONG, 2),
|
||||
new Tile(TileSuit.TONG, 3),
|
||||
new Tile(TileSuit.TONG, 4),
|
||||
new Tile(TileSuit.TIAO, 5),
|
||||
new Tile(TileSuit.TIAO, 6),
|
||||
new Tile(TileSuit.TIAO, 7),
|
||||
new Tile(TileSuit.TONG, 9)
|
||||
),
|
||||
seat(2, "u2"),
|
||||
seat(3, "u3")
|
||||
));
|
||||
|
||||
Optional<ResponseActionWindow> result = builder.buildForDiscard(session, 0, discardedTile);
|
||||
|
||||
assertThat(result).isPresent();
|
||||
assertThat(result.orElseThrow().seatCandidates().get(0).options())
|
||||
.extracting(option -> option.actionType())
|
||||
.containsExactly(ActionType.HU, ActionType.PASS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSkipSeatsThatAlreadyWonWhenBuildingResponseWindow() {
|
||||
Tile discardedTile = new Tile(TileSuit.WAN, 3);
|
||||
GameSeat wonSeat = seat(1, "u1", discardedTile, discardedTile, discardedTile);
|
||||
wonSeat.declareHu();
|
||||
GameSession session = new GameSession("room-1", createPlayingTable(
|
||||
seat(0, "u0"),
|
||||
wonSeat,
|
||||
seat(2, "u2", discardedTile, discardedTile),
|
||||
seat(3, "u3")
|
||||
));
|
||||
|
||||
Optional<ResponseActionWindow> result = builder.buildForDiscard(session, 0, discardedTile);
|
||||
|
||||
assertThat(result).isPresent();
|
||||
assertThat(result.orElseThrow().seatCandidates())
|
||||
.extracting(ResponseActionSeatCandidate::seatNo)
|
||||
.containsExactly(2);
|
||||
}
|
||||
|
||||
private GameTable createPlayingTable(GameSeat... seats) {
|
||||
GameTable table = new GameTable(List.of(seats), new ArrayList<>());
|
||||
table.setPhase(GamePhase.PLAYING);
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.xuezhanmaster.game.domain.ActionType;
|
||||
import com.xuezhanmaster.game.domain.ResponseActionOption;
|
||||
import com.xuezhanmaster.game.domain.ResponseActionSeatCandidate;
|
||||
import com.xuezhanmaster.game.domain.ResponseActionWindow;
|
||||
import com.xuezhanmaster.ws.dto.PrivateActionCandidate;
|
||||
import com.xuezhanmaster.ws.dto.PrivateActionMessage;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -22,17 +23,25 @@ class GameMessagePublisherTest {
|
||||
|
||||
@Test
|
||||
void shouldPublishStructuredTurnActionMessage() {
|
||||
gameMessagePublisher.publishPrivateTurnActionRequired("game-1", "user-1", List.of("DISCARD"), 2);
|
||||
gameMessagePublisher.publishPrivateTurnActionCandidates(
|
||||
"game-1",
|
||||
"user-1",
|
||||
List.of(
|
||||
new PrivateActionCandidate("HU", null),
|
||||
new PrivateActionCandidate("DISCARD", null)
|
||||
),
|
||||
2
|
||||
);
|
||||
|
||||
ArgumentCaptor<PrivateActionMessage> captor = ArgumentCaptor.forClass(PrivateActionMessage.class);
|
||||
verify(messagingTemplate).convertAndSend(org.mockito.ArgumentMatchers.eq("/topic/users/user-1/actions"), captor.capture());
|
||||
|
||||
PrivateActionMessage message = captor.getValue();
|
||||
assertThat(message.actionScope()).isEqualTo("TURN");
|
||||
assertThat(message.availableActions()).containsExactly("DISCARD");
|
||||
assertThat(message.availableActions()).containsExactly("HU", "DISCARD");
|
||||
assertThat(message.currentSeatNo()).isEqualTo(2);
|
||||
assertThat(message.candidates()).hasSize(1);
|
||||
assertThat(message.candidates().get(0).actionType()).isEqualTo("DISCARD");
|
||||
assertThat(message.candidates()).hasSize(2);
|
||||
assertThat(message.candidates().get(0).actionType()).isEqualTo("HU");
|
||||
assertThat(message.candidates().get(0).tile()).isNull();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user