first commit

This commit is contained in:
hujun
2026-03-20 12:50:41 +08:00
commit 24fce055fd
88 changed files with 7655 additions and 0 deletions

View File

@@ -0,0 +1,256 @@
package com.xuezhanmaster.game.service;
import com.xuezhanmaster.common.exception.BusinessException;
import com.xuezhanmaster.game.domain.ActionType;
import com.xuezhanmaster.game.domain.GamePhase;
import com.xuezhanmaster.game.domain.GameSeat;
import com.xuezhanmaster.game.domain.GameSession;
import com.xuezhanmaster.game.domain.GameTable;
import com.xuezhanmaster.game.domain.Tile;
import com.xuezhanmaster.game.dto.GameActionRequest;
import com.xuezhanmaster.game.dto.GameStateResponse;
import com.xuezhanmaster.game.dto.PublicSeatView;
import com.xuezhanmaster.game.dto.SelfSeatView;
import com.xuezhanmaster.game.engine.GameEngine;
import com.xuezhanmaster.game.event.GameEvent;
import com.xuezhanmaster.game.event.GameEventType;
import com.xuezhanmaster.room.domain.Room;
import com.xuezhanmaster.room.service.RoomService;
import com.xuezhanmaster.strategy.domain.StrategyDecision;
import com.xuezhanmaster.strategy.service.StrategyService;
import com.xuezhanmaster.teaching.domain.PlayerVisibleGameState;
import com.xuezhanmaster.teaching.domain.TeachingMode;
import com.xuezhanmaster.teaching.dto.TeachingAdviceResponse;
import com.xuezhanmaster.teaching.service.PlayerVisibilityService;
import com.xuezhanmaster.teaching.service.TeachingService;
import com.xuezhanmaster.ws.service.GameMessagePublisher;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class GameSessionService {
private final RoomService roomService;
private final GameEngine gameEngine;
private final GameActionProcessor gameActionProcessor;
private final StrategyService strategyService;
private final PlayerVisibilityService playerVisibilityService;
private final TeachingService teachingService;
private final GameMessagePublisher gameMessagePublisher;
private final Map<String, GameSession> sessions = new ConcurrentHashMap<>();
public GameSessionService(
RoomService roomService,
GameEngine gameEngine,
GameActionProcessor gameActionProcessor,
StrategyService strategyService,
PlayerVisibilityService playerVisibilityService,
TeachingService teachingService,
GameMessagePublisher gameMessagePublisher
) {
this.roomService = roomService;
this.gameEngine = gameEngine;
this.gameActionProcessor = gameActionProcessor;
this.strategyService = strategyService;
this.playerVisibilityService = playerVisibilityService;
this.teachingService = teachingService;
this.gameMessagePublisher = gameMessagePublisher;
}
public GameStateResponse startGame(String roomId, String operatorUserId) {
Room room = roomService.prepareRoomForGame(roomId, operatorUserId);
GameTable table = gameEngine.createTableFromRoom(room);
GameSession session = new GameSession(room.getRoomId(), table);
sessions.put(session.getGameId(), session);
appendAndPublish(session, GameEvent.gameStarted(session.getGameId(), roomId));
notifyActionIfHumanTurn(session);
return toStateResponse(session, operatorUserId);
}
public GameStateResponse getState(String gameId, String userId) {
GameSession session = getRequiredSession(gameId);
return toStateResponse(session, userId);
}
public GameStateResponse performAction(String gameId, GameActionRequest request) {
GameSession session = getRequiredSession(gameId);
ActionType actionType = parseActionType(request.actionType());
List<GameEvent> events = gameActionProcessor.process(session, request);
appendAndPublish(session, events);
handlePostActionEffects(session, actionType);
return toStateResponse(session, request.userId());
}
public GameStateResponse selectLackSuit(String gameId, String userId, String lackSuitValue) {
return performAction(gameId, new GameActionRequest(userId, "SELECT_LACK_SUIT", lackSuitValue, null));
}
public GameStateResponse discardTile(String gameId, String userId, String tileDisplayName) {
return performAction(gameId, new GameActionRequest(userId, "DISCARD", tileDisplayName, null));
}
private GameSession getRequiredSession(String gameId) {
GameSession session = sessions.get(gameId);
if (session == null) {
throw new BusinessException("GAME_NOT_FOUND", "对局不存在");
}
return session;
}
private GameSeat findSeatByUserId(GameTable table, String userId) {
return table.getSeats().stream()
.filter(seat -> seat.getPlayerId().equals(userId))
.findFirst()
.orElseThrow(() -> new BusinessException("GAME_SEAT_NOT_FOUND", "当前玩家不在对局中"));
}
private GameStateResponse toStateResponse(GameSession session, String userId) {
GameTable table = session.getTable();
GameSeat self = findSeatByUserId(table, userId);
return new GameStateResponse(
session.getGameId(),
table.getPhase().name(),
table.getDealerSeatNo(),
table.getCurrentSeatNo(),
table.getWallTiles().size(),
new SelfSeatView(
self.getSeatNo(),
self.getPlayerId(),
self.getNickname(),
self.getLackSuit() == null ? null : self.getLackSuit().name(),
self.getHandTiles().stream().map(Tile::getDisplayName).toList(),
self.getDiscardTiles().stream().map(Tile::getDisplayName).toList()
),
buildPublicSeats(table)
);
}
private void moveToNextSeat(GameTable table, String gameId) {
int nextSeatNo = (table.getCurrentSeatNo() + 1) % table.getSeats().size();
GameSeat nextSeat = table.getSeats().get(nextSeatNo);
if (table.getWallTiles().isEmpty()) {
table.setPhase(GamePhase.FINISHED);
gameMessagePublisher.publishPublicEvent(GameEvent.phaseChanged(gameId, table.getPhase().name()));
return;
}
Tile drawnTile = table.getWallTiles().remove(0);
nextSeat.receiveTile(drawnTile);
table.setCurrentSeatNo(nextSeatNo);
gameMessagePublisher.publishPublicEvent(GameEvent.tileDrawn(gameId, nextSeatNo, table.getWallTiles().size()));
gameMessagePublisher.publishPublicEvent(GameEvent.turnSwitched(gameId, nextSeatNo));
}
private void autoPlayBots(GameSession session) {
GameTable table = session.getTable();
while (table.getPhase() == GamePhase.PLAYING) {
GameSeat currentSeat = table.getSeats().get(table.getCurrentSeatNo());
if (!currentSeat.isAi()) {
return;
}
PlayerVisibleGameState visibleState = playerVisibilityService.buildVisibleState(table, currentSeat);
StrategyDecision decision = strategyService.evaluateDiscard(visibleState);
List<GameEvent> events = gameActionProcessor.process(
session,
new GameActionRequest(
currentSeat.getPlayerId(),
"DISCARD",
decision.recommendedAction().getTile().getDisplayName(),
null
)
);
appendAndPublish(session, events);
moveToNextSeat(table, session.getGameId());
if (table.getPhase() == GamePhase.FINISHED) {
return;
}
}
}
private void appendAndPublish(GameSession session, GameEvent event) {
session.getEvents().add(event);
gameMessagePublisher.publishPublicEvent(event);
}
private void appendAndPublish(GameSession session, List<GameEvent> events) {
for (GameEvent event : events) {
appendAndPublish(session, event);
}
}
private void notifyActionIfHumanTurn(GameSession session) {
if (session.getTable().getPhase() != GamePhase.PLAYING) {
return;
}
GameSeat currentSeat = session.getTable().getSeats().get(session.getTable().getCurrentSeatNo());
if (currentSeat.isAi()) {
return;
}
gameMessagePublisher.publishPrivateActionRequired(
session.getGameId(),
currentSeat.getPlayerId(),
List.of("DISCARD"),
currentSeat.getSeatNo()
);
PlayerVisibleGameState visibleState = playerVisibilityService.buildVisibleState(session.getTable(), currentSeat);
StrategyDecision decision = strategyService.evaluateDiscard(visibleState);
TeachingAdviceResponse advice = teachingService.buildAdvice(decision, TeachingMode.BRIEF);
gameMessagePublisher.publishPrivateTeaching(
session.getGameId(),
currentSeat.getPlayerId(),
advice.teachingMode(),
advice.recommendedAction(),
advice.explanation()
);
}
private void handlePostActionEffects(GameSession session, ActionType actionType) {
switch (actionType) {
case SELECT_LACK_SUIT -> {
if (session.getTable().getPhase() == GamePhase.PLAYING) {
notifyActionIfHumanTurn(session);
}
}
case DISCARD -> {
moveToNextSeat(session.getTable(), session.getGameId());
autoPlayBots(session);
notifyActionIfHumanTurn(session);
}
default -> {
// 其余动作在后续 Sprint 中补充对应副作用
}
}
}
private ActionType parseActionType(String actionType) {
try {
return ActionType.valueOf(actionType.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException exception) {
throw new BusinessException("GAME_ACTION_INVALID", "动作类型不合法");
}
}
private List<PublicSeatView> buildPublicSeats(GameTable table) {
return table.getSeats().stream()
.map(seat -> new PublicSeatView(
seat.getSeatNo(),
seat.getPlayerId(),
seat.getNickname(),
seat.isAi(),
seat.getLackSuit() == null ? null : seat.getLackSuit().name(),
seat.getHandTiles().size(),
seat.getDiscardTiles().stream().map(Tile::getDisplayName).toList()
))
.toList();
}
}