From 6a067d0c1e4715dd08033e4b2f74ea895e930ece Mon Sep 17 00:00:00 2001 From: eric <3024947@stud.hs-mannheim.de> Date: Fri, 8 Aug 2025 03:29:15 +0200 Subject: [PATCH 1/5] Neue GUI --- src/main/resources/public/game.html | 184 ++++++++++++++++++++++----- src/main/resources/public/js/game.js | 74 +++++------ 2 files changed, 187 insertions(+), 71 deletions(-) diff --git a/src/main/resources/public/game.html b/src/main/resources/public/game.html index 0577766..0e221fd 100644 --- a/src/main/resources/public/game.html +++ b/src/main/resources/public/game.html @@ -1,43 +1,159 @@ - - - Spotify Roulette – Spiel - + + + Spotify Roulette – Spiel + -

Spotify Roulette

-

Spiel-Code:

- -
-

Geladene Songs

- -
- -

Teilnehmer

- - -
- -
- - - -

Scoreboard

- - +
+

Spotify Roulette

+
Spiel-Code:
+
+
+
+
Teilnehmer
+ +
+
+ +
+ +
+
Scoreboard
+ +
+
+ + + + -
diff --git a/src/main/resources/public/js/game.js b/src/main/resources/public/js/game.js index a091ee6..348a353 100644 --- a/src/main/resources/public/js/game.js +++ b/src/main/resources/public/js/game.js @@ -98,7 +98,7 @@ function connectWebSocket() { } connectWebSocket(); -// 8) Funktion zum Anzeigen einer neuen Runde +// Zugriff auf DOM-Elemente const startBtn = document.getElementById("startRound"); const roundArea = document.getElementById("roundArea"); const songEmbed = document.getElementById("songEmbed"); @@ -106,6 +106,7 @@ const optionsDiv = document.getElementById("options"); const resultP = document.getElementById("result"); const scoreboard = document.getElementById("scoreboard"); +// 8) Funktion zum Anzeigen einer neuen Runde function handleRoundStart({ ownerOptions, songUri, allTracks, trackInfos }) { // UI zurücksetzen resultP.textContent = ""; @@ -120,36 +121,44 @@ function handleRoundStart({ ownerOptions, songUri, allTracks, trackInfos }) { width="100%" height="80" frameborder="0" allow="encrypted-media"> `; - // Song automatisch abspielen (sofern deviceId bereit und Username vorhanden) + if (window.playOnSpotify && typeof window.playOnSpotify === "function") { window.playOnSpotify(songUri, username); } - // Tipp-Buttons erzeugen - ownerOptions.forEach(user => { - const btn = document.createElement("button"); - btn.textContent = user; - btn.addEventListener("click", () => { - socket.send(JSON.stringify({ - type: "guess", - username: username, - guess: user - })); - // Nach Tipp alle Buttons deaktivieren - optionsDiv.querySelectorAll("button").forEach(b => b.disabled = true); - }); - optionsDiv.append(btn); - }); + // Dynamische Kreisverteilung der Buttons + // Warten, bis #options gerendert ist + setTimeout(() => { + const optsRect = optionsDiv.getBoundingClientRect(); + const radius = Math.min(optsRect.width, optsRect.height) / 2 - 50; // 50px Abstand zum Rand + + ownerOptions.forEach((user, i) => { + const btn = document.createElement("button"); + btn.textContent = user; + btn.classList.add("player-option"); + const angle = 360 * i / ownerOptions.length; + btn.style.transform = `rotate(${angle}deg) translateY(-${radius}px) rotate(${-angle}deg)`; + btn.addEventListener("click", () => { + socket.send(JSON.stringify({ + type: "guess", + username: username, + guess: user + })); + optionsDiv.querySelectorAll("button").forEach(b => b.disabled = true); + }); + optionsDiv.appendChild(btn); + }); + }, 0); - // UI anzeigen startBtn.hidden = true; roundArea.hidden = false; + const songList = document.getElementById("songList"); songList.innerHTML = ""; if (Array.isArray(trackInfos)) { trackInfos.forEach(trackInfo => { const li = document.createElement("li"); - li.textContent = trackInfo + li.textContent = trackInfo; songList.appendChild(li); }); } @@ -166,43 +175,38 @@ function renderScoreboard(scores) { } function handleRoundResult({ scores, guesses, owner }) { - // Scoreboard updaten renderScoreboard(scores); - // Ergebnis für alle Spieler anzeigen - resultP.innerHTML = ""; // Vorher leeren + resultP.innerHTML = ""; Object.entries(guesses).forEach(([user, guess]) => { const correct = guess === owner; const icon = correct ? "✅" : "❌"; - const msg = `${icon} ${user} hat auf ${guess} getippt${correct ? " (richtig!)" : " (falsch)"}`; + const delta = correct ? "+3" : "-1"; + const msg = `${icon} ${user} hat auf ${guess} getippt${correct ? " (richtig!)" : " (falsch)"} [${delta}]`; const p = document.createElement("p"); p.textContent = msg; resultP.appendChild(p); }); - // Nach kurzer Pause für die nächste Runde vorbereiten setTimeout(() => { resultP.textContent = ""; startBtn.hidden = true; startBtn.disabled = true; roundArea.hidden = true; }, 3000); - } function handleGameEnd({winner}) { resultP.textContent = `🎉 ${winner} hat gewonnen!`; setTimeout(() => { - startBtn.hidden = false; - roundArea.hidden = true; - startBtn.disabled = false; - // scoreboard leeren + startBtn.hidden = false; + roundArea.hidden = true; + startBtn.disabled = false; scoreboard.innerHTML = ""; }, 6000); } -// public/js/game.js - +// Spotify-Playback Funktion async function playOnSpotify(trackUri, username) { const deviceId = document.getElementById("deviceSelect")?.value; if (!deviceId) { @@ -213,11 +217,7 @@ async function playOnSpotify(trackUri, username) { const response = await fetch("/api/spotify/play", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username, - device_id: deviceId, - track_uri: trackUri - }) + body: JSON.stringify({ username, device_id: deviceId, track_uri: trackUri }) }); if (!response.ok) { const error = await response.text(); @@ -227,4 +227,4 @@ async function playOnSpotify(trackUri, username) { alert(`Netzwerkfehler: ${err.message}`); } } -window.playOnSpotify = playOnSpotify; \ No newline at end of file +window.playOnSpotify = playOnSpotify; From e9ea337ef97f34240030569097c3674ee68ad68d Mon Sep 17 00:00:00 2001 From: eric <3024947@stud.hs-mannheim.de> Date: Fri, 8 Aug 2025 04:07:12 +0200 Subject: [PATCH 2/5] Darkmode beta --- src/main/java/eric/Roullette/App.java | 3 +- .../eric/Roullette/service/GameService.java | 137 +++++++++--------- .../websocket/GameWebSocketHandler.java | 13 +- src/main/resources/public/game.html | 15 +- src/main/resources/public/js/game.js | 18 ++- .../Roullette/service/GameServiceTest.java | 22 +-- 6 files changed, 112 insertions(+), 96 deletions(-) diff --git a/src/main/java/eric/Roullette/App.java b/src/main/java/eric/Roullette/App.java index 723777b..282d686 100644 --- a/src/main/java/eric/Roullette/App.java +++ b/src/main/java/eric/Roullette/App.java @@ -23,12 +23,13 @@ public class App { if (cfg.spotifyClientId == null || cfg.spotifyClientSecret == null || cfg.spotifyRedirectUri == null) { throw new IllegalStateException("Spotify-Konfiguration fehlt: Bitte stelle sicher, dass ClientId, ClientSecret und RedirectUri gesetzt sind."); } - GameService gs = new GameService(); + SpotifyAuthService sas = new SpotifyAuthService( cfg.spotifyClientId, cfg.spotifyClientSecret, cfg.spotifyRedirectUri ); + GameService gs = new GameService(sas); Javalin app = Javalin.create(config -> { config.showJavalinBanner = false; diff --git a/src/main/java/eric/Roullette/service/GameService.java b/src/main/java/eric/Roullette/service/GameService.java index 8fbd73b..b1aea36 100644 --- a/src/main/java/eric/Roullette/service/GameService.java +++ b/src/main/java/eric/Roullette/service/GameService.java @@ -1,78 +1,85 @@ package eric.Roullette.service; -import eric.Roullette.dto.PlayersMessage; -import eric.Roullette.util.JsonUtil; -import io.javalin.websocket.WsContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.util.*; -import java.util.concurrent.*; + import eric.Roullette.dto.PlayersMessage; + import eric.Roullette.util.JsonUtil; + import io.javalin.websocket.WsContext; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + import java.util.*; + import java.util.concurrent.*; -public class GameService { - private static final Logger log = LoggerFactory.getLogger(GameService.class); - private final Map> sessions = new ConcurrentHashMap<>(); - private final Map games = new ConcurrentHashMap<>(); + public class GameService { + private static final Logger log = LoggerFactory.getLogger(GameService.class); + private final SpotifyAuthService authService; + private final Map> sessions = new ConcurrentHashMap<>(); + private final Map games = new ConcurrentHashMap<>(); - public record Game(String id, List players, Map scores,String currentOwner, - String currentSong,List allTracks) { - public static Game create(String id) { - return new Game(id, new CopyOnWriteArrayList<>(), new ConcurrentHashMap<>(), null, null, new ArrayList<>()); + public GameService(SpotifyAuthService authService) { // <-- Konstruktor + this.authService = authService; } - } - - public Game getOrCreateGame(String gameId) { - return games.computeIfAbsent(gameId, id -> Game.create(id)); - } - - public void addPlayer(String gameId, String user) { - Game g = getOrCreateGame(gameId); - if (user != null && !g.players().contains(user)) { - g.players().add(user); - g.scores().putIfAbsent(user, 0); + public record Game(String id, List players, Map scores, String currentOwner, + String currentSong, List allTracks, Map> playerTracks) { + public static Game create(String id) { + return new Game(id, new CopyOnWriteArrayList<>(), new ConcurrentHashMap<>(), null, null, new ArrayList<>(), new ConcurrentHashMap<>()); + } } - } - public void registerSession(String gameId, WsContext ctx) { - sessions - .computeIfAbsent(gameId, id -> ConcurrentHashMap.newKeySet()) - .add(ctx); - broadcastPlayers(gameId); - } + public Game getOrCreateGame(String gameId) { + return games.computeIfAbsent(gameId, id -> Game.create(id)); + } - public void removeSession(String gameId, WsContext ctx) { - sessions.getOrDefault(gameId, Collections.emptySet()).remove(ctx); - broadcastPlayers(gameId); - } + public void addPlayer(String gameId, String user) { + Game g = getOrCreateGame(gameId); + if (user != null && !g.players().contains(user)) { + g.players().add(user); + g.scores().putIfAbsent(user, 0); + // Songs einmalig laden und speichern + List tracks = authService.getRecentTracks(user); + g.playerTracks().put(user, tracks); + } + } - public void broadcastPlayers(String gameId) { - Game game = games.get(gameId); - if (game == null) return; - PlayersMessage msg = new PlayersMessage(new ArrayList<>(game.players())); - sessions.getOrDefault(gameId, Collections.emptySet()) - .forEach(ctx -> ctx.send(JsonUtil.toJson(msg))); - } + public void registerSession(String gameId, WsContext ctx) { + sessions + .computeIfAbsent(gameId, id -> ConcurrentHashMap.newKeySet()) + .add(ctx); + broadcastPlayers(gameId); + } - public void createGame(String gameId) { - Game game = new Game(gameId, new CopyOnWriteArrayList<>(), new ConcurrentHashMap<>(), null, null, new ArrayList<>()); - games.put(gameId, game); - } + public void removeSession(String gameId, WsContext ctx) { + sessions.getOrDefault(gameId, Collections.emptySet()).remove(ctx); + broadcastPlayers(gameId); + } - public boolean gameExists(String gameId) { - return games.containsKey(gameId); - } - public Game startRound(String gameId, List uris) { - Game g = getOrCreateGame(gameId); - if (g.players().isEmpty()) throw new IllegalStateException("No players"); - String owner = g.players().get(ThreadLocalRandom.current().nextInt(g.players().size())); - String song = uris.get(ThreadLocalRandom.current().nextInt(uris.size())); - Game updated = new Game(gameId, g.players(), g.scores(), owner, song, uris); - games.put(gameId, updated); - return updated; - } - // In GameService.java - public Set getSessions(String gameId) { - return sessions.getOrDefault(gameId, Collections.emptySet()); - } + public void broadcastPlayers(String gameId) { + Game game = games.get(gameId); + if (game == null) return; + PlayersMessage msg = new PlayersMessage(new ArrayList<>(game.players())); + sessions.getOrDefault(gameId, Collections.emptySet()) + .forEach(ctx -> ctx.send(JsonUtil.toJson(msg))); + } -} + public void createGame(String gameId) { + Game game = new Game(gameId, new CopyOnWriteArrayList<>(), new ConcurrentHashMap<>(), null, null, new ArrayList<>(), new ConcurrentHashMap<>()); + games.put(gameId, game); + } + + public boolean gameExists(String gameId) { + return games.containsKey(gameId); + } + + public Game startRound(String gameId, List uris) { + Game g = getOrCreateGame(gameId); + if (g.players().isEmpty()) throw new IllegalStateException("No players"); + String owner = g.players().get(ThreadLocalRandom.current().nextInt(g.players().size())); + String song = uris.get(ThreadLocalRandom.current().nextInt(uris.size())); + Game updated = new Game(gameId, g.players(), g.scores(), owner, song, uris, g.playerTracks()); + games.put(gameId, updated); + return updated; + } + + public Set getSessions(String gameId) { + return sessions.getOrDefault(gameId, Collections.emptySet()); + } + } \ No newline at end of file diff --git a/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java b/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java index b98b534..32d1fd1 100644 --- a/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java +++ b/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java @@ -78,7 +78,7 @@ public class GameWebSocketHandler { // Songs von allen Spielern sammeln List allTracks = new ArrayList<>(); for (String player : game.players()) { - allTracks.addAll(authService.getRecentTracks(player)); + allTracks.addAll(game.playerTracks().getOrDefault(player, List.of())); } if (allTracks.isEmpty()) { // TODO: Fehler an Client senden, dass keine Songs da sind @@ -90,6 +90,17 @@ public class GameWebSocketHandler { // Jetzt Broadcast mit den aktuellen Daten broadcastRoundStart(gameId); } + case "next-round" -> { + var currentGame = service.getOrCreateGame(gameId); + List allTracks = new ArrayList<>(); + for (String player : currentGame.players()) { + allTracks.addAll(authService.getRecentTracks(player)); + } + if (!allTracks.isEmpty()) { + service.startRound(gameId, allTracks); + } + broadcastRoundStart(gameId); + } } }); } diff --git a/src/main/resources/public/game.html b/src/main/resources/public/game.html index 0e221fd..e27eafc 100644 --- a/src/main/resources/public/game.html +++ b/src/main/resources/public/game.html @@ -7,12 +7,13 @@