const express = require("express"); const http = require("http"); const WebSocket = require("ws"); const { randomBytes } = require("crypto"); const app = express(); app.use(express.static("public")); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); const board_wolf = { 0: [1, 2, 3], 1: [0, 2, 4, 5], 2: [0, 1, 3, 5], 3: [0, 2, 5, 6], 4: [1, 5, 7], 5: [1, 2, 3, 4, 6, 7, 8, 9], 6: [3, 5, 9], 7: [4, 5, 8, 10], 8: [5, 7, 9, 10], 9: [5, 6, 8, 10], 10: [7, 8, 9] }; const board_sheep = { 0: [1, 2, 3], 1: [2, 4, 5], 2: [1, 3, 5], 3: [2, 5, 6], 4: [5, 7], 5: [4, 6, 7, 8, 9], 6: [5, 9], 7: [8, 10], 8: [7, 9, 10], 9: [8, 10], 10: [] }; const rooms = {}; function broadcast(roomId) { const room = rooms[roomId]; if (!room) return; room.players.forEach(p => { if (p.ws && p.ws.readyState === WebSocket.OPEN) { p.ws.send(JSON.stringify({ type: "state", game: room.game, yourRole: p.role })); } }); } // Heartbeat: Check if clients are still alive every 30s const interval = setInterval(() => { wss.clients.forEach(ws => { if (ws.isAlive === false) return ws.terminate(); ws.isAlive = false; ws.ping(); }); }, 30000); wss.on("connection", (ws, req) => { const urlParams = new URLSearchParams(req.url.split('?')[1]); let roomId = urlParams.get("room"); let sessionId = urlParams.get("sessionId"); ws.isAlive = true; ws.on('pong', () => ws.isAlive = true); if (!roomId || !rooms[roomId]) { if (!roomId || roomId === "null") roomId = randomBytes(3).toString("hex"); rooms[roomId] = { game: { wolf: 5, sheep: [0, 1, 3], turn: "sheep", moveCount: 0, winner: null }, players: [], nextFirstRole: "sheep" }; } const room = rooms[roomId]; let player = room.players.find(p => p.sessionId === sessionId); if (!player && room.players.length >= 2) { ws.send(JSON.stringify({ type: "full" })); ws.close(); return; } if (player) { player.ws = ws; } else { const role = room.players.length === 0 ? room.nextFirstRole : (room.nextFirstRole === "sheep" ? "wolf" : "sheep"); player = { ws, role, sessionId }; room.players.push(player); } ws.send(JSON.stringify({ type: "init", role: player.role, roomId })); broadcast(roomId); ws.on("message", msg => { let data = JSON.parse(msg); if (data.type === "restart") { room.players.forEach(p => p.role = (p.role === "wolf" ? "sheep" : "wolf")); room.nextFirstRole = room.nextFirstRole === "sheep" ? "wolf" : "sheep"; room.game = { wolf: 5, sheep: [0, 1, 3], turn: "sheep", moveCount: 0, winner: null }; broadcast(roomId); } else if (data.type === "leave") { room.players = room.players.filter(p => p.sessionId !== sessionId); if (room.players.length === 0) delete rooms[roomId]; } else if (data.from !== undefined && data.to !== undefined) { handleMove(roomId, player.role, data); } }); ws.on("close", () => { // We keep the player in the room for a while to allow reconnects }); }); function handleMove(roomId, role, { from, to }) { const room = rooms[roomId]; if (!room || room.game.winner || room.game.turn !== role) return; const game = room.game; const allowed = (role === "wolf") ? board_wolf[from] : board_sheep[from]; if (!allowed?.includes(to) || game.sheep.includes(to) || game.wolf === to) return; if (role === "wolf") { game.wolf = to; game.turn = "sheep"; } else { game.sheep[game.sheep.indexOf(from)] = to; game.turn = "wolf"; } game.moveCount++; game.winner = checkWinConditions(game); broadcast(roomId); } function checkWinConditions(game) { if (game.wolf === 0) return "wolf"; if (game.moveCount >= 40) return "wolf"; const canWolfMove = board_wolf[game.wolf].some(to => !game.sheep.includes(to)); const canSheepMove = game.sheep.some(from => board_sheep[from].some(to => to !== game.wolf && !game.sheep.includes(to))); if (!canWolfMove) return "sheep"; if (!canSheepMove && game.turn === "sheep") return "wolf"; return null; } server.listen(3030, () => console.log("Server running on port 3030"));