feat(结算): 实现血战麻将查叫与退税功能
新增局终处理逻辑,当牌墙耗尽且有多家未胡时: 1. 退税:未听牌玩家需退还此前杠分收入 2. 查叫:未听牌玩家向听牌玩家赔付最大理论点炮值 新增 SettlementType.TUI_SHUI 和 SettlementType.CHA_JIAO 结算类型 新增 ReadyHandOption 记录最优听牌选项 支持一炮多响裁决,新增 ResponseActionResolutionBatch 承载多赢家结果 在 GameSession 中新增 settlementHistory 保留结算记录供复用 更新开发文档要求加强关键区域的中文注释
This commit is contained in:
@@ -16,6 +16,8 @@ public class GameSession {
|
||||
private final Instant createdAt;
|
||||
private final GameTable table;
|
||||
private final List<GameEvent> events;
|
||||
// 保留已应用结算结果,供局终退税与查叫直接复用,避免从事件载荷反推。
|
||||
private final List<SettlementResult> settlementHistory;
|
||||
private ResponseActionWindow pendingResponseActionWindow;
|
||||
// 记录最近一次杠后补摸窗口,用于判断杠上花与杠上炮。
|
||||
private PostGangContext postGangContext;
|
||||
@@ -27,6 +29,7 @@ public class GameSession {
|
||||
this.createdAt = Instant.now();
|
||||
this.table = table;
|
||||
this.events = new ArrayList<>();
|
||||
this.settlementHistory = new ArrayList<>();
|
||||
this.responseActionSelections = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
@@ -50,6 +53,10 @@ public class GameSession {
|
||||
return events;
|
||||
}
|
||||
|
||||
public List<SettlementResult> getSettlementHistory() {
|
||||
return settlementHistory;
|
||||
}
|
||||
|
||||
public ResponseActionWindow getPendingResponseActionWindow() {
|
||||
return pendingResponseActionWindow;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.xuezhanmaster.game.domain;
|
||||
|
||||
public record ReadyHandOption(
|
||||
String triggerTile,
|
||||
SettlementDetail settlementDetail
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.xuezhanmaster.game.domain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record ResponseActionResolutionBatch(
|
||||
ActionType actionType,
|
||||
List<ResponseActionResolution> resolutions
|
||||
) {
|
||||
}
|
||||
@@ -6,5 +6,7 @@ public enum SettlementType {
|
||||
ZI_MO_HU,
|
||||
BU_GANG,
|
||||
MING_GANG,
|
||||
AN_GANG
|
||||
AN_GANG,
|
||||
TUI_SHUI,
|
||||
CHA_JIAO
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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.ReadyHandOption;
|
||||
import com.xuezhanmaster.game.domain.SettlementDetail;
|
||||
import com.xuezhanmaster.game.domain.SettlementFan;
|
||||
import com.xuezhanmaster.game.domain.Tile;
|
||||
@@ -13,6 +14,7 @@ import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
public class BloodBattleScoringService {
|
||||
@@ -61,6 +63,22 @@ public class BloodBattleScoringService {
|
||||
return new SettlementDetail(BASE_SCORE, 0, CONCEALED_GANG_SCORE, List.of());
|
||||
}
|
||||
|
||||
public Optional<ReadyHandOption> findBestReadyHandOption(GameSeat seat) {
|
||||
ReadyHandOption bestOption = null;
|
||||
for (Tile candidateTile : buildAllTileCandidates()) {
|
||||
if (!huEvaluator.canHuWithClaimedTile(seat.getHandTiles(), candidateTile)) {
|
||||
continue;
|
||||
}
|
||||
GameSeat virtualSeat = copySeatWithClaimedTile(seat, candidateTile);
|
||||
SettlementDetail settlementDetail = buildDiscardHuDetail(virtualSeat, false, false);
|
||||
ReadyHandOption candidateOption = new ReadyHandOption(candidateTile.getDisplayName(), settlementDetail);
|
||||
if (bestOption == null || isBetterReadyHandOption(candidateOption, bestOption)) {
|
||||
bestOption = candidateOption;
|
||||
}
|
||||
}
|
||||
return Optional.ofNullable(bestOption);
|
||||
}
|
||||
|
||||
private SettlementDetail buildHuDetail(
|
||||
GameSeat winnerSeat,
|
||||
boolean robbingGang,
|
||||
@@ -164,4 +182,35 @@ public class BloodBattleScoringService {
|
||||
case MING_GANG, BU_GANG, AN_GANG -> 4;
|
||||
};
|
||||
}
|
||||
|
||||
private List<Tile> buildAllTileCandidates() {
|
||||
List<Tile> candidates = new ArrayList<>();
|
||||
for (TileSuit tileSuit : TileSuit.values()) {
|
||||
for (int rank = 1; rank <= 9; rank++) {
|
||||
candidates.add(new Tile(tileSuit, rank));
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private GameSeat copySeatWithClaimedTile(GameSeat seat, Tile claimedTile) {
|
||||
GameSeat virtualSeat = new GameSeat(seat.getSeatNo(), seat.isAi(), seat.getPlayerId(), seat.getNickname());
|
||||
virtualSeat.setLackSuit(seat.getLackSuit());
|
||||
virtualSeat.setWon(seat.isWon());
|
||||
virtualSeat.addScore(seat.getScore());
|
||||
virtualSeat.getHandTiles().addAll(seat.getHandTiles());
|
||||
virtualSeat.getMeldGroups().addAll(seat.getMeldGroups());
|
||||
virtualSeat.receiveTile(claimedTile);
|
||||
return virtualSeat;
|
||||
}
|
||||
|
||||
private boolean isBetterReadyHandOption(ReadyHandOption candidate, ReadyHandOption currentBest) {
|
||||
if (candidate.settlementDetail().paymentScore() != currentBest.settlementDetail().paymentScore()) {
|
||||
return candidate.settlementDetail().paymentScore() > currentBest.settlementDetail().paymentScore();
|
||||
}
|
||||
if (candidate.settlementDetail().totalFan() != currentBest.settlementDetail().totalFan()) {
|
||||
return candidate.settlementDetail().totalFan() > currentBest.settlementDetail().totalFan();
|
||||
}
|
||||
return candidate.triggerTile().compareTo(currentBest.triggerTile()) < 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,15 @@ 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.ReadyHandOption;
|
||||
import com.xuezhanmaster.game.domain.ResponseActionResolution;
|
||||
import com.xuezhanmaster.game.domain.ResponseActionResolutionBatch;
|
||||
import com.xuezhanmaster.game.domain.ResponseActionSeatCandidate;
|
||||
import com.xuezhanmaster.game.domain.ResponseActionWindow;
|
||||
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 com.xuezhanmaster.game.domain.Tile;
|
||||
import com.xuezhanmaster.game.domain.TileSuit;
|
||||
import com.xuezhanmaster.game.dto.GameActionRequest;
|
||||
@@ -35,9 +39,11 @@ import com.xuezhanmaster.ws.dto.PrivateActionCandidate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
@@ -49,6 +55,7 @@ public class GameSessionService {
|
||||
private final ResponseActionWindowBuilder responseActionWindowBuilder;
|
||||
private final ResponseActionResolver responseActionResolver;
|
||||
private final SettlementService settlementService;
|
||||
private final BloodBattleScoringService scoringService;
|
||||
private final HuEvaluator huEvaluator;
|
||||
private final StrategyService strategyService;
|
||||
private final PlayerVisibilityService playerVisibilityService;
|
||||
@@ -63,6 +70,7 @@ public class GameSessionService {
|
||||
ResponseActionWindowBuilder responseActionWindowBuilder,
|
||||
ResponseActionResolver responseActionResolver,
|
||||
SettlementService settlementService,
|
||||
BloodBattleScoringService scoringService,
|
||||
HuEvaluator huEvaluator,
|
||||
StrategyService strategyService,
|
||||
PlayerVisibilityService playerVisibilityService,
|
||||
@@ -75,6 +83,7 @@ public class GameSessionService {
|
||||
this.responseActionWindowBuilder = responseActionWindowBuilder;
|
||||
this.responseActionResolver = responseActionResolver;
|
||||
this.settlementService = settlementService;
|
||||
this.scoringService = scoringService;
|
||||
this.huEvaluator = huEvaluator;
|
||||
this.strategyService = strategyService;
|
||||
this.playerVisibilityService = playerVisibilityService;
|
||||
@@ -155,17 +164,16 @@ public class GameSessionService {
|
||||
);
|
||||
}
|
||||
|
||||
private void moveToNextSeat(GameTable table, String gameId) {
|
||||
if (shouldFinishTable(table)) {
|
||||
table.setPhase(GamePhase.FINISHED);
|
||||
gameMessagePublisher.publishPublicEvent(GameEvent.phaseChanged(gameId, table.getPhase().name()));
|
||||
private void moveToNextSeat(GameSession session) {
|
||||
GameTable table = session.getTable();
|
||||
if (finishTableIfNeeded(session)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<GameSeat> nextSeatOptional = findNextActiveSeat(table, table.getCurrentSeatNo());
|
||||
if (nextSeatOptional.isEmpty()) {
|
||||
table.setPhase(GamePhase.FINISHED);
|
||||
gameMessagePublisher.publishPublicEvent(GameEvent.phaseChanged(gameId, table.getPhase().name()));
|
||||
appendAndPublish(session, GameEvent.phaseChanged(session.getGameId(), table.getPhase().name()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -173,8 +181,8 @@ public class GameSessionService {
|
||||
Tile drawnTile = table.getWallTiles().remove(0);
|
||||
nextSeat.receiveTile(drawnTile);
|
||||
table.setCurrentSeatNo(nextSeat.getSeatNo());
|
||||
gameMessagePublisher.publishPublicEvent(GameEvent.tileDrawn(gameId, nextSeat.getSeatNo(), table.getWallTiles().size()));
|
||||
gameMessagePublisher.publishPublicEvent(GameEvent.turnSwitched(gameId, nextSeat.getSeatNo()));
|
||||
gameMessagePublisher.publishPublicEvent(GameEvent.tileDrawn(session.getGameId(), nextSeat.getSeatNo(), table.getWallTiles().size()));
|
||||
gameMessagePublisher.publishPublicEvent(GameEvent.turnSwitched(session.getGameId(), nextSeat.getSeatNo()));
|
||||
}
|
||||
|
||||
private void autoPlayBots(GameSession session) {
|
||||
@@ -182,7 +190,7 @@ public class GameSessionService {
|
||||
while (table.getPhase() == GamePhase.PLAYING) {
|
||||
GameSeat currentSeat = table.getSeats().get(table.getCurrentSeatNo());
|
||||
if (currentSeat.isWon()) {
|
||||
moveToNextSeat(table, session.getGameId());
|
||||
moveToNextSeat(session);
|
||||
continue;
|
||||
}
|
||||
if (!currentSeat.isAi()) {
|
||||
@@ -248,7 +256,7 @@ public class GameSessionService {
|
||||
)
|
||||
);
|
||||
appendAndPublish(session, events);
|
||||
moveToNextSeat(table, session.getGameId());
|
||||
moveToNextSeat(session);
|
||||
if (table.getPhase() == GamePhase.FINISHED) {
|
||||
return;
|
||||
}
|
||||
@@ -434,14 +442,14 @@ public class GameSessionService {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<ResponseActionResolution> resolution = responseActionResolver.resolve(
|
||||
Optional<ResponseActionResolutionBatch> resolutionBatch = responseActionResolver.resolve(
|
||||
responseActionWindow,
|
||||
session.getResponseActionSelections(),
|
||||
session.getTable().getSeats().size()
|
||||
);
|
||||
|
||||
if (resolution.isPresent()) {
|
||||
executeResolvedResponseAction(session, responseActionWindow, resolution.get());
|
||||
if (resolutionBatch.isPresent()) {
|
||||
executeResolvedResponseAction(session, responseActionWindow, resolutionBatch.get());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -497,7 +505,7 @@ public class GameSessionService {
|
||||
|
||||
private void continueAfterDiscardWithoutResponse(GameSession session) {
|
||||
session.clearPostGangContext();
|
||||
moveToNextSeat(session.getTable(), session.getGameId());
|
||||
moveToNextSeat(session);
|
||||
autoPlayBots(session);
|
||||
notifyActionIfHumanTurn(session);
|
||||
}
|
||||
@@ -505,13 +513,13 @@ public class GameSessionService {
|
||||
private void executeResolvedResponseAction(
|
||||
GameSession session,
|
||||
ResponseActionWindow responseActionWindow,
|
||||
ResponseActionResolution resolution
|
||||
ResponseActionResolutionBatch resolutionBatch
|
||||
) {
|
||||
closeResponseWindowWithResolution(session, responseActionWindow, resolution.actionType());
|
||||
switch (resolution.actionType()) {
|
||||
case PENG -> executePeng(session, resolution);
|
||||
case GANG -> executeGang(session, resolution);
|
||||
case HU -> executeHu(session, responseActionWindow, resolution);
|
||||
closeResponseWindowWithResolution(session, responseActionWindow, resolutionBatch.actionType());
|
||||
switch (resolutionBatch.actionType()) {
|
||||
case PENG -> executePeng(session, resolutionBatch.resolutions().get(0));
|
||||
case GANG -> executeGang(session, resolutionBatch.resolutions().get(0));
|
||||
case HU -> executeHu(session, responseActionWindow, resolutionBatch.resolutions());
|
||||
default -> throw new BusinessException("GAME_ACTION_INVALID", "未知的响应裁决结果");
|
||||
}
|
||||
}
|
||||
@@ -576,9 +584,7 @@ public class GameSessionService {
|
||||
claimedTile.getDisplayName()
|
||||
));
|
||||
|
||||
if (table.getWallTiles().isEmpty()) {
|
||||
table.setPhase(GamePhase.FINISHED);
|
||||
appendAndPublish(session, GameEvent.phaseChanged(session.getGameId(), table.getPhase().name()));
|
||||
if (finishTableIfNeeded(session)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -602,55 +608,57 @@ public class GameSessionService {
|
||||
private void executeHu(
|
||||
GameSession session,
|
||||
ResponseActionWindow responseActionWindow,
|
||||
ResponseActionResolution resolution
|
||||
List<ResponseActionResolution> resolutions
|
||||
) {
|
||||
Tile claimedTile = parseTile(resolution.triggerTile());
|
||||
ResponseActionResolution primaryResolution = resolutions.get(0);
|
||||
Tile claimedTile = parseTile(primaryResolution.triggerTile());
|
||||
GameTable table = session.getTable();
|
||||
GameSeat winnerSeat = table.getSeats().get(resolution.winnerSeatNo());
|
||||
GameSeat sourceSeat = table.getSeats().get(resolution.sourceSeatNo());
|
||||
GameSeat sourceSeat = table.getSeats().get(primaryResolution.sourceSeatNo());
|
||||
|
||||
winnerSeat.receiveTile(claimedTile);
|
||||
if (isSupplementalGangWindow(responseActionWindow)) {
|
||||
sourceSeat.removeMatchingHandTiles(claimedTile, 1);
|
||||
} else {
|
||||
sourceSeat.removeMatchingDiscardTile(claimedTile);
|
||||
}
|
||||
winnerSeat.declareHu();
|
||||
// 一炮多响时多个赢家共用同一张弃牌或补杠牌,牌源只移除一次,赢家逐个补牌并独立结算。
|
||||
for (ResponseActionResolution resolution : resolutions) {
|
||||
GameSeat winnerSeat = table.getSeats().get(resolution.winnerSeatNo());
|
||||
winnerSeat.receiveTile(claimedTile);
|
||||
winnerSeat.declareHu();
|
||||
|
||||
appendAndPublish(session, GameEvent.responseActionDeclared(
|
||||
session.getGameId(),
|
||||
GameEventType.HU_DECLARED,
|
||||
winnerSeat.getSeatNo(),
|
||||
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,
|
||||
appendAndPublish(session, GameEvent.responseActionDeclared(
|
||||
session.getGameId(),
|
||||
GameEventType.HU_DECLARED,
|
||||
winnerSeat.getSeatNo(),
|
||||
sourceSeat.getSeatNo(),
|
||||
claimedTile.getDisplayName()
|
||||
));
|
||||
} else {
|
||||
// 牌墙已空时,这次点炮对应“最后一张牌后的弃牌胡”,按海底炮加番。
|
||||
boolean gangShangPao = !isSupplementalGangWindow(responseActionWindow)
|
||||
&& isPostGangDiscard(session, sourceSeat.getSeatNo());
|
||||
boolean haiDiPao = !isSupplementalGangWindow(responseActionWindow)
|
||||
&& table.getWallTiles().isEmpty();
|
||||
if (isSupplementalGangWindow(responseActionWindow)) {
|
||||
appendSettlementEvents(session, settlementService.settleRobbingGangHu(
|
||||
table,
|
||||
winnerSeat.getSeatNo(),
|
||||
sourceSeat.getSeatNo(),
|
||||
claimedTile.getDisplayName()
|
||||
));
|
||||
continue;
|
||||
}
|
||||
appendSettlementEvents(session, settlementService.settleDiscardHu(
|
||||
table,
|
||||
winnerSeat.getSeatNo(),
|
||||
sourceSeat.getSeatNo(),
|
||||
claimedTile.getDisplayName(),
|
||||
gangShangPao,
|
||||
haiDiPao
|
||||
table,
|
||||
winnerSeat.getSeatNo(),
|
||||
sourceSeat.getSeatNo(),
|
||||
claimedTile.getDisplayName(),
|
||||
gangShangPao,
|
||||
haiDiPao
|
||||
));
|
||||
}
|
||||
session.clearPostGangContext();
|
||||
|
||||
if (shouldFinishTable(table)) {
|
||||
table.setPhase(GamePhase.FINISHED);
|
||||
appendAndPublish(session, GameEvent.phaseChanged(session.getGameId(), table.getPhase().name()));
|
||||
if (finishTableIfNeeded(session)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -681,13 +689,11 @@ public class GameSessionService {
|
||||
));
|
||||
session.clearPostGangContext();
|
||||
|
||||
if (shouldFinishTable(table)) {
|
||||
table.setPhase(GamePhase.FINISHED);
|
||||
appendAndPublish(session, GameEvent.phaseChanged(session.getGameId(), table.getPhase().name()));
|
||||
if (finishTableIfNeeded(session)) {
|
||||
return;
|
||||
}
|
||||
|
||||
moveToNextSeat(table, session.getGameId());
|
||||
moveToNextSeat(session);
|
||||
autoPlayBots(session);
|
||||
notifyActionIfHumanTurn(session);
|
||||
}
|
||||
@@ -714,9 +720,7 @@ public class GameSessionService {
|
||||
gangTile.getDisplayName()
|
||||
));
|
||||
|
||||
if (table.getWallTiles().isEmpty()) {
|
||||
table.setPhase(GamePhase.FINISHED);
|
||||
appendAndPublish(session, GameEvent.phaseChanged(session.getGameId(), table.getPhase().name()));
|
||||
if (finishTableIfNeeded(session)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -776,9 +780,7 @@ public class GameSessionService {
|
||||
gangTile.getDisplayName()
|
||||
));
|
||||
|
||||
if (table.getWallTiles().isEmpty()) {
|
||||
table.setPhase(GamePhase.FINISHED);
|
||||
appendAndPublish(session, GameEvent.phaseChanged(session.getGameId(), table.getPhase().name()));
|
||||
if (finishTableIfNeeded(session)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -816,6 +818,7 @@ public class GameSessionService {
|
||||
}
|
||||
|
||||
private void appendSettlementEvents(GameSession session, SettlementResult settlementResult) {
|
||||
session.getSettlementHistory().add(settlementResult);
|
||||
appendAndPublish(session, GameEvent.settlementApplied(session.getGameId(), settlementResult));
|
||||
for (ScoreChange scoreChange : settlementResult.scoreChanges()) {
|
||||
appendAndPublish(session, GameEvent.scoreChanged(
|
||||
@@ -828,6 +831,99 @@ public class GameSessionService {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean finishTableIfNeeded(GameSession session) {
|
||||
GameTable table = session.getTable();
|
||||
if (!shouldFinishTable(table)) {
|
||||
return false;
|
||||
}
|
||||
// 血战流局场景下,牌墙耗尽但仍有多人未胡,需要先做退税和查叫,再真正结束牌局。
|
||||
if (table.getWallTiles().isEmpty() && countActiveSeats(table) > 1) {
|
||||
applyDrawEndSettlements(session);
|
||||
}
|
||||
session.clearPostGangContext();
|
||||
table.setPhase(GamePhase.FINISHED);
|
||||
appendAndPublish(session, GameEvent.phaseChanged(session.getGameId(), table.getPhase().name()));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void applyDrawEndSettlements(GameSession session) {
|
||||
Map<Integer, ReadyHandOption> readyHandOptions = buildReadyHandOptions(session.getTable());
|
||||
Set<Integer> notReadySeatNos = session.getTable().getSeats().stream()
|
||||
.filter(seat -> !seat.isWon())
|
||||
.map(GameSeat::getSeatNo)
|
||||
.filter(seatNo -> !readyHandOptions.containsKey(seatNo))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
applyTaxRefunds(session, notReadySeatNos);
|
||||
applyChaJiao(session, readyHandOptions, notReadySeatNos);
|
||||
}
|
||||
|
||||
private Map<Integer, ReadyHandOption> buildReadyHandOptions(GameTable table) {
|
||||
Map<Integer, ReadyHandOption> readyHandOptions = new LinkedHashMap<>();
|
||||
for (GameSeat seat : table.getSeats()) {
|
||||
if (seat.isWon()) {
|
||||
continue;
|
||||
}
|
||||
scoringService.findBestReadyHandOption(seat)
|
||||
.ifPresent(option -> readyHandOptions.put(seat.getSeatNo(), option));
|
||||
}
|
||||
return readyHandOptions;
|
||||
}
|
||||
|
||||
private void applyTaxRefunds(GameSession session, Set<Integer> notReadySeatNos) {
|
||||
if (notReadySeatNos.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 退税只针对未听牌玩家此前“已经拿到手”的杠分收入,按原支付关系逐笔退回。
|
||||
for (SettlementResult settlementResult : List.copyOf(session.getSettlementHistory())) {
|
||||
if (!isGangSettlement(settlementResult) || !notReadySeatNos.contains(settlementResult.actorSeatNo())) {
|
||||
continue;
|
||||
}
|
||||
for (ScoreChange scoreChange : settlementResult.scoreChanges()) {
|
||||
if (scoreChange.delta() >= 0) {
|
||||
continue;
|
||||
}
|
||||
appendSettlementEvents(session, settlementService.settleTaxRefund(
|
||||
session.getTable(),
|
||||
scoreChange.seatNo(),
|
||||
settlementResult.actorSeatNo(),
|
||||
-scoreChange.delta(),
|
||||
settlementResult.triggerTile()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyChaJiao(
|
||||
GameSession session,
|
||||
Map<Integer, ReadyHandOption> readyHandOptions,
|
||||
Set<Integer> notReadySeatNos
|
||||
) {
|
||||
if (notReadySeatNos.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 查叫按“未听牌向所有听牌玩家分别赔付其最大理论点炮值”处理,先不做花猪等额外分支。
|
||||
for (Integer notReadySeatNo : notReadySeatNos) {
|
||||
for (Map.Entry<Integer, ReadyHandOption> entry : readyHandOptions.entrySet()) {
|
||||
ReadyHandOption readyHandOption = entry.getValue();
|
||||
SettlementDetail settlementDetail = readyHandOption.settlementDetail();
|
||||
appendSettlementEvents(session, settlementService.settleChaJiao(
|
||||
session.getTable(),
|
||||
entry.getKey(),
|
||||
notReadySeatNo,
|
||||
readyHandOption.triggerTile(),
|
||||
settlementDetail
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isGangSettlement(SettlementResult settlementResult) {
|
||||
return settlementResult.settlementType() == SettlementType.MING_GANG
|
||||
|| settlementResult.settlementType() == SettlementType.BU_GANG
|
||||
|| settlementResult.settlementType() == SettlementType.AN_GANG;
|
||||
}
|
||||
|
||||
private boolean shouldFinishTable(GameTable table) {
|
||||
return table.getWallTiles().isEmpty() || countActiveSeats(table) <= 1;
|
||||
}
|
||||
|
||||
@@ -2,27 +2,52 @@ package com.xuezhanmaster.game.service;
|
||||
|
||||
import com.xuezhanmaster.game.domain.ActionType;
|
||||
import com.xuezhanmaster.game.domain.ResponseActionResolution;
|
||||
import com.xuezhanmaster.game.domain.ResponseActionResolutionBatch;
|
||||
import com.xuezhanmaster.game.domain.ResponseActionWindow;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
public class ResponseActionResolver {
|
||||
|
||||
public Optional<ResponseActionResolution> resolve(ResponseActionWindow window, Map<Integer, ActionType> selections, int seatCount) {
|
||||
return selections.entrySet().stream()
|
||||
.filter(entry -> entry.getValue() != ActionType.PASS)
|
||||
.min(Comparator
|
||||
.comparingInt((Map.Entry<Integer, ActionType> entry) -> priority(entry.getValue()))
|
||||
.thenComparingInt(entry -> seatDistance(window.sourceSeatNo(), entry.getKey(), seatCount)))
|
||||
public Optional<ResponseActionResolutionBatch> resolve(
|
||||
ResponseActionWindow window,
|
||||
Map<Integer, ActionType> selections,
|
||||
int seatCount
|
||||
) {
|
||||
// 只要有胡牌声明,就按一炮多响返回全部胡牌赢家,其它动作全部失效。
|
||||
List<ResponseActionResolution> huResolutions = selections.entrySet().stream()
|
||||
.filter(entry -> entry.getValue() == ActionType.HU)
|
||||
.sorted(Comparator.comparingInt(entry -> seatDistance(window.sourceSeatNo(), entry.getKey(), seatCount)))
|
||||
.map(entry -> new ResponseActionResolution(
|
||||
entry.getValue(),
|
||||
entry.getKey(),
|
||||
window.sourceSeatNo(),
|
||||
window.triggerTile()
|
||||
))
|
||||
.toList();
|
||||
if (!huResolutions.isEmpty()) {
|
||||
// 一炮多响时保留所有胡牌赢家,后续由执行层逐个结算。
|
||||
return Optional.of(new ResponseActionResolutionBatch(ActionType.HU, huResolutions));
|
||||
}
|
||||
|
||||
return selections.entrySet().stream()
|
||||
.filter(entry -> entry.getValue() != ActionType.PASS)
|
||||
.min(Comparator
|
||||
.comparingInt((Map.Entry<Integer, ActionType> entry) -> priority(entry.getValue()))
|
||||
.thenComparingInt(entry -> seatDistance(window.sourceSeatNo(), entry.getKey(), seatCount)))
|
||||
.map(entry -> new ResponseActionResolutionBatch(
|
||||
entry.getValue(),
|
||||
List.of(new ResponseActionResolution(
|
||||
entry.getValue(),
|
||||
entry.getKey(),
|
||||
window.sourceSeatNo(),
|
||||
window.triggerTile()
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,49 @@ public class SettlementService {
|
||||
);
|
||||
}
|
||||
|
||||
public SettlementResult settleTaxRefund(
|
||||
GameTable table,
|
||||
int recipientSeatNo,
|
||||
int sourceSeatNo,
|
||||
int refundAmount,
|
||||
String triggerTile
|
||||
) {
|
||||
return applySettlement(
|
||||
table,
|
||||
SettlementType.TUI_SHUI,
|
||||
ActionType.GANG,
|
||||
recipientSeatNo,
|
||||
sourceSeatNo,
|
||||
triggerTile,
|
||||
buildFlatDetail(refundAmount),
|
||||
orderedDeltas(recipientSeatNo, refundAmount, sourceSeatNo, -refundAmount)
|
||||
);
|
||||
}
|
||||
|
||||
public SettlementResult settleChaJiao(
|
||||
GameTable table,
|
||||
int winnerSeatNo,
|
||||
int sourceSeatNo,
|
||||
String triggerTile,
|
||||
SettlementDetail settlementDetail
|
||||
) {
|
||||
return applySettlement(
|
||||
table,
|
||||
SettlementType.CHA_JIAO,
|
||||
ActionType.HU,
|
||||
winnerSeatNo,
|
||||
sourceSeatNo,
|
||||
triggerTile,
|
||||
settlementDetail,
|
||||
orderedDeltas(
|
||||
winnerSeatNo,
|
||||
settlementDetail.paymentScore(),
|
||||
sourceSeatNo,
|
||||
-settlementDetail.paymentScore()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private SettlementResult applySettlement(
|
||||
GameTable table,
|
||||
SettlementType settlementType,
|
||||
@@ -228,4 +271,8 @@ public class SettlementService {
|
||||
deltas.put(sourceSeatNo, sourceDelta);
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private SettlementDetail buildFlatDetail(int paymentScore) {
|
||||
return new SettlementDetail(1, 0, paymentScore, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user