@@ -7,7 +7,10 @@ import com.xuezhanmaster.game.domain.ScoreChange;
import com.xuezhanmaster.game.domain.SettlementFan ;
import com.xuezhanmaster.game.domain.SettlementResult ;
import com.xuezhanmaster.game.domain.SettlementType ;
import com.xuezhanmaster.game.event.GameEvent ;
import com.xuezhanmaster.game.event.GameEventType ;
import com.xuezhanmaster.game.service.GameSessionService ;
import com.xuezhanmaster.review.dto.ReviewActionTimelineItem ;
import com.xuezhanmaster.review.dto.ReviewMistakeItem ;
import com.xuezhanmaster.review.dto.ReviewSettlementItem ;
import com.xuezhanmaster.review.dto.ReviewSummaryResponse ;
@@ -18,6 +21,7 @@ import java.util.ArrayList;
import java.util.Comparator ;
import java.util.LinkedHashSet ;
import java.util.List ;
import java.util.Map ;
import java.util.Set ;
@Service
@@ -37,8 +41,9 @@ public class ReviewService {
. orElseThrow ( ( ) - > new BusinessException ( " GAME_SEAT_NOT_FOUND " , " 当前玩家不在对局中 " ) ) ;
List < SettlementResult > settlementHistory = List . copyOf ( session . getSettlementHistory ( ) ) ;
List < ReviewActionTimelineItem > actionTimeline = buildActionTimeline ( session , reviewSeat ) ;
List < ReviewSettlementItem > settlementTimeline = buildSettlementTimeline ( settlementHistory , reviewSeat . getSeatNo ( ) ) ;
List < ReviewMistakeItem > mistakeInsights = buildMistakeInsights ( settlementHistory , reviewSeat . getSeatNo ( ) ) ;
List < ReviewMistakeItem > mistakeInsights = buildMistakeInsights ( session , settlementHistory, reviewSeat . getSeatNo ( ) ) ;
List < ReviewTrainingFocusItem > trainingFocuses = buildTrainingFocuses (
settlementHistory ,
reviewSeat . getSeatNo ( ) ,
@@ -54,12 +59,40 @@ public class ReviewService {
reviewSeat . getScore ( ) ,
buildResultLabel ( reviewSeat . getScore ( ) ) ,
buildConclusion ( reviewSeat , settlementTimeline , mistakeInsights ) ,
actionTimeline ,
settlementTimeline ,
mistakeInsights ,
trainingFocuses
) ;
}
private List < ReviewActionTimelineItem > buildActionTimeline ( GameSession session , GameSeat reviewSeat ) {
List < ReviewActionTimelineItem > items = new ArrayList < > ( ) ;
List < GameEvent > events = session . getEvents ( ) ;
for ( int index = 0 ; index < events . size ( ) ; index + + ) {
GameEvent event = events . get ( index ) ;
if ( ! shouldIncludeInTimeline ( event , reviewSeat . getSeatNo ( ) ) ) {
continue ;
}
items . add ( new ReviewActionTimelineItem (
index + 1 ,
event . eventType ( ) . name ( ) ,
buildActionTitle ( event , reviewSeat . getSeatNo ( ) ) ,
buildActionSummary ( event , reviewSeat )
) ) ;
}
if ( items . isEmpty ( ) ) {
items . add ( new ReviewActionTimelineItem (
0 ,
" NO_RELEVANT_EVENT " ,
" 暂无关键动作 " ,
" 当前对局还没有形成与你直接相关的关键动作样本,后续随着摸打、响应和结算发生会逐步补齐。 "
) ) ;
}
int fromIndex = Math . max ( items . size ( ) - 12 , 0 ) ;
return List . copyOf ( items . subList ( fromIndex , items . size ( ) ) ) ;
}
private List < ReviewSettlementItem > buildSettlementTimeline ( List < SettlementResult > settlementHistory , int seatNo ) {
List < ReviewSettlementItem > items = new ArrayList < > ( ) ;
for ( SettlementResult settlementResult : settlementHistory ) {
@@ -86,7 +119,7 @@ public class ReviewService {
return List . copyOf ( items ) ;
}
private List < ReviewMistakeItem > buildMistakeInsights ( List < SettlementResult > settlementHistory , int seatNo ) {
private List < ReviewMistakeItem > buildMistakeInsights ( GameSession session , List< SettlementResult > settlementHistory , int seatNo ) {
List < SettlementResult > negativeSettlements = settlementHistory . stream ( )
. filter ( result - > {
Integer delta = resolveSeatDelta ( result , seatNo ) ;
@@ -99,48 +132,131 @@ public class ReviewService {
List < ReviewMistakeItem > items = new ArrayList < > ( ) ;
for ( SettlementResult settlementResult : negativeSettlements ) {
int scoreDelta = resolveSeatDelta ( settlementResult , seatNo ) ;
ReviewActionTimelineItem relatedAction = findRelatedMistakeAction ( session , settlementResult , seatNo ) ;
MistakeRecommendation recommendation = buildMistakeRecommendation ( settlementResult ) ;
List < String > contextSignals = buildContextSignals ( session , settlementResult , seatNo , relatedAction ) ;
PressureSource pressureSource = buildPressureSource ( session , settlementResult , seatNo , relatedAction ) ;
String pressureSourceDetail = buildPressureSourceDetail ( session , settlementResult , seatNo , relatedAction ) ;
String pressureSummary = buildPressureSummary ( session , settlementResult , seatNo , relatedAction ) ;
items . add ( switch ( settlementResult . settlementType ( ) ) {
case DIAN_PAO_HU - > new ReviewMistakeItem (
" HIGH " ,
" 点炮导致失分 " ,
" 你在他人弃牌胡结算中承担了 " + formatScore ( scoreDelta ) + " ,说明中后盘危险张控制仍有明显缺口。 " ,
" 当对手已经副露成型或番型抬高时,优先保留现成安全张,避免继续压高危中张。 "
" 当对手已经副露成型或番型抬高时,优先保留现成安全张,避免继续压高危中张。 " ,
recommendation . directionType ( ) ,
recommendation . directionLabel ( ) ,
recommendation . recommendedAction ( ) ,
pressureSource . sourceType ( ) ,
pressureSource . sourceLabel ( ) ,
pressureSourceDetail ,
pressureSummary ,
contextSignals ,
relatedAction = = null ? null : relatedAction . stepNo ( ) ,
relatedAction = = null ? null : relatedAction . eventType ( ) ,
relatedAction = = null ? null : relatedAction . title ( )
) ;
case QIANG_GANG_HU - > new ReviewMistakeItem (
" HIGH " ,
" 补杠时机过激 " ,
" 你在补杠后被对手抢杠胡,直接产生 " + formatScore ( scoreDelta ) + " 的回撤。 " ,
" 补杠前先检查场上是否已出现高压听牌信号,必要时放弃补杠收益,优先确保不放大失分。 "
" 补杠前先检查场上是否已出现高压听牌信号,必要时放弃补杠收益,优先确保不放大失分。 " ,
recommendation . directionType ( ) ,
recommendation . directionLabel ( ) ,
recommendation . recommendedAction ( ) ,
pressureSource . sourceType ( ) ,
pressureSource . sourceLabel ( ) ,
pressureSourceDetail ,
pressureSummary ,
contextSignals ,
relatedAction = = null ? null : relatedAction . stepNo ( ) ,
relatedAction = = null ? null : relatedAction . eventType ( ) ,
relatedAction = = null ? null : relatedAction . title ( )
) ;
case ZI_MO_HU - > new ReviewMistakeItem (
" MEDIUM " ,
" 未能压住对手自摸 " ,
" 对手自摸时你被扣除 " + formatScore ( scoreDelta ) + " ,说明本局在速度或成叫质量上没有形成足够压制。 " ,
" 后续训练应强化中盘成型速度判断,避免在明显落后牌速时仍维持松散手型。 "
" 后续训练应强化中盘成型速度判断,避免在明显落后牌速时仍维持松散手型。 " ,
recommendation . directionType ( ) ,
recommendation . directionLabel ( ) ,
recommendation . recommendedAction ( ) ,
pressureSource . sourceType ( ) ,
pressureSource . sourceLabel ( ) ,
pressureSourceDetail ,
pressureSummary ,
contextSignals ,
relatedAction = = null ? null : relatedAction . stepNo ( ) ,
relatedAction = = null ? null : relatedAction . eventType ( ) ,
relatedAction = = null ? null : relatedAction . title ( )
) ;
case TUI_SHUI - > new ReviewMistakeItem (
" MEDIUM " ,
" 杠分未守住 " ,
" 本局出现退税,导致你回吐 " + formatScore ( scoreDelta ) + " ,说明前序杠牌收益没有稳定兑现。 " ,
" 练习在流局风险升高时复盘杠牌收益与听牌质量,避免为短期税分牺牲整体成叫。 "
" 练习在流局风险升高时复盘杠牌收益与听牌质量,避免为短期税分牺牲整体成叫。 " ,
recommendation . directionType ( ) ,
recommendation . directionLabel ( ) ,
recommendation . recommendedAction ( ) ,
pressureSource . sourceType ( ) ,
pressureSource . sourceLabel ( ) ,
pressureSourceDetail ,
pressureSummary ,
contextSignals ,
relatedAction = = null ? null : relatedAction . stepNo ( ) ,
relatedAction = = null ? null : relatedAction . eventType ( ) ,
relatedAction = = null ? null : relatedAction . title ( )
) ;
case CHA_JIAO - > new ReviewMistakeItem (
" HIGH " ,
" 流局未成叫 " ,
" 流局查叫阶段你承担了 " + formatScore ( scoreDelta ) + " ,反映出收尾阶段的成叫效率不足。 " ,
" 进入残局后要更早切换到保叫路线,优先确保最小可支付听牌,而不是继续追求高番远型。 "
" 进入残局后要更早切换到保叫路线,优先确保最小可支付听牌,而不是继续追求高番远型。 " ,
recommendation . directionType ( ) ,
recommendation . directionLabel ( ) ,
recommendation . recommendedAction ( ) ,
pressureSource . sourceType ( ) ,
pressureSource . sourceLabel ( ) ,
pressureSourceDetail ,
pressureSummary ,
contextSignals ,
relatedAction = = null ? null : relatedAction . stepNo ( ) ,
relatedAction = = null ? null : relatedAction . eventType ( ) ,
relatedAction = = null ? null : relatedAction . title ( )
) ;
case MING_GANG - > new ReviewMistakeItem (
" MEDIUM " ,
" 放杠失分 " ,
" 你在他人明杠结算里承担了 " + formatScore ( scoreDelta ) + " ,说明对明显碰后加杠的防范还不够。 " ,
" 看到对手碰后,应更早记录其可能补强的牌张,减少继续喂牌或协助做大的情况。 "
" 看到对手碰后,应更早记录其可能补强的牌张,减少继续喂牌或协助做大的情况。 " ,
recommendation . directionType ( ) ,
recommendation . directionLabel ( ) ,
recommendation . recommendedAction ( ) ,
pressureSource . sourceType ( ) ,
pressureSource . sourceLabel ( ) ,
pressureSourceDetail ,
pressureSummary ,
contextSignals ,
relatedAction = = null ? null : relatedAction . stepNo ( ) ,
relatedAction = = null ? null : relatedAction . eventType ( ) ,
relatedAction = = null ? null : relatedAction . title ( )
) ;
case BU_GANG , AN_GANG - > new ReviewMistakeItem (
" LOW " ,
" 杠分压制不足 " ,
" 对手通过杠牌从你这里拿走了 " + formatScore ( scoreDelta ) + " ,本局资源交换略偏被动。 " ,
" 后续可专项训练杠前后局势判断,明确什么时候该抢速度,什么时候该减少被动付分。 "
" 后续可专项训练杠前后局势判断,明确什么时候该抢速度,什么时候该减少被动付分。 " ,
recommendation . directionType ( ) ,
recommendation . directionLabel ( ) ,
recommendation . recommendedAction ( ) ,
pressureSource . sourceType ( ) ,
pressureSource . sourceLabel ( ) ,
pressureSourceDetail ,
pressureSummary ,
contextSignals ,
relatedAction = = null ? null : relatedAction . stepNo ( ) ,
relatedAction = = null ? null : relatedAction . eventType ( ) ,
relatedAction = = null ? null : relatedAction . title ( )
) ;
} ) ;
}
@@ -397,4 +513,553 @@ public class ReviewService {
private String formatScore ( int scoreDelta ) {
return scoreDelta > 0 ? " + " + scoreDelta : String . valueOf ( scoreDelta ) ;
}
private MistakeRecommendation buildMistakeRecommendation ( SettlementResult settlementResult ) {
return switch ( settlementResult . settlementType ( ) ) {
case DIAN_PAO_HU - > new MistakeRecommendation (
" RISK_CONTROL " ,
" 优先处理危险张 " ,
" 若桌面已经出现连续副露或高番压力,下一次应先切出现成安全张,再决定是否继续进攻。 "
) ;
case QIANG_GANG_HU - > new MistakeRecommendation (
" ABANDON_GANG " ,
" 高压局面不补杠 " ,
" 当补杠只带来小收益、却可能暴露给多家抢杠时,优先保留碰牌形态继续过渡。 "
) ;
case ZI_MO_HU - > new MistakeRecommendation (
" SPEED_UP " ,
" 提速成型并压制 " ,
" 中盘若牌速落后,应尽快收缩远型组合,把手牌切回更快的成型路线。 "
) ;
case TUI_SHUI - > new MistakeRecommendation (
" TAX_PROTECTION " ,
" 杠后优先守住收益 " ,
" 拿到杠分后要尽快检查成叫质量,若流局风险升高,应转成保税而不是继续冒进。 "
) ;
case CHA_JIAO - > new MistakeRecommendation (
" READY_HAND " ,
" 残局优先保叫 " ,
" 进入残局后先确保最小可支付听牌,再考虑是否有空间继续追求更高番型。 "
) ;
case MING_GANG - > new MistakeRecommendation (
" DANGER_TRACKING " ,
" 记录对手碰后危险张 " ,
" 对手碰牌后,优先把该组相关牌张标成高危候选,减少继续喂牌的概率。 "
) ;
case BU_GANG , AN_GANG - > new MistakeRecommendation (
" RISK_CONTROL " ,
" 减少被动付分窗口 " ,
" 当对手已经连续拿杠分时,后续应更快转守,避免继续给出额外交换空间。 "
) ;
} ;
}
private List < String > buildContextSignals (
GameSession session ,
SettlementResult settlementResult ,
int seatNo ,
ReviewActionTimelineItem relatedAction
) {
List < String > signals = new ArrayList < > ( ) ;
if ( settlementResult . triggerTile ( ) ! = null & & ! settlementResult . triggerTile ( ) . isBlank ( ) ) {
signals . add ( " 触发牌: " + settlementResult . triggerTile ( ) ) ;
}
int settlementEventIndex = findSettlementEventIndex ( session . getEvents ( ) , settlementResult , seatNo ) ;
int referenceIndex = relatedAction ! = null ? relatedAction . stepNo ( ) - 1 : settlementEventIndex ;
if ( referenceIndex < 0 ) {
return List . copyOf ( signals ) ;
}
int remainingWallCount = findLatestRemainingWallCount ( session . getEvents ( ) , referenceIndex ) ;
if ( remainingWallCount > = 0 ) {
if ( remainingWallCount < = 16 ) {
signals . add ( " 已接近流局,牌墙剩余 " + remainingWallCount + " 张 " ) ;
} else {
signals . add ( " 当时牌墙剩余 " + remainingWallCount + " 张 " ) ;
}
}
List < String > opponentMeldSignals = buildOpponentMeldSignals ( session . getEvents ( ) , seatNo , referenceIndex ) ;
signals . addAll ( opponentMeldSignals ) ;
if ( relatedAction ! = null & & " 个人出牌 " . equals ( relatedAction . title ( ) ) ) {
signals . add ( " 你的这次出牌直接连到了后续失分结算 " ) ;
}
if ( settlementResult . settlementType ( ) = = SettlementType . QIANG_GANG_HU ) {
signals . add ( " 这是一次补杠窗口,场上存在抢杠风险 " ) ;
}
if ( settlementResult . settlementType ( ) = = SettlementType . CHA_JIAO ) {
signals . add ( " 流局阶段仍未形成有效成叫 " ) ;
}
return List . copyOf ( signals ) ;
}
private String buildPressureSummary (
GameSession session ,
SettlementResult settlementResult ,
int seatNo ,
ReviewActionTimelineItem relatedAction
) {
int settlementEventIndex = findSettlementEventIndex ( session . getEvents ( ) , settlementResult , seatNo ) ;
int referenceIndex = relatedAction ! = null ? relatedAction . stepNo ( ) - 1 : settlementEventIndex ;
int remainingWallCount = referenceIndex > = 0 ? findLatestRemainingWallCount ( session . getEvents ( ) , referenceIndex ) : - 1 ;
int actorSeatNo = settlementResult . actorSeatNo ( ) ;
int actorMeldCount = referenceIndex > = 0 ? countSeatMeldBefore ( session . getEvents ( ) , actorSeatNo , referenceIndex ) : 0 ;
if ( settlementResult . settlementType ( ) = = SettlementType . CHA_JIAO ) {
return " 主要压力来自残局收尾:你在流局前仍未形成有效成叫,最终被座位 "
+ actorSeatNo + " 在查叫阶段完成收分。 " ;
}
if ( settlementResult . settlementType ( ) = = SettlementType . QIANG_GANG_HU ) {
return " 主要压力来自补杠暴露:你的补杠把结算窗口直接交给了座位 "
+ actorSeatNo + " ,这类高压局面不适合继续追求小额杠分。 " ;
}
if ( actorSeatNo ! = seatNo & & actorMeldCount > = 2 ) {
return " 主要压力来自座位 " + actorSeatNo + " :在这一步前其已完成 "
+ actorMeldCount + " 次副露,公开进攻信号已经明显抬高。 " ;
}
if ( remainingWallCount > = 0 & & remainingWallCount < = 16 ) {
return " 主要压力来自残局阶段:当时牌墙仅剩 " + remainingWallCount
+ " 张,局面已经更偏向保叫和防守,而不是继续放大交换。 " ;
}
if ( settlementResult . settlementType ( ) = = SettlementType . ZI_MO_HU ) {
return " 主要压力来自速度落后:对手在你完成有效压制前率先成牌,自摸把这手的被动直接放大。 " ;
}
if ( actorSeatNo ! = seatNo ) {
return " 主要压力来自座位 " + actorSeatNo + " :这次失分说明你没有及时根据该家的公开动作调整守备等级。 " ;
}
return " 主要压力来自场面节奏失衡:当前这手交换更偏被动,应更早识别风险并切回稳健路线。 " ;
}
private String buildPressureSourceDetail (
GameSession session ,
SettlementResult settlementResult ,
int seatNo ,
ReviewActionTimelineItem relatedAction
) {
List < GameEvent > events = session . getEvents ( ) ;
int settlementEventIndex = findSettlementEventIndex ( events , settlementResult , seatNo ) ;
int referenceIndex = relatedAction ! = null ? relatedAction . stepNo ( ) - 1 : settlementEventIndex ;
int remainingWallCount = referenceIndex > = 0 ? findLatestRemainingWallCount ( events , referenceIndex ) : - 1 ;
int actorSeatNo = settlementResult . actorSeatNo ( ) ;
int actorMeldCount = referenceIndex > = 0 ? countSeatMeldBefore ( events , actorSeatNo , referenceIndex ) : 0 ;
GameEvent latestActorMeld = referenceIndex > = 0 ? findLatestSeatMeldEventBefore ( events , actorSeatNo , referenceIndex ) : null ;
String triggerPhrase = buildPressureTriggerPhrase ( settlementResult , relatedAction ) ;
String latestMeldSummary = buildLatestMeldSummary ( latestActorMeld ) ;
if ( settlementResult . settlementType ( ) = = SettlementType . CHA_JIAO ) {
String wallSummary = remainingWallCount > = 0 ? " 牌墙只剩 " + remainingWallCount + " 张时, " : " " ;
return " 座位 " + actorSeatNo + " 把这手拖进了查叫收尾窗口; "
+ wallSummary
+ triggerPhrase
+ " 仍没能换来有效成叫,最终让这家把残局压力直接兑现成查叫收分。 " ;
}
if ( settlementResult . settlementType ( ) = = SettlementType . QIANG_GANG_HU ) {
return " 座位 " + actorSeatNo + " 等到你的补杠才真正拿到结算入口; "
+ triggerPhrase
+ " 直接把隐藏风险翻成可抢窗口,收益和失分上限明显不对称。 " ;
}
if ( actorSeatNo ! = seatNo & & actorMeldCount > = 2 ) {
return " 座位 " + actorSeatNo + " 在这步前已连续副露 " + actorMeldCount + " 次 "
+ latestMeldSummary
+ " ; "
+ triggerPhrase
+ " ,等于继续把交换留给已经亮出进攻线的玩家。 " ;
}
if ( actorSeatNo ! = seatNo & & actorMeldCount = = 1 ) {
return " 座位 " + actorSeatNo + " 在这步前已经出现过一次公开副露 "
+ latestMeldSummary
+ " ; "
+ triggerPhrase
+ " 时,危险等级其实已经高于普通平推局。 " ;
}
if ( remainingWallCount > = 0 & & remainingWallCount < = 16 ) {
return " 这手已经进入残局窗口,牌墙只剩 " + remainingWallCount + " 张; "
+ triggerPhrase
+ " 时更该优先保留容错,而不是继续放大交换。 " ;
}
if ( settlementResult . settlementType ( ) = = SettlementType . ZI_MO_HU ) {
return " 座位 " + actorSeatNo + " 在公开动作并不夸张的情况下仍先完成自摸,说明其速度线已经提前成型; "
+ triggerPhrase
+ " 前你没有建立起足够的压制。 " ;
}
if ( actorSeatNo ! = seatNo ) {
return " 这次压力主要是由座位 " + actorSeatNo + " 主导出来的; "
+ triggerPhrase
+ " 前你没有及时根据该家的公开动作切换守备等级。 " ;
}
return " 这次失分更多来自节奏判断偏慢; "
+ triggerPhrase
+ " 时,你没有及时把交换切回更稳的路线。 " ;
}
private PressureSource buildPressureSource (
GameSession session ,
SettlementResult settlementResult ,
int seatNo ,
ReviewActionTimelineItem relatedAction
) {
int settlementEventIndex = findSettlementEventIndex ( session . getEvents ( ) , settlementResult , seatNo ) ;
int referenceIndex = relatedAction ! = null ? relatedAction . stepNo ( ) - 1 : settlementEventIndex ;
int remainingWallCount = referenceIndex > = 0 ? findLatestRemainingWallCount ( session . getEvents ( ) , referenceIndex ) : - 1 ;
int actorSeatNo = settlementResult . actorSeatNo ( ) ;
int actorMeldCount = referenceIndex > = 0 ? countSeatMeldBefore ( session . getEvents ( ) , actorSeatNo , referenceIndex ) : 0 ;
if ( settlementResult . settlementType ( ) = = SettlementType . CHA_JIAO ) {
return new PressureSource ( " ENDGAME " , " 残局查叫收尾(座位 " + actorSeatNo + " ) " ) ;
}
if ( settlementResult . settlementType ( ) = = SettlementType . QIANG_GANG_HU ) {
return new PressureSource ( " ROBBING_GANG " , " 补杠暴露为抢杠窗口 " ) ;
}
if ( actorSeatNo ! = seatNo & & actorMeldCount > = 2 ) {
return new PressureSource ( " MULTI_MELD " , " 座位 " + actorSeatNo + " 已连续副露 " + actorMeldCount + " 次 " ) ;
}
if ( actorSeatNo ! = seatNo & & actorMeldCount = = 1 ) {
return new PressureSource ( " SINGLE_MELD " , " 座位 " + actorSeatNo + " 已出现副露 " ) ;
}
if ( remainingWallCount > = 0 & & remainingWallCount < = 16 ) {
return new PressureSource ( " ENDGAME " , " 残局阶段,牌墙剩余 " + remainingWallCount + " 张 " ) ;
}
if ( settlementResult . settlementType ( ) = = SettlementType . ZI_MO_HU ) {
return new PressureSource ( " SPEED_LOSS " , " 对手先成型,你在速度上已落后 " ) ;
}
return new PressureSource ( " TABLE_TENSION " , " 公开动作已使场面压强上升 " ) ;
}
private ReviewActionTimelineItem findRelatedMistakeAction ( GameSession session , SettlementResult settlementResult , int seatNo ) {
List < GameEvent > events = session . getEvents ( ) ;
int settlementEventIndex = findSettlementEventIndex ( events , settlementResult , seatNo ) ;
if ( settlementEventIndex < 0 ) {
return null ;
}
GameEvent relatedEvent = switch ( settlementResult . settlementType ( ) ) {
case DIAN_PAO_HU , MING_GANG , CHA_JIAO - > findLatestEventBefore (
events ,
settlementEventIndex ,
event - > event . eventType ( ) = = GameEventType . TILE_DISCARDED & & event . seatNo ( ) ! = null & & event . seatNo ( ) = = seatNo
) ;
case QIANG_GANG_HU - > findLatestEventBefore (
events ,
settlementEventIndex ,
event - > event . eventType ( ) = = GameEventType . GANG_DECLARED & & event . seatNo ( ) ! = null & & event . seatNo ( ) = = seatNo
) ;
case ZI_MO_HU - > findLatestEventBefore (
events ,
settlementEventIndex ,
event - > event . seatNo ( ) ! = null & & event . seatNo ( ) = = seatNo
& & ( event . eventType ( ) = = GameEventType . TILE_DISCARDED
| | event . eventType ( ) = = GameEventType . TURN_SWITCHED
| | event . eventType ( ) = = GameEventType . TILE_DRAWN )
) ;
case TUI_SHUI , BU_GANG , AN_GANG - > findLatestEventBefore (
events ,
settlementEventIndex ,
event - > event . seatNo ( ) ! = null & & event . seatNo ( ) = = seatNo
& & ( event . eventType ( ) = = GameEventType . GANG_DECLARED
| | event . eventType ( ) = = GameEventType . TILE_DISCARDED )
) ;
} ;
if ( relatedEvent = = null ) {
return null ;
}
int relatedIndex = events . indexOf ( relatedEvent ) ;
return new ReviewActionTimelineItem (
relatedIndex + 1 ,
relatedEvent . eventType ( ) . name ( ) ,
buildActionTitle ( relatedEvent , seatNo ) ,
buildActionSummary ( relatedEvent , session . getTable ( ) . getSeats ( ) . get ( seatNo ) )
) ;
}
private int findSettlementEventIndex ( List < GameEvent > events , SettlementResult settlementResult , int seatNo ) {
for ( int index = events . size ( ) - 1 ; index > = 0 ; index - - ) {
GameEvent event = events . get ( index ) ;
if ( event . eventType ( ) ! = GameEventType . SETTLEMENT_APPLIED ) {
continue ;
}
if ( ! safeString ( event . payload ( ) . get ( " settlementType " ) ) . equals ( settlementResult . settlementType ( ) . name ( ) ) ) {
continue ;
}
if ( ! safeNumber ( event . payload ( ) . get ( " actorSeatNo " ) ) . equals ( String . valueOf ( settlementResult . actorSeatNo ( ) ) ) ) {
continue ;
}
if ( ! safeNumber ( event . payload ( ) . get ( " sourceSeatNo " ) ) . equals ( String . valueOf ( settlementResult . sourceSeatNo ( ) ) ) ) {
continue ;
}
Integer scoreDelta = resolveSeatDeltaFromPayload ( event . payload ( ) , seatNo ) ;
Integer settlementDelta = resolveSeatDelta ( settlementResult , seatNo ) ;
if ( scoreDelta ! = null & & scoreDelta . equals ( settlementDelta ) ) {
return index ;
}
}
return - 1 ;
}
private GameEvent findLatestEventBefore ( List < GameEvent > events , int endExclusive , java . util . function . Predicate < GameEvent > predicate ) {
for ( int index = endExclusive - 1 ; index > = 0 ; index - - ) {
GameEvent event = events . get ( index ) ;
if ( predicate . test ( event ) ) {
return event ;
}
}
return null ;
}
private GameEvent findLatestSeatMeldEventBefore ( List < GameEvent > events , int seatNo , int endExclusive ) {
return findLatestEventBefore (
events ,
endExclusive ,
event - > event . seatNo ( ) ! = null
& & event . seatNo ( ) = = seatNo
& & ( event . eventType ( ) = = GameEventType . PENG_DECLARED
| | event . eventType ( ) = = GameEventType . GANG_DECLARED )
) ;
}
private boolean shouldIncludeInTimeline ( GameEvent event , int seatNo ) {
return switch ( event . eventType ( ) ) {
case GAME_STARTED , GAME_PHASE_CHANGED - > true ;
case LACK_SELECTED , TILE_DISCARDED , TILE_DRAWN , TURN_SWITCHED ,
PENG_DECLARED , GANG_DECLARED , HU_DECLARED , PASS_DECLARED - > event . seatNo ( ) ! = null & & event . seatNo ( ) = = seatNo ;
case RESPONSE_WINDOW_OPENED , RESPONSE_WINDOW_CLOSED - > event . seatNo ( ) ! = null & & event . seatNo ( ) = = seatNo ;
case SETTLEMENT_APPLIED - > hasSettlementImpact ( event . payload ( ) , seatNo ) ;
case SCORE_CHANGED , ACTION_REQUIRED - > false ;
} ;
}
@SuppressWarnings ( " unchecked " )
private boolean hasSettlementImpact ( Map < String , Object > payload , int seatNo ) {
Object scoreChangesObject = payload . get ( " scoreChanges " ) ;
if ( ! ( scoreChangesObject instanceof List < ? > scoreChanges ) ) {
return false ;
}
for ( Object scoreChangeObject : scoreChanges ) {
if ( ! ( scoreChangeObject instanceof Map < ? , ? > rawItem ) ) {
continue ;
}
Object impactedSeatNo = rawItem . get ( " seatNo " ) ;
if ( impactedSeatNo instanceof Number number & & number . intValue ( ) = = seatNo ) {
return true ;
}
}
return false ;
}
private String buildActionTitle ( GameEvent event , int seatNo ) {
return switch ( event . eventType ( ) ) {
case GAME_STARTED - > " 牌局开始 " ;
case GAME_PHASE_CHANGED - > " 阶段切换 " ;
case LACK_SELECTED - > " 完成定缺 " ;
case TILE_DISCARDED - > " 个人出牌 " ;
case TILE_DRAWN - > " 个人摸牌 " ;
case TURN_SWITCHED - > " 轮到自己 " ;
case RESPONSE_WINDOW_OPENED - > " 进入响应窗口 " ;
case RESPONSE_WINDOW_CLOSED - > " 响应窗口关闭 " ;
case PENG_DECLARED - > " 执行碰牌 " ;
case GANG_DECLARED - > " 执行杠牌 " ;
case HU_DECLARED - > " 宣告胡牌 " ;
case PASS_DECLARED - > " 选择过 " ;
case SETTLEMENT_APPLIED - > buildSettlementEventTitle ( event . payload ( ) , seatNo ) ;
case SCORE_CHANGED , ACTION_REQUIRED - > event . eventType ( ) . name ( ) ;
} ;
}
private String buildActionSummary ( GameEvent event , GameSeat reviewSeat ) {
return switch ( event . eventType ( ) ) {
case GAME_STARTED - > " 本局已创建,你当前以“ " + reviewSeat . getNickname ( ) + " ”视角复盘整局关键节点。 " ;
case GAME_PHASE_CHANGED - > " 牌局阶段切换为 " + safeString ( event . payload ( ) . get ( " phase " ) ) + " 。 " ;
case LACK_SELECTED - > " 你已完成定缺,选择花色为 " + safeString ( event . payload ( ) . get ( " lackSuit " ) ) + " 。 " ;
case TILE_DISCARDED - > " 你打出了 " + safeString ( event . payload ( ) . get ( " tile " ) ) + " 。 " ;
case TILE_DRAWN - > " 你摸牌后,牌墙剩余 " + safeNumber ( event . payload ( ) . get ( " remainingWallCount " ) ) + " 张。 " ;
case TURN_SWITCHED - > " 当前轮到你行动,可以据此回看本轮之前的桌面信息。 " ;
case RESPONSE_WINDOW_OPENED - > " 你获得了一个响应窗口,来源座位 "
+ safeNumber ( event . payload ( ) . get ( " sourceSeatNo " ) ) + " ,目标牌为 "
+ safeString ( event . payload ( ) . get ( " triggerTile " ) ) + " 。 " ;
case RESPONSE_WINDOW_CLOSED - > " 你的响应窗口已关闭,最终裁决动作为 "
+ safeString ( event . payload ( ) . get ( " resolvedActionType " ) ) + " 。 " ;
case PENG_DECLARED , GANG_DECLARED , HU_DECLARED , PASS_DECLARED - > " 你执行了 "
+ actionTypeLabel ( event . eventType ( ) ) + " ,关联牌为 "
+ safeString ( event . payload ( ) . get ( " tile " ) ) + " 。 " ;
case SETTLEMENT_APPLIED - > buildSettlementEventSummary ( event . payload ( ) , reviewSeat . getSeatNo ( ) ) ;
case SCORE_CHANGED , ACTION_REQUIRED - > event . eventType ( ) . name ( ) ;
} ;
}
@SuppressWarnings ( " unchecked " )
private String buildSettlementEventTitle ( Map < String , Object > payload , int seatNo ) {
Integer scoreDelta = resolveSeatDeltaFromPayload ( payload , seatNo ) ;
String settlementType = safeString ( payload . get ( " settlementType " ) ) ;
return switch ( settlementType ) {
case " DIAN_PAO_HU " - > scoreDelta ! = null & & scoreDelta > 0 ? " 点炮胡结算 " : " 点炮失分结算 " ;
case " QIANG_GANG_HU " - > scoreDelta ! = null & & scoreDelta > 0 ? " 抢杠胡结算 " : " 补杠被抢结算 " ;
case " ZI_MO_HU " - > scoreDelta ! = null & & scoreDelta > 0 ? " 自摸胡结算 " : " 对手自摸结算 " ;
case " BU_GANG " - > scoreDelta ! = null & & scoreDelta > 0 ? " 补杠收益结算 " : " 补杠相关失分 " ;
case " MING_GANG " - > scoreDelta ! = null & & scoreDelta > 0 ? " 明杠收益结算 " : " 明杠相关失分 " ;
case " AN_GANG " - > scoreDelta ! = null & & scoreDelta > 0 ? " 暗杠收益结算 " : " 暗杠相关失分 " ;
case " TUI_SHUI " - > scoreDelta ! = null & & scoreDelta > 0 ? " 退税回补结算 " : " 退税回吐结算 " ;
case " CHA_JIAO " - > scoreDelta ! = null & & scoreDelta > 0 ? " 查叫收益结算 " : " 查叫失分结算 " ;
default - > " 关键结算 " ;
} ;
}
private String buildSettlementEventSummary ( Map < String , Object > payload , int seatNo ) {
Integer scoreDelta = resolveSeatDeltaFromPayload ( payload , seatNo ) ;
String settlementType = settlementTypeLabelFromPayload ( payload ) ;
String triggerTile = safeString ( payload . get ( " triggerTile " ) ) ;
String actorSeatNo = safeNumber ( payload . get ( " actorSeatNo " ) ) ;
String sourceSeatNo = safeNumber ( payload . get ( " sourceSeatNo " ) ) ;
String tileSuffix = triggerTile . isBlank ( ) ? " " : " ,触发牌 " + triggerTile ;
return " 发生 " + settlementType + " ,执行座位 " + actorSeatNo
+ " ,来源座位 " + sourceSeatNo
+ " ,你本次分值变化为 " + formatScore ( scoreDelta = = null ? 0 : scoreDelta )
+ tileSuffix + " 。 " ;
}
@SuppressWarnings ( " unchecked " )
private Integer resolveSeatDeltaFromPayload ( Map < String , Object > payload , int seatNo ) {
Object scoreChangesObject = payload . get ( " scoreChanges " ) ;
if ( ! ( scoreChangesObject instanceof List < ? > scoreChanges ) ) {
return null ;
}
for ( Object scoreChangeObject : scoreChanges ) {
if ( ! ( scoreChangeObject instanceof Map < ? , ? > scoreChangeMap ) ) {
continue ;
}
Object impactedSeatNo = scoreChangeMap . get ( " seatNo " ) ;
if ( impactedSeatNo instanceof Number number & & number . intValue ( ) = = seatNo ) {
Object delta = scoreChangeMap . get ( " delta " ) ;
return delta instanceof Number deltaNumber ? deltaNumber . intValue ( ) : null ;
}
}
return null ;
}
private String settlementTypeLabelFromPayload ( Map < String , Object > payload ) {
return switch ( safeString ( payload . get ( " settlementType " ) ) ) {
case " DIAN_PAO_HU " - > " 点炮胡 " ;
case " QIANG_GANG_HU " - > " 抢杠胡 " ;
case " ZI_MO_HU " - > " 自摸胡 " ;
case " BU_GANG " - > " 补杠 " ;
case " MING_GANG " - > " 明杠 " ;
case " AN_GANG " - > " 暗杠 " ;
case " TUI_SHUI " - > " 退税 " ;
case " CHA_JIAO " - > " 查叫 " ;
default - > " 关键结算 " ;
} ;
}
private String actionTypeLabel ( GameEventType eventType ) {
return switch ( eventType ) {
case PENG_DECLARED - > " 碰 " ;
case GANG_DECLARED - > " 杠 " ;
case HU_DECLARED - > " 胡 " ;
case PASS_DECLARED - > " 过 " ;
default - > eventType . name ( ) ;
} ;
}
private String safeString ( Object value ) {
return value = = null ? " " : String . valueOf ( value ) ;
}
private String safeNumber ( Object value ) {
return value instanceof Number number ? String . valueOf ( number . intValue ( ) ) : " - " ;
}
private int findLatestRemainingWallCount ( List < GameEvent > events , int endInclusive ) {
for ( int index = endInclusive ; index > = 0 ; index - - ) {
GameEvent event = events . get ( index ) ;
if ( event . eventType ( ) ! = GameEventType . TILE_DRAWN ) {
continue ;
}
Object value = event . payload ( ) . get ( " remainingWallCount " ) ;
if ( value instanceof Number number ) {
return number . intValue ( ) ;
}
}
return - 1 ;
}
private int countSeatMeldBefore ( List < GameEvent > events , int seatNo , int endInclusive ) {
int meldCount = 0 ;
for ( int index = 0 ; index < = endInclusive ; index + + ) {
GameEvent event = events . get ( index ) ;
if ( event . seatNo ( ) = = null | | event . seatNo ( ) ! = seatNo ) {
continue ;
}
if ( event . eventType ( ) = = GameEventType . PENG_DECLARED | | event . eventType ( ) = = GameEventType . GANG_DECLARED ) {
meldCount + + ;
}
}
return meldCount ;
}
private List < String > buildOpponentMeldSignals ( List < GameEvent > events , int seatNo , int endInclusive ) {
Map < Integer , Integer > meldCounts = new java . util . LinkedHashMap < > ( ) ;
for ( int index = 0 ; index < = endInclusive ; index + + ) {
GameEvent event = events . get ( index ) ;
if ( ( event . eventType ( ) ! = GameEventType . PENG_DECLARED & & event . eventType ( ) ! = GameEventType . GANG_DECLARED )
| | event . seatNo ( ) = = null
| | event . seatNo ( ) = = seatNo ) {
continue ;
}
meldCounts . merge ( event . seatNo ( ) , 1 , Integer : : sum ) ;
}
List < String > signals = new ArrayList < > ( ) ;
for ( Map . Entry < Integer , Integer > entry : meldCounts . entrySet ( ) ) {
if ( entry . getValue ( ) < = 0 ) {
continue ;
}
signals . add ( " 座位 " + entry . getKey ( ) + " 当时已有 " + entry . getValue ( ) + " 次副露 " ) ;
}
return signals ;
}
private String buildPressureTriggerPhrase ( SettlementResult settlementResult , ReviewActionTimelineItem relatedAction ) {
String triggerTile = settlementResult . triggerTile ( ) ;
if ( settlementResult . settlementType ( ) = = SettlementType . QIANG_GANG_HU ) {
return triggerTile = = null | | triggerTile . isBlank ( )
? " 你的这次补杠 "
: " 你的这次补杠( " + triggerTile + " ) " ;
}
if ( triggerTile ! = null & & ! triggerTile . isBlank ( ) ) {
if ( relatedAction ! = null & & " 个人出牌 " . equals ( relatedAction . title ( ) ) ) {
return " 你打出的 " + triggerTile ;
}
return " 你围绕 " + triggerTile + " 做出的这次交换 " ;
}
if ( relatedAction ! = null & & relatedAction . title ( ) ! = null & & ! relatedAction . title ( ) . isBlank ( ) ) {
return " 你执行的“ " + relatedAction . title ( ) + " ” " ;
}
return " 你的这次处理 " ;
}
private String buildLatestMeldSummary ( GameEvent latestMeldEvent ) {
if ( latestMeldEvent = = null ) {
return " " ;
}
String actionLabel = actionTypeLabel ( latestMeldEvent . eventType ( ) ) ;
String tile = safeString ( latestMeldEvent . payload ( ) . get ( " tile " ) ) ;
if ( tile . isBlank ( ) ) {
return " ,最近一次公开动作是 " + actionLabel ;
}
return " ,最近一次公开动作是 " + actionLabel + " " + tile ;
}
private record MistakeRecommendation (
String directionType ,
String directionLabel ,
String recommendedAction
) {
}
private record PressureSource (
String sourceType ,
String sourceLabel
) {
}
}