Skip to content
Open
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
10 changes: 5 additions & 5 deletions backend/TASK.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ This document contains step-by-step tasks for implementing the backend API for C

### TASK-B403: Chess Real-time Features

- [ ] Setup chess room events
- [ ] Emit move updates
- [ ] Add turn timer
- [ ] Handle disconnection/reconnection
- [ ] Broadcast game results
- [x] Setup chess room events
- [x] Emit move updates
- [x] Add turn timer
- [x] Handle disconnection/reconnection
- [x] Broadcast game results

## Game: Pac-Man

Expand Down
9 changes: 5 additions & 4 deletions backend/migrations/007_chess_game.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
CREATE TABLE Games (
CREATE TABLE games (
game_id SERIAL PRIMARY KEY,

white_player_id INTEGER NOT NULL REFERENCES Users(id),
black_player_id INTEGER NOT NULL REFERENCES Users(id),
white_player_id INTEGER NOT NULL,
black_player_id INTEGER NOT NULL,

start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
end_time TIMESTAMP,
Expand All @@ -22,7 +22,8 @@ CREATE TABLE Games (

current_turn CHAR(1) NOT NULL
CHECK (current_turn IN ('w','b')),
is_check BOOLEAN,

is_check BOOLEAN DEFAULT false,
draw_offer_by INTEGER,
draw_status VARCHAR(20) DEFAULT 'none'
);
16 changes: 2 additions & 14 deletions backend/migrations/008_chess_Move.sql
Original file line number Diff line number Diff line change
@@ -1,32 +1,20 @@
CREATE TABLE Moves (
move_id SERIAL PRIMARY KEY,

game_id INTEGER NOT NULL,

move_number INTEGER NOT NULL,

player_color CHAR(1) NOT NULL
CHECK (player_color IN ('w','b')),

from_square VARCHAR(2) NOT NULL,
to_square VARCHAR(2) NOT NULL,

piece VARCHAR(2),
capture BOOLEAN DEFAULT FALSE,
capture VARCHAR(5),
promotion VARCHAR(2),

notation VARCHAR(10) NOT NULL,

fen_after TEXT NOT NULL,

move_time INTEGER,

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT fk_game
FOREIGN KEY (game_id)
REFERENCES Games(game_id)
ON DELETE CASCADE,

UNIQUE (game_id, move_number)
ON DELETE CASCADE
);
119 changes: 119 additions & 0 deletions backend/src/config/handlers/chess-events.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@

import { Server, Socket } from "socket.io";
import { RoomManager } from "@/utils/room-manager";
import { ChessService } from "@/services/chess/chess.services";
import { ChessModel } from "@/models/chess.model";
import { ChessSocket } from "@/sockets/chess.socket";
import { GameDisconnectHandler } from "@/services/chess/chess.disconnecthandler";
import logger from "@/utils/logger";

const chessSocket = new ChessSocket();
const chessService = new ChessService(chessSocket, new ChessModel());

const clockIntervals = new Map<string, NodeJS.Timeout>();

function startClockSync(io: Server, gameId: string, getGame: () => Promise<any>) {
if (clockIntervals.has(gameId)) clearInterval(clockIntervals.get(gameId)!);

const interval = setInterval(async () => {
try {
const game = await getGame();
if (!game || game.status !== "ongoing") {
clearInterval(interval);
clockIntervals.delete(gameId);
return;
}
io.to(`game:${gameId}`).emit("chess:clockSync", {
w: Math.floor(game.white_time_left / 1000),
b: Math.floor(game.black_time_left / 1000),
});
} catch {
clearInterval(interval);
clockIntervals.delete(gameId);
}
}, 1000);

clockIntervals.set(gameId, interval);
}



export function registerChessEvents(
io: Server,
socket: Socket,
roomManager: RoomManager,
userSocketMap: Map<string, string>,
) {
socket.on("chess:startClock", (data: { gameId: string }) => {
const userId = socket.data.userId;
if (!userId) { socket.emit("chess:error", { message: "Not authenticated" }); return; }
startClockSync(io, data.gameId, () => chessService.fetchGame(data.gameId));
logger.info(`Clock sync started for game ${data.gameId}`);
});

socket.on("chess:resign", async (data: { gameId: string }) => {
try {
const userId = socket.data.userId;
if (!userId) { socket.emit("chess:error", { message: "Not authenticated" }); return; }
await chessService.resignGame(data.gameId, userId);
if (clockIntervals.has(data.gameId)) {
clearInterval(clockIntervals.get(data.gameId)!);
clockIntervals.delete(data.gameId);
}
logger.info(`User ${userId} resigned game ${data.gameId}`);
} catch (err: any) {
socket.emit("chess:error", { message: err.message || "Failed to resign" });
}
});

socket.on("chess:offerDraw", async (data: { gameId: string }) => {
try {
const userId = socket.data.userId;
if (!userId) { socket.emit("chess:error", { message: "Not authenticated" }); return; }
await chessService.offerDraw(data.gameId, userId);
} catch (err: any) {
socket.emit("chess:error", { message: err.message || "Failed to offer draw" });
}
});

socket.on("chess:acceptDraw", async (data: { gameId: string }) => {
try {
const userId = socket.data.userId;
if (!userId) { socket.emit("chess:error", { message: "Not authenticated" }); return; }
await chessService.acceptDraw(data.gameId, userId);
if (clockIntervals.has(data.gameId)) {
clearInterval(clockIntervals.get(data.gameId)!);
clockIntervals.delete(data.gameId);
}
} catch (err: any) {
socket.emit("chess:error", { message: err.message || "Failed to accept draw" });
}
});

socket.on("chess:declineDraw", async (data: { gameId: string }) => {
try {
const userId = socket.data.userId;
if (!userId) { socket.emit("chess:error", { message: "Not authenticated" }); return; }
await chessService.rejectDraw(data.gameId, userId);
} catch (err: any) {
socket.emit("chess:error", { message: err.message || "Failed to decline draw" });
}
});

socket.on("disconnect", () => {
const userId = socket.data.userId;
if (!userId) return;
const playerRoom = roomManager.getPlayerRoom(socket.id);
if (!playerRoom || playerRoom.gameType !== "chess") return;
const gameId = playerRoom.gameId;
if (!gameId) return;

const disconnectHandler = new GameDisconnectHandler(io, chessService, new ChessModel(), chessSocket);
disconnectHandler.handleDisconnect(gameId, userId);

if (clockIntervals.has(gameId)) {
clearInterval(clockIntervals.get(gameId)!);
clockIntervals.delete(gameId);
}
});
}
6 changes: 5 additions & 1 deletion backend/src/config/handlers/game-events.handler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import { Server, Socket } from "socket.io";
import { RoomManager } from "@/utils/room-manager";
import { TicTacToeService } from "@/services/tictactoe/tictactoe.service";
Expand Down Expand Up @@ -55,6 +56,8 @@ export function registerGameEvents(
});

socket.on("game:move", async (data: { gameId: string; gameType?: string; moveData: any }) => {
console.log("backend game:move received from", socket.id, "gameId:", data.gameId);

try {
const playerRoom = roomManager.getPlayerRoom(socket.id);
if (!playerRoom) {
Expand Down Expand Up @@ -133,7 +136,7 @@ export function registerGameEvents(
roomManager.setGameState(playerRoom.id, game);

// Broadcast move to all players in game room
io.to(`game:${data.gameId}`).emit("game:move", {
socket.to(`game:${data.gameId}`).emit("game:move", {
gameId: data.gameId,
gameType,
moveData: data.moveData,
Expand All @@ -144,6 +147,7 @@ export function registerGameEvents(
logger.info(`User ${socket.id} made move in game ${data.gameId} (${gameType})`);
} catch (err) {
logger.error("Error making move", { error: err });
console.error("Full move error:", err);
socket.emit("game:error", { message: "Failed to make move" });
}
});
Expand Down
1 change: 1 addition & 0 deletions backend/src/config/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./room-events.handler";
export * from "./game-events.handler";
export * from "./chat-events.handler";
export * from "./disconnect-events.handler";
export * from "./chess-events.handler";
Loading