From cc7f8a8ef3319f67a07db379e171457a2b23df40 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Tue, 21 Apr 2026 17:42:38 +0200 Subject: [PATCH] Feat: Add GET_HIGHSCORES and CLEAR_HIGHSCORES server commands (Issue #110) --- .../dbis/cs108/casono/server/ServerApp.java | 18 +++ .../ClearHighscoresHandler.java | 22 ++++ .../ClearHighscoresParser.java | 12 ++ .../ClearHighscoresRequest.java | 11 ++ .../get_highscores/GetHighscoresHandler.java | 22 ++++ .../get_highscores/GetHighscoresParser.java | 12 ++ .../get_highscores/GetHighscoresRequest.java | 11 ++ .../get_highscores/GetHighscoresResponse.java | 23 ++++ .../domain/highscore/HighscoreService.java | 107 ++++++++++++++++++ 9 files changed, 238 insertions(+) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/clear_highscores/ClearHighscoresHandler.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/clear_highscores/ClearHighscoresParser.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/clear_highscores/ClearHighscoresRequest.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresHandler.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresParser.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresRequest.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresResponse.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/highscore/HighscoreService.java diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java index 3a1820d..f93ef7c 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java @@ -27,6 +27,12 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.Get import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageRequest; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores.ClearHighscoresHandler; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores.ClearHighscoresParser; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores.ClearHighscoresRequest; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores.GetHighscoresHandler; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores.GetHighscoresParser; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores.GetHighscoresRequest; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest; @@ -249,6 +255,18 @@ public class ServerApp { new GetGameStateHandler( responseDispatcher, context.lobbyManager(), userRegistry)); + parserDispatcher.register("GET_HIGHSCORES", new GetHighscoresParser()); + commandRouter.register( + GetHighscoresRequest.class, + (CommandHandler) + new GetHighscoresHandler(responseDispatcher)); + + parserDispatcher.register("CLEAR_HIGHSCORES", new ClearHighscoresParser()); + commandRouter.register( + ClearHighscoresRequest.class, + (CommandHandler) + new ClearHighscoresHandler(responseDispatcher)); + // BET registration parserDispatcher.register("BET", new PlayerBetParser()); commandRouter.register( diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/clear_highscores/ClearHighscoresHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/clear_highscores/ClearHighscoresHandler.java new file mode 100644 index 0000000..fb39b0d --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/clear_highscores/ClearHighscoresHandler.java @@ -0,0 +1,22 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores; + +import ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore.HighscoreService; +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; + +/** Handler for CLEAR_HIGHSCORES. */ +public class ClearHighscoresHandler extends CommandHandler { + private final HighscoreService highscoreService; + + public ClearHighscoresHandler(ResponseDispatcher responseDispatcher) { + super(responseDispatcher); + this.highscoreService = HighscoreService.getInstance(); + } + + @Override + public void execute(ClearHighscoresRequest request) { + highscoreService.clearAll(); + responseDispatcher.dispatch(new OkResponse(request.getContext())); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/clear_highscores/ClearHighscoresParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/clear_highscores/ClearHighscoresParser.java new file mode 100644 index 0000000..7de094f --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/clear_highscores/ClearHighscoresParser.java @@ -0,0 +1,12 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest; + +/** Parser for CLEAR_HIGHSCORES. */ +public class ClearHighscoresParser implements CommandParser { + @Override + public ClearHighscoresRequest parse(PrimitiveRequest primitiveRequest) { + return new ClearHighscoresRequest(primitiveRequest.context()); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/clear_highscores/ClearHighscoresRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/clear_highscores/ClearHighscoresRequest.java new file mode 100644 index 0000000..2613c9e --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/clear_highscores/ClearHighscoresRequest.java @@ -0,0 +1,11 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; + +/** Request object for CLEAR_HIGHSCORES. */ +public class ClearHighscoresRequest extends Request { + public ClearHighscoresRequest(RequestContext context) { + super(context); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresHandler.java new file mode 100644 index 0000000..6f4ef84 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresHandler.java @@ -0,0 +1,22 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores; + +import ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore.HighscoreService; +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; + +/** Handler for GET_HIGHSCORES. */ +public class GetHighscoresHandler extends CommandHandler { + private final HighscoreService highscoreService; + + public GetHighscoresHandler(ResponseDispatcher responseDispatcher) { + super(responseDispatcher); + this.highscoreService = HighscoreService.getInstance(); + } + + @Override + public void execute(GetHighscoresRequest request) { + responseDispatcher.dispatch( + new GetHighscoresResponse( + request.getContext(), highscoreService.readFormattedEntries())); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresParser.java new file mode 100644 index 0000000..cd73dae --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresParser.java @@ -0,0 +1,12 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest; + +/** Parser for GET_HIGHSCORES. */ +public class GetHighscoresParser implements CommandParser { + @Override + public GetHighscoresRequest parse(PrimitiveRequest primitiveRequest) { + return new GetHighscoresRequest(primitiveRequest.context()); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresRequest.java new file mode 100644 index 0000000..998e205 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresRequest.java @@ -0,0 +1,11 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; + +/** Request object for GET_HIGHSCORES. */ +public class GetHighscoresRequest extends Request { + public GetHighscoresRequest(RequestContext context) { + super(context); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresResponse.java new file mode 100644 index 0000000..98b76ec --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/highscore/get_highscores/GetHighscoresResponse.java @@ -0,0 +1,23 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody; +import java.util.List; + +/** Success response containing repeated HIGHSCORE parameters. */ +public class GetHighscoresResponse extends SuccessResponse { + public GetHighscoresResponse(RequestContext context, List entries) { + super( + context, + ResponseBody.builder() + .block( + "HIGHSCORES", + block -> { + for (String entry : entries) { + block.param("HIGHSCORE", entry); + } + }) + .build()); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/highscore/HighscoreService.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/highscore/HighscoreService.java new file mode 100644 index 0000000..929b3e3 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/highscore/HighscoreService.java @@ -0,0 +1,107 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** Stores and reads persistent highscore entries in timestamp + winner format. */ +public final class HighscoreService { + + private static final HighscoreService INSTANCE = new HighscoreService(); + private static final DateTimeFormatter DISPLAY_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()); + private static final int MAX_RETURNED_ENTRIES = 100; + private static final String LINE_SEPARATOR = "\t"; + + private final Path storagePath; + + private HighscoreService() { + this.storagePath = + Path.of(System.getProperty("user.home"), ".casono", "highscores", "winners.log"); + } + + public static HighscoreService getInstance() { + return INSTANCE; + } + + public synchronized void appendWinner(String winnerName) { + if (winnerName == null) { + return; + } + + String sanitized = sanitizeName(winnerName); + if (sanitized.isEmpty()) { + return; + } + + String line = Instant.now() + LINE_SEPARATOR + sanitized + System.lineSeparator(); + + try { + Files.createDirectories(storagePath.getParent()); + Files.writeString( + storagePath, + line, + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.APPEND); + } catch (IOException ignored) { + // Highscore persistence must never break the game state response path. + } + } + + public synchronized List readFormattedEntries() { + if (!Files.exists(storagePath)) { + return List.of(); + } + + List lines; + try { + lines = Files.readAllLines(storagePath, StandardCharsets.UTF_8); + } catch (IOException ignored) { + return List.of(); + } + + List formatted = new ArrayList<>(); + + for (String raw : lines) { + if (raw == null || raw.isBlank()) { + continue; + } + + String[] parts = raw.split(LINE_SEPARATOR, 2); + if (parts.length < 2) { + continue; + } + + try { + Instant ts = Instant.parse(parts[0].trim()); + String display = DISPLAY_FORMATTER.format(ts) + " | " + parts[1].trim(); + formatted.add(display); + } catch (Exception ignored) { + // Skip malformed lines and keep all valid entries. + } + } + + int from = Math.max(0, formatted.size() - MAX_RETURNED_ENTRIES); + return new ArrayList<>(formatted.subList(from, formatted.size())); + } + + public synchronized void clearAll() { + try { + Files.deleteIfExists(storagePath); + } catch (IOException ignored) { + // Clearing highscores must not break request handling. + } + } + + private static String sanitizeName(String winnerName) { + return winnerName.trim().replace("\n", " ").replace("\r", " ").replace("\t", " "); + } +} -- 2.52.0