// java package eric.Roullette.websocket; 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; /** * WebSocket-Handler für Spiel-Sessions. Verwaltet Spieler-Sessions, * broadcastet Spieler-Listen, Runden-Starts und Rundenergebnisse. */ public class GameWebSocketHandler { private final GameService service; // Spiel-ID → (Username → deren Guesses) private final Map>> currentGuesses = new ConcurrentHashMap<>(); // Cache: Spiel-ID → (Username → TrackInfos) private final Map>> playerTrackInfoCache = new ConcurrentHashMap<>(); public GameWebSocketHandler(GameService gameService) { this.service = gameService; } /** * 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"); service.addPlayer(gameId, username); 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); var game = service.getOrCreateGame(gameId); if (username != null && game.players().contains(username)) { game.players().remove(username); game.scores().remove(username); game.playerTracks().remove(username); } service.broadcastPlayers(gameId); }); // 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(); 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); } } 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); } } }); } 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; } service.startRound(gameId, allTracks); broadcastRoundStart(gameId); } /** 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); 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). */ 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 } } /** 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) {} }); } }