commit a50c03bbe658be712763ac85aaf68797689eba0b Author: Radek Davidek Date: Sun Dec 28 18:50:44 2025 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a06ec0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +servers +target +bin +.settings +.metadata +.classpath +.project +.vscode diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3c7554f --- /dev/null +++ b/pom.xml @@ -0,0 +1,50 @@ + + 4.0.0 + + com.darts.stats + darts-stats-web + 1.0-SNAPSHOT + + + 11 + 11 + UTF-8 + + + + + com.google.code.gson + gson + 2.10.1 + + + org.projectlombok + lombok + 1.18.28 + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + 11 + + + org.projectlombok + lombok + 1.18.28 + + + + + + + diff --git a/src/main/java/com/darts/stats/App.java b/src/main/java/com/darts/stats/App.java new file mode 100644 index 0000000..61b5923 --- /dev/null +++ b/src/main/java/com/darts/stats/App.java @@ -0,0 +1,210 @@ +package com.darts.stats; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import java.io.*; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +public class App { + private static final Gson GSON = new Gson(); + private static final String DATA_FILE = "stats.json"; + + public static void main(String[] args) throws IOException { + int port = 8080; + HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); + + server.createContext("/", new StaticHandler()); + server.createContext("/api/stats", new StatsHandler()); + server.createContext("/api/upload", new UploadHandler()); + + server.setExecutor(null); + System.out.println("Server started on port " + port); + server.start(); + } + + static class StaticHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + String path = exchange.getRequestURI().getPath(); + if (path.equals("/")) path = "/index.html"; + + InputStream is = App.class.getResourceAsStream("/web" + path); + if (is == null) { + String response = "404 Not Found"; + exchange.sendResponseHeaders(404, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + return; + } + + byte[] content = is.readAllBytes(); + String contentType = "text/html"; + if (path.endsWith(".js")) contentType = "application/javascript"; + if (path.endsWith(".css")) contentType = "text/css"; + + exchange.getResponseHeaders().set("Content-Type", contentType); + exchange.sendResponseHeaders(200, content.length); + OutputStream os = exchange.getResponseBody(); + os.write(content); + os.close(); + } + } + + static class UploadHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"POST".equals(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(405, -1); + return; + } + + InputStream is = exchange.getRequestBody(); + String body = new String(is.readAllBytes(), StandardCharsets.UTF_8); + + try { + List rawGames = GSON.fromJson(body, new TypeToken>(){}.getType()); + List convertedGames = convertRawToGames(rawGames); + + Files.writeString(Paths.get(DATA_FILE), GSON.toJson(convertedGames)); + + String response = "Upload successful"; + exchange.sendResponseHeaders(200, response.length()); + exchange.getResponseBody().write(response.getBytes()); + } catch (Exception e) { + e.printStackTrace(); + String response = "Error processing upload: " + e.getMessage(); + exchange.sendResponseHeaders(400, response.length()); + exchange.getResponseBody().write(response.getBytes()); + } finally { + exchange.getResponseBody().close(); + } + } + + private List convertRawToGames(List rawGames) { + List result = new ArrayList<>(); + for (RawGame rg : rawGames) { + for (RawGame.RawPlayer rp : rg.getPlayers()) { + DartsGame game = new DartsGame(); + game.setDate(rg.getDate()); + game.setPlayer(rp.getName()); + game.setOpponent(rg.getPlayers().stream() + .filter(p -> !p.getName().equals(rp.getName())) + .map(RawGame.RawPlayer::getName) + .collect(Collectors.joining(", "))); + game.setGameType(String.valueOf(rg.getTargetScore())); + game.setResult(rp.getName().equals(rg.getWinnerName()) ? "WIN" : "LOSS"); + + List throwsList = rp.getThrowsList(); + game.setDartsThrown(throwsList.size()); + + int totalScore = throwsList.stream().filter(t -> !t.isBust()).mapToInt(RawGame.RawThrow::getValue).sum(); + game.setAverage(throwsList.isEmpty() ? 0 : Math.round((double) totalScore / throwsList.size() * 3 * 100.0) / 100.0); + + // Calculate visits + int h100 = 0, h140 = 0, h180 = 0, highest = 0; + for (int i = 0; i < throwsList.size(); i += 3) { + int visitSum = 0; + boolean bust = false; + for (int j = 0; j < 3 && (i + j) < throwsList.size(); j++) { + if (throwsList.get(i + j).isBust()) bust = true; + visitSum += throwsList.get(i + j).getValue(); + } + if (bust) visitSum = 0; + + if (visitSum >= 180) h180++; + else if (visitSum >= 140) h140++; + else if (visitSum >= 100) h100++; + + if (visitSum > highest) highest = visitSum; + } + + game.setHundredPlus(h100); + game.setHundredFortyPlus(h140); + game.setHundredEighty(h180); + game.setHighestVisit(highest); + + result.add(game); + } + } + return result; + } + } + + static class StatsHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"GET".equals(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(405, -1); + return; + } + + List games = loadGames(); + StatsResponse response = aggregateStats(games); + + String json = GSON.toJson(response); + byte[] bytes = json.getBytes(StandardCharsets.UTF_8); + + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, bytes.length); + OutputStream os = exchange.getResponseBody(); + os.write(bytes); + os.close(); + } + + private List loadGames() { + try { + if (!Files.exists(Paths.get(DATA_FILE))) { + return Collections.emptyList(); + } + String content = Files.readString(Paths.get(DATA_FILE)); + return GSON.fromJson(content, new TypeToken>(){}.getType()); + } catch (IOException e) { + e.printStackTrace(); + return Collections.emptyList(); + } + } + + private StatsResponse aggregateStats(List games) { + Map> byPlayer = games.stream() + .collect(Collectors.groupingBy(DartsGame::getPlayer)); + + Map playerStatsMap = new HashMap<>(); + + for (Map.Entry> entry : byPlayer.entrySet()) { + String player = entry.getKey(); + List playerGames = entry.getValue(); + + double avg = playerGames.stream().mapToDouble(DartsGame::getAverage).average().orElse(0.0); + int wins = (int) playerGames.stream().filter(g -> "WIN".equalsIgnoreCase(g.getResult())).count(); + int h100 = playerGames.stream().mapToInt(DartsGame::getHundredPlus).sum(); + int h140 = playerGames.stream().mapToInt(DartsGame::getHundredFortyPlus).sum(); + int h180 = playerGames.stream().mapToInt(DartsGame::getHundredEighty).sum(); + + playerStatsMap.put(player, StatsResponse.PlayerStats.builder() + .overallAverage(Math.round(avg * 100.0) / 100.0) + .totalGames(playerGames.size()) + .wins(wins) + .winRate(Math.round((double) wins / playerGames.size() * 10000.0) / 100.0) + .total100plus(h100) + .total140plus(h140) + .total180s(h180) + .build()); + } + + return StatsResponse.builder() + .games(games) + .playerStats(playerStatsMap) + .build(); + } + } +} diff --git a/src/main/java/com/darts/stats/DartsGame.java b/src/main/java/com/darts/stats/DartsGame.java new file mode 100644 index 0000000..0e41c2d --- /dev/null +++ b/src/main/java/com/darts/stats/DartsGame.java @@ -0,0 +1,23 @@ +package com.darts.stats; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DartsGame { + private String date; + private String player; + private String opponent; + private String gameType; + private String result; + private int dartsThrown; + private double average; + private int checkout; + private int highestVisit; + private int hundredPlus; + private int hundredFortyPlus; + private int hundredEighty; +} diff --git a/src/main/java/com/darts/stats/RawGame.java b/src/main/java/com/darts/stats/RawGame.java new file mode 100644 index 0000000..f27518f --- /dev/null +++ b/src/main/java/com/darts/stats/RawGame.java @@ -0,0 +1,25 @@ +package com.darts.stats; + +import lombok.Data; +import java.util.List; + +@Data +public class RawGame { + private String date; + private List players; + private String winnerName; + private int targetScore; + + @Data + public static class RawPlayer { + private String name; + private List throwsList; + } + + @Data + public static class RawThrow { + private int value; + private boolean isBust; + private String label; + } +} diff --git a/src/main/java/com/darts/stats/StatsResponse.java b/src/main/java/com/darts/stats/StatsResponse.java new file mode 100644 index 0000000..f76c51b --- /dev/null +++ b/src/main/java/com/darts/stats/StatsResponse.java @@ -0,0 +1,25 @@ +package com.darts.stats; + +import lombok.Builder; +import lombok.Data; +import java.util.List; +import java.util.Map; + +@Data +@Builder +public class StatsResponse { + private List games; + private Map playerStats; + + @Data + @Builder + public static class PlayerStats { + private double overallAverage; + private int totalGames; + private int wins; + private int total100plus; + private int total140plus; + private int total180s; + private double winRate; + } +} diff --git a/src/main/resources/web/index.html b/src/main/resources/web/index.html new file mode 100644 index 0000000..4ff2084 --- /dev/null +++ b/src/main/resources/web/index.html @@ -0,0 +1,319 @@ + + + + + + Darts Stats Dashboard + + + + +
+

🎯 Darts Statistics Dashboard

+ +
+
+

Upload Stats JSON

+ + +
+ +
+ +
+ +
+

Average Progression

+
+ +
+
+ +
+

Recent Games

+ + + + + + + + + + + + + +
Date ↕Player ↕Opponent ↕Avg ↕Darts ↕Round ↕Result ↕
+
+
+ + + +