feat: 实现最小正式版过水不胡规则并完善前端动作面板
- 后端实现最小正式版过水不胡规则:玩家在响应窗口选择PASS后,直到下次摸牌前不能响应胡 - 完善GameSeat状态管理,新增passedHuBlocked字段及相关方法 - 在ResponseActionWindowBuilder和GameActionProcessor中增加过水不胡校验 - 前端重构动作面板,区分回合动作和响应动作,支持多用户视角切换 - 优化公共事件处理逻辑,自动清理失效的私有动作面板 - 更新相关文档说明当前实现的规则范围和工程取舍 - 补充测试用例验证过水不胡规则的正确性
This commit is contained in:
@@ -16,6 +16,8 @@ public class GameSeat {
|
||||
private TileSuit lackSuit;
|
||||
private boolean won;
|
||||
private int score;
|
||||
// 过水不胡最小版:只限制“响应胡”,直到该玩家下一次摸牌后解除;不影响自摸胡。
|
||||
private boolean passedHuBlocked;
|
||||
|
||||
public GameSeat(int seatNo, boolean ai, String nickname) {
|
||||
this(seatNo, ai, UUID.randomUUID().toString(), nickname);
|
||||
@@ -76,10 +78,22 @@ public class GameSeat {
|
||||
return score;
|
||||
}
|
||||
|
||||
public boolean isPassedHuBlocked() {
|
||||
return passedHuBlocked;
|
||||
}
|
||||
|
||||
public void addScore(int delta) {
|
||||
this.score += delta;
|
||||
}
|
||||
|
||||
public void markPassedHuBlocked() {
|
||||
this.passedHuBlocked = true;
|
||||
}
|
||||
|
||||
public void clearPassedHuBlocked() {
|
||||
this.passedHuBlocked = false;
|
||||
}
|
||||
|
||||
public void receiveTile(Tile tile) {
|
||||
handTiles.add(tile);
|
||||
}
|
||||
|
||||
@@ -287,6 +287,14 @@ public class GameActionProcessor {
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new BusinessException("GAME_ACTION_WINDOW_INVALID", "当前玩家不在" + actionName + "响应候选中"));
|
||||
|
||||
if ("胡".equals(actionName) && actorSeat.isPassedHuBlocked()) {
|
||||
boolean hasHuOption = seatCandidate.options().stream()
|
||||
.anyMatch(option -> option.actionType() == ActionType.HU);
|
||||
if (!hasHuOption) {
|
||||
throw new BusinessException("GAME_ACTION_WINDOW_INVALID", "当前处于过水不胡限制中,需待下次摸牌后才能再胡");
|
||||
}
|
||||
}
|
||||
|
||||
boolean matched = seatCandidate.options().stream()
|
||||
.anyMatch(option -> option.actionType().name().equals(toActionTypeName(actionName))
|
||||
&& (tileDisplayName == null || tileDisplayName.equals(option.tile())));
|
||||
|
||||
@@ -180,6 +180,8 @@ public class GameSessionService {
|
||||
GameSeat nextSeat = nextSeatOptional.get();
|
||||
Tile drawnTile = table.getWallTiles().remove(0);
|
||||
nextSeat.receiveTile(drawnTile);
|
||||
// 玩家真正完成下一次摸牌后,才解除此前由“过水不胡”带来的响应胡限制。
|
||||
nextSeat.clearPassedHuBlocked();
|
||||
table.setCurrentSeatNo(nextSeat.getSeatNo());
|
||||
gameMessagePublisher.publishPublicEvent(GameEvent.tileDrawn(session.getGameId(), nextSeat.getSeatNo(), table.getWallTiles().size()));
|
||||
gameMessagePublisher.publishPublicEvent(GameEvent.turnSwitched(session.getGameId(), nextSeat.getSeatNo()));
|
||||
@@ -366,6 +368,7 @@ public class GameSessionService {
|
||||
private void handlePassPostAction(GameSession session, GameActionRequest request) {
|
||||
ResponseActionWindow responseActionWindow = requirePendingResponseWindow(session);
|
||||
GameSeat actorSeat = findSeatByUserId(session.getTable(), request.userId());
|
||||
markPassedHuBlockedIfNeeded(responseActionWindow, actorSeat);
|
||||
session.getResponseActionSelections().put(actorSeat.getSeatNo(), ActionType.PASS);
|
||||
tryResolveResponseWindow(session, responseActionWindow);
|
||||
}
|
||||
@@ -413,6 +416,7 @@ public class GameSessionService {
|
||||
for (ResponseActionSeatCandidate seatCandidate : responseActionWindow.seatCandidates()) {
|
||||
GameSeat seat = session.getTable().getSeats().get(seatCandidate.seatNo());
|
||||
if (seat.isAi()) {
|
||||
markPassedHuBlockedIfNeeded(responseActionWindow, seat);
|
||||
session.getResponseActionSelections().put(seatCandidate.seatNo(), ActionType.PASS);
|
||||
}
|
||||
}
|
||||
@@ -590,6 +594,7 @@ public class GameSessionService {
|
||||
|
||||
Tile drawnTile = table.getWallTiles().remove(0);
|
||||
winnerSeat.receiveTile(drawnTile);
|
||||
winnerSeat.clearPassedHuBlocked();
|
||||
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()));
|
||||
@@ -726,6 +731,7 @@ public class GameSessionService {
|
||||
|
||||
Tile drawnTile = table.getWallTiles().remove(0);
|
||||
winnerSeat.receiveTile(drawnTile);
|
||||
winnerSeat.clearPassedHuBlocked();
|
||||
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()));
|
||||
@@ -786,12 +792,21 @@ public class GameSessionService {
|
||||
|
||||
Tile drawnTile = table.getWallTiles().remove(0);
|
||||
winnerSeat.receiveTile(drawnTile);
|
||||
winnerSeat.clearPassedHuBlocked();
|
||||
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);
|
||||
}
|
||||
|
||||
private void markPassedHuBlockedIfNeeded(ResponseActionWindow responseActionWindow, GameSeat seat) {
|
||||
responseActionWindow.seatCandidates().stream()
|
||||
.filter(candidate -> candidate.seatNo() == seat.getSeatNo())
|
||||
.findFirst()
|
||||
.filter(candidate -> candidate.options().stream().anyMatch(option -> option.actionType() == ActionType.HU))
|
||||
.ifPresent(candidate -> seat.markPassedHuBlocked());
|
||||
}
|
||||
|
||||
private ActionType parseActionType(String actionType) {
|
||||
try {
|
||||
return ActionType.valueOf(actionType.toUpperCase(Locale.ROOT));
|
||||
|
||||
@@ -63,7 +63,8 @@ public class ResponseActionWindowBuilder {
|
||||
if (seat.getSeatNo() == sourceSeatNo || seat.isWon()) {
|
||||
continue;
|
||||
}
|
||||
if (!huEvaluator.canHuWithClaimedTile(seat.getHandTiles(), gangTile)) {
|
||||
// 抢杠胡也属于“响应胡”,因此同样受过水不胡限制。
|
||||
if (!canHuByResponse(seat, gangTile)) {
|
||||
continue;
|
||||
}
|
||||
seatCandidates.add(new ResponseActionSeatCandidate(
|
||||
@@ -91,7 +92,8 @@ public class ResponseActionWindowBuilder {
|
||||
|
||||
private List<ResponseActionOption> buildSeatOptions(GameSeat seat, Tile discardedTile) {
|
||||
int sameTileCount = countSameTileInHand(seat, discardedTile);
|
||||
boolean canHu = huEvaluator.canHuWithClaimedTile(seat.getHandTiles(), discardedTile);
|
||||
// 过水不胡只屏蔽响应胡候选,不影响同窗口内仍可用的碰、杠。
|
||||
boolean canHu = canHuByResponse(seat, discardedTile);
|
||||
if (!canHu && sameTileCount < 2) {
|
||||
return List.of();
|
||||
}
|
||||
@@ -114,6 +116,13 @@ public class ResponseActionWindowBuilder {
|
||||
return options;
|
||||
}
|
||||
|
||||
private boolean canHuByResponse(GameSeat seat, Tile triggerTile) {
|
||||
if (seat.isPassedHuBlocked()) {
|
||||
return false;
|
||||
}
|
||||
return huEvaluator.canHuWithClaimedTile(seat.getHandTiles(), triggerTile);
|
||||
}
|
||||
|
||||
private int countSameTileInHand(GameSeat seat, Tile discardedTile) {
|
||||
int count = 0;
|
||||
for (Tile tile : seat.getHandTiles()) {
|
||||
|
||||
Reference in New Issue
Block a user