From eaef3b32b777a9174b9ab349acb3504cc2c02563 Mon Sep 17 00:00:00 2001 From: Foulest <43710301+Foulest@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:42:20 -0800 Subject: [PATCH] Initial release for sanity --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- .../documentation-improvement.md | 2 +- .github/ISSUE_TEMPLATE/feature-request.md | 2 +- .github/ISSUE_TEMPLATE/question.md | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/dependency-submission.yml | 2 +- build.gradle | 14 +- settings.gradle | 2 +- src/main/java/net/foulest/swiss/Bracket.java | 529 ++++++++++++++++++ src/main/java/net/foulest/swiss/Main.java | 80 +++ src/main/java/net/foulest/swiss/Match.java | 63 +++ src/main/java/net/foulest/swiss/Pair.java | 26 + src/main/java/net/foulest/swiss/Team.java | 25 + src/main/java/net/foulest/template/Main.java | 35 -- 14 files changed, 737 insertions(+), 49 deletions(-) create mode 100644 src/main/java/net/foulest/swiss/Bracket.java create mode 100644 src/main/java/net/foulest/swiss/Main.java create mode 100644 src/main/java/net/foulest/swiss/Match.java create mode 100644 src/main/java/net/foulest/swiss/Pair.java create mode 100644 src/main/java/net/foulest/swiss/Team.java delete mode 100644 src/main/java/net/foulest/template/Main.java diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 06914ab..bc66860 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -3,7 +3,7 @@ name: Report a bug about: Create a report to help us improve. title: '' labels: 'bug' -assignees: 'Foulest' # TODO: Change this to your username +assignees: 'Foulest' --- diff --git a/.github/ISSUE_TEMPLATE/documentation-improvement.md b/.github/ISSUE_TEMPLATE/documentation-improvement.md index f6d70dd..ca9bf06 100644 --- a/.github/ISSUE_TEMPLATE/documentation-improvement.md +++ b/.github/ISSUE_TEMPLATE/documentation-improvement.md @@ -3,7 +3,7 @@ name: Documentation improvement about: Suggest an improvement for the project documentation. title: '' labels: 'documentation' -assignees: 'Foulest' # TODO: Change this to your username +assignees: 'Foulest' --- diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 61024d8..c4f3c3e 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -3,7 +3,7 @@ name: Feature request about: Suggest an idea for this project. title: '' labels: 'request' -assignees: 'Foulest' # TODO: Change this to your username +assignees: 'Foulest' --- diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 5d3d731..a147a95 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -3,7 +3,7 @@ name: Ask a question about: Ask a question about the project. title: '' labels: 'question' -assignees: 'Foulest' # TODO: Change this to your username +assignees: 'Foulest' --- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb9e000..acdac41 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: 8 # TODO: Change this to your Java version + java-version: 8 - name: 'Setup Gradle' uses: gradle/actions/setup-gradle@v3 diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml index 46c3f9d..fdef490 100644 --- a/.github/workflows/dependency-submission.yml +++ b/.github/workflows/dependency-submission.yml @@ -20,7 +20,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: 8 # TODO: Change this to your Java version + java-version: 8 - name: 'Submit Dependency Graph' uses: gradle/actions/dependency-submission@v3 diff --git a/build.gradle b/build.gradle index dba6a39..7ed7fe9 100644 --- a/build.gradle +++ b/build.gradle @@ -5,14 +5,14 @@ plugins { id 'java' } -group = 'net.foulest' // TODO: Change this to your group -version = '1.0.0' // TODO: Change this to your version -description = 'JavaTemplate' // TODO: Change this to your description +group = 'net.foulest' +version = '1.0.0' +description = 'Swiss' // Set the project's language level java { toolchain { - languageVersion.set(JavaLanguageVersion.of(8)) // TODO: Change this to your Java version + languageVersion.set(JavaLanguageVersion.of(8)) } } @@ -45,12 +45,12 @@ tasks { // Set the 'Main-Class' attribute in the JAR manifest manifest { - attributes 'Main-Class': 'net.foulest.template.Main' // TODO: Change this to your main class + attributes 'Main-Class': 'net.foulest.swiss.Main' } } shadowJar { - mainClassName = 'net.foulest.template.Main' // TODO: Change this to your main class + mainClassName = 'net.foulest.swiss.Main' archiveFileName.set("${project.name}-${project.version}.jar") } @@ -87,7 +87,7 @@ tasks { publishing { publications { mavenJava(MavenPublication) { - groupId = 'net.foulest.template' // TODO: Change this to your group + groupId = 'net.foulest.swiss' artifactId = project.name version = project.version diff --git a/settings.gradle b/settings.gradle index 77a1e1b..2f409c6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'JavaTemplate' // TODO: Change this to your project name +rootProject.name = 'Swiss' diff --git a/src/main/java/net/foulest/swiss/Bracket.java b/src/main/java/net/foulest/swiss/Bracket.java new file mode 100644 index 0000000..a66c0e8 --- /dev/null +++ b/src/main/java/net/foulest/swiss/Bracket.java @@ -0,0 +1,529 @@ +package net.foulest.swiss; + +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +@Data +class Bracket { + + private List teams; + // Pairwise 3-0 + private Map> pairwise3_0 = new HashMap<>(); + + Bracket(@NotNull List teams) { + this.teams = teams; + } + + @SuppressWarnings("NestedMethodCall") + void simulateMultipleBrackets(int numSimulations) { + // Map to track results for each team + Map> results = new HashMap<>(); + + // Initialize results map for each team + teams.forEach(team -> results.put(team, new HashMap<>())); + + // Run simulations sequentially + for (int i = 0; i < numSimulations; i++) { + simulateBracket(results); // Simulate one bracket + } + + // Analyze and print the results after all simulations are complete + printResults(results, numSimulations); + } + + @SuppressWarnings("NestedMethodCall") + private void simulateBracket(Map> results) { + Map records = new HashMap<>(); + Map buchholzScores = new HashMap<>(); + Map> pastOpponents = new HashMap<>(); + + // Initialize all the records to 0-0 + for (Team team : teams) { + records.put(team, new int[]{0, 0}); + buchholzScores.put(team, 0); + pastOpponents.put(team, new ArrayList<>()); + } + + // Sort teams by their seeding in ascending order + List seededTeams = new ArrayList<>(teams); + seededTeams.sort(Comparator.comparingInt(Team::getSeeding)); + + List activeTeams = new ArrayList<>(); + + // Generate first-round matchups based on seeding + for (int i = 0; i < seededTeams.size() / 2; i++) { + Team higherSeed = seededTeams.get(i); // Top seed + Team lowerSeed = seededTeams.get(seededTeams.size() / 2 + i); // Bottom seed + Match match = new Match(higherSeed, lowerSeed, false); + + Team winner = match.simulate(); + Team loser = (winner == higherSeed) ? lowerSeed : higherSeed; + + // Update records + updateRecords(records, winner, loser); + +// // Print the match result +// System.out.println(winner.getName() + " (" + records.get(winner)[0] + "-" + records.get(winner)[1] + ")" + +// " beat " + loser.getName() + " (" + records.get(loser)[0] + "-" + records.get(loser)[1] + ")"); + + // Update past opponents + updatePastOpponents(pastOpponents, winner, loser); + + // Add only teams that haven't been eliminated + if (records.get(winner)[1] < 3) { + activeTeams.add(winner); + } + if (records.get(loser)[1] < 3) { + activeTeams.add(loser); + } + } + + // Calculate Buchholz scores at the end of the group stage + calculateBuchholz(activeTeams, pastOpponents, records, buchholzScores); + + // Proceed to simulate remaining rounds + while (!allTeamsDecided(activeTeams, records)) { + Map> groups = new HashMap<>(); + + // Group teams by their current record (e.g., "2-0", "1-1") + for (Team team : activeTeams) { + int[] record = records.get(team); + String key = record[0] + "-" + record[1]; + groups.putIfAbsent(key, new ArrayList<>()); + groups.get(key).add(team); + } + + List nextRoundTeams = new ArrayList<>(); + Map currentStandings = new HashMap<>(); + + // Find the current round by taking the first team in the activeTeams and adding its record + int currentRound = records.get(activeTeams.get(0))[0] + records.get(activeTeams.get(0))[1] + 1; + + // Calculate the current standings for the current round + calculateCurrentStandings(currentRound, records, currentStandings, buchholzScores); + + // Sort the current standings by their standing + currentStandings = new LinkedHashMap<>(currentStandings); + currentStandings = currentStandings.entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .collect(LinkedHashMap::new, (map, entry) -> map.put(entry.getKey(), entry.getValue()), Map::putAll); + +// // Print the current standings (sorted) +// System.out.println(); +// System.out.println("Round: " + currentRound); +// System.out.println("Current Standings:"); +// for (Map.Entry entry : currentStandings.entrySet()) { +// Team team = entry.getKey(); +// int standing = entry.getValue(); +// +// System.out.println(standing + ". " + team.getName() + " (" + records.get(team)[0] + "-" + records.get(team)[1] + ")"); +// } + + // Process each group separately + for (Map.Entry> entry : groups.entrySet()) { + List group = entry.getValue(); + + // Sort teams by their standing + group.sort(Comparator.comparingInt(currentStandings::get)); + + List matches = new ArrayList<>(); + List availableTeams = new ArrayList<>(group); + + // Create the matchups for the group + for (Team team : group) { + // If the team has already played all of its games, skip it + if (records.get(team)[0] == 3 || records.get(team)[1] == 3) { + availableTeams.remove(team); + continue; + } + + // Continue if the team isn't in the list of available teams + if (!availableTeams.contains(team)) { + continue; + } + +// System.out.println(); +// System.out.println("List of opponents for " + team.getName() +// + " (" + records.get(team)[0] + "-" + records.get(team)[1] + ")" +// + " (Seed: " + team.getSeeding() + ")" +// + " (Standing: " + currentStandings.get(team) + ")" +// + " (Buchholz: " + buchholzScores.get(team) + "):" +// ); + + // Remove the team from the list of available teams + availableTeams.remove(team); + + List uniqueOpponents = new ArrayList<>(); + + for (Team opponent : availableTeams) { + if (!pastOpponents.get(team).contains(opponent)) { + uniqueOpponents.add(opponent); + } + } + + List differentBuchholz = new ArrayList<>(); + List sameBuchholz = new ArrayList<>(); + + for (Team unique : uniqueOpponents) { + if (buchholzScores.get(unique).equals(buchholzScores.get(team))) { + sameBuchholz.add(unique); + } else { + differentBuchholz.add(unique); + } + } + + Team idealMatchup = null; + Team highestStandingDifferentBuchholz = null; + Team highestStandingSameBuchholz = null; + + for (Team other : differentBuchholz) { + if (highestStandingDifferentBuchholz != null) { + int currentDifference = Math.abs(buchholzScores.get(other) - buchholzScores.get(team)); + int highestDifference = Math.abs(buchholzScores.get(highestStandingDifferentBuchholz) - buchholzScores.get(team)); + + if (currentDifference > highestDifference) { + highestStandingDifferentBuchholz = other; + } else if (currentDifference == highestDifference) { + // If the differences are the same, fall back to comparing standings + if (currentStandings.get(other) > currentStandings.get(highestStandingDifferentBuchholz)) { + highestStandingDifferentBuchholz = other; + } + } + } else { + highestStandingDifferentBuchholz = other; + } + +// System.out.println(" + " + other.getName() + " (" + records.get(other)[0] + "-" + records.get(other)[1] + ")" +// + " (Seed: " + other.getSeeding() + ")" +// + " (Standing: " + currentStandings.get(other) + ")" +// + " (Buchholz: " + buchholzScores.get(other) + ")"); + } + + for (Team same : sameBuchholz) { + if (highestStandingSameBuchholz == null || currentStandings.get(same) > currentStandings.get(highestStandingSameBuchholz)) { + highestStandingSameBuchholz = same; + } + +// System.out.println(" - " + same.getName() + " (" + records.get(same)[0] + "-" + records.get(same)[1] + ")" +// + " (Seed: " + same.getSeeding() + ")" +// + " (Standing: " + currentStandings.get(same) + ")" +// + " (Buchholz: " + buchholzScores.get(same) + ")"); + } + + // Choose the highest standing different Buchholz team + // If that's null, choose the highest standing same Buchholz team + // If that's null, output debug information + if (highestStandingDifferentBuchholz != null) { +// System.out.println("Ideal matchup for " + team.getName() + " is " + highestStandingDifferentBuchholz.getName()); + idealMatchup = highestStandingDifferentBuchholz; + } else if (highestStandingSameBuchholz != null) { +// System.out.println("Ideal matchup for " + team.getName() + " is " + highestStandingSameBuchholz.getName()); + idealMatchup = highestStandingSameBuchholz; + } else { + for (Team available : availableTeams) { + if (availableTeams.size() == 1) { +// System.out.println("Ideal matchup for " + team.getName() + " is " + available.getName()); + idealMatchup = available; + break; + } else { + if (idealMatchup == null || currentStandings.get(available) > currentStandings.get(idealMatchup)) { + idealMatchup = available; + } + } + } + } + + if (idealMatchup != null) { + Match match = new Match(team, idealMatchup, true); + matches.add(match); + availableTeams.remove(idealMatchup); + } + } + + // Print all matches information + for (Match match : matches) { + Team team1 = match.getTeam1(); + Team team2 = match.getTeam2(); + +// System.out.println("Match: " + team1.getName() + " (" + records.get(team1)[0] + "-" + records.get(team1)[1] + ")" +// + " (Buchholz: " + buchholzScores.get(team1) + ")" +// + " (Seed: " + team1.getSeeding() + ")" +// + " (Standing: " + currentStandings.get(team1) + ")" +// + " vs " + team2.getName() + " (" + records.get(team2)[0] + "-" + records.get(team2)[1] + ")" +// + " (Buchholz: " + buchholzScores.get(team2) + ")" +// + " (Standing: " + currentStandings.get(team2) + ")" +// + " (Seed: " + team2.getSeeding() + ")" +// ); + + // Simulate the match + Team winner = match.simulate(); + Team loser = (winner == team1) ? team2 : team1; + + // Update records + updateRecords(records, winner, loser); + +// // Print the match result +// printMatchResult(winner, records, loser); + + // Update past opponents + updatePastOpponents(pastOpponents, winner, loser); + + // Add only teams that haven't been eliminated + if (records.get(winner)[1] < 3) { + nextRoundTeams.add(winner); + } + if (records.get(loser)[1] < 3) { + nextRoundTeams.add(loser); + } + } + + // If there's an odd number of teams, the middle-ranked team gets a bye + if (group.size() % 2 == 1) { + Team byeTeam = group.get(group.size() / 2); + nextRoundTeams.add(byeTeam); + } + } + + // Update the list of active teams for the next round + activeTeams = nextRoundTeams; + + // Calculate buchholz scores for the next round + calculateBuchholz(activeTeams, pastOpponents, records, buchholzScores); + } + + // Record final results + for (Map.Entry entry : records.entrySet()) { + Team team = entry.getKey(); + int[] record = entry.getValue(); + String result = record[0] + "-" + record[1]; // Format: "XW-XL" + + // Record the individual team's result (if needed) + if (record[0] <= 3) { + results.get(team).put(result, results.get(team).getOrDefault(result, 0) + 1); + } + } + } + + /** + * Calculate current standings based on the seeding of the teams in the following group order: + * (If no teams are in the group, skip to the next group) + *

+ * First Round: + * - 0-0 + *

+ * Second Round: + * - 1-0 + * - 0-1 + *

+ * Third Round: + * - 2-0 + * - 1-1 + * - 0-2 + *

+ * Fourth Round: + * - 3-0 + * - 2-1 + * - 1-2 + * - 0-3 + *

+ * Fifth Round: + * - 3-0 + * - 3-1 + * - 2-2 + * - 1-3 + * - 0-3 + *

+ * Sixth Round: + * - 3-0 + * - 3-1 + * - 3-2 + * - 2-3 + * - 1-3 + * - 0-3 + * + * @param currentRound The current round of the group stage + * @param records The records of each team + * @param currentStandings The current standings of each team + */ + private static void calculateCurrentStandings(int currentRound, Map records, + Map currentStandings, + Map buchholzScores) { + List> groups = new ArrayList<>(); + + // Define group conditions for each round + switch (currentRound) { + case 1: + groups.add(getTeamsByRecord(records, 0, 0)); + break; + case 2: + groups.add(getTeamsByRecord(records, 1, 0)); + groups.add(getTeamsByRecord(records, 0, 1)); + break; + case 3: + groups.add(getTeamsByRecord(records, 2, 0)); + groups.add(getTeamsByRecord(records, 1, 1)); + groups.add(getTeamsByRecord(records, 0, 2)); + break; + case 4: + groups.add(getTeamsByRecord(records, 3, 0)); + groups.add(getTeamsByRecord(records, 2, 1)); + groups.add(getTeamsByRecord(records, 1, 2)); + groups.add(getTeamsByRecord(records, 0, 3)); + break; + case 5: + groups.add(getTeamsByRecord(records, 3, 0)); + groups.add(getTeamsByRecord(records, 3, 1)); + groups.add(getTeamsByRecord(records, 2, 2)); + groups.add(getTeamsByRecord(records, 1, 3)); + groups.add(getTeamsByRecord(records, 0, 3)); + break; + case 6: + groups.add(getTeamsByRecord(records, 3, 0)); + groups.add(getTeamsByRecord(records, 3, 1)); + groups.add(getTeamsByRecord(records, 3, 2)); + groups.add(getTeamsByRecord(records, 2, 3)); + groups.add(getTeamsByRecord(records, 1, 3)); + groups.add(getTeamsByRecord(records, 0, 3)); + break; + default: + throw new IllegalArgumentException("Unsupported round: " + currentRound); + } + + int standing = 0; + + // Process each group + for (List group : groups) { + sortTeams(group, buchholzScores); + + for (Team team : group) { + standing++; + currentStandings.put(team, standing); + } + } + } + + private static @NotNull List getTeamsByRecord(@NotNull Map records, + int wins, int losses) { + List group = new ArrayList<>(); + + records.forEach((team, record) -> { + if (record[0] == wins && record[1] == losses) { + group.add(team); + } + }); + return group; + } + + private static void sortTeams(@NotNull List group, Map buchholzScores) { + // Sort the teams by seeding first + group.sort(Comparator.comparingInt(Team::getSeeding)); + + // Sort the teams by Buchholz score, then by seeding + group.sort((team1, team2) -> { + int buchholzComparison = Integer.compare(buchholzScores.get(team2), buchholzScores.get(team1)); + + if (buchholzComparison != 0) { + return buchholzComparison; + } + return Integer.compare(team1.getSeeding(), team2.getSeeding()); + }); + } + + private static void printMatchResult(@NotNull Team winner, + @NotNull Map records, + @NotNull Team loser) { + String winnerName = winner.getName(); + String loserName = loser.getName(); + + int[] winnerRecords = records.get(winner); + int[] loserRecords = records.get(loser); + + System.out.println(winnerName + " (" + winnerRecords[0] + "-" + winnerRecords[1] + ")" + + " beat " + loserName + " (" + loserRecords[0] + "-" + loserRecords[1] + ")"); + } + + private static void updateRecords(@NotNull Map records, Team winner, Team loser) { + records.get(winner)[0] += 1; + records.get(loser)[1] += 1; + } + + private static void updatePastOpponents(@NotNull Map> pastOpponents, Team winner, Team loser) { + pastOpponents.get(winner).add(loser); + pastOpponents.get(loser).add(winner); + } + + private static void calculateBuchholz(@NotNull List activeTeams, + Map> pastOpponents, + Map records, + Map buchholzScores) { + // Calculate buchholz scores for the first round + for (Team team : activeTeams) { + int buchholz = 0; + + // Combine the buchholz score of the team's past opponents + for (Team opponent : pastOpponents.get(team)) { + buchholz += records.get(opponent)[0] - records.get(opponent)[1]; + } + + // Set the buchholz score for the team + buchholzScores.put(team, buchholz); + } + } + + private static boolean allTeamsDecided(@NotNull List activeTeams, Map records) { + for (Team team : activeTeams) { + int[] record = records.get(team); + + if (record[0] < 3 && record[1] < 3) { + return false; // At least one team still has games to play + } + } + return true; // All teams have either 3 wins or 3 losses + } + + @SuppressWarnings("NestedMethodCall") + private static void printResults(@NotNull Map> results, int numSimulations) { + System.out.println(); + System.out.println("Individual Team Results:"); + + for (Map.Entry> entry : results.entrySet()) { + Team team = entry.getKey(); + Map recordCounts = entry.getValue(); + + // Create a map to combine probabilities for 3-X and X-3 + double probability3X = 0.0; + double probabilityX3 = 0.0; + + // Combine the 3-1 and 3-2 probabilities into 3-X + probability3X += (recordCounts.getOrDefault("3-1", 0) + + recordCounts.getOrDefault("3-2", 0)) * 100.0 / numSimulations; + + // Combine the 1-3 and 2-3 probabilities into X-3 + probabilityX3 += (recordCounts.getOrDefault("1-3", 0) + + recordCounts.getOrDefault("2-3", 0)) * 100.0 / numSimulations; + + // Prepare the result string + String teamName = team.getName(); + StringBuilder resultString = new StringBuilder("Team: " + teamName + " | "); + + // Add the combined 3-X and X-3 probabilities to the result string + resultString.append("3-X (").append(String.format("%.2f", probability3X)).append("%) "); + resultString.append("X-3 (").append(String.format("%.2f", probabilityX3)).append("%) "); + + // Add the individual probabilities for 3-0, 0-3, and 3-1/3-2, 1-3/2-3 + if (recordCounts.containsKey("3-0")) { + double probability3_0 = (recordCounts.getOrDefault("3-0", 0) * 100.0) / numSimulations; + resultString.append("3-0 (").append(String.format("%.2f", probability3_0)).append("%) "); + } + if (recordCounts.containsKey("0-3")) { + double probability0_3 = (recordCounts.getOrDefault("0-3", 0) * 100.0) / numSimulations; + resultString.append("0-3 (").append(String.format("%.2f", probability0_3)).append("%) "); + } + + // Print the team's result + System.out.println(resultString.toString().trim()); + } + } +} diff --git a/src/main/java/net/foulest/swiss/Main.java b/src/main/java/net/foulest/swiss/Main.java new file mode 100644 index 0000000..93f6795 --- /dev/null +++ b/src/main/java/net/foulest/swiss/Main.java @@ -0,0 +1,80 @@ +/* + * JavaTemplate - a fully featured Java template with Gradle. + * Copyright (C) 2024 Foulest (https://github.com/Foulest) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.foulest.swiss; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * Main class for the program. + * + * @author Foulest + */ +@Data +public final class Main { + + public static final List teams = new ArrayList<>(); + + /** + * The main method of the program. + * + * @param args The program's arguments. + */ + public static void main(String[] args) { + Team g2 = new Team("G2", 1, 2, 1.046); + Team natusVincere = new Team("Natus Vincere", 2, 1, 1.098); + Team vitality = new Team("Vitality", 3, 3, 1.110); + Team spirit = new Team("Spirit", 4, 4, 1.074); + Team mouz = new Team("MOUZ", 5, 5, 1.034); + Team faze = new Team("FaZe", 6, 6, 1.068); + Team heroic = new Team("Heroic", 7, 7, 1.098); + Team _3DMAX = new Team("3DMAX", 8, 16, 1.056); + Team theMongolZ = new Team("The MongolZ", 9, 9, 1.128); + Team liquid = new Team("Liquid", 10, 13, 1.028); + Team gamerLegion = new Team("GamerLegion", 11, 20, 1.032); + Team furia = new Team("FURIA", 12, 8, 1.044); + Team paiN = new Team("paiN", 13, 15, 1.108); + Team wildcard = new Team("Wildcard", 14, 22, 1.056); + Team big = new Team("BIG", 15, 19, 1.060); + Team mibr = new Team("MIBR", 16, 17, 1.064); + + teams.add(g2); + teams.add(natusVincere); + teams.add(vitality); + teams.add(spirit); + teams.add(mouz); + teams.add(faze); + teams.add(heroic); + teams.add(_3DMAX); + teams.add(theMongolZ); + teams.add(liquid); + teams.add(gamerLegion); + teams.add(furia); + teams.add(paiN); + teams.add(wildcard); + teams.add(big); + teams.add(mibr); + + // At 1,000,000 simulations, the data is as accurate as it can be. + // On average, the default simulation of 1,000,000 brackets takes 60 seconds. + Bracket bracket = new Bracket(teams); + bracket.simulateMultipleBrackets(1000000); + } +} diff --git a/src/main/java/net/foulest/swiss/Match.java b/src/main/java/net/foulest/swiss/Match.java new file mode 100644 index 0000000..affbfa0 --- /dev/null +++ b/src/main/java/net/foulest/swiss/Match.java @@ -0,0 +1,63 @@ +package net.foulest.swiss; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +import java.util.Random; + +@Data +@AllArgsConstructor +class Match { + + private Team team1; + private Team team2; + private boolean bestOfThree; + + Team simulate() { + int team1Wins = 0; + int team2Wins = 0; + double winProbability = calculateWinProbability(team1, team2); + + // Generate random numbers in bulk for up to 5 rounds + double[] randomNumbers = new Random().doubles(5).toArray(); + int round = 0; + int maxRounds = bestOfThree ? 3 : 1; + + while (team1Wins < maxRounds && team2Wins < maxRounds) { + if (randomNumbers[round] < winProbability) { + team1Wins++; + } else { + team2Wins++; + } + + round++; + } + return team1Wins == maxRounds ? team1 : team2; + } + + private static double calculateWinProbability(@NotNull Team t1, @NotNull Team t2) { + final double W1_DIV_MAX_RANK = 0.20 / 100.0; // Precompute constant + final double W2 = 0.80; + final double SCALE = 5.0; + + // Compute scores + double t1Score = W1_DIV_MAX_RANK * (100.0 - t1.getWorldRanking()) + W2 * t1.getAvgPlayerRating(); + double t2Score = W1_DIV_MAX_RANK * (100.0 - t2.getWorldRanking()) + W2 * t2.getAvgPlayerRating(); + + // Calculate win probability + return 1.0 / (1.0 + Math.exp(-SCALE * (t1Score - t2Score))); + } + + public static void displayWinnerFromProbability(@NotNull Team t1, @NotNull Team t2) { + double winProbability = calculateWinProbability(t1, t2); + String t1Name = t1.getName(); + String t2Name = t2.getName(); + + if (winProbability >= 0.5) { + System.out.println(t1Name + " has a " + (winProbability * 100) + "% chance of winning against " + t2Name); + } else { + System.out.println(t2Name + " has a " + ((1 - winProbability) * 100) + "% chance of winning against " + t1Name); + } + } +} diff --git a/src/main/java/net/foulest/swiss/Pair.java b/src/main/java/net/foulest/swiss/Pair.java new file mode 100644 index 0000000..d5aff05 --- /dev/null +++ b/src/main/java/net/foulest/swiss/Pair.java @@ -0,0 +1,26 @@ +package net.foulest.swiss; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +/** + * A pair of objects. + * + * @param The type of the first object. + * @param The type of the second object. + * @author Foulest + */ +@Data +@AllArgsConstructor +public class Pair { + + private final X first; + private final Y last; + + @Contract("_, _ -> new") + public static @NotNull Pair of(X x, Y y) { + return new Pair<>(x, y); + } +} diff --git a/src/main/java/net/foulest/swiss/Team.java b/src/main/java/net/foulest/swiss/Team.java new file mode 100644 index 0000000..be38380 --- /dev/null +++ b/src/main/java/net/foulest/swiss/Team.java @@ -0,0 +1,25 @@ +package net.foulest.swiss; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.Nullable; + +@Data +@AllArgsConstructor +public class Team { + + private String name; + private int seeding; // Higher number = better seed + private int worldRanking; // Lower number = better rank + private double avgPlayerRating; // Average player rating + + // Method to get the Team object based on the team name + public static @Nullable Team getTeamByName(String name) { + for (Team team : Main.teams) { + if (team.name.equalsIgnoreCase(name)) { + return team; + } + } + return null; + } +} diff --git a/src/main/java/net/foulest/template/Main.java b/src/main/java/net/foulest/template/Main.java deleted file mode 100644 index a6d0c5c..0000000 --- a/src/main/java/net/foulest/template/Main.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * JavaTemplate - a fully featured Java template with Gradle. - * Copyright (C) 2024 Foulest (https://github.com/Foulest) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.foulest.template; - -/** - * Main class for the program. - * - * @author Foulest - */ -public final class Main { - - /** - * The main method of the program. - * - * @param args The program's arguments. - */ - public static void main(String[] args) { - System.out.println("Hello, world!"); - } -}