Repository navigation
Expand file tree
/
Copy pathserver.js
More file actions
128 lines (113 loc) · 4.45 KB
/
Copy pathserver.js
File metadata and controls
128 lines (113 loc) · 4.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// server.js — Static-Server (Frontend) + WebSocket-Server für den Online-Modus.
// Der Server ist autoritativ: er hält den Spielzustand pro Raum und validiert
// jeden Zug mit derselben Engine wie der Client.
import express from "express";
import http from "node:http";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { WebSocketServer } from "ws";
import { genCode, newGame, tryMove } from "./rooms.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export function startServer(port = process.env.PORT || 3000) {
const app = express();
app.use(express.static(path.join(__dirname, "public")));
app.get("/health", (_req, res) => res.json({ status: "ok" }));
const server = http.createServer(app);
const wss = new WebSocketServer({ server });
// roomId -> { id, game, names:{1,2}, players:{1:ws|null, 2:ws|null} }
const rooms = new Map();
const send = (ws, msg) => {
if (ws && ws.readyState === ws.OPEN) ws.send(JSON.stringify(msg));
};
const broadcast = (room, msg) => {
send(room.players[1], msg);
send(room.players[2], msg);
};
const stateMsg = (room, frames) => ({
type: "state",
frames: frames || [room.game.board],
current: room.game.current,
moves: room.game.moves,
gameOver: room.game.gameOver,
winner: room.game.winner,
names: room.names,
connected: { 1: !!room.players[1], 2: !!room.players[2] },
});
wss.on("connection", (ws) => {
ws.roomId = null;
ws.player = null;
ws.on("message", (data) => {
let msg;
try { msg = JSON.parse(data); } catch { return; }
if (msg.type === "create") {
let id;
do { id = genCode(); } while (rooms.has(id));
const room = {
id,
size: msg.size,
game: newGame(msg.size),
names: { 1: (msg.name || "Spieler 1").slice(0, 20), 2: "Spieler 2" },
players: { 1: ws, 2: null },
};
rooms.set(id, room);
ws.roomId = id;
ws.player = 1;
send(ws, { type: "joined", roomId: id, player: 1 });
send(ws, stateMsg(room));
} else if (msg.type === "join") {
const id = (msg.roomId || "").toUpperCase().trim();
const room = rooms.get(id);
if (!room) { send(ws, { type: "error", message: "Raum nicht gefunden" }); return; }
const slot2 = room.players[2];
if (slot2 && slot2.readyState === slot2.OPEN) {
send(ws, { type: "error", message: "Raum ist voll" });
return;
}
room.players[2] = ws;
room.names[2] = (msg.name || "Spieler 2").slice(0, 20);
ws.roomId = id;
ws.player = 2;
send(ws, { type: "joined", roomId: id, player: 2 });
broadcast(room, stateMsg(room));
} else if (msg.type === "move") {
const room = rooms.get(ws.roomId);
if (!room) return;
if (!(room.players[1] && room.players[2])) {
send(ws, { type: "error", message: "Warte auf Mitspieler" });
return;
}
const result = tryMove(room.game, ws.player, msg.r, msg.c);
if (result.error) { send(ws, { type: "error", message: result.error }); return; }
broadcast(room, stateMsg(room, result.frames));
} else if (msg.type === "restart") {
const room = rooms.get(ws.roomId);
if (!room) return;
room.game = newGame(room.game.board.length); // gleiche Feldgröße beibehalten
broadcast(room, stateMsg(room));
} else if (msg.type === "rename") {
const room = rooms.get(ws.roomId);
if (!room || !ws.player) return;
room.names[ws.player] = (msg.name || `Spieler ${ws.player}`).slice(0, 20);
broadcast(room, stateMsg(room));
}
});
ws.on("close", () => {
const room = rooms.get(ws.roomId);
if (!room) return;
if (ws.player && room.players[ws.player] === ws) room.players[ws.player] = null;
if (!room.players[1] && !room.players[2]) {
rooms.delete(room.id); // leerer Raum wird aufgeräumt
return;
}
broadcast(room, stateMsg(room)); // verbleibender Spieler sieht "getrennt"
});
});
server.listen(port, () => {
const p = server.address().port;
console.log(`Lawine läuft auf http://localhost:${p}`);
});
return server;
}
// Nur automatisch starten, wenn direkt ausgeführt (nicht beim Import in Tests).
const invoked = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
if (invoked) startServer();