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