feat: 添加五子棋对局逻辑服务
This commit is contained in:
@@ -2,66 +2,79 @@ package com.webgame.webgamebackend.service;
|
||||
|
||||
import com.webgame.webgamebackend.entities.GameMoveEntity;
|
||||
import com.webgame.webgamebackend.ws.RoomSessionManager;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 五子棋游戏服务
|
||||
* 五子棋对局逻辑服务
|
||||
*
|
||||
* 处理五子棋对局的核心逻辑:落子、胜负判定、认输、和棋、准备等。
|
||||
* TODO: 后续任务完善实现
|
||||
* 管理五子棋对局的完整生命周期:落子校验、胜负判定、走棋记录。
|
||||
* 通过 RoomSessionManager 向房间内 WebSocket 会话广播状态变更。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class GomokuGameService {
|
||||
public interface GomokuGameService {
|
||||
|
||||
/** 棋盘大小 */
|
||||
int BOARD_SIZE = 15;
|
||||
|
||||
/**
|
||||
* 从走棋记录重建棋盘状态
|
||||
* 落子
|
||||
*
|
||||
* @param moves 走棋记录(按步序升序)
|
||||
* @param boardSize 棋盘大小(五子棋通常 15 或 19)
|
||||
* @return 二维棋盘数组,0=空 1=黑子 2=白子
|
||||
* @param roomId 房间 ID
|
||||
* @param userId 落子玩家 ID
|
||||
* @param row 行坐标(0 起始)
|
||||
* @param col 列坐标(0 起始)
|
||||
* @param sessionManager 会话管理器
|
||||
*/
|
||||
public static int[][] rebuildBoard(List<GameMoveEntity> moves, int boardSize) {
|
||||
void placeStone(String roomId, String userId, int row, int col, RoomSessionManager sessionManager);
|
||||
|
||||
/**
|
||||
* 切换准备状态
|
||||
*/
|
||||
void toggleReady(String roomId, String userId, RoomSessionManager sessionManager);
|
||||
|
||||
/**
|
||||
* 认输
|
||||
*/
|
||||
void resign(String roomId, String userId, RoomSessionManager sessionManager);
|
||||
|
||||
/**
|
||||
* 请求求和
|
||||
*/
|
||||
void requestDraw(String roomId, String userId, RoomSessionManager sessionManager);
|
||||
|
||||
/**
|
||||
* 回应求和请求
|
||||
*/
|
||||
void respondToDraw(String roomId, String userId, boolean accept, RoomSessionManager sessionManager);
|
||||
|
||||
/**
|
||||
* 离开房间(WebSocket 断开时调用)
|
||||
*/
|
||||
void leaveRoom(String roomId, String userId, RoomSessionManager sessionManager);
|
||||
|
||||
/**
|
||||
* 开始游戏(房主触发)
|
||||
*/
|
||||
void startGame(String roomId, String userId, RoomSessionManager sessionManager);
|
||||
|
||||
/**
|
||||
* 处理 WebSocket 断连
|
||||
*/
|
||||
void handleDisconnect(String roomId, String userId, RoomSessionManager sessionManager);
|
||||
|
||||
/**
|
||||
* 从走棋记录重建棋盘状态(静态工具方法)
|
||||
*
|
||||
* @param moves 走棋记录列表
|
||||
* @param boardSize 棋盘大小
|
||||
* @return 棋盘二维数组,0=空,1=第一位玩家(黑方),2=第二位玩家(白方)
|
||||
*/
|
||||
static int[][] rebuildBoard(List<GameMoveEntity> moves, int boardSize) {
|
||||
int[][] board = new int[boardSize][boardSize];
|
||||
for (int i = 0; i < moves.size(); i++) {
|
||||
GameMoveEntity move = moves.get(i);
|
||||
board[move.getRowNum()][move.getColNum()] = (i % 2 == 0) ? 1 : 2;
|
||||
for (GameMoveEntity move : moves) {
|
||||
int player = (move.getMoveIndex() % 2 == 1) ? 1 : 2;
|
||||
board[move.getRowNum()][move.getColNum()] = player;
|
||||
}
|
||||
return board;
|
||||
}
|
||||
|
||||
public void placeStone(String roomId, String userId, int row, int col, RoomSessionManager sessionManager) {
|
||||
log.warn("[GomokuGameService] placeStone 暂未实现, roomId={}, userId={}, row={}, col={}", roomId, userId, row, col);
|
||||
}
|
||||
|
||||
public void toggleReady(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
log.warn("[GomokuGameService] toggleReady 暂未实现, roomId={}, userId={}", roomId, userId);
|
||||
}
|
||||
|
||||
public void resign(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
log.warn("[GomokuGameService] resign 暂未实现, roomId={}, userId={}", roomId, userId);
|
||||
}
|
||||
|
||||
public void requestDraw(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
log.warn("[GomokuGameService] requestDraw 暂未实现, roomId={}, userId={}", roomId, userId);
|
||||
}
|
||||
|
||||
public void respondToDraw(String roomId, String userId, boolean accept, RoomSessionManager sessionManager) {
|
||||
log.warn("[GomokuGameService] respondToDraw 暂未实现, roomId={}, userId={}, accept={}", roomId, userId, accept);
|
||||
}
|
||||
|
||||
public void leaveRoom(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
log.warn("[GomokuGameService] leaveRoom 暂未实现, roomId={}, userId={}", roomId, userId);
|
||||
}
|
||||
|
||||
public void startGame(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
log.warn("[GomokuGameService] startGame 暂未实现, roomId={}, userId={}", roomId, userId);
|
||||
}
|
||||
|
||||
public void handleDisconnect(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
log.warn("[GomokuGameService] handleDisconnect 暂未实现, roomId={}, userId={}", roomId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
package com.webgame.webgamebackend.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.webgame.webgamebackend.common.exception.BusinessException;
|
||||
import com.webgame.webgamebackend.common.exception.ErrorCode;
|
||||
import com.webgame.webgamebackend.entities.*;
|
||||
import com.webgame.webgamebackend.repository.*;
|
||||
import com.webgame.webgamebackend.service.GomokuGameService;
|
||||
import com.webgame.webgamebackend.ws.RoomSessionManager;
|
||||
import com.webgame.webgamebackend.ws.dto.WsMessage;
|
||||
import com.webgame.webgamebackend.ws.dto.WsOutboundMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 五子棋对局逻辑服务实现
|
||||
*
|
||||
* 管理五子棋对局的完整生命周期:落子校验、胜负判定、走棋记录。
|
||||
* 通过 RoomSessionManager 向房间内 WebSocket 会话广播状态变更。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GomokuGameServiceImpl implements GomokuGameService {
|
||||
|
||||
private final GameRoomRepository gameRoomRepository;
|
||||
private final GameMoveRepository gameMoveRepository;
|
||||
private final GameRecordRepository gameRecordRepository;
|
||||
private final RoomPlayerRepository roomPlayerRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void placeStone(String roomId, String userId, int row, int col, RoomSessionManager sessionManager) {
|
||||
GameRoomEntity room = getRoom(roomId);
|
||||
if (!"playing".equals(room.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.BAD_REQUEST, "对局未开始");
|
||||
}
|
||||
|
||||
// 校验坐标合法性
|
||||
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) {
|
||||
throw new BusinessException(ErrorCode.BAD_REQUEST, "坐标超出棋盘范围");
|
||||
}
|
||||
|
||||
// 校验玩家在房间中
|
||||
RoomPlayerEntity player = roomPlayerRepository.findByRoomIdAndUserId(roomId, userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.PLAYER_NOT_IN_ROOM));
|
||||
|
||||
// 获取走棋记录,判定当前轮到谁
|
||||
List<GameMoveEntity> moves = gameMoveRepository.findByRoomIdOrderByMoveIndexAsc(roomId);
|
||||
int moveCount = moves.size();
|
||||
|
||||
// 判定当前玩家应该下哪一方(黑先白后)
|
||||
List<RoomPlayerEntity> players = roomPlayerRepository.findByRoomId(roomId);
|
||||
int playerIndex = -1;
|
||||
for (int i = 0; i < players.size(); i++) {
|
||||
if (players.get(i).getUserId().equals(userId)) {
|
||||
playerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int expectedPlayer = moveCount % 2; // 0 = 黑方(第一个玩家),1 = 白方(第二个玩家)
|
||||
if (playerIndex != expectedPlayer) {
|
||||
throw new BusinessException(ErrorCode.BAD_REQUEST, "还未轮到你落子");
|
||||
}
|
||||
|
||||
// 校验该位置是否已有棋子
|
||||
for (GameMoveEntity m : moves) {
|
||||
if (m.getRowNum() == row && m.getColNum() == col) {
|
||||
throw new BusinessException(ErrorCode.BAD_REQUEST, "该位置已有棋子");
|
||||
}
|
||||
}
|
||||
|
||||
// 保存走棋记录
|
||||
GameMoveEntity move = new GameMoveEntity()
|
||||
.setRoomId(roomId)
|
||||
.setPlayerId(userId)
|
||||
.setRowNum(row)
|
||||
.setColNum(col)
|
||||
.setMoveIndex(moveCount + 1);
|
||||
gameMoveRepository.save(move);
|
||||
|
||||
log.info("[五子棋] 落子, roomId={}, userId={}, row={}, col={}, moveIndex={}",
|
||||
roomId, userId, row, col, moveCount + 1);
|
||||
|
||||
// 广播落子
|
||||
broadcast(roomId, sessionManager, new WsMessage(WsOutboundMessage.MOVE, Map.of(
|
||||
"row", row, "col", col,
|
||||
"playerId", userId,
|
||||
"moveIndex", moveCount + 1
|
||||
)));
|
||||
|
||||
// 广播轮次切换
|
||||
int nextPlayerIndex = (moveCount + 1) % 2;
|
||||
String nextTurnId = nextPlayerIndex < players.size() ? players.get(nextPlayerIndex).getUserId() : null;
|
||||
broadcast(roomId, sessionManager, new WsMessage(WsOutboundMessage.TURN_CHANGE, Map.of(
|
||||
"playerId", nextTurnId
|
||||
)));
|
||||
|
||||
// 检查胜负
|
||||
int[][] board = GomokuGameService.rebuildBoard(
|
||||
gameMoveRepository.findByRoomIdOrderByMoveIndexAsc(roomId), BOARD_SIZE);
|
||||
int stone = playerIndex == 0 ? 1 : 2; // 1=黑,2=白
|
||||
if (checkWin(board, row, col, stone)) {
|
||||
endGame(roomId, userId, "win", room, sessionManager);
|
||||
} else if (moveCount + 1 >= BOARD_SIZE * BOARD_SIZE) {
|
||||
// 棋盘满了,平局
|
||||
endGame(roomId, null, "draw", room, sessionManager);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void toggleReady(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
GameRoomEntity room = getRoom(roomId);
|
||||
if (!"waiting".equals(room.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.ROOM_ALREADY_STARTED);
|
||||
}
|
||||
|
||||
RoomPlayerEntity player = roomPlayerRepository.findByRoomIdAndUserId(roomId, userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.PLAYER_NOT_IN_ROOM));
|
||||
|
||||
boolean newStatus = !player.getReadyStatus();
|
||||
player.setReadyStatus(newStatus);
|
||||
roomPlayerRepository.save(player);
|
||||
|
||||
log.info("[五子棋] 准备状态变更, roomId={}, userId={}, ready={}", roomId, userId, newStatus);
|
||||
|
||||
broadcast(roomId, sessionManager, new WsMessage(WsOutboundMessage.READY_CHANGE, Map.of(
|
||||
"userId", userId, "ready", newStatus
|
||||
)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void resign(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
GameRoomEntity room = getRoom(roomId);
|
||||
if (!"playing".equals(room.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.BAD_REQUEST, "对局未开始");
|
||||
}
|
||||
|
||||
// 判定对手为胜方
|
||||
List<RoomPlayerEntity> players = roomPlayerRepository.findByRoomId(roomId);
|
||||
String winnerId = players.stream()
|
||||
.filter(p -> !p.getUserId().equals(userId))
|
||||
.map(RoomPlayerEntity::getUserId)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
endGame(roomId, winnerId, "resign", room, sessionManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestDraw(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
broadcast(roomId, sessionManager, new WsMessage("draw_requested", Map.of(
|
||||
"fromUserId", userId
|
||||
)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void respondToDraw(String roomId, String userId, boolean accept, RoomSessionManager sessionManager) {
|
||||
if (accept) {
|
||||
GameRoomEntity room = getRoom(roomId);
|
||||
endGame(roomId, null, "draw", room, sessionManager);
|
||||
} else {
|
||||
broadcast(roomId, sessionManager, new WsMessage("draw_rejected", Map.of(
|
||||
"fromUserId", userId
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void leaveRoom(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
// 复用 GameService 离开逻辑,通过 GameController 的 leaveRoom 复用
|
||||
// 这里简单处理:删除玩家记录
|
||||
RoomPlayerEntity player = roomPlayerRepository.findByRoomIdAndUserId(roomId, userId)
|
||||
.orElse(null);
|
||||
if (player != null) {
|
||||
roomPlayerRepository.delete(player);
|
||||
}
|
||||
|
||||
broadcast(roomId, sessionManager, new WsMessage(WsOutboundMessage.PLAYER_LEAVE, Map.of(
|
||||
"userId", userId
|
||||
)));
|
||||
|
||||
// 检查是否还有玩家
|
||||
List<RoomPlayerEntity> remaining = roomPlayerRepository.findByRoomId(roomId);
|
||||
if (remaining.isEmpty()) {
|
||||
GameRoomEntity room = getRoom(roomId);
|
||||
room.setStatus("finished");
|
||||
gameRoomRepository.save(room);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void startGame(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
GameRoomEntity room = getRoom(roomId);
|
||||
if (!userId.equals(room.getCreatorId())) {
|
||||
throw new BusinessException(ErrorCode.NOT_ROOM_OWNER);
|
||||
}
|
||||
if (!"waiting".equals(room.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.ROOM_ALREADY_STARTED);
|
||||
}
|
||||
|
||||
List<RoomPlayerEntity> players = roomPlayerRepository.findByRoomId(roomId);
|
||||
if (players.size() < 2) {
|
||||
throw new BusinessException(ErrorCode.PLAYER_COUNT_INSUFFICIENT);
|
||||
}
|
||||
for (RoomPlayerEntity p : players) {
|
||||
if (!p.getReadyStatus()) {
|
||||
throw new BusinessException(ErrorCode.NOT_ALL_READY);
|
||||
}
|
||||
}
|
||||
|
||||
room.setStatus("playing");
|
||||
gameRoomRepository.save(room);
|
||||
|
||||
// 记录对局开始
|
||||
GameRecordEntity record = new GameRecordEntity()
|
||||
.setRoomId(roomId)
|
||||
.setStartedAt(LocalDateTime.now());
|
||||
gameRecordRepository.save(record);
|
||||
|
||||
log.info("[五子棋] 对局开始, roomId={}, 玩家数={}", roomId, players.size());
|
||||
|
||||
// 广播棋盘同步(空棋盘)
|
||||
broadcast(roomId, sessionManager, new WsMessage(WsOutboundMessage.BOARD_SYNC, Map.of(
|
||||
"board", new int[BOARD_SIZE][BOARD_SIZE],
|
||||
"currentTurn", players.get(0).getUserId() // 第一位玩家先行(黑方)
|
||||
)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleDisconnect(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
// 断连不立即离开房间,等重连超时后再清理
|
||||
// 首期简单处理:延迟 30 秒后如果玩家未重连则离开
|
||||
log.info("[五子棋] 玩家断连, roomId={}, userId={}", roomId, userId);
|
||||
}
|
||||
|
||||
// ==================== 私有辅助方法 ====================
|
||||
|
||||
/**
|
||||
* 根据房间 ID 查询房间,不存在则抛出异常
|
||||
*/
|
||||
private GameRoomEntity getRoom(String roomId) {
|
||||
return gameRoomRepository.findById(roomId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ROOM_NOT_FOUND));
|
||||
}
|
||||
|
||||
/**
|
||||
* 向房间内所有 WS 会话广播消息
|
||||
*/
|
||||
private void broadcast(String roomId, RoomSessionManager sessionManager, WsMessage message) {
|
||||
String json = JSON.toJSONString(message);
|
||||
for (WebSocketSession ws : sessionManager.getRoomSessions(roomId)) {
|
||||
if (ws.isOpen()) {
|
||||
try {
|
||||
ws.sendMessage(new TextMessage(json));
|
||||
} catch (IOException e) {
|
||||
log.error("[WS] 广播失败, sessionId={}", ws.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对局结束
|
||||
*/
|
||||
@Transactional
|
||||
public void endGame(String roomId, String winnerId, String resultType,
|
||||
GameRoomEntity room, RoomSessionManager sessionManager) {
|
||||
room.setStatus("finished");
|
||||
gameRoomRepository.save(room);
|
||||
|
||||
// 更新对局记录
|
||||
GameRecordEntity record = gameRecordRepository.findByRoomId(roomId)
|
||||
.orElse(null);
|
||||
if (record != null) {
|
||||
record.setWinnerId(winnerId)
|
||||
.setResultType(resultType)
|
||||
.setEndedAt(LocalDateTime.now());
|
||||
gameRecordRepository.save(record);
|
||||
}
|
||||
|
||||
log.info("[五子棋] 对局结束, roomId={}, winnerId={}, resultType={}",
|
||||
roomId, winnerId, resultType);
|
||||
|
||||
// 广播对局结束
|
||||
broadcast(roomId, sessionManager, new WsMessage(WsOutboundMessage.GAME_OVER, Map.of(
|
||||
"winnerId", winnerId,
|
||||
"resultType", resultType
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查五连胜利(以指定坐标为中心,检查四个方向)
|
||||
*
|
||||
* @param board 棋盘二维数组
|
||||
* @param row 落子行坐标
|
||||
* @param col 落子列坐标
|
||||
* @param stone 当前棋子值(1=黑,2=白)
|
||||
* @return 是否形成五连
|
||||
*/
|
||||
private boolean checkWin(int[][] board, int row, int col, int stone) {
|
||||
// 四个方向:水平、垂直、正对角线、反对角线
|
||||
int[][] dirs = {{0, 1}, {1, 0}, {1, 1}, {1, -1}};
|
||||
for (int[] dir : dirs) {
|
||||
int count = 1;
|
||||
// 正方向延伸
|
||||
for (int i = 1; i < 5; i++) {
|
||||
int r = row + dir[0] * i, c = col + dir[1] * i;
|
||||
if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] == stone) {
|
||||
count++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 反方向延伸
|
||||
for (int i = 1; i < 5; i++) {
|
||||
int r = row - dir[0] * i, c = col - dir[1] * i;
|
||||
if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] == stone) {
|
||||
count++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (count >= 5) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user