From 96f7a77b05001274c6f42ffcdc04b7fcbdeb21e0 Mon Sep 17 00:00:00 2001 From: RoopjeetSingh Date: Mon, 29 Jun 2026 21:18:26 -0400 Subject: [PATCH 1/2] fixed Tournament should be refactored to avoid hardcoding maps Fixes #154 Specifically the hardcoded maps part of it --- .../application/TournamentMatchScheduler.java | 31 +++- ...urnamentMatchSchedulerIntegrationTest.java | 133 +++++++++--------- ...ournamentResultHandlerIntegrationTest.java | 5 + .../TournamentServiceIntegrationTest.java | 5 + 4 files changed, 103 insertions(+), 71 deletions(-) diff --git a/src/main/java/org/bytefight/webserver/tournament/application/TournamentMatchScheduler.java b/src/main/java/org/bytefight/webserver/tournament/application/TournamentMatchScheduler.java index f15bcc64..50c2dce4 100644 --- a/src/main/java/org/bytefight/webserver/tournament/application/TournamentMatchScheduler.java +++ b/src/main/java/org/bytefight/webserver/tournament/application/TournamentMatchScheduler.java @@ -11,6 +11,7 @@ import java.util.Set; import java.util.concurrent.ThreadLocalRandom; +import org.bytefight.webserver.competition.domain.Competition; import org.bytefight.webserver.gamematch.application.GameMatchService; import org.bytefight.webserver.gamematch.domain.GameMatch; import org.bytefight.webserver.gamematch.domain.MatchReason; @@ -47,8 +48,7 @@ @RequiredArgsConstructor public class TournamentMatchScheduler { private static final String MAP_SETTING_KEY = "map"; - private static final List TOURNAMENT_SERIES_MAPS = - List.of("butterfly", "pumpkin", "ghost", "catzilla", "pickaxe", "shuriken", "squid"); + private static final String TOURNAMENT_MAPS_SETTING_KEY = "tournamentMaps"; private final TournamentMatchRepository tournamentMatchRepository; private final TournamentGameRepository tournamentGameRepository; @@ -233,7 +233,8 @@ public void queueSeriesGame(TournamentMatch match) { // Determine the next game number (1-based). int nextGameNumber = existingGames.size() + 1; - Map matchSettings = buildSeriesMatchSettings(existingGames); + List tournamentMaps = getTournamentMaps(match.getTournament().getCompetition()); + Map matchSettings = buildSeriesMatchSettings(existingGames, tournamentMaps); // Create the underlying GameMatch and push it to the queue. GameMatch gameMatch = @@ -263,11 +264,31 @@ public void queueSeriesGame(TournamentMatch match) { tournamentMatchRepository.save(match); } + /** + * Reads {@code tournamentMaps} from the competition's settings. Throws if the key is absent or + * not a List, since map selection cannot proceed without it. + */ + @SuppressWarnings("unchecked") + private List getTournamentMaps(Competition competition) { + Map settings = competition.getSettings(); + Object value = settings == null ? null : settings.get(TOURNAMENT_MAPS_SETTING_KEY); + if (!(value instanceof List)) { + throw new IllegalStateException( + "Competition '" + + competition.getSlug() + + "' is missing the required '" + + TOURNAMENT_MAPS_SETTING_KEY + + "' setting."); + } + return (List) value; + } + /** * Ensures map uniqueness inside a single best-of series. Chooses randomly from the remaining * unused maps. Once all known maps have been used, returns null so the engine can auto-select. */ - private Map buildSeriesMatchSettings(List existingGames) { + private Map buildSeriesMatchSettings( + List existingGames, List tournamentMaps) { Set usedMaps = new HashSet<>(); for (TournamentGame game : existingGames) { Map settings = game.getGameMatch().getMatchSettings(); @@ -281,7 +302,7 @@ private Map buildSeriesMatchSettings(List existi } List availableMaps = new ArrayList<>(); - for (String mapName : TOURNAMENT_SERIES_MAPS) { + for (String mapName : tournamentMaps) { if (!usedMaps.contains(mapName)) { availableMaps.add(mapName); } diff --git a/src/test/java/org/bytefight/webserver/tournament/TournamentMatchSchedulerIntegrationTest.java b/src/test/java/org/bytefight/webserver/tournament/TournamentMatchSchedulerIntegrationTest.java index 3b224d1e..71a8f0af 100644 --- a/src/test/java/org/bytefight/webserver/tournament/TournamentMatchSchedulerIntegrationTest.java +++ b/src/test/java/org/bytefight/webserver/tournament/TournamentMatchSchedulerIntegrationTest.java @@ -2,8 +2,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import jakarta.transaction.Transactional; + +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; import org.bytefight.webserver.FullStackIntegrationTestBase; @@ -24,6 +30,7 @@ import org.bytefight.webserver.tournament.domain.Tournament; import org.bytefight.webserver.tournament.domain.TournamentBracketType; import org.bytefight.webserver.tournament.domain.TournamentEntry; +import org.bytefight.webserver.tournament.domain.TournamentGame; import org.bytefight.webserver.tournament.domain.TournamentMatch; import org.bytefight.webserver.tournament.domain.TournamentMatchState; import org.bytefight.webserver.tournament.domain.TournamentStatus; @@ -147,72 +154,62 @@ void processTournamentAfterManualCompletionAdvancesBracket() { assertEquals(2, queued.size()); } - // the test has incorrect map data which is why I commented it out for now - // @Test - // @Transactional - // void queueSeriesGameUsesUniqueMapsThenFallsBackToEngineChoice() { - // Competition competition = createCompetition("comp-scheduler-maps", true); - // Tournament tournament = createTournament(competition); - // Team teamOne = createTeamWithSubmission(competition, "Team One"); - // Team teamTwo = createTeamWithSubmission(competition, "Team Two"); - - // TournamentEntry entryOne = tournamentEntryRepository.save(createEntry(tournament, teamOne, - // 1)); - // TournamentEntry entryTwo = tournamentEntryRepository.save(createEntry(tournament, teamTwo, - // 2)); - - // TournamentMatch match = TournamentMatch.builder() - // .tournament(tournament) - // .bracketType(TournamentBracketType.GRAND_FINAL) - // .roundNumber(1) - // .matchIndex(1) - // .teamOneEntry(entryOne) - // .teamTwoEntry(entryTwo) - // .state(TournamentMatchState.PENDING) - // .seriesLength(TournamentBracketBuilder.GRAND_FINAL_SERIES_LENGTH) - // .teamOneSeriesWins(0) - // .teamTwoSeriesWins(0) - // .build(); - // match = tournamentMatchRepository.save(match); - - // for (int i = 0; i < 8; i++) { - // tournamentMatchScheduler.queueSeriesGame(match); - // } - - // TournamentMatch refreshed = - // tournamentMatchRepository.findById(match.getId()).orElseThrow(); - // List games = - // tournamentGameRepository.findByTournamentMatchOrderByGameNumberAsc(refreshed); - // assertEquals(8, games.size()); - - // List expectedMaps = List.of( - // "the temple", - // "the complex", - // "matrix", - // "maze", - // "spiral", - // "disjoint", - // "big spiral" - // ); - - // Set expectedMapSet = Set.copyOf(expectedMaps); - // Set usedMapsInSeries = new HashSet<>(); - // for (int i = 0; i < expectedMaps.size(); i++) { - // Object mapNameValue = games.get(i).getGameMatch().getMatchSettings().get("map"); - // assertTrue(mapNameValue instanceof String, "Map should be present for the first seven - // games."); - // String mapName = (String) mapNameValue; - // assertTrue(expectedMapSet.contains(mapName), "Map should come from the tournament map - // pool."); - // assertTrue(usedMapsInSeries.add(mapName), "Map should be unique within the series."); - // } - // assertEquals(expectedMaps.size(), usedMapsInSeries.size(), "All unique maps should be - // consumed first."); - - // assertTrue(games.get(7).getGameMatch().getMatchSettings().isEmpty(), - // "After all unique maps are used, match settings should be empty so engine can - // choose."); - // } + @Test + @Transactional + void queueSeriesGameUsesUniqueMapsThenFallsBackToEngineChoice() { + Competition competition = createCompetition("comp-scheduler-maps", true); + Tournament tournament = createTournament(competition); + Team teamOne = createTeamWithSubmission(competition, "Team One"); + Team teamTwo = createTeamWithSubmission(competition, "Team Two"); + + TournamentEntry entryOne = tournamentEntryRepository.save(createEntry(tournament, teamOne, 1)); + TournamentEntry entryTwo = tournamentEntryRepository.save(createEntry(tournament, teamTwo, 2)); + + TournamentMatch match = + TournamentMatch.builder() + .tournament(tournament) + .bracketType(TournamentBracketType.GRAND_FINAL) + .roundNumber(1) + .matchIndex(1) + .teamOneEntry(entryOne) + .teamTwoEntry(entryTwo) + .state(TournamentMatchState.PENDING) + .seriesLength(TournamentBracketBuilder.GRAND_FINAL_SERIES_LENGTH) + .teamOneSeriesWins(0) + .teamTwoSeriesWins(0) + .build(); + match = tournamentMatchRepository.save(match); + + for (int i = 0; i < 8; i++) { + tournamentMatchScheduler.queueSeriesGame(match); + } + + TournamentMatch refreshed = tournamentMatchRepository.findById(match.getId()).orElseThrow(); + List games = + tournamentGameRepository.findByTournamentMatchOrderByGameNumberAsc(refreshed); + assertEquals(8, games.size()); + + // These must match the tournamentMaps set on the competition in createCompetition(). + List expectedMaps = + List.of("butterfly", "pumpkin", "ghost", "catzilla", "pickaxe", "shuriken", "squid"); + + Set expectedMapSet = Set.copyOf(expectedMaps); + Set usedMapsInSeries = new HashSet<>(); + for (int i = 0; i < expectedMaps.size(); i++) { + Object mapNameValue = games.get(i).getGameMatch().getMatchSettings().get("map"); + assertTrue(mapNameValue instanceof String, "Map should be present for the first seven games."); + String mapName = (String) mapNameValue; + assertTrue( + expectedMapSet.contains(mapName), "Map should come from the tournament map pool."); + assertTrue(usedMapsInSeries.add(mapName), "Map should be unique within the series."); + } + assertEquals( + expectedMaps.size(), usedMapsInSeries.size(), "All unique maps should be consumed first."); + + assertTrue( + games.get(7).getGameMatch().getMatchSettings().isEmpty(), + "After all unique maps are used, match settings should be empty so engine can choose."); + } private void createSixSeededEntries(Tournament tournament, Competition competition) { Team team1 = createTeamWithSubmission(competition, "Seed 1"); @@ -269,6 +266,10 @@ private Competition createCompetition(String slug, boolean active) { competition.setActive(active); competition.setWhitelisted(false); competition.setMaxPlayersPerTeam(2); + competition.setSettings( + Map.of( + "tournamentMaps", + List.of("butterfly", "pumpkin", "ghost", "catzilla", "pickaxe", "shuriken", "squid"))); Competition saved = competitionRepository.save(competition); ensureTournamentLadder(saved); return saved; diff --git a/src/test/java/org/bytefight/webserver/tournament/TournamentResultHandlerIntegrationTest.java b/src/test/java/org/bytefight/webserver/tournament/TournamentResultHandlerIntegrationTest.java index 3b2d7d5f..60560423 100644 --- a/src/test/java/org/bytefight/webserver/tournament/TournamentResultHandlerIntegrationTest.java +++ b/src/test/java/org/bytefight/webserver/tournament/TournamentResultHandlerIntegrationTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import java.util.List; +import java.util.Map; import java.util.UUID; import org.bytefight.webserver.FullStackIntegrationTestBase; @@ -597,6 +598,10 @@ private Competition createCompetition(String slug, boolean active) { competition.setActive(active); competition.setWhitelisted(false); competition.setMaxPlayersPerTeam(2); + competition.setSettings( + Map.of( + "tournamentMaps", + List.of("butterfly", "pumpkin", "ghost", "catzilla", "pickaxe", "shuriken", "squid"))); Competition saved = competitionRepository.save(competition); ensureTournamentLadder(saved); return saved; diff --git a/src/test/java/org/bytefight/webserver/tournament/TournamentServiceIntegrationTest.java b/src/test/java/org/bytefight/webserver/tournament/TournamentServiceIntegrationTest.java index f05fe5fd..d51a3a79 100644 --- a/src/test/java/org/bytefight/webserver/tournament/TournamentServiceIntegrationTest.java +++ b/src/test/java/org/bytefight/webserver/tournament/TournamentServiceIntegrationTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; +import java.util.Map; import java.util.UUID; import org.bytefight.webserver.FullStackIntegrationTestBase; @@ -339,6 +340,10 @@ private Competition createCompetition(String slug, boolean active) { competition.setActive(active); competition.setWhitelisted(false); competition.setMaxPlayersPerTeam(2); + competition.setSettings( + Map.of( + "tournamentMaps", + List.of("butterfly", "pumpkin", "ghost", "catzilla", "pickaxe", "shuriken", "squid"))); Competition saved = competitionRepository.save(competition); ensureTournamentLadder(saved); return saved; From 7d9774c6553cb740816c7cfc4bfd75f645b5853b Mon Sep 17 00:00:00 2001 From: Jaeheon Shim Date: Fri, 24 Jul 2026 00:30:17 -0400 Subject: [PATCH 2/2] Remove RequireTurnstile from createGameMatch endpoint Turnstile creates unnecessary friction for competitors. It has been shown that Turnstile can be bypassed with little effort, so it only harms the good guys. We should remove it and rely on monitoring to detect automation. --- .../webserver/gamematch/infra/PrivateGameMatchController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/bytefight/webserver/gamematch/infra/PrivateGameMatchController.java b/src/main/java/org/bytefight/webserver/gamematch/infra/PrivateGameMatchController.java index 1416fba6..85a84524 100644 --- a/src/main/java/org/bytefight/webserver/gamematch/infra/PrivateGameMatchController.java +++ b/src/main/java/org/bytefight/webserver/gamematch/infra/PrivateGameMatchController.java @@ -43,7 +43,6 @@ public class PrivateGameMatchController { private final GameMatchService gameMatchService; @PostMapping - @RequireTurnstile public ResponseEntity> createGameMatch( @AuthenticationPrincipal User user, @RequestBody CreateMatchDto createMatchDto) { Player player =