diff --git a/README.md b/README.md index 7366f92..057bf70 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,174 @@ -Setup: +# Spotify Roulette -- Spotify developer app öffnen (free) -- in user management jeden mitspieler mit email und username (anmeldename bei spotify) eintragen, geht seit mai nurnoch manuell da extended quota mode nurnoch für firmen ist -- maven projekt compilieren, ClientID, redirectURL und Clientsecret auf server eintragen und Website als service ausführen \ No newline at end of file +IDK für wen das ist, aber hier ist ein Guide, wie man das bei sich selbst aufsetzt, weil Spotify kein extended quota mode für Privatpersonen mehr erlaubt (sprich: man muss jeden Mitspieler manuell im Usermanagement eintragen). + +## Wichtig vorab +- Für Spotify‑Login und Playback brauchst du eine Domain mit HTTPS. Nur IP ohne Domain funktioniert für Spotify OAuth nicht. +- Jeder Spieler muss sich einmal mit Spotify anmelden. Spotify Playback erfordert in der Regel Premium. +- Tokens und Spielzustand werden im Speicher gehalten. Neustart der App ⇒ erneute Logins nötig. + +## 1\) Voraussetzungen +- Server mit Ubuntu 22\.04 \(oder ähnlich\), SSH‑Zugang. +- Domain, A‑Record zeigt auf deine Server‑IP. +- Spotify Developer Konto: + - App anlegen, Client ID und Client Secret notieren. + - Redirect‑URI setzen, z\.B. `https://deine-domain.tld/callback` + - Jeden Mitspieler in der Spotify‑App als „User“ hinzufügen \(\*ohne extended quota\*\). + +## 2\) Server vorbereiten +```bash +# System aktualisieren +sudo apt update && sudo apt upgrade -y + +# Java (z.B. Temurin 21), Maven, Git, Nginx, Certbot +sudo apt install -y wget gnupg2 ca-certificates lsb-release apt-transport-https +sudo apt install -y openjdk-21-jre maven git nginx certbot python3-certbot-nginx + +# Java prüfen +java -version +``` + +## 3\) Projekt beziehen und bauen +```bash +# In ein Verzeichnis deiner Wahl wechseln +cd /opt +sudo git clone spotify-roulette +cd spotify-roulette + +# Build (ohne Tests) +mvn -DskipTests package + +# Das erzeugte JAR liegt meist unter: target/-.jar +ls -lh target +``` + +## 4\) Spotify‑Konfiguration eintragen +- Suche, wo `SpotifyAuthService` erzeugt wird \(Workspace‑Suche nach `new SpotifyAuthService(`\). +- Trage dort `clientId`, `clientSecret` und `redirectUri` für deine Domain ein \(oder lies sie aus Umgebungsvariablen\). +- Redirect‑URI muss exakt zur Spotify Developer Console passen \(Schema, Host, Pfad\), z\.B. `https://deine-domain.tld/callback`. + +Hinweis: +- Die App speichert pro Benutzer eine `SpotifyApi`\-Instanz im Speicher. `exchangeCode` im Callback muss aufgerufen werden, damit `getRecentTracks` und Playback funktionieren. +- `GameService` lädt pro Spieler die zuletzt gehörten Tracks beim Beitritt \(`getRecentTracks`\) und cached sie pro Spieler. + +## 5\) App als Systemdienst einrichten +Kurz erklärt: Systemd startet deine Java‑App beim Booten neu und hält sie am Laufen. +```ini +# /etc/systemd/system/spotify-roulette.service +[Unit] +Description=Spotify Roulette +After=network.target + +[Service] +User=www-data +WorkingDirectory=/opt/spotify-roulette +Environment=SPOTIFY_CLIENT_ID= +Environment=SPOTIFY_CLIENT_SECRET= +Environment=APP_BASE_URL=https://deine-domain.tld +# Falls deine App Umgebungsvariablen liest, nutzt sie. Andernfalls Werte direkt im Code setzen. +ExecStart=/usr/bin/java -jar /opt/spotify-roulette/target/.jar +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +Aktivieren und starten: +```bash +sudo systemctl daemon-reload +sudo systemctl enable spotify-roulette +sudo systemctl start spotify-roulette +sudo systemctl status spotify-roulette --no-pager +``` + +## 6\) Reverse Proxy mit Nginx und HTTPS +Proxy leitet Port 80/443 auf deine App \(typisch Port 8080\). WebSockets brauchen Upgrade‑Header. +```nginx +# /etc/nginx/sites-available/spotify-roulette.conf +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + server_name deine-domain.tld; + + location / { + proxy_pass http://127.0.0.1:8080; # ggf. Port an deine App anpassen + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 3600; + } +} +``` + +Aktivieren: +```bash +sudo ln -s /etc/nginx/sites-available/spotify-roulette.conf /etc/nginx/sites-enabled/spotify-roulette.conf +sudo nginx -t +sudo systemctl reload nginx +``` + +TLS Zertifikat: +```bash +sudo certbot --nginx -d deine-domain.tld --redirect +``` + +## 7\) Firewall \(falls aktiv\) +```bash +sudo ufw allow OpenSSH +sudo ufw allow 'Nginx Full' +sudo ufw enable +sudo ufw status +``` + +## 8\) Start lokal \(Dev\) +- Spotify erlaubt `http://localhost` als Redirect‑URI. Füge z\.B. `http://localhost:8080/callback` in der Developer Console hinzu und setze denselben Wert im Code. +- Start: +```bash +mvn -DskipTests package +java -jar target/.jar +``` + +## 9\) Spielablauf \(UI\) +- Öffne `https://deine-domain.tld`. +- Spiel erstellen/beitreten. +- Jeder Spieler meldet sich mit Spotify an \(erforderliche Scopes: `user-read-recently-played`, `user-library-read`, `user-modify-playback-state`, `user-read-playback-state`, `streaming`\). +- Songliste lädt automatisch \(`getRecentTracks` + Fallback `getUsersSavedTracks`\). +- „Runde starten“ löst über WebSocket den Start aus. Es wird ein zufälliger Owner und ein Song aus dessen Tracks gewählt. +- Guesses über die Kreis‑Optionen \(Mehrfachauswahl möglich\). Scoring: + - +3, wenn der ausgeählte den Song gehört hat + - −1 pro falschem Tipp + - +1 Bonus, wenn kein falscher Tipp \(fehlerfrei\) +- Ab 30 Punkten und eindeutiger Spitze: Spielende, Overlay mit Leaderboard, Scores werden auf 0 zurückgesetzt. + +## 10\) Geräte & Playback +- Geräte laden: `GET /api/spotify/devices?username=` +- Abspielen: `POST /api/spotify/play` mit JSON `{ "username": "", "device_id": "", "track_uri": "spotify:track:..." }` +- Der Track wird ab der Hälfte gestartet \(`position_ms = duration_ms / 2`\). + +## 11\) API \& WS Übersicht +- `POST /api/create-game` ⇒ `{ username }` → erstellt Spiel, fügt Spieler hinzu. +- `POST /api/join-game` ⇒ `{ username, gameId }` → Spieler tritt bei. +- `GET /api/game/{gameId}/players` ⇒ Liste der Spieler. +- `POST /api/game/{gameId}/start-round` ⇒ Broadcast des Rundenstarts \(Fallback, primär via WebSocket `type: "start-round"`\). +- `POST /api/game/{gameId}/guess` ⇒ `{ username, guess }` \(Legacy Single‑Guess; UI nutzt Multi‑Guess via WS\). +- WebSocket: Route mit Pfadparam `gameId` und Query `username` \(Client sendet `type: "start-round" | "submit-guesses" | "requestPlayers" | "next-round"`\). +- Server pusht `round-start`, `round-result`, `game-end`. + +## 12\) Häufige Stolpersteine +- 404 oder OAuth Fehler: Redirect‑URI in Spotify Console muss exakt passen \(Schema, Host, Pfad\). +- Kein Start der Runde: Spieler ohne geladene Tracks → sicherstellen, dass Spotify Login erfolgreich war. +- WebSockets brechen ab: Nginx Upgrade/Connection Header wie oben setzen. +- HTTP statt HTTPS: Spotify akzeptiert im Internet i\.d\.R. nur HTTPS \(keine reine IP\). +- Tokens verloren nach Neustart: App speichert nur im Speicher. Spieler müssen sich erneut anmelden. +- manchmal kriegt man einen 24h ratelimit bann von alle oder bestimmte features von spotify nicht funktionieren +``` \ No newline at end of file diff --git a/src/main/java/eric/Roullette/controller/GameController.java b/src/main/java/eric/Roullette/controller/GameController.java index ca15429..428607e 100644 --- a/src/main/java/eric/Roullette/controller/GameController.java +++ b/src/main/java/eric/Roullette/controller/GameController.java @@ -117,7 +117,7 @@ public class GameController { private void startRound(Context ctx) { String gameId = ctx.pathParam("gameId"); ctx.json(Map.of("status", "ok")); - webSocketHandler.broadcastRoundStart(gameId); + webSocketHandler.broadcastRoundStart(gameId,true); } private void guess(Context ctx) { @@ -132,7 +132,8 @@ public class GameController { return; } boolean correct = guess.equals(owner); - if (correct) game.scores().merge(user, 1, Integer::sum); + game.scores().merge(user, correct ? 3 : -1, Integer::sum); // an WS-Logik angeglichen + //if (correct) game.scores().merge(user, 3, Integer::sum); ctx.json(Map.of( "correct", correct, "owner", owner, @@ -152,7 +153,11 @@ public class GameController { try { //String accessToken = authService.getAccessTokenForUser(username); - String accessToken = userAccessTokens.get(username); + String accessToken = userAccessTokens.computeIfAbsent(username, authService::getAccessTokenForUser); + if (accessToken == null || accessToken.isBlank()) { + ctx.status(401).result("Kein Zugriffstoken für " + username); return; + } + //String accessToken = userAccessTokens.get(username); OkHttpClient client = httpClient; String trackId = trackUri.split(":")[2]; Request getTrack = new Request.Builder() diff --git a/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java b/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java index f8bcf1e..1c8f74b 100644 --- a/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java +++ b/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java @@ -1,251 +1,237 @@ -package eric.Roullette.websocket; +// java + package eric.Roullette.websocket; -import com.fasterxml.jackson.databind.JsonNode; -import eric.Roullette.service.GameService; -//import eric.Roullette.service.SpotifyAuthService; -import eric.Roullette.util.JsonUtil; -import io.javalin.websocket.WsConfig; -import io.javalin.websocket.WsContext; + import com.fasterxml.jackson.databind.JsonNode; + import eric.Roullette.service.GameService; + import eric.Roullette.util.JsonUtil; + import io.javalin.websocket.WsConfig; + import io.javalin.websocket.WsContext; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; + import java.util.*; + import java.util.concurrent.ConcurrentHashMap; -/** - * WebSocket-Handler für Spiel-Sessions. Verwaltet Spieler-Sessions, - * broadcastet Spieler-Listen, Runden-Starts und Rundenergebnisse. - */ -public class GameWebSocketHandler { + /** + * WebSocket-Handler für Spiel-Sessions. Verwaltet Spieler-Sessions, + * broadcastet Spieler-Listen, Runden-Starts und Rundenergebnisse. + */ + public class GameWebSocketHandler { - private final GameService service; - //private final SpotifyAuthService authService; + private final GameService service; - // Spiel-ID → (Username → deren Guess) - private final Map> currentGuesses = new ConcurrentHashMap<>(); + // Spiel-ID → (Username → deren Guesses) + private final Map>> currentGuesses = new ConcurrentHashMap<>(); -// private final Map> trackInfoCache = new ConcurrentHashMap<>(); -// private final Map> allTracksCache = new ConcurrentHashMap<>(); - //Map> - //private Map>> playerTracksCache = new ConcurrentHashMap<>(); - private Map>> playerTrackInfoCache = new ConcurrentHashMap<>(); + // Cache: Spiel-ID → (Username → TrackInfos) + private final Map>> playerTrackInfoCache = new ConcurrentHashMap<>(); - public GameWebSocketHandler(GameService gameService) { - this.service = gameService; - //this.authService = authService; - } - - /** - * Registriert Connect/Close/Message-Handler für eine WebSocket-Route. - */ - public void register(WsConfig ws) { - - // Neue Connection - ws.onConnect(ctx -> { - String gameId = ctx.pathParam("gameId"); - String username = ctx.queryParam("username"); - // Spiel- und Session-Registrierung - service.addPlayer(gameId, username); - // Alle Clients über neue Spielerliste informieren - service.registerSession(gameId, ctx); - service.broadcastPlayers(gameId); - }); - - // Connection geschlossen - ws.onClose(ctx -> { - String gameId = ctx.pathParam("gameId"); - String username = ctx.queryParam("username"); - service.removeSession(gameId, ctx); - - // Spieler aus der Spielerliste entfernen - var game = service.getOrCreateGame(gameId); - if (username != null && game.players().contains(username)) { - game.players().remove(username); - game.scores().remove(username); - // Optional: auch die Tracks entfernen - game.playerTracks().remove(username); + public GameWebSocketHandler(GameService gameService) { + this.service = gameService; } - service.broadcastPlayers(gameId); - }); + /** + * Registriert Connect/Close/Message-Handler für eine WebSocket-Route. + */ + public void register(WsConfig ws) { - // Eingehende Nachrichten (Guesses & Player-Requests) - ws.onMessage(ctx -> { - String gameId = ctx.pathParam("gameId"); - JsonNode node = JsonUtil.fromJson(ctx.message(), JsonNode.class); - String type = node.get("type").asText(); + // Neue Connection + ws.onConnect(ctx -> { + String gameId = ctx.pathParam("gameId"); + String username = ctx.queryParam("username"); + service.addPlayer(gameId, username); + service.registerSession(gameId, ctx); + service.broadcastPlayers(gameId); + }); - switch (type) { - case "guess" -> { - String user = node.get("username").asText(); - String guess = node.get("guess").asText(); - // Guess speichern - currentGuesses - .computeIfAbsent(gameId, id -> new ConcurrentHashMap<>()) - .put(user, guess); - // Wenn alle getippt haben, Ergebnis broadcasten - int numPlayers = service.getOrCreateGame(gameId).players().size(); - if (currentGuesses.get(gameId).size() == numPlayers) { - broadcastRoundResult(gameId); + // Connection geschlossen + ws.onClose(ctx -> { + String gameId = ctx.pathParam("gameId"); + String username = ctx.queryParam("username"); + service.removeSession(gameId, ctx); + + var game = service.getOrCreateGame(gameId); + if (username != null && game.players().contains(username)) { + game.players().remove(username); + game.scores().remove(username); + game.playerTracks().remove(username); } - } - case "requestPlayers" -> service.broadcastPlayers(gameId); + service.broadcastPlayers(gameId); + }); - case "next-round" -> nextround(gameId); - case "start-round" -> { - var currentGame = service.getOrCreateGame(gameId); - if (currentGame.players().isEmpty()) return; - // Tracks pro Spieler sammeln - Map> allPlayerTracks = currentGame.playerTracks(); - // alle tracks sammeln - List allTracks = allPlayerTracks.values().stream() - .flatMap(List::stream) - .toList(); - System.out.println("AlltracksCache für Spiel " + gameId + " hat " + allTracks.size() + " Songs (rundenstart)"); - //Trackinfos für alle Spieler sammeln + // Eingehende Nachrichten (Guesses & Player-Requests) + ws.onMessage(ctx -> { + String gameId = ctx.pathParam("gameId"); + JsonNode node = JsonUtil.fromJson(ctx.message(), JsonNode.class); + String type = node.get("type").asText(); - if(playerTrackInfoCache.containsKey(gameId)){ - // Wenn der Cache schon existiert, dann nur die Trackinfos nutzen - System.out.println("TrackInfosCache für Spiel " + gameId + " existiert bereits (rundenstart)"); - // prüfen ob ein neuer spieler dazugekommen ist - if( allPlayerTracks.size() > playerTrackInfoCache.get(gameId).size()) { - System.out.println("Neuer Spieler hinzugefügt, Trackinfos werden aktualisiert (rundenstart)"); - Map> allTrackInfos = service.getTrackInfos(allPlayerTracks); - // Cache für Trackinfos pro Spiel-ID aktualisieren - playerTrackInfoCache.put(gameId, allTrackInfos); - } else { - System.out.println("Keine neuen Spieler, Trackinfos bleiben unverändert (rundenstart)"); + switch (type) { + case "guess" -> { // Legacy: Single-Guess + String user = node.get("username").asText(); + String single = node.get("guess").asText(); + Map> byUser = + currentGuesses.computeIfAbsent(gameId, id -> new ConcurrentHashMap<>()); + byUser.put(user, List.of(single)); + + int numPlayers = service.getOrCreateGame(gameId).players().size(); + if (byUser.size() == numPlayers) { + broadcastRoundResult(gameId); + } } - } else { - // Wenn der Cache nicht existiert, dann Trackinfos sammeln - System.out.println("TrackInfosCache für Spiel " + gameId + " wird erstellt (rundenstart)"); - Map> allTrackInfos = service.getTrackInfos(allPlayerTracks); -// Cache für Trackinfos pro Spiel-ID - playerTrackInfoCache.put(gameId, allTrackInfos); + case "submit-guesses" -> { // Multi-Select + String user = node.get("username").asText(); + List picks = new ArrayList<>(); + JsonNode arr = node.get("guesses"); + if (arr != null && arr.isArray()) { + arr.forEach(j -> picks.add(j.asText())); + } + + Map> byUser = + currentGuesses.computeIfAbsent(gameId, id -> new ConcurrentHashMap<>()); + if (user != null && !picks.isEmpty()) { + byUser.put(user, new ArrayList<>(picks)); + } + + int numPlayers = service.getOrCreateGame(gameId).players().size(); + if (byUser.size() == numPlayers) { + broadcastRoundResult(gameId); + } + } + + case "requestPlayers" -> service.broadcastPlayers(gameId); + + case "next-round" -> nextround(gameId); + + case "start-round" -> { + // Guesses für dieses Spiel zurücksetzen + currentGuesses.put(gameId, new ConcurrentHashMap<>()); + + var currentGame = service.getOrCreateGame(gameId); + if (currentGame.players().isEmpty()) return; + + Map> allPlayerTracks = currentGame.playerTracks(); + List allTracks = allPlayerTracks.values().stream() + .flatMap(List::stream) + .toList(); + + if (playerTrackInfoCache.containsKey(gameId)) { + if (allPlayerTracks.size() > playerTrackInfoCache.get(gameId).size()) { + Map> allTrackInfos = service.getTrackInfos(allPlayerTracks); + playerTrackInfoCache.put(gameId, allTrackInfos); + } + } else { + Map> allTrackInfos = service.getTrackInfos(allPlayerTracks); + playerTrackInfoCache.put(gameId, allTrackInfos); + } + + if (!allTracks.isEmpty()) { + service.startRound(gameId, allTracks); + } + broadcastRoundStart(gameId, true); + } } + }); + } - System.out.println("TrackInfosCache für Spiel " + gameId + " hat " + playerTrackInfoCache.get(gameId).size() + " Spieler (rundenstart)"); + public void nextround(String gameId) { + currentGuesses.put(gameId, new ConcurrentHashMap<>()); // nur dieses Spiel leeren + var game = service.getOrCreateGame(gameId); + if (game.players().isEmpty()) return; + List allTracks = new ArrayList<>(); + for (String player : game.players()) { + allTracks.addAll(game.playerTracks().getOrDefault(player, List.of())); + } + if (allTracks.isEmpty()) { + broadcastToAll(gameId, JsonUtil.toJson(Map.of("type","error","message","Keine Tracks geladen"))); + return; + } - if (!allTracks.isEmpty()) { - service.startRound(gameId, allTracks); - } -// - broadcastRoundStart(gameId); + service.startRound(gameId, allTracks); + broadcastRoundStart(gameId, false); + } + + /** Broadcastet den Runden-Start (Song + Optionen) an alle Clients. */ + public void broadcastRoundStart(String gameId, boolean initial) { + var game = service.getOrCreateGame(gameId); + List opts = game.players(); + String songUri = game.currentSong(); + List allTracks = game.allTracks(); + Map> trackInfos = playerTrackInfoCache.get(gameId); + + String msg = JsonUtil.toJson(Map.of( + "type", "round-start", + "ownerOptions", opts, + "songUri", songUri, + "allTracks", allTracks, + "trackInfos", trackInfos, + "initial", initial + )); + broadcastToAll(gameId, msg); + } + + /** Broadcastet das Rundenergebnis (Scores, wer richtig, wer getippt hat). */ + private void broadcastRoundResult(String gameId) { + var game = service.getOrCreateGame(gameId); + String owner = game.currentOwner(); + + Map> byUser = + currentGuesses.getOrDefault(gameId, Collections.emptyMap()); + + // Scoring: +3 falls Auswahl Owner enthält, -1 pro falschem Tipp + // Bonus: +1, wenn kein falscher Tipp in der Runde (fehlerfrei) + + for (var e : byUser.entrySet()) { + String user = e.getKey(); + List guesses = e.getValue(); + if (guesses == null) continue; + boolean correct = owner != null && guesses.contains(owner); + int wrong = guesses.size() - (correct ? 1 : 0); + //int delta = (correct ? 3 : 0) - wrong; + int bonus = (wrong == 0) ? 1 : 0; // fehlerfrei-Bonus + int delta = (correct ? 3 : 0) - wrong + bonus; + if (delta != 0) game.scores().merge(user, delta, Integer::sum); + } + + var scores = game.scores(); + + String msg = JsonUtil.toJson(Map.of( + "type", "round-result", + "scores", scores, + "guesses", byUser, + "owner", owner + )); + broadcastToAll(gameId, msg); + + // Gewinner prüfen + int winScore = 6; + int max = scores.values().stream().mapToInt(Integer::intValue).max().orElse(0); + List topScorers = scores.entrySet().stream() + .filter(e -> e.getValue() == max && max >= winScore) + .map(Map.Entry::getKey) + .toList(); + + if (topScorers.size() == 1) { + String winner = topScorers.get(0); + String winMsg = JsonUtil.toJson(Map.of( + "type", "game-end", + "winner", winner, + "scores", scores + )); + broadcastToAll(gameId, winMsg); + game.scores().replaceAll((user, pts) -> 0); // Reset Scores } } - }); - } - public void nextround(String gameId) { - var game = service.getOrCreateGame(gameId); - if (game.players().isEmpty()) return; - // Songs von allen Spielern sammeln - List allTracks = new ArrayList<>(); - for (String player : game.players()) { - allTracks.addAll(game.playerTracks().getOrDefault(player, List.of())); - } - if (allTracks.isEmpty()) { - // TODO: Fehler an Client senden, dass keine Songs da sind - return; - } - // TODO funktionalität bei neu joinenden Spielern überprüfen - // Runde im Service starten, um Song und Owner zu setzen - service.startRound(gameId, allTracks); - // Jetzt Broadcast mit den aktuellen Daten - broadcastRoundStart(gameId); - } - // ----- Broadcast-Methoden ----- - - /** Broadcastet den Runden-Start (Song + Optionen) an alle Clients. */ - public void broadcastRoundStart(String gameId) { - var game = service.getOrCreateGame(gameId); - List opts = game.players(); - String songUri = game.currentSong(); - List allTracks = game.allTracks(); - Map> trackInfos = playerTrackInfoCache.get(gameId); - // Cache pro Spiel-ID nutzen - - String msg = JsonUtil.toJson(Map.of( - "type", "round-start", - "ownerOptions", opts, - "songUri", songUri, - "allTracks", allTracks, - "trackInfos", trackInfos - )); - broadcastToAll(gameId, msg); - } - - /** Broadcastet das Rundenergebnis (Scores, wer richtig, wer getippt hat). */ -// Punkte für alle Guess-Teilnehmer anpassen -private void broadcastRoundResult(String gameId) { - var game = service.getOrCreateGame(gameId); - Map scores = game.scores(); - Map guesses = currentGuesses.remove(gameId); - String owner = game.currentOwner(); - -// System.out.println("Owner: " + owner); -// System.out.println("Guesses: " + guesses); -// System.out.println("Scores vor Auswertung: " + scores); - - // Für jeden Tippenden Score anpassen - for (Map.Entry entry : guesses.entrySet()) { - String guesser = entry.getKey(); - boolean correct = owner.equals(entry.getValue()); - scores.merge(guesser, correct ? 3 : -1, Integer::sum); - } -// System.out.println("Owner: " + owner); -// System.out.println("Guesses: " + guesses); -// System.out.println("Scores nach Auswertung: " + scores); - - String msg = JsonUtil.toJson(Map.of( - "type", "round-result", - "scores", scores, - "guesses", guesses, - "owner", owner - )); - broadcastToAll(gameId, msg); - - // Prüfe auf Gewinner - // Nur beenden, wenn EIN Spieler allein die höchste Punktzahl >= score hat - int score = 6; - int max = scores.values().stream().max(Integer::compareTo).orElse(0); - List topScorers = scores.entrySet().stream() - .filter(e -> e.getValue() == max && max >= score) - .map(Map.Entry::getKey) - .toList(); - - if (topScorers.size() == 1) { - String winner = topScorers.get(0); - String winMsg = JsonUtil.toJson(Map.of( - "type", "game-end", - "winner", winner, - "scores", scores - )); - broadcastToAll(gameId, winMsg); - game.scores().replaceAll((user , pts) -> 0); // Reset Scores + /** Hilfsmethode: Sendet eine Nachricht an alle WebSocket-Sessions eines Spiels. */ + private void broadcastToAll(String gameId, String msg) { + Set sessions = service.getSessions(gameId); + if (sessions == null) return; + sessions.stream() + .filter(s -> s.session.isOpen()) + .forEach(ctx -> { + try { + ctx.send(msg); + } catch (Exception ignore) {} + }); } -// else{ -// // nächste Runde starten -// // ... -//// new Thread(() -> { -//// try { Thread.sleep(2000); } catch (InterruptedException ignored) {} -//// nextround(gameId); -//// }).start(); -// } -} - - /** Hilfsmethode: Sendet eine Nachricht an alle WebSocket-Sessions eines Spiels. */ - private void broadcastToAll(String gameId, String msg) { - // Holt alle WsContext, die der Service beim Connect registriert hat - Set sessions = service.getSessions(gameId); - if (sessions == null) return; - sessions.stream() - .filter(s -> s.session.isOpen()) - .forEach(ctx -> { - try { - ctx.send(msg); - } catch (Exception ignore) {} - }); - } - -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/main/resources/public/create-game.html b/src/main/resources/public/create-game.html index f99bfe5..9f3761c 100644 --- a/src/main/resources/public/create-game.html +++ b/src/main/resources/public/create-game.html @@ -10,7 +10,7 @@
+