From 0fb5b515627acb3be944afda25ede7b19896c3e6 Mon Sep 17 00:00:00 2001 From: Tr4nMorDev Date: Sun, 15 Jun 2025 22:42:28 +0700 Subject: [PATCH 1/2] add-makemove-socket --- src/services/game.service.ts | 60 ++++++++++++++++---------- src/socket/matchmaking.socket.ts | 72 +++++++++++++++++++++++++++++++- src/utils/gameLogic.ts | 43 +++++++++++++++++-- 3 files changed, 148 insertions(+), 27 deletions(-) diff --git a/src/services/game.service.ts b/src/services/game.service.ts index 6729a41..5fe8ae5 100644 --- a/src/services/game.service.ts +++ b/src/services/game.service.ts @@ -2,33 +2,47 @@ import redis from "../lib/redis"; import { PrismaClient } from "../generated/prisma/client"; const prisma = new PrismaClient(); import { checkWinner } from "../utils/gameLogic"; +import { Game } from "../generated/prisma/client"; -export async function makeMove( - gameId: number, +export async function checkGameResult( + game: Game, index: number, - playerId: number + symbol: "X" | "O", + userId: number ) { - const game = await prisma.game.findUnique({ where: { id: gameId } }); + const board = game.boardState as ("X" | "O" | null)[]; + 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"; - 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 }, - }); - } + const updatedGame = await prisma.game.update({ + where: { id: game.id }, + data: { + boardState: board, + winnerId: isWin ? userId : undefined, + finishedAt: isWin || isDraw ? new Date() : undefined, + }, + }); + + return { + board, + isWin, + isDraw, + nextTurn, + updatedGame, + }; +} +export async function getGame(gameId: number): Promise { + return await prisma.game.findUnique({ where: { id: gameId } }); } +export async function processMove( + userId: number, + matchId: number, + index: number +) {} diff --git a/src/socket/matchmaking.socket.ts b/src/socket/matchmaking.socket.ts index 4fec010..53fc7b7 100644 --- a/src/socket/matchmaking.socket.ts +++ b/src/socket/matchmaking.socket.ts @@ -14,7 +14,9 @@ import { SeverToClientEvents, MatchData, } from "../types/express"; +import { checkGameResult } from "../services/game.service"; import redis from "../lib/redis"; +import { match } from "node:assert"; const userSocketMap = new Map< number, @@ -38,7 +40,25 @@ 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 + await redis.set( + `user:${userId}:matchState`, + JSON.stringify({ + matchId: match.id, + symbol: userId === match.playerXId ? "X" : "O", + opponentId: opponentId, + }) + ); + await redis.set( + `user:${opponentId}:matchState`, + JSON.stringify({ + matchId: match.id, + symbol: opponentId === match.playerXId ? "X" : "O", + opponentId: userId, + }) + ); + // 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 +85,57 @@ 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 matchStateStr = await redis.get(`user:${userId}:matchState`); + 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 { + matchId: number; + symbol: "X" | "O"; + opponentId: number; + }; + if (matchState.matchId !== matchId) { + socket.emit("error", "Bạn không thuộc trận này"); + return; + } + + if (matchState.symbol !== symbol) { + socket.emit("error", "Không đúng lượt của bạn"); + return; + } + try { + const { isWin, isDraw, updatedGame, nextTurn } = await checkGameResult( + game, + index, + symbol, + userId + ); + const opponentSocketId = await redis.get( + `socket:${matchState.opponentId}` + ); // Lấy socket ID của đối thủ + + const payload = { index, symbol }; + socket.emit("moveMake", payload); + if (opponentSocketId) { + io.to(opponentSocketId).emit("move", payload); + } + if (isWin || isDraw) { + await redis.del(`user:${userId}:matchState`); + await redis.del(`user:${matchState.opponentId}:matchState`); + } + } catch (err: any) { + socket.emit("error", err.message); + } + }); + socket.on("disconnect", async () => { console.log(`❌ Disconnected: ${socket.id}`); diff --git a/src/utils/gameLogic.ts b/src/utils/gameLogic.ts index cc991aa..6b1362a 100644 --- a/src/utils/gameLogic.ts +++ b/src/utils/gameLogic.ts @@ -1,6 +1,43 @@ export function checkWinner( - board: ("X" | "O" | null)[][], - winLength = 5 + board: ("X" | "O" | null)[], + index: number, + symbol: "X" | "O", + boardSize: number = 20, + winLength: number = 5 ): "X" | "O" | null { - // check theo hàng, cột, chéo + 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; + + 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 symbol; + } + + return null; // chưa ai thắng } From 2de2b4bca51ede89cc817730ab073ecbee7c21ec Mon Sep 17 00:00:00 2001 From: Tr4nMorDev Date: Sat, 21 Jun 2025 10:05:16 +0700 Subject: [PATCH 2/2] storage-achi --- src/services/game.service.ts | 32 ++------ src/socket/matchmaking.socket.ts | 124 ++++++++++++++++++++++--------- src/utils/gameLogic.ts | 26 ++++++- 3 files changed, 118 insertions(+), 64 deletions(-) diff --git a/src/services/game.service.ts b/src/services/game.service.ts index 5fe8ae5..8a486d1 100644 --- a/src/services/game.service.ts +++ b/src/services/game.service.ts @@ -3,14 +3,14 @@ import { PrismaClient } from "../generated/prisma/client"; const prisma = new PrismaClient(); import { checkWinner } from "../utils/gameLogic"; import { Game } from "../generated/prisma/client"; - -export async function checkGameResult( - game: Game, +export function checkGameResultFromBoard( + board: ("X" | "O" | null)[], index: number, symbol: "X" | "O", - userId: number + userId: number, + playerXId: number, + playerOId: number ) { - const board = game.boardState as ("X" | "O" | null)[]; if (board[index]) { throw new Error("Ô đã được đánh"); } @@ -20,29 +20,13 @@ export async function checkGameResult( const isWin = checkWinner(board, index, symbol); const isDraw = !board.includes(null); const nextTurn = symbol === "X" ? "O" : "X"; - - const updatedGame = await prisma.game.update({ - where: { id: game.id }, - data: { - boardState: board, - winnerId: isWin ? userId : undefined, - finishedAt: isWin || isDraw ? new Date() : undefined, - }, - }); + const winnerId = isWin ? userId : null; return { - board, isWin, isDraw, nextTurn, - updatedGame, + board, + winnerId, }; } -export async function getGame(gameId: number): Promise { - return await prisma.game.findUnique({ where: { id: gameId } }); -} -export async function processMove( - userId: number, - matchId: number, - index: number -) {} diff --git a/src/socket/matchmaking.socket.ts b/src/socket/matchmaking.socket.ts index 53fc7b7..40d2c9e 100644 --- a/src/socket/matchmaking.socket.ts +++ b/src/socket/matchmaking.socket.ts @@ -14,7 +14,7 @@ import { SeverToClientEvents, MatchData, } from "../types/express"; -import { checkGameResult } from "../services/game.service"; +import { checkGameResultFromBoard } from "../services/game.service"; import redis from "../lib/redis"; import { match } from "node:assert"; @@ -41,23 +41,22 @@ export function matchmakingSocket(io: Server) { 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 - await redis.set( - `user:${userId}:matchState`, - JSON.stringify({ - matchId: match.id, - symbol: userId === match.playerXId ? "X" : "O", - opponentId: opponentId, - }) - ); + const initialBoard = Array(400).fill(null); // 20x20 board await redis.set( - `user:${opponentId}:matchState`, + `match:${match.id}:state`, JSON.stringify({ - matchId: match.id, - symbol: opponentId === match.playerXId ? "X" : "O", - opponentId: userId, + 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); @@ -92,50 +91,86 @@ export function matchmakingSocket(io: Server) { console.log( `🎮 User ${userId} attempting move in match ${matchId} at index ${index} with symbol ${symbol}` ); - const matchStateStr = await redis.get(`user:${userId}:matchState`); + 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 { - matchId: number; - symbol: "X" | "O"; - opponentId: number; + board: (null | "X" | "O")[]; + turn: "X" | "O"; + playerXId: number; + playerOId: number; }; - if (matchState.matchId !== matchId) { - socket.emit("error", "Bạn không thuộc trận này"); - return; - } + // Xác thực lượt đi + const expectedSymbol = matchState.turn; + const isUserX = userId === matchState.playerXId; + const isUserO = userId === matchState.playerOId; - if (matchState.symbol !== symbol) { + 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 { isWin, isDraw, updatedGame, nextTurn } = await checkGameResult( - game, - index, - symbol, - userId - ); - const opponentSocketId = await redis.get( - `socket:${matchState.opponentId}` - ); // Lấy socket ID của đối thủ + const { board, turn, playerXId, playerOId } = matchState; + const { isWin, isDraw, nextTurn, winnerId } = + await checkGameResultFromBoard( + board, + index, + symbol, + userId, + playerXId, + playerOId + ); - const payload = { index, symbol }; - socket.emit("moveMake", payload); + 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("move", payload); + io.to(opponentSocketId).emit("moveMade", payload); } + if (isWin || isDraw) { - await redis.del(`user:${userId}:matchState`); - await redis.del(`user:${matchState.opponentId}:matchState`); + 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}`); @@ -157,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 6b1362a..f77fd27 100644 --- a/src/utils/gameLogic.ts +++ b/src/utils/gameLogic.ts @@ -4,7 +4,7 @@ export function checkWinner( symbol: "X" | "O", boardSize: number = 20, winLength: number = 5 -): "X" | "O" | null { +): boolean { const directions = [ { dx: 1, dy: 0 }, // ngang → { dx: 0, dy: 1 }, // dọc ↓ @@ -18,6 +18,7 @@ export function checkWinner( 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; @@ -36,8 +37,27 @@ export function checkWinner( count++; } - if (count >= winLength) return symbol; + // Đ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 null; // chưa ai thắng + return false; }