Spaces:
Paused
Paused
| 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}`); | |
| }); | |