feat(结算): 实现血战麻将查叫与退税功能

新增局终处理逻辑,当牌墙耗尽且有多家未胡时:
1. 退税:未听牌玩家需退还此前杠分收入
2. 查叫:未听牌玩家向听牌玩家赔付最大理论点炮值
新增 SettlementType.TUI_SHUI 和 SettlementType.CHA_JIAO 结算类型
新增 ReadyHandOption 记录最优听牌选项

支持一炮多响裁决,新增 ResponseActionResolutionBatch 承载多赢家结果
在 GameSession 中新增 settlementHistory 保留结算记录供复用
更新开发文档要求加强关键区域的中文注释
This commit is contained in:
hujun
2026-03-20 15:05:00 +08:00
parent 34809fd0f3
commit b84d0e8980
11 changed files with 501 additions and 78 deletions

View File

@@ -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()
))
));
}