diff --git a/README.md b/README.md
index 130a30b..afea277 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,13 @@ The game supports single-player sessions with a local leaderboard tracking top s
## How to start
-Open the `frontend/index.html` file in a browser. That's it.
+From the project root, run:
+
+```powershell
+php serve.php
+```
+Then open `http://localhost:8000/` in a browser.
+You can override the port, but the default will be `8000`.
---
@@ -95,7 +101,13 @@ The drawing part will likely be handled by WebGL and a canvas.
Your browser must support **WebGL**. If you are uncertain, [check this website](https://get.webgl.org/).
-Open the `frontend/index.html` file in a browser. That's it.
+Start the app from the project root:
+
+```powershell
+php serve.php
+```
+
+Then open `http://localhost:8000/` in a browser.
### Backend
diff --git a/backend/data/lobbies.json b/backend/data/lobbies.json
new file mode 100644
index 0000000..472acdf
--- /dev/null
+++ b/backend/data/lobbies.json
@@ -0,0 +1,7 @@
+{
+ "Test": {
+ "name": "Test",
+ "players": [],
+ "leaderboard": []
+ }
+}
diff --git a/backend/index.php b/backend/index.php
new file mode 100644
index 0000000..3dda880
--- /dev/null
+++ b/backend/index.php
@@ -0,0 +1,238 @@
+ false, 'error' => 'Could not open data file.'], 500);
+ }
+
+ flock($handle, $write ? LOCK_EX : LOCK_SH);
+ rewind($handle);
+ $raw = stream_get_contents($handle);
+ $lobbies = json_decode($raw ?: '{}', true);
+ if (!is_array($lobbies)) {
+ $lobbies = [];
+ }
+
+ $result = $callback($lobbies);
+
+ if ($write) {
+ rewind($handle);
+ ftruncate($handle, 0);
+ fwrite($handle, json_encode($lobbies, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
+ fflush($handle);
+ }
+
+ flock($handle, LOCK_UN);
+ fclose($handle);
+
+ return $result;
+}
+
+function ensure_lobby(array &$lobbies, string $lobbyName): array
+{
+ if (!isset($lobbies[$lobbyName]) || !is_array($lobbies[$lobbyName])) {
+ $lobbies[$lobbyName] = [
+ 'name' => $lobbyName,
+ 'players' => [],
+ 'leaderboard' => [],
+ ];
+ }
+
+ $lobbies[$lobbyName]['players'] = array_values($lobbies[$lobbyName]['players'] ?? []);
+ $lobbies[$lobbyName]['leaderboard'] = array_values($lobbies[$lobbyName]['leaderboard'] ?? []);
+
+ return $lobbies[$lobbyName];
+}
+
+$action = $_GET['action'] ?? '';
+$body = read_body();
+
+if ($action === 'createLobby') {
+ $lobbyName = clean_name($body['lobbyName'] ?? '');
+ if ($lobbyName === '') {
+ respond(['ok' => false, 'error' => 'Lobby name is required.'], 400);
+ }
+
+ $lobby = with_lobbies(static function (array &$lobbies) use ($lobbyName): array {
+ return ensure_lobby($lobbies, $lobbyName);
+ }, true);
+
+ respond(['ok' => true, 'lobby' => ['name' => $lobby['name']]]);
+}
+
+if ($action === 'joinLobby') {
+ $lobbyName = clean_name($body['lobbyName'] ?? '');
+ $playerName = clean_name($body['playerName'] ?? '', 24);
+ if ($lobbyName === '' || $playerName === '') {
+ respond(['ok' => false, 'error' => 'Lobby name and player name are required.'], 400);
+ }
+
+ $lobby = with_lobbies(static function (array &$lobbies) use ($lobbyName, $playerName): array {
+ ensure_lobby($lobbies, $lobbyName);
+ if (!in_array($playerName, $lobbies[$lobbyName]['players'], true)) {
+ $lobbies[$lobbyName]['players'][] = $playerName;
+ }
+ return $lobbies[$lobbyName];
+ }, true);
+
+ respond(['ok' => true, 'lobby' => ['name' => $lobby['name'], 'players' => $lobby['players']]]);
+}
+
+if ($action === 'leaveLobby') {
+ $lobbyName = clean_name($body['lobbyName'] ?? '');
+ $playerName = clean_name($body['playerName'] ?? '', 24);
+
+ with_lobbies(static function (array &$lobbies) use ($lobbyName, $playerName): void {
+ if ($lobbyName === '' || $playerName === '' || !isset($lobbies[$lobbyName])) {
+ return;
+ }
+
+ $lobbies[$lobbyName]['players'] = array_values(array_filter(
+ $lobbies[$lobbyName]['players'] ?? [],
+ static fn ($name): bool => $name !== $playerName,
+ ));
+ }, true);
+
+ respond(['ok' => true]);
+}
+
+if ($action === 'submitScore') {
+ $lobbyName = clean_name($body['lobbyName'] ?? '');
+ $playerName = clean_name($body['playerName'] ?? '', 24);
+ if ($lobbyName === '' || $playerName === '') {
+ respond(['ok' => false, 'error' => 'Lobby name and player name are required.'], 400);
+ }
+
+ $entry = [
+ 'playerName' => $playerName,
+ 'totalScore' => max(0, min(300, (int) ($body['totalScore'] ?? 0))),
+ 'scores' => clean_scores($body['scores'] ?? []),
+ 'countries' => clean_countries($body['countries'] ?? []),
+ 'date' => gmdate('c'),
+ ];
+
+ with_lobbies(static function (array &$lobbies) use ($lobbyName, $playerName, $entry): void {
+ ensure_lobby($lobbies, $lobbyName);
+ if (!in_array($playerName, $lobbies[$lobbyName]['players'], true)) {
+ $lobbies[$lobbyName]['players'][] = $playerName;
+ }
+
+ $lobbies[$lobbyName]['leaderboard'][] = $entry;
+ usort(
+ $lobbies[$lobbyName]['leaderboard'],
+ static fn (array $a, array $b): int => ($b['totalScore'] ?? 0) <=> ($a['totalScore'] ?? 0),
+ );
+ $lobbies[$lobbyName]['leaderboard'] = array_slice($lobbies[$lobbyName]['leaderboard'], 0, 20);
+ }, true);
+
+ respond(['ok' => true, 'entry' => $entry]);
+}
+
+if ($action === 'getLeaderboard') {
+ $lobbyName = clean_name($_GET['lobbyName'] ?? '');
+ if ($lobbyName === '') {
+ respond(['ok' => false, 'error' => 'Lobby name is required.'], 400);
+ }
+
+ $leaderboard = with_lobbies(static function (array &$lobbies) use ($lobbyName): array {
+ return $lobbies[$lobbyName]['leaderboard'] ?? [];
+ });
+
+ respond(['ok' => true, 'leaderboard' => $leaderboard]);
+}
+
+if ($action === 'getLobby') {
+ $lobbyName = clean_name($_GET['lobbyName'] ?? '');
+ if ($lobbyName === '') {
+ respond(['ok' => false, 'error' => 'Lobby name is required.'], 400);
+ }
+
+ $lobby = with_lobbies(static function (array &$lobbies) use ($lobbyName): array {
+ if (!isset($lobbies[$lobbyName])) {
+ return ['name' => $lobbyName, 'players' => []];
+ }
+
+ return [
+ 'name' => $lobbies[$lobbyName]['name'] ?? $lobbyName,
+ 'players' => array_values($lobbies[$lobbyName]['players'] ?? []),
+ ];
+ });
+
+ respond(['ok' => true, 'lobby' => $lobby]);
+}
+
+respond(['ok' => false, 'error' => 'Unknown action.'], 404);
diff --git a/frontend/game.html b/frontend/game.html
index 9aaec39..90f225f 100644
--- a/frontend/game.html
+++ b/frontend/game.html
@@ -142,44 +142,6 @@
-
-
-
-
-
-
+