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:
-
-
-
-Teilnehmer
-
-
-
-
-
-
-
-
Wer hat’s gehört?
-
-
-
-
-
-Scoreboard
-
-
+
+ Spotify Roulette
+ Spiel-Code:
+
+
+
+
+
+
+
+ Wer hat’s gehört?
+
+
+
+
+
+
+
+
+
+
+
+
-
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 @@