257 lines
11 KiB
Java
257 lines
11 KiB
Java
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();
|
|
}
|
|
}
|