236 lines
11 KiB
Java
236 lines
11 KiB
Java
// 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<String, Map<String, List<String>>> currentGuesses = new ConcurrentHashMap<>();
|
|
|
|
// Cache: Spiel-ID → (Username → TrackInfos)
|
|
private final Map<String, Map<String, List<String>>> 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<String, List<String>> 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<String> picks = new ArrayList<>();
|
|
JsonNode arr = node.get("guesses");
|
|
if (arr != null && arr.isArray()) {
|
|
arr.forEach(j -> picks.add(j.asText()));
|
|
}
|
|
|
|
Map<String, List<String>> 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<String, List<String>> allPlayerTracks = currentGame.playerTracks();
|
|
List<String> allTracks = allPlayerTracks.values().stream()
|
|
.flatMap(List::stream)
|
|
.toList();
|
|
|
|
if (playerTrackInfoCache.containsKey(gameId)) {
|
|
if (allPlayerTracks.size() > playerTrackInfoCache.get(gameId).size()) {
|
|
Map<String, List<String>> allTrackInfos = service.getTrackInfos(allPlayerTracks);
|
|
playerTrackInfoCache.put(gameId, allTrackInfos);
|
|
}
|
|
} else {
|
|
Map<String, List<String>> 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<String> 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<String> opts = game.players();
|
|
String songUri = game.currentSong();
|
|
List<String> allTracks = game.allTracks();
|
|
Map<String, List<String>> 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<String, List<String>> 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<String> 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<String> 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<WsContext> sessions = service.getSessions(gameId);
|
|
if (sessions == null) return;
|
|
sessions.stream()
|
|
.filter(s -> s.session.isOpen())
|
|
.forEach(ctx -> {
|
|
try {
|
|
ctx.send(msg);
|
|
} catch (Exception ignore) {}
|
|
});
|
|
}
|
|
} |