dotsandboxes / server.js
Sebebeb's picture
Upload 994 files
ea62c79 verified
Raw
History Blame Contribute Delete
8.41 kB
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}`);
});