feat(教学): 扩展教学建议链路以支持候选牌列表
扩展教学建议链路,在 PrivateTeachingMessage 中增加 candidates 字段,支持前端展示候选牌、评分和原因标签。同时优化前端组件结构,抽离共享类型和工具函数,为后续页面拆分做准备。 - 后端:在 GameSessionService 和 GameMessagePublisher 中透传候选牌列表 - 前端:新增 GameMessageStack 组件展示教学候选,优化手牌区推荐牌高亮 - 测试:补充 GameMessagePublisherTest 验证候选牌消息结构 - 文档:更新 DEVELOPMENT_PLAN 和 H5_GAME_PAGE_ARCHITECTURE 说明当前前端结构
This commit is contained in:
@@ -301,7 +301,8 @@ public class GameSessionService {
|
||||
currentSeat.getPlayerId(),
|
||||
advice.teachingMode(),
|
||||
advice.recommendedAction(),
|
||||
advice.explanation()
|
||||
advice.explanation(),
|
||||
advice.candidates()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.xuezhanmaster.ws.dto;
|
||||
|
||||
import com.xuezhanmaster.teaching.dto.CandidateAdviceItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record PrivateTeachingMessage(
|
||||
String gameId,
|
||||
String userId,
|
||||
String teachingMode,
|
||||
String recommendedAction,
|
||||
String explanation
|
||||
String explanation,
|
||||
List<CandidateAdviceItem> candidates
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.xuezhanmaster.ws.service;
|
||||
import com.xuezhanmaster.game.domain.ResponseActionSeatCandidate;
|
||||
import com.xuezhanmaster.game.domain.ResponseActionWindow;
|
||||
import com.xuezhanmaster.game.event.GameEvent;
|
||||
import com.xuezhanmaster.teaching.dto.CandidateAdviceItem;
|
||||
import com.xuezhanmaster.ws.dto.PrivateActionCandidate;
|
||||
import com.xuezhanmaster.ws.dto.PrivateActionMessage;
|
||||
import com.xuezhanmaster.ws.dto.PrivateTeachingMessage;
|
||||
@@ -97,10 +98,18 @@ public class GameMessagePublisher {
|
||||
);
|
||||
}
|
||||
|
||||
public void publishPrivateTeaching(String gameId, String userId, String teachingMode, String recommendedAction, String explanation) {
|
||||
public void publishPrivateTeaching(
|
||||
String gameId,
|
||||
String userId,
|
||||
String teachingMode,
|
||||
String recommendedAction,
|
||||
String explanation,
|
||||
List<CandidateAdviceItem> candidates
|
||||
) {
|
||||
messagingTemplate.convertAndSend(
|
||||
"/topic/users/" + userId + "/teaching",
|
||||
new PrivateTeachingMessage(gameId, userId, teachingMode, recommendedAction, explanation)
|
||||
// 教学消息需要同时携带推荐结果与备选列表,前端才能把“为什么是这张牌”解释完整。
|
||||
new PrivateTeachingMessage(gameId, userId, teachingMode, recommendedAction, explanation, candidates)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -786,8 +786,9 @@ class GameSessionServiceTest {
|
||||
prepareWinningHand(winnerSeat);
|
||||
session.getTable().setCurrentSeatNo(2);
|
||||
ensureSeatHasMatchingTiles(session.getTable().getSeats().get(2), "9筒", 1);
|
||||
ensureSeatHasMatchingTiles(session.getTable().getSeats().get(3), "9筒", 1);
|
||||
removeMatchingTilesFromOtherSeats(session, "9筒", 1, 2, 3);
|
||||
// 这里只需要保证 seat 1 能对 seat 2 的 9筒形成“可胡后选择 PASS”的场景。
|
||||
// 不再额外给 seat 3 补 9筒,避免它也意外成为同一响应窗里的第二个胡牌候选,导致测试依赖随机手牌分布。
|
||||
removeMatchingTilesFromOtherSeats(session, "9筒", 1, 2);
|
||||
|
||||
gameSessionService.performAction(
|
||||
started.gameId(),
|
||||
@@ -801,9 +802,12 @@ class GameSessionServiceTest {
|
||||
assertThat(winnerSeat.isPassedHuBlocked()).isTrue();
|
||||
assertThat(afterPass.currentSeatNo()).isEqualTo(3);
|
||||
|
||||
// 这里取 seat 3 当前手里的安全牌继续推进牌局,只验证“未摸到自己下一张牌前仍然不能响应胡”。
|
||||
String seatThreeSafeDiscard = session.getTable().getSeats().get(3).getHandTiles().get(0).getDisplayName();
|
||||
removeMatchingTilesFromOtherSeats(session, seatThreeSafeDiscard, 3);
|
||||
GameStateResponse afterSecondDiscard = gameSessionService.performAction(
|
||||
started.gameId(),
|
||||
new GameActionRequest("player-4", "DISCARD", "9筒", null)
|
||||
new GameActionRequest("player-4", "DISCARD", seatThreeSafeDiscard, null)
|
||||
);
|
||||
|
||||
assertThat(session.getPendingResponseActionWindow()).isNull();
|
||||
@@ -863,12 +867,15 @@ class GameSessionServiceTest {
|
||||
|
||||
assertThat(afterSeatZeroDiscard.currentSeatNo()).isEqualTo(1);
|
||||
assertThat(winnerSeat.isPassedHuBlocked()).isFalse();
|
||||
removeMatchingTilesFromOtherSeats(session, "8万", 1);
|
||||
// 解锁完成后,重新稳定构造一个“seat 2 打出 9筒,seat 1 可以响应胡”的窗口,
|
||||
// 避免继续依赖中间多轮摸打与随机初始牌造成的测试波动。
|
||||
prepareWinningHand(winnerSeat);
|
||||
GameSeat sourceSeat = session.getTable().getSeats().get(2);
|
||||
sourceSeat.getHandTiles().clear();
|
||||
sourceSeat.receiveTile(parseTile("9筒"));
|
||||
session.getTable().setCurrentSeatNo(2);
|
||||
removeMatchingTilesFromOtherSeats(session, "9筒", 1, 2);
|
||||
|
||||
gameSessionService.performAction(
|
||||
started.gameId(),
|
||||
new GameActionRequest("player-2", "DISCARD", "8万", null)
|
||||
);
|
||||
GameStateResponse afterHu = gameSessionService.performAction(
|
||||
started.gameId(),
|
||||
new GameActionRequest("player-3", "DISCARD", "9筒", null)
|
||||
|
||||
@@ -4,8 +4,10 @@ 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.teaching.dto.CandidateAdviceItem;
|
||||
import com.xuezhanmaster.ws.dto.PrivateActionCandidate;
|
||||
import com.xuezhanmaster.ws.dto.PrivateActionMessage;
|
||||
import com.xuezhanmaster.ws.dto.PrivateTeachingMessage;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
@@ -84,4 +86,29 @@ class GameMessagePublisherTest {
|
||||
.extracting(candidate -> candidate.actionType())
|
||||
.containsExactly("PENG", "PASS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPublishStructuredTeachingMessageWithCandidateList() {
|
||||
gameMessagePublisher.publishPrivateTeaching(
|
||||
"game-1",
|
||||
"user-1",
|
||||
"BRIEF",
|
||||
"9筒",
|
||||
"建议先打孤张。",
|
||||
List.of(
|
||||
new CandidateAdviceItem("9筒", 90, List.of("ISOLATED_TILE")),
|
||||
new CandidateAdviceItem("1万", 70, List.of("EDGE_TILE"))
|
||||
)
|
||||
);
|
||||
|
||||
ArgumentCaptor<PrivateTeachingMessage> captor = ArgumentCaptor.forClass(PrivateTeachingMessage.class);
|
||||
verify(messagingTemplate).convertAndSend(org.mockito.ArgumentMatchers.eq("/topic/users/user-1/teaching"), captor.capture());
|
||||
|
||||
PrivateTeachingMessage message = captor.getValue();
|
||||
assertThat(message.teachingMode()).isEqualTo("BRIEF");
|
||||
assertThat(message.recommendedAction()).isEqualTo("9筒");
|
||||
assertThat(message.candidates()).hasSize(2);
|
||||
assertThat(message.candidates().get(0).tile()).isEqualTo("9筒");
|
||||
assertThat(message.candidates().get(0).reasonTags()).containsExactly("ISOLATED_TILE");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user