diff --git a/src/services/game.service.ts b/src/services/game.service.ts index 6729a41..8a486d1 100644 --- a/src/services/game.service.ts +++ b/src/services/game.service.ts @@ -2,33 +2,31 @@ import redis from "../lib/redis"; import { PrismaClient } from "../generated/prisma/client"; const prisma = new PrismaClient(); import { checkWinner } from "../utils/gameLogic"; - -export async function makeMove( - gameId: number, +import { Game } from "../generated/prisma/client"; +export function checkGameResultFromBoard( + board: ("X" | "O" | null)[], index: number, - playerId: number + symbol: "X" | "O", + userId: number, + playerXId: number, + playerOId: number ) { - const game = await prisma.game.findUnique({ where: { id: gameId } }); + if (board[index]) { + throw new Error("Ô đã được đánh"); + } - // Update boardState và kiểm tra kết quả - const board = game.boardState; // array 2D - board[row][col] = playerId === game.xPlayerId ? "X" : "O"; + board[index] = symbol; - const winner = checkWinner(board); + const isWin = checkWinner(board, index, symbol); + const isDraw = !board.includes(null); + const nextTurn = symbol === "X" ? "O" : "X"; + const winnerId = isWin ? userId : null; - if (winner) { - await prisma.game.update({ - where: { id: gameId }, - data: { - boardState: board, - finishedAt: new Date(), - winnerId: winner === "X" ? game.xPlayerId : game.oPlayerId, - }, - }); - } else { - await prisma.game.update({ - where: { id: gameId }, - data: { boardState: board }, - }); - } + return { + isWin, + isDraw, + nextTurn, + board, + winnerId, + }; } diff --git a/src/socket/matchmaking.socket.ts b/src/socket/matchmaking.socket.ts index 4fec010..40d2c9e 100644 --- a/src/socket/matchmaking.socket.ts +++ b/src/socket/matchmaking.socket.ts @@ -14,7 +14,9 @@ import { SeverToClientEvents, MatchData, } from "../types/express"; +import { checkGameResultFromBoard } from "../services/game.service"; import redis from "../lib/redis"; +import { match } from "node:assert"; const userSocketMap = new Map< number, @@ -38,7 +40,24 @@ export function matchmakingSocket(io: Server) { if (opponentId) { const match = await createMatch(userId, opponentId); const opponentSocketId = await redis.get(`socket:${opponentId}`); + // lưu trạng thái match cho cả hai người chơi + const initialBoard = Array(400).fill(null); // 20x20 board + await redis.set( + `match:${match.id}:state`, + JSON.stringify({ + board: initialBoard, + symbol: "X", + playerXId: match.playerXId, + playerOId: match.playerOId, + }) + ); + + // Lưu ánh xạ user → match để xử lý disconnect + await redis.set(`user:${userId}:matchId`, match.id.toString()); + await redis.set(`user:${opponentId}:matchId`, match.id.toString()); + + // xóa ra khỏi một queue báo hiệu 2 người này đã match await removeUserFromQueue(userId); await removeUserFromQueue(opponentId); @@ -65,7 +84,93 @@ export function matchmakingSocket(io: Server) { }, 5000); } }); - socket.on("makeMove", async ({ matchId, index, symbol }) => {}); + + socket.on("makeMove", async ({ matchId, index, symbol }) => { + //xác thực + const userId = socket.data.user.id; + console.log( + `🎮 User ${userId} attempting move in match ${matchId} at index ${index} with symbol ${symbol}` + ); + const matchIdStr = await redis.get(`user:${userId}:matchId`); + if (!matchIdStr) { + socket.emit("error", "Không tìm thấy matchId của bạn"); + return; + } + const matchStateStr = await redis.get(`match:${matchId}:state`); + if (!matchStateStr) { + socket.emit("error", "Không tìm thấy trạng thái trận đấu"); + return; + } + + const matchState = JSON.parse(matchStateStr) as { + board: (null | "X" | "O")[]; + turn: "X" | "O"; + playerXId: number; + playerOId: number; + }; + // Xác thực lượt đi + const expectedSymbol = matchState.turn; + const isUserX = userId === matchState.playerXId; + const isUserO = userId === matchState.playerOId; + + if ( + (expectedSymbol === "X" && !isUserX) || + (expectedSymbol === "O" && !isUserO) + ) { + socket.emit("error", "Không đúng lượt của bạn"); + return; + } + + // Tìm opponentId để gửi socket về + const opponentId = isUserX ? matchState.playerOId : matchState.playerXId; + try { + const { board, turn, playerXId, playerOId } = matchState; + const { isWin, isDraw, nextTurn, winnerId } = + await checkGameResultFromBoard( + board, + index, + symbol, + userId, + playerXId, + playerOId + ); + + if (!isWin && !isDraw) { + matchState.board[index] = symbol; + matchState.turn = nextTurn as "X" | "O"; + await redis.set(`match:${matchId}:state`, JSON.stringify(matchState)); + } + + const opponentSocketId = await redis.get(`socket:${opponentId}`); + const payload = { index, symbol, nextTurn, isWin, winnerId }; + + socket.emit("moveMade", payload); + if (opponentSocketId) { + io.to(opponentSocketId).emit("moveMade", payload); + } + + if (isWin || isDraw) { + console.log("Game kết thúc, thông báo cho cả hai người chơi"); + + const gameEndPayload = { + winnerId, + isDraw, + reason: isDraw ? "draw" : "win", + }; + + socket.emit("gameEnd", gameEndPayload); + if (opponentSocketId) { + io.to(opponentSocketId).emit("gameEnd", gameEndPayload); + } + await redis.del(`match:${matchId}:state`); + await redis.del(`user:${userId}:matchId`); + await redis.del(`user:${opponentId}:matchId`); + } + } catch (err: any) { + console.error("❌ Lỗi:", err.message); + socket.emit("error", err.message); + } + }); socket.on("disconnect", async () => { console.log(`❌ Disconnected: ${socket.id}`); @@ -87,3 +192,18 @@ export function matchmakingSocket(io: Server) { }); }); } +function emitWithAck(socket, event, data, timeoutMs = 3000): Promise { + return new Promise((resolve) => { + let acknowledged = false; + + const timeout = setTimeout(() => { + if (!acknowledged) resolve(false); // timeout + }, timeoutMs); + + socket.emit(event, data, (ack: boolean) => { + acknowledged = true; + clearTimeout(timeout); + resolve(ack); + }); + }); +} diff --git a/src/utils/gameLogic.ts b/src/utils/gameLogic.ts index cc991aa..f77fd27 100644 --- a/src/utils/gameLogic.ts +++ b/src/utils/gameLogic.ts @@ -1,6 +1,63 @@ export function checkWinner( - board: ("X" | "O" | null)[][], - winLength = 5 -): "X" | "O" | null { - // check theo hàng, cột, chéo + board: ("X" | "O" | null)[], + index: number, + symbol: "X" | "O", + boardSize: number = 20, + winLength: number = 5 +): boolean { + const directions = [ + { dx: 1, dy: 0 }, // ngang → + { dx: 0, dy: 1 }, // dọc ↓ + { dx: 1, dy: 1 }, // chéo chính ↘ + { dx: -1, dy: 1 }, // chéo phụ ↙ + ]; + + const row = Math.floor(index / boardSize); + const col = index % boardSize; + + for (const { dx, dy } of directions) { + let count = 1; + + // Đi về 1 phía + for (let step = 1; step < winLength; step++) { + const newRow = row + dy * step; + const newCol = col + dx * step; + + if ( + newRow < 0 || + newRow >= boardSize || + newCol < 0 || + newCol >= boardSize + ) + break; + + const newIndex = newRow * boardSize + newCol; + if (board[newIndex] !== symbol) break; + + count++; + } + + // Đi về phía ngược lại + for (let step = 1; step < winLength; step++) { + const newRow = row - dy * step; + const newCol = col - dx * step; + + if ( + newRow < 0 || + newRow >= boardSize || + newCol < 0 || + newCol >= boardSize + ) + break; + + const newIndex = newRow * boardSize + newCol; + if (board[newIndex] !== symbol) break; + + count++; + } + + if (count >= winLength) return true; // Đủ 5 ô liên tiếp + } + + return false; }