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 = 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;