From bd9db87ccb14cf946f57a19963cc3fade33303c7 Mon Sep 17 00:00:00 2001 From: Simon Oberzier Date: Mon, 9 Feb 2026 23:06:15 +0100 Subject: [PATCH] Heartbeat server --- public/index.html | 129 +++++++++++++++++++++++++++------------------- server.js | 56 +++++++++++--------- 2 files changed, 107 insertions(+), 78 deletions(-) diff --git a/public/index.html b/public/index.html index 3b59099..79faafa 100644 --- a/public/index.html +++ b/public/index.html @@ -14,6 +14,7 @@ --sheep-light: #7db0cf; --black: #1d3557; --white: #ffffff; + --green: #10b981; } body { @@ -55,6 +56,12 @@ gap: 10px; } + .room-info { + display: flex; + align-items: center; + gap: 10px; + } + .room-tag { font-size: 0.8rem; font-weight: bold; @@ -64,6 +71,26 @@ text-transform: uppercase; } + /* Connection Indicator */ + .pulse { + width: 10px; + height: 10px; + border-radius: 50%; + background: #94a3b8; + display: inline-block; + transition: background 0.3s; + } + + .online { + background: var(--green); + box-shadow: 0 0 8px var(--green); + } + + .offline { + background: var(--wolf); + box-shadow: 0 0 8px var(--wolf); + } + .id-box { font-size: 1.2rem; font-weight: 900; @@ -87,7 +114,6 @@ aspect-ratio: 600 / 420; border: 3px solid var(--black); background: #fff; - position: relative; touch-action: manipulation; } @@ -106,13 +132,13 @@ stroke: var(--black); stroke-width: 2.5; transition: all 0.15s ease; + cursor: pointer; } .empty { fill: #fff; } - /* Piece Colors */ .sheep { fill: var(--sheep); } @@ -121,7 +147,6 @@ fill: var(--wolf); } - /* Subtle Select: Lightens the fill and adds a clean 4px border */ .wolf.selected { fill: var(--wolf-light); stroke-width: 4; @@ -134,7 +159,7 @@ .possible { fill: #fff; - stroke: #10b981; + stroke: var(--green); stroke-width: 3; stroke-dasharray: 4; } @@ -176,8 +201,6 @@ @media (max-width: 600px) { .main-card { padding: 10px; - box-shadow: 4px 4px 0px var(--black); - gap: 10px; } .info-bar { @@ -193,10 +216,6 @@ .controls { grid-template-columns: 1fr; } - - .id-box { - font-size: 1rem; - } } @@ -205,17 +224,16 @@
-
-
RAUM: ...
-
...
+
+
+
RAUM: ...
+
...
+
-
Laden...
+
Verbinde...
- -
- -
- +
@@ -227,47 +245,53 @@ diff --git a/server.js b/server.js index 564239d..472a27b 100644 --- a/server.js +++ b/server.js @@ -17,29 +17,36 @@ 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 - })); + 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 = randomBytes(3).toString("hex"); + 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" // Tracks who should be sheep next game + nextFirstRole: "sheep" }; } @@ -55,7 +62,6 @@ wss.on("connection", (ws, req) => { if (player) { player.ws = ws; } else { - // Assign role based on who is already there const role = room.players.length === 0 ? room.nextFirstRole : (room.nextFirstRole === "sheep" ? "wolf" : "sheep"); player = { ws, role, sessionId }; room.players.push(player); @@ -66,31 +72,23 @@ wss.on("connection", (ws, req) => { ws.on("message", msg => { let data = JSON.parse(msg); - if (data.type === "restart") { - // Swap roles for the next round 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 { + } else if (data.from !== undefined && data.to !== undefined) { handleMove(roomId, player.role, data); } }); -}); -// Reuse the handleMove and checkWinConditions from previous scripts... -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; -} + 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]; @@ -107,4 +105,14 @@ function handleMove(roomId, role, { from, to }) { broadcast(roomId); } -server.listen(3030); \ No newline at end of file +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")); \ No newline at end of file