Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 22 additions & 24 deletions src/services/game.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
122 changes: 121 additions & 1 deletion src/socket/matchmaking.socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);

Expand All @@ -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}`);

Expand All @@ -87,3 +192,18 @@ export function matchmakingSocket(io: Server) {
});
});
}
function emitWithAck(socket, event, data, timeoutMs = 3000): Promise<boolean> {
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);
});
});
}
65 changes: 61 additions & 4 deletions src/utils/gameLogic.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading