Persistant rooms
This commit is contained in:
@@ -75,6 +75,14 @@
|
|||||||
button:hover {
|
button:hover {
|
||||||
background: #34495e;
|
background: #34495e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#exitBtn {
|
||||||
|
background: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#exitBtn:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -86,8 +94,16 @@
|
|||||||
<br>
|
<br>
|
||||||
<button id="restartBtn">🔄 Neustart</button>
|
<button id="restartBtn">🔄 Neustart</button>
|
||||||
<button id="copyRoomBtn">📋 Link kopieren</button>
|
<button id="copyRoomBtn">📋 Link kopieren</button>
|
||||||
|
<button id="exitBtn">🚪 Raum verlassen</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Session Management
|
||||||
|
let sessionId = localStorage.getItem("game_session_id");
|
||||||
|
if (!sessionId) {
|
||||||
|
sessionId = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
||||||
|
localStorage.setItem("game_session_id", sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
let roomId = params.get("room");
|
let roomId = params.get("room");
|
||||||
|
|
||||||
@@ -96,7 +112,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
||||||
const ws = new WebSocket(`${protocol}://${location.host}?room=${roomId}`);
|
const ws = new WebSocket(`${protocol}://${location.host}?room=${roomId}&sessionId=${sessionId}`);
|
||||||
const svg = document.querySelector("svg");
|
const svg = document.querySelector("svg");
|
||||||
|
|
||||||
let myRole = null, state = null, selected = null, currentRoomId = null;
|
let myRole = null, state = null, selected = null, currentRoomId = null;
|
||||||
@@ -112,19 +128,17 @@
|
|||||||
myRole = msg.role;
|
myRole = msg.role;
|
||||||
currentRoomId = msg.roomId;
|
currentRoomId = msg.roomId;
|
||||||
document.getElementById("roleHeader").innerText = `Du bist: ${myRole.toUpperCase()} | Raum: ${currentRoomId}`;
|
document.getElementById("roleHeader").innerText = `Du bist: ${myRole.toUpperCase()} | Raum: ${currentRoomId}`;
|
||||||
|
if (!params.has("room")) {
|
||||||
|
window.history.replaceState({}, '', `?room=${currentRoomId}`);
|
||||||
}
|
}
|
||||||
if (msg.type === "state") {
|
|
||||||
state = msg.game;
|
|
||||||
render();
|
|
||||||
}
|
}
|
||||||
|
if (msg.type === "state") { state = msg.game; render(); }
|
||||||
if (msg.type === "full") alert("Dieser Raum ist leider voll!");
|
if (msg.type === "full") alert("Dieser Raum ist leider voll!");
|
||||||
};
|
};
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
svg.innerHTML = "";
|
svg.innerHTML = "";
|
||||||
|
|
||||||
// Update Status UI
|
|
||||||
const statusEl = document.getElementById("status");
|
const statusEl = document.getElementById("status");
|
||||||
if (state.winner) {
|
if (state.winner) {
|
||||||
statusEl.innerHTML = `<span style="color:${state.winner === myRole ? 'green' : 'red'}">SPIEL ENDE: ${state.winner.toUpperCase()} GEWINNT!</span>`;
|
statusEl.innerHTML = `<span style="color:${state.winner === myRole ? 'green' : 'red'}">SPIEL ENDE: ${state.winner.toUpperCase()} GEWINNT!</span>`;
|
||||||
@@ -133,17 +147,14 @@
|
|||||||
statusEl.innerHTML = `${turnText}<br><small>Zug: ${state.moveCount} / 40</small>`;
|
statusEl.innerHTML = `${turnText}<br><small>Zug: ${state.moveCount} / 40</small>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw Lines
|
|
||||||
edges.forEach(([a, b]) => {
|
edges.forEach(([a, b]) => {
|
||||||
const [x1, y1] = pos[a], [x2, y2] = pos[b];
|
const [x1, y1] = pos[a], [x2, y2] = pos[b];
|
||||||
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
||||||
line.setAttribute("x1", x1); line.setAttribute("y1", y1);
|
line.setAttribute("x1", x1); line.setAttribute("y1", y1); line.setAttribute("x2", x2); line.setAttribute("y2", y2);
|
||||||
line.setAttribute("x2", x2); line.setAttribute("y2", y2);
|
|
||||||
line.setAttribute("stroke", "#bdc3c7"); line.setAttribute("stroke-width", "3");
|
line.setAttribute("stroke", "#bdc3c7"); line.setAttribute("stroke-width", "3");
|
||||||
svg.appendChild(line);
|
svg.appendChild(line);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Draw Nodes
|
|
||||||
for (let i = 0; i <= 10; i++) {
|
for (let i = 0; i <= 10; i++) {
|
||||||
const [x, y] = pos[i];
|
const [x, y] = pos[i];
|
||||||
let type = "empty";
|
let type = "empty";
|
||||||
@@ -157,7 +168,6 @@
|
|||||||
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||||
circle.setAttribute("cx", x); circle.setAttribute("cy", y); circle.setAttribute("r", 25);
|
circle.setAttribute("cx", x); circle.setAttribute("cy", y); circle.setAttribute("r", 25);
|
||||||
circle.setAttribute("class", `node ${type} ${selected === i ? 'selected' : ''} ${isPossible ? 'possible' : ''}`);
|
circle.setAttribute("class", `node ${type} ${selected === i ? 'selected' : ''} ${isPossible ? 'possible' : ''}`);
|
||||||
|
|
||||||
circle.onclick = () => handleClick(i);
|
circle.onclick = () => handleClick(i);
|
||||||
svg.appendChild(circle);
|
svg.appendChild(circle);
|
||||||
|
|
||||||
@@ -172,15 +182,12 @@
|
|||||||
|
|
||||||
function handleClick(i) {
|
function handleClick(i) {
|
||||||
if (!state || state.turn !== myRole || state.winner) return;
|
if (!state || state.turn !== myRole || state.winner) return;
|
||||||
|
const isMyPiece = (myRole === "wolf" && state.wolf === i) || (myRole === "sheep" && state.sheep.includes(i));
|
||||||
const isMyPiece = (myRole === "wolf" && state.wolf === i) ||
|
|
||||||
(myRole === "sheep" && state.sheep.includes(i));
|
|
||||||
|
|
||||||
if (selected === null) {
|
if (selected === null) {
|
||||||
if (isMyPiece) { selected = i; render(); }
|
if (isMyPiece) { selected = i; render(); }
|
||||||
} else {
|
} else {
|
||||||
if (isMyPiece) {
|
if (isMyPiece) {
|
||||||
selected = (selected === i) ? null : i; // Toggle selection
|
selected = (selected === i) ? null : i;
|
||||||
} else {
|
} else {
|
||||||
ws.send(JSON.stringify({ from: selected, to: i }));
|
ws.send(JSON.stringify({ from: selected, to: i }));
|
||||||
selected = null;
|
selected = null;
|
||||||
@@ -190,11 +197,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("restartBtn").onclick = () => ws.send(JSON.stringify({ type: "restart" }));
|
document.getElementById("restartBtn").onclick = () => ws.send(JSON.stringify({ type: "restart" }));
|
||||||
|
|
||||||
document.getElementById("copyRoomBtn").onclick = () => {
|
document.getElementById("copyRoomBtn").onclick = () => {
|
||||||
const url = `${location.origin}${location.pathname}?room=${currentRoomId}`;
|
const url = `${location.origin}${location.pathname}?room=${currentRoomId}`;
|
||||||
navigator.clipboard.writeText(url).then(() => alert("Link kopiert!"));
|
navigator.clipboard.writeText(url).then(() => alert("Link kopiert!"));
|
||||||
};
|
};
|
||||||
|
document.getElementById("exitBtn").onclick = () => {
|
||||||
|
if (confirm("Möchtest du den Raum verlassen?")) {
|
||||||
|
ws.send(JSON.stringify({ type: "leave" }));
|
||||||
|
window.location.href = window.location.pathname;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
55
server.js
55
server.js
@@ -9,7 +9,6 @@ app.use(express.static("public"));
|
|||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const wss = new WebSocket.Server({ server });
|
const wss = new WebSocket.Server({ server });
|
||||||
|
|
||||||
// Movement logic: Wolf can move anywhere, Sheep only forward
|
|
||||||
const board_wolf = {
|
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],
|
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],
|
5: [1, 2, 3, 4, 6, 7, 8, 9], 6: [3, 5, 9], 7: [4, 5, 8, 10], 8: [5, 7, 9, 10],
|
||||||
@@ -28,39 +27,27 @@ function broadcast(roomId) {
|
|||||||
if (!room) return;
|
if (!room) return;
|
||||||
const msg = JSON.stringify({ type: "state", game: room.game });
|
const msg = JSON.stringify({ type: "state", game: room.game });
|
||||||
room.players.forEach(p => {
|
room.players.forEach(p => {
|
||||||
if (p.ws.readyState === WebSocket.OPEN) p.ws.send(msg);
|
if (p.ws && p.ws.readyState === WebSocket.OPEN) p.ws.send(msg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkWinConditions(game) {
|
function checkWinConditions(game) {
|
||||||
// 1. Wolf reaches node 0
|
|
||||||
if (game.wolf === 0) return "wolf";
|
if (game.wolf === 0) return "wolf";
|
||||||
|
|
||||||
// 2. Turn limit (40 moves)
|
|
||||||
if (game.moveCount >= 40) return "wolf";
|
if (game.moveCount >= 40) return "wolf";
|
||||||
|
|
||||||
// Mobility Check helpers
|
const canWolfMove = board_wolf[game.wolf].some(to => !game.sheep.includes(to));
|
||||||
const canWolfMove = board_wolf[game.wolf].some(to =>
|
|
||||||
!game.sheep.includes(to)
|
|
||||||
);
|
|
||||||
|
|
||||||
const canSheepMove = game.sheep.some(from =>
|
const canSheepMove = game.sheep.some(from =>
|
||||||
board_sheep[from].some(to => to !== game.wolf && !game.sheep.includes(to))
|
board_sheep[from].some(to => to !== game.wolf && !game.sheep.includes(to))
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Wolf trapped
|
|
||||||
if (!canWolfMove) return "sheep";
|
if (!canWolfMove) return "sheep";
|
||||||
|
|
||||||
// 4. Sheep cannot move
|
|
||||||
if (!canSheepMove && game.turn === "sheep") return "wolf";
|
if (!canSheepMove && game.turn === "sheep") return "wolf";
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMove(roomId, role, { from, to }) {
|
function handleMove(roomId, role, { from, to }) {
|
||||||
const room = rooms[roomId];
|
const room = rooms[roomId];
|
||||||
if (!room || room.game.winner) return;
|
if (!room || room.game.winner) return;
|
||||||
|
|
||||||
const game = room.game;
|
const game = room.game;
|
||||||
if (game.turn !== role) return;
|
if (game.turn !== role) return;
|
||||||
|
|
||||||
@@ -78,20 +65,16 @@ function handleMove(roomId, role, { from, to }) {
|
|||||||
game.sheep[i] = to;
|
game.sheep[i] = to;
|
||||||
game.turn = "wolf";
|
game.turn = "wolf";
|
||||||
}
|
}
|
||||||
|
|
||||||
game.moveCount++;
|
game.moveCount++;
|
||||||
game.winner = checkWinConditions(game);
|
game.winner = checkWinConditions(game);
|
||||||
broadcast(roomId);
|
broadcast(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
wss.on("connection", (ws, req) => {
|
wss.on("connection", (ws, req) => {
|
||||||
ws.isAlive = true;
|
|
||||||
ws.on("pong", () => ws.isAlive = true);
|
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(req.url.split('?')[1]);
|
const urlParams = new URLSearchParams(req.url.split('?')[1]);
|
||||||
let roomId = urlParams.get("room");
|
let roomId = urlParams.get("room");
|
||||||
|
let sessionId = urlParams.get("sessionId");
|
||||||
|
|
||||||
// Room creation/joining logic
|
|
||||||
if (!roomId || !rooms[roomId]) {
|
if (!roomId || !rooms[roomId]) {
|
||||||
if (!roomId) roomId = randomBytes(3).toString("hex");
|
if (!roomId) roomId = randomBytes(3).toString("hex");
|
||||||
rooms[roomId] = {
|
rooms[roomId] = {
|
||||||
@@ -101,16 +84,23 @@ wss.on("connection", (ws, req) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const room = rooms[roomId];
|
const room = rooms[roomId];
|
||||||
if (room.players.length >= 2) {
|
let player = room.players.find(p => p.sessionId === sessionId);
|
||||||
|
|
||||||
|
if (!player && room.players.length >= 2) {
|
||||||
ws.send(JSON.stringify({ type: "full" }));
|
ws.send(JSON.stringify({ type: "full" }));
|
||||||
ws.close();
|
ws.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (player) {
|
||||||
|
player.ws = ws; // Re-attach new socket to existing session
|
||||||
|
} else {
|
||||||
const role = room.players.length === 0 ? "sheep" : "wolf";
|
const role = room.players.length === 0 ? "sheep" : "wolf";
|
||||||
room.players.push({ ws, role });
|
player = { ws, role, sessionId };
|
||||||
|
room.players.push(player);
|
||||||
|
}
|
||||||
|
|
||||||
ws.send(JSON.stringify({ type: "role", role, roomId }));
|
ws.send(JSON.stringify({ type: "role", role: player.role, roomId }));
|
||||||
broadcast(roomId);
|
broadcast(roomId);
|
||||||
|
|
||||||
ws.on("message", msg => {
|
ws.on("message", msg => {
|
||||||
@@ -120,23 +110,18 @@ wss.on("connection", (ws, req) => {
|
|||||||
if (data.type === "restart") {
|
if (data.type === "restart") {
|
||||||
room.game = { wolf: 5, sheep: [0, 1, 3], turn: "sheep", moveCount: 0, winner: null };
|
room.game = { wolf: 5, sheep: [0, 1, 3], turn: "sheep", moveCount: 0, winner: null };
|
||||||
broadcast(roomId);
|
broadcast(roomId);
|
||||||
return;
|
} else if (data.type === "leave") {
|
||||||
|
room.players = room.players.filter(p => p.sessionId !== sessionId);
|
||||||
|
if (room.players.length === 0) delete rooms[roomId];
|
||||||
|
ws.close();
|
||||||
|
} else {
|
||||||
|
handleMove(roomId, player.role, data);
|
||||||
}
|
}
|
||||||
handleMove(roomId, role, data);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
room.players = room.players.filter(p => p.ws !== ws);
|
// We don't remove the player here so they can reconnect on refresh
|
||||||
if (room.players.length === 0) delete rooms[roomId];
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
wss.clients.forEach(ws => {
|
|
||||||
if (!ws.isAlive) return ws.terminate();
|
|
||||||
ws.isAlive = false;
|
|
||||||
ws.ping();
|
|
||||||
});
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
server.listen(3030, () => console.log("Server läuft auf Port 3030"));
|
server.listen(3030, () => console.log("Server läuft auf Port 3030"));
|
||||||
Reference in New Issue
Block a user