mehrere auswählbar

GUI
eric 2025-08-14 18:33:35 +02:00
parent b55314fd1a
commit 739db427ab
2 changed files with 321 additions and 251 deletions

View File

@ -1,36 +1,31 @@
package eric.Roullette.websocket; // java
package eric.Roullette.websocket;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import eric.Roullette.service.GameService; import eric.Roullette.service.GameService;
//import eric.Roullette.service.SpotifyAuthService; import eric.Roullette.util.JsonUtil;
import eric.Roullette.util.JsonUtil; import io.javalin.websocket.WsConfig;
import io.javalin.websocket.WsConfig; import io.javalin.websocket.WsContext;
import io.javalin.websocket.WsContext;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* WebSocket-Handler für Spiel-Sessions. Verwaltet Spieler-Sessions, * WebSocket-Handler für Spiel-Sessions. Verwaltet Spieler-Sessions,
* broadcastet Spieler-Listen, Runden-Starts und Rundenergebnisse. * broadcastet Spieler-Listen, Runden-Starts und Rundenergebnisse.
*/ */
public class GameWebSocketHandler { public class GameWebSocketHandler {
private final GameService service; private final GameService service;
//private final SpotifyAuthService authService;
// Spiel-ID → (Username → deren Guess) // Spiel-ID → (Username → deren Guesses)
private final Map<String, Map<String, String>> currentGuesses = new ConcurrentHashMap<>(); private final Map<String, Map<String, List<String>>> currentGuesses = new ConcurrentHashMap<>();
// private final Map<String, List<String>> trackInfoCache = new ConcurrentHashMap<>(); // Cache: Spiel-ID → (Username → TrackInfos)
// private final Map<String, List<String>> allTracksCache = new ConcurrentHashMap<>(); private final Map<String, Map<String, List<String>>> playerTrackInfoCache = new ConcurrentHashMap<>();
//Map<gameId, Map<player, List<String>>
//private Map<String, Map<String, List<String>>> playerTracksCache = new ConcurrentHashMap<>();
private Map<String, Map<String, List<String>>> playerTrackInfoCache = new ConcurrentHashMap<>();
public GameWebSocketHandler(GameService gameService) { public GameWebSocketHandler(GameService gameService) {
this.service = gameService; this.service = gameService;
//this.authService = authService;
} }
/** /**
@ -42,9 +37,7 @@ public class GameWebSocketHandler {
ws.onConnect(ctx -> { ws.onConnect(ctx -> {
String gameId = ctx.pathParam("gameId"); String gameId = ctx.pathParam("gameId");
String username = ctx.queryParam("username"); String username = ctx.queryParam("username");
// Spiel- und Session-Registrierung
service.addPlayer(gameId, username); service.addPlayer(gameId, username);
// Alle Clients über neue Spielerliste informieren
service.registerSession(gameId, ctx); service.registerSession(gameId, ctx);
service.broadcastPlayers(gameId); service.broadcastPlayers(gameId);
}); });
@ -55,16 +48,13 @@ public class GameWebSocketHandler {
String username = ctx.queryParam("username"); String username = ctx.queryParam("username");
service.removeSession(gameId, ctx); service.removeSession(gameId, ctx);
// Spieler aus der Spielerliste entfernen
var game = service.getOrCreateGame(gameId); var game = service.getOrCreateGame(gameId);
if (username != null && game.players().contains(username)) { if (username != null && game.players().contains(username)) {
game.players().remove(username); game.players().remove(username);
game.scores().remove(username); game.scores().remove(username);
// Optional: auch die Tracks entfernen
game.playerTracks().remove(username); game.playerTracks().remove(username);
} }
service.broadcastPlayers(gameId); service.broadcastPlayers(gameId);
}); });
// Eingehende Nachrichten (Guesses & Player-Requests) // Eingehende Nachrichten (Guesses & Player-Requests)
@ -74,62 +64,68 @@ public class GameWebSocketHandler {
String type = node.get("type").asText(); String type = node.get("type").asText();
switch (type) { switch (type) {
case "guess" -> { case "guess" -> { // Legacy: Single-Guess
String user = node.get("username").asText(); String user = node.get("username").asText();
String guess = node.get("guess").asText(); String single = node.get("guess").asText();
// Guess speichern Map<String, List<String>> byUser =
currentGuesses currentGuesses.computeIfAbsent(gameId, id -> new ConcurrentHashMap<>());
.computeIfAbsent(gameId, id -> new ConcurrentHashMap<>()) byUser.put(user, List.of(single));
.put(user, guess);
// Wenn alle getippt haben, Ergebnis broadcasten
int numPlayers = service.getOrCreateGame(gameId).players().size(); int numPlayers = service.getOrCreateGame(gameId).players().size();
if (currentGuesses.get(gameId).size() == numPlayers) { if (byUser.size() == numPlayers) {
broadcastRoundResult(gameId); 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 "requestPlayers" -> service.broadcastPlayers(gameId);
case "next-round" -> nextround(gameId); case "next-round" -> nextround(gameId);
case "start-round" -> { case "start-round" -> {
// Guesses für dieses Spiel zurücksetzen
currentGuesses.put(gameId, new ConcurrentHashMap<>());
var currentGame = service.getOrCreateGame(gameId); var currentGame = service.getOrCreateGame(gameId);
if (currentGame.players().isEmpty()) return; if (currentGame.players().isEmpty()) return;
// Tracks pro Spieler sammeln
Map<String, List<String>> allPlayerTracks = currentGame.playerTracks(); Map<String, List<String>> allPlayerTracks = currentGame.playerTracks();
// alle tracks sammeln
List<String> allTracks = allPlayerTracks.values().stream() List<String> allTracks = allPlayerTracks.values().stream()
.flatMap(List::stream) .flatMap(List::stream)
.toList(); .toList();
System.out.println("AlltracksCache für Spiel " + gameId + " hat " + allTracks.size() + " Songs (rundenstart)");
//Trackinfos für alle Spieler sammeln
if(playerTrackInfoCache.containsKey(gameId)){ if (playerTrackInfoCache.containsKey(gameId)) {
// Wenn der Cache schon existiert, dann nur die Trackinfos nutzen if (allPlayerTracks.size() > playerTrackInfoCache.get(gameId).size()) {
System.out.println("TrackInfosCache für Spiel " + gameId + " existiert bereits (rundenstart)"); Map<String, List<String>> allTrackInfos = service.getTrackInfos(allPlayerTracks);
// prüfen ob ein neuer spieler dazugekommen ist playerTrackInfoCache.put(gameId, allTrackInfos);
if( allPlayerTracks.size() > playerTrackInfoCache.get(gameId).size()) { }
System.out.println("Neuer Spieler hinzugefügt, Trackinfos werden aktualisiert (rundenstart)"); } else {
Map<String, List<String>> allTrackInfos = service.getTrackInfos(allPlayerTracks); Map<String, List<String>> 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)");
}
} else {
// Wenn der Cache nicht existiert, dann Trackinfos sammeln
System.out.println("TrackInfosCache für Spiel " + gameId + " wird erstellt (rundenstart)");
Map<String, List<String>> allTrackInfos = service.getTrackInfos(allPlayerTracks);
// Cache für Trackinfos pro Spiel-ID
playerTrackInfoCache.put(gameId, allTrackInfos); playerTrackInfoCache.put(gameId, allTrackInfos);
} }
System.out.println("TrackInfosCache für Spiel " + gameId + " hat " + playerTrackInfoCache.get(gameId).size() + " Spieler (rundenstart)");
if (!allTracks.isEmpty()) { if (!allTracks.isEmpty()) {
service.startRound(gameId, allTracks); service.startRound(gameId, allTracks);
} }
//
broadcastRoundStart(gameId); broadcastRoundStart(gameId);
} }
} }
@ -137,9 +133,10 @@ public class GameWebSocketHandler {
} }
public void nextround(String gameId) { public void nextround(String gameId) {
currentGuesses.put(gameId, new ConcurrentHashMap<>()); // nur dieses Spiel leeren
var game = service.getOrCreateGame(gameId); var game = service.getOrCreateGame(gameId);
if (game.players().isEmpty()) return; if (game.players().isEmpty()) return;
// Songs von allen Spielern sammeln
List<String> allTracks = new ArrayList<>(); List<String> allTracks = new ArrayList<>();
for (String player : game.players()) { for (String player : game.players()) {
allTracks.addAll(game.playerTracks().getOrDefault(player, List.of())); allTracks.addAll(game.playerTracks().getOrDefault(player, List.of()));
@ -149,12 +146,9 @@ public class GameWebSocketHandler {
return; return;
} }
// Runde im Service starten, um Song und Owner zu setzen
service.startRound(gameId, allTracks); service.startRound(gameId, allTracks);
// Jetzt Broadcast mit den aktuellen Daten
broadcastRoundStart(gameId); broadcastRoundStart(gameId);
} }
// ----- Broadcast-Methoden -----
/** Broadcastet den Runden-Start (Song + Optionen) an alle Clients. */ /** Broadcastet den Runden-Start (Song + Optionen) an alle Clients. */
public void broadcastRoundStart(String gameId) { public void broadcastRoundStart(String gameId) {
@ -163,7 +157,6 @@ public class GameWebSocketHandler {
String songUri = game.currentSong(); String songUri = game.currentSong();
List<String> allTracks = game.allTracks(); List<String> allTracks = game.allTracks();
Map<String, List<String>> trackInfos = playerTrackInfoCache.get(gameId); Map<String, List<String>> trackInfos = playerTrackInfoCache.get(gameId);
// Cache pro Spiel-ID nutzen
String msg = JsonUtil.toJson(Map.of( String msg = JsonUtil.toJson(Map.of(
"type", "round-start", "type", "round-start",
@ -176,41 +169,39 @@ public class GameWebSocketHandler {
} }
/** Broadcastet das Rundenergebnis (Scores, wer richtig, wer getippt hat). */ /** Broadcastet das Rundenergebnis (Scores, wer richtig, wer getippt hat). */
// Punkte für alle Guess-Teilnehmer anpassen private void broadcastRoundResult(String gameId) {
private void broadcastRoundResult(String gameId) {
var game = service.getOrCreateGame(gameId); var game = service.getOrCreateGame(gameId);
Map<String,Integer> scores = game.scores();
Map<String,String> guesses = currentGuesses.remove(gameId);
String owner = game.currentOwner(); String owner = game.currentOwner();
// System.out.println("Owner: " + owner); Map<String, List<String>> byUser =
// System.out.println("Guesses: " + guesses); currentGuesses.getOrDefault(gameId, Collections.emptyMap());
// System.out.println("Scores vor Auswertung: " + scores);
// Für jeden Tippenden Score anpassen // Scoring: +3 falls Auswahl Owner enthält, -1 pro falschem Tipp
for (Map.Entry<String, String> entry : guesses.entrySet()) { for (var e : byUser.entrySet()) {
String guesser = entry.getKey(); String user = e.getKey();
boolean correct = owner.equals(entry.getValue()); List<String> guesses = e.getValue();
scores.merge(guesser, correct ? 3 : -1, Integer::sum); if (guesses == null) continue;
boolean correct = owner != null && guesses.contains(owner);
int wrong = guesses.size() - (correct ? 1 : 0);
int delta = (correct ? 3 : 0) - wrong;
if (delta != 0) game.scores().merge(user, delta, Integer::sum);
} }
// System.out.println("Owner: " + owner);
// System.out.println("Guesses: " + guesses); var scores = game.scores();
// System.out.println("Scores nach Auswertung: " + scores);
String msg = JsonUtil.toJson(Map.of( String msg = JsonUtil.toJson(Map.of(
"type", "round-result", "type", "round-result",
"scores", scores, "scores", scores,
"guesses", guesses, "guesses", byUser,
"owner", owner "owner", owner
)); ));
broadcastToAll(gameId, msg); broadcastToAll(gameId, msg);
// Prüfe auf Gewinner // Gewinner prüfen
// Nur beenden, wenn EIN Spieler allein die höchste Punktzahl >= score hat int winScore = 6;
int score = 6; int max = scores.values().stream().mapToInt(Integer::intValue).max().orElse(0);
int max = scores.values().stream().max(Integer::compareTo).orElse(0);
List<String> topScorers = scores.entrySet().stream() List<String> topScorers = scores.entrySet().stream()
.filter(e -> e.getValue() == max && max >= score) .filter(e -> e.getValue() == max && max >= winScore)
.map(Map.Entry::getKey) .map(Map.Entry::getKey)
.toList(); .toList();
@ -222,21 +213,12 @@ private void broadcastRoundResult(String gameId) {
"scores", scores "scores", scores
)); ));
broadcastToAll(gameId, winMsg); broadcastToAll(gameId, winMsg);
game.scores().replaceAll((user , pts) -> 0); // Reset Scores game.scores().replaceAll((user, pts) -> 0); // Reset Scores
}
} }
// 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. */ /** Hilfsmethode: Sendet eine Nachricht an alle WebSocket-Sessions eines Spiels. */
private void broadcastToAll(String gameId, String msg) { private void broadcastToAll(String gameId, String msg) {
// Holt alle WsContext, die der Service beim Connect registriert hat
Set<WsContext> sessions = service.getSessions(gameId); Set<WsContext> sessions = service.getSessions(gameId);
if (sessions == null) return; if (sessions == null) return;
sessions.stream() sessions.stream()
@ -247,5 +229,4 @@ private void broadcastRoundResult(String gameId) {
} catch (Exception ignore) {} } catch (Exception ignore) {}
}); });
} }
}
}

View File

@ -133,6 +133,25 @@ function wedgePath(cx, cy, r, a0, a1){
return `M ${cx} ${cy} L ${p0.x} ${p0.y} A ${r} ${r} 0 ${largeArc} 1 ${p1.x} ${p1.y} Z`; return `M ${cx} ${cy} L ${p0.x} ${p0.y} A ${r} ${r} 0 ${largeArc} 1 ${p1.x} ${p1.y} Z`;
} }
// Auswahl-Set für Multi-Guess (pro Runde zurückgesetzt)
let selectedGuesses = new Set();
let lastScores = null;
function ensureSubmitBtn(optionsDiv) {
let btn = document.getElementById("submitGuesses");
if (!btn) {
btn = document.createElement("button");
btn.id = "submitGuesses";
btn.className = "btn btn-primary";
btn.style.marginTop = "8px";
btn.textContent = "Tipps abgeben";
// Button direkt unter den Optionen platzieren
optionsDiv.parentElement.appendChild(btn);
}
return btn;
}
// 6) Neue Runde anzeigen // 6) Neue Runde anzeigen
async function handleRoundStart({ ownerOptions, songUri, trackInfos }) { async function handleRoundStart({ ownerOptions, songUri, trackInfos }) {
// UI zurücksetzen // UI zurücksetzen
@ -141,6 +160,7 @@ async function handleRoundStart({ ownerOptions, songUri, trackInfos }) {
songEmbed.innerHTML = ""; songEmbed.innerHTML = "";
//scoreboard zurücksetzen //scoreboard zurücksetzen
scoreboard.innerHTML = ""; scoreboard.innerHTML = "";
selectedGuesses = new Set();
ownerOptions.forEach(user => { ownerOptions.forEach(user => {
const li = document.createElement("li"); const li = document.createElement("li");
li.textContent = `${user}: 0 Punkte`; li.textContent = `${user}: 0 Punkte`;
@ -176,14 +196,23 @@ async function handleRoundStart({ ownerOptions, songUri, trackInfos }) {
path.setAttribute("d", wedgePath(CX, CY, R, a0, a1)); path.setAttribute("d", wedgePath(CX, CY, R, a0, a1));
path.setAttribute("class", "wedge"); path.setAttribute("class", "wedge");
path.setAttribute("data-user", user); path.setAttribute("data-user", user);
// path.addEventListener("click", () => {
// socket.send(JSON.stringify({
// type: "guess",
// username: username,
// guess: user
// }));
// svg.querySelectorAll(".wedge").forEach(w => w.classList.add("disabled"));
// path.classList.add("selected");
// });
// Toggle-Selection statt sofort zu senden
path.addEventListener("click", () => { path.addEventListener("click", () => {
socket.send(JSON.stringify({ const u = path.getAttribute("data-user");
type: "guess", if (path.classList.contains("disabled")) return;
username: username, if (path.classList.toggle("selected")) selectedGuesses.add(u);
guess: user else selectedGuesses.delete(u);
})); submitBtn.disabled = selectedGuesses.size === 0;
svg.querySelectorAll(".wedge").forEach(w => w.classList.add("disabled"));
path.classList.add("selected");
}); });
// Label mittig im Segment // Label mittig im Segment
@ -212,6 +241,32 @@ async function handleRoundStart({ ownerOptions, songUri, trackInfos }) {
svg.appendChild(text); svg.appendChild(text);
}); });
// Submit-Button dynamisch hinzufügen
let submitBtn = document.getElementById("submitGuesses");
if (!submitBtn) {
submitBtn = document.createElement("button");
submitBtn.id = "submitGuesses";
submitBtn.className = "btn btn-primary";
submitBtn.style.marginTop = "8px";
submitBtn.textContent = "submit";
optionsDiv.parentElement.appendChild(submitBtn);
}
submitBtn.hidden = false;
// warum disabled?
submitBtn.disabled = true;
submitBtn.onclick = () => {
if (!selectedGuesses.size) return;
const guesses = Array.from(selectedGuesses);
socket.send(JSON.stringify({
type: "submit-guesses",
username,
guesses
}));
submitBtn.disabled = true;
// Sperre Eingaben nach Abgabe
optionsDiv.querySelectorAll(".wedge").forEach(w => w.classList.add("disabled"));
};
// Start-Button ausblenden + Rundensektion einblenden // Start-Button ausblenden + Rundensektion einblenden
startBtn.hidden = true; startBtn.hidden = true;
startBtn.disabled = true; startBtn.disabled = true;
@ -260,13 +315,20 @@ function renderScoreboard(scores) {
} }
} }
let lastScores = null; //let lastScores = null;
function handleRoundResult({ scores, guesses, owner }) { function handleRoundResult({ scores, guesses, owner }) {
renderScoreboard(scores); renderScoreboard(scores);
lastScores = scores; lastScores = scores;
(() => {
// Button verstecken/disable und Eingaben sperren
const sb = document.getElementById('submitGuesses');
if (sb) { sb.disabled = true; sb.hidden = true; }
document.querySelectorAll('#options .wedge').forEach(w => w.classList.add('disabled'));
// lokale Auswahl leeren (optional)
try { selectedGuesses.clear?.(); } catch(_) {}
})();
try { try {
const wedges = document.querySelectorAll("#options .wedge"); const wedges = document.querySelectorAll("#options .wedge");
@ -277,26 +339,51 @@ function handleRoundResult({ scores, guesses, owner }) {
}); });
// Nur die EIGENE Auswahl einfärben: rot wenn falsch, sonst grün // Nur die EIGENE Auswahl einfärben: rot wenn falsch, sonst grün
const myGuess = guesses?.[username]; // const myGuess = guesses?.[username];
if (myGuess) { // if (myGuess) {
const myWedge = Array.from(wedges).find(w => w.getAttribute("data-user") === myGuess); // const myWedge = Array.from(wedges).find(w => w.getAttribute("data-user") === myGuess);
if (myWedge) { // if (myWedge) {
if (myGuess === owner) { // if (myGuess === owner) {
myWedge.classList.add("correct"); // myWedge.classList.add("correct");
} else { // } else {
myWedge.classList.add("wrong"); // nur dieser wird rot // myWedge.classList.add("wrong"); // nur dieser wird rot
// }
// }
// }
// } catch (e) {}
const my = guesses?.[username];
const myArr = Array.isArray(my) ? my : (typeof my === "string" ? [my] : []);
if (myArr.length) {
myArr.forEach(sel => {
const w = Array.from(wedges).find(x => x.getAttribute("data-user") === sel);
if (!w) return;
if (sel === owner) w.classList.add("correct");
else w.classList.add("wrong");
});
} }
}
}
} catch (e) {}
} catch (_) {}
// resultP.innerHTML = "";
// Object.entries(guesses || {}).forEach(([user, guess]) => {
// const correct = guess === owner;
// const icon = correct ? "✅" : "❌";
// const delta = correct ? "+3" : "-1";
// const p = document.createElement("p");
// p.textContent = `${icon} ${user} hat auf ${guess} getippt${correct ? " (richtig!)" : " (falsch)"} [${delta}]`;
// resultP.appendChild(p);
// });
resultP.innerHTML = ""; resultP.innerHTML = "";
Object.entries(guesses || {}).forEach(([user, guess]) => { Object.entries(guesses || {}).forEach(([user, g]) => {
const correct = guess === owner; const list = Array.isArray(g) ? g : (typeof g === "string" ? [g] : []);
const correct = list.includes(owner);
const wrongCount = list.length - (correct ? 1 : 0);
const delta = (correct ? 3 : 0) - wrongCount;
const icon = correct ? "✅" : "❌"; const icon = correct ? "✅" : "❌";
const delta = correct ? "+3" : "-1"; const picks = list.length ? list.join(", ") : "—";
const p = document.createElement("p"); const p = document.createElement("p");
p.textContent = `${icon} ${user} hat auf ${guess} getippt${correct ? " (richtig!)" : " (falsch)"} [${delta}]`; p.textContent = `${icon} ${user} hat auf ${picks} getippt${correct ? " (richtig!)" : ""} [${delta >= 0 ? "+" : ""}${delta}]`;
resultP.appendChild(p); resultP.appendChild(p);
}); });
@ -311,6 +398,8 @@ function handleRoundResult({ scores, guesses, owner }) {
startBtn.hidden = true; startBtn.hidden = true;
startBtn.disabled = true; startBtn.disabled = true;
roundArea.hidden = true; roundArea.hidden = true;
const submitBtn = document.getElementById("submitGuesses");
if (submitBtn) submitBtn.hidden = true;
}; };
} }