const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const PORT = process.env.PORT || 7860; const app = express(); app.use(express.static('public')); const server = http.createServer(app); const io = new Server(server); const PLAYERS = [ { symbol: 'X', color: '#FF4757', label: 'Player 1' }, { symbol: 'Circle', color: '#2ED7F8', label: 'Player 2' }, { symbol: 'Triangle',color: '#2ECC71', label: 'Player 3' }, { symbol: 'Square', color: '#FFD32A', label: 'Player 4' }, ]; function makeBoard(size) { const hLines = Array.from({ length: size }, () => Array(size - 1).fill(null)); const vLines = Array.from({ length: size - 1 }, () => Array(size).fill(null)); const boxes = Array.from({ length: size - 1 }, () => Array(size - 1).fill(null)); return { size, hLines, vLines, boxes }; } function getCompletedBoxes(hLines, vLines, boxes, row, col, isHorizontal) { const size = hLines.length; const completed = []; if (isHorizontal) { if (row > 0 && vLines[row-1][col] && vLines[row-1][col+1] && hLines[row-1][col]) { completed.push([row-1, col]); } if (row < size - 1 && vLines[row][col] && vLines[row][col+1] && hLines[row+1][col]) { completed.push([row, col]); } } else { if (col > 0 && hLines[row][col-1] && hLines[row+1][col-1] && vLines[row][col-1]) { completed.push([row, col-1]); } if (col < size - 1 && hLines[row][col] && hLines[row+1][col] && vLines[row][col+1]) { completed.push([row, col]); } } return completed.filter(([br, bc]) => !boxes[br][bc]); } function isGameOver(boxes) { return boxes.every(row => row.every(cell => cell !== null)); } function getWinner(boxes, players) { const counts = {}; players.forEach(p => counts[p.symbol] = 0); for (let r = 0; r < boxes.length; r++) for (let c = 0; c < boxes[r].length; c++) if (boxes[r][c]) counts[boxes[r][c]]++; const max = Math.max(...Object.values(counts)); const tops = Object.keys(counts).filter(k => counts[k] === max); if (tops.length === 1) return tops[0]; return null; } function getRoomState(room) { return { players: room.players, hostId: room.hostId, started: room.started, gameOver: room.gameOver, board: room.board, currentTurn: room.currentTurn, gridSize: room.gridSize, }; } function systemMsg(code, text) { io.to(code).emit('chatMsg', { type: 'system', text, ts: Date.now() }); } // ── Rooms ── const rooms = {}; function generateCode() { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; let code = ''; for (let i = 0; i < 6; i++) code += chars[Math.floor(Math.random() * chars.length)]; return code; } io.on('connection', (socket) => { // ── Create Room ── socket.on('createRoom', ({ playerName, gridSize }) => { let code; do { code = generateCode(); } while (rooms[code]); const player = { id: socket.id, name: playerName || 'Host', ...PLAYERS[0], }; rooms[code] = { code, hostId: socket.id, players: [player], gridSize: gridSize || 5, board: null, currentTurn: 0, started: false, gameOver: false, winner: null, winType: null, }; socket.join(code); socket.roomCode = code; socket.emit('roomCreated', { code, state: getRoomState(rooms[code]) }); systemMsg(code, `${player.name} created the room.`); }); // ── Join Room ── socket.on('joinRoom', ({ code, playerName }) => { const room = rooms[code.toUpperCase()]; if (!room) return socket.emit('error', 'Room not found.'); if (room.started) return socket.emit('error', 'Game already started.'); if (room.players.length >= 4) return socket.emit('error', 'Room is full.'); const idx = room.players.length; const player = { id: socket.id, name: playerName || `Player ${idx + 1}`, ...PLAYERS[idx], }; room.players.push(player); socket.join(code.toUpperCase()); socket.roomCode = code.toUpperCase(); socket.emit('joinedRoom', { code: code.toUpperCase(), state: getRoomState(room) }); io.to(code.toUpperCase()).emit('roomUpdate', getRoomState(room)); systemMsg(code.toUpperCase(), `${player.name} joined as ${player.symbol}.`); }); // ── Start Game ── socket.on('startGame', () => { const room = rooms[socket.roomCode]; if (!room || socket.id !== room.hostId) return; if (room.players.length < 2) return socket.emit('error', 'Need at least 2 players.'); room.started = true; room.board = makeBoard(room.gridSize); room.currentTurn = 0; room.gameOver = false; room.winner = null; room.winType = null; io.to(room.code).emit('gameStarted', getRoomState(room)); systemMsg(room.code, `Game started! ${room.players[0].name}'s turn.`); }); // ── Make Move ── socket.on('makeMove', ({ type, r, c }) => { const room = rooms[socket.roomCode]; if (!room || !room.started || room.gameOver) return; const player = room.players.find(p => p.id === socket.id); if (!player) return; if (room.players[room.currentTurn]?.id !== socket.id) return; const board = room.board; if (type === 'h') { if (board.hLines[r][c] !== null) return; board.hLines[r][c] = player.symbol; } else { if (board.vLines[r][c] !== null) return; board.vLines[r][c] = player.symbol; } const completed = getCompletedBoxes(board.hLines, board.vLines, board.boxes, r, c, type === 'h'); let scored = false; completed.forEach(([br, bc]) => { board.boxes[br][bc] = player.symbol; scored = true; }); if (isGameOver(board.boxes)) { const winner = getWinner(board.boxes, room.players); const winType = winner ? 'boxes' : 'draw'; room.gameOver = true; room.winner = winner; room.winType = winType; const state = getRoomState(room); io.to(room.code).emit('gameOver', { winner, winType, state }); if (winner) { const wp = room.players.find(p => p.symbol === winner); systemMsg(room.code, `${wp?.name || winner} wins with the most boxes!`); } else { systemMsg(room.code, "It's a draw!"); } } else { if (!scored) { room.currentTurn = (room.currentTurn + 1) % room.players.length; } const state = getRoomState(room); io.to(room.code).emit('boardUpdate', state); if (!scored) { const nextPlayer = room.players[room.currentTurn]; systemMsg(room.code, `${nextPlayer.name}'s turn.`); } } }); // ── Chat ── socket.on('chatMsg', ({ text }) => { const room = rooms[socket.roomCode]; if (!room) return; const player = room.players.find(p => p.id === socket.id); if (!player) return; if (!text || text.trim().length === 0) return; io.to(room.code).emit('chatMsg', { type: 'player', name: player.name, symbol: player.symbol, color: player.color, text: text.trim().substring(0, 200), ts: Date.now(), }); }); // ── Restart ── socket.on('restartGame', () => { const room = rooms[socket.roomCode]; if (!room || socket.id !== room.hostId) return; room.board = makeBoard(room.gridSize); room.currentTurn = 0; room.gameOver = false; room.winner = null; room.winType = null; io.to(room.code).emit('gameStarted', getRoomState(room)); systemMsg(room.code, 'Game restarted!'); }); // ── Disconnect ── socket.on('disconnect', () => { for (const code in rooms) { const room = rooms[code]; const idx = room.players.findIndex(p => p.id === socket.id); if (idx === -1) continue; const player = room.players[idx]; room.players.splice(idx, 1); if (room.players.length === 0) { delete rooms[code]; return; } if (socket.id === room.hostId) { room.hostId = room.players[0].id; } socket.leave(code); io.to(code).emit('roomUpdate', getRoomState(room)); systemMsg(code, `${player.name} disconnected.`); if (room.hostId === room.players[0].id) { systemMsg(code, `${room.players[0].name} is now the host.`); } break; } }); }); server.listen(PORT, '0.0.0.0', () => { console.log(`Dots & Boxes running on port ${PORT}`); });