Fix not optimal matchlist found with recently played matches

This commit is contained in:
Kalle
2026-08-08 15:27:11 +03:00
parent 4f56f2a79f
commit fcde55644c
2 changed files with 62 additions and 18 deletions

View File

@@ -55,7 +55,11 @@ function generateWithInput(
if (validationError) return err(validationError);
const { seededShuffle } = seededRandom(input.seed);
const stages = seededShuffle(resolveCommonStages());
// trying the least recently played maps first lets the search find a good
// enough list before the recursion depth cap is hit
const stages = seededShuffle(resolveCommonStages()).sort(
(a, b) => recencyPenalty(a) - recencyPenalty(b),
);
const mapList: Array<ModeWithStageAndScore & { score: number }> = [];
const bestMapList: { maps?: Array<ModeWithStageAndScore>; score: number } = {
score: Number.POSITIVE_INFINITY,
@@ -108,10 +112,12 @@ function generateWithInput(
return true;
};
const success = backtrack();
const searchExhausted = backtrack();
if (!success) return err("MAX_RECURSION_DEPTH_EXCEEDED");
// a complete list found before the depth cap was hit is still valid,
// only its optimality is unproven
if (bestMapList.maps) return ok(bestMapList.maps);
if (!searchExhausted) return err("MAX_RECURSION_DEPTH_EXCEEDED");
return err("COULD_NOT_GENERATE_MAPLIST");
@@ -435,26 +441,24 @@ function generateWithInput(
score += 100;
}
if (input.recentlyPlayedMaps) {
for (const map of mapList) {
const recentIndex = input.recentlyPlayedMaps.findIndex(
(recent) =>
recent.stageId === map.stageId && recent.mode === map.mode,
);
if (recentIndex !== -1) {
const recencyPenalty = Math.max(
10 - Math.floor(recentIndex / 2) * 2,
0,
);
score += recencyPenalty;
}
}
for (const map of mapList) {
score += recencyPenalty(map);
}
return score;
}
function recencyPenalty(map: Pick<TournamentMapListMap, "stageId" | "mode">) {
if (!input.recentlyPlayedMaps) return 0;
const recentIndex = input.recentlyPlayedMaps.findIndex(
(recent) => recent.stageId === map.stageId && recent.mode === map.mode,
);
if (recentIndex === -1) return 0;
return Math.max(10 - Math.floor(recentIndex / 2) * 2, 0);
}
function lastMapIsAGoodTieBreaker() {
// guaranteed to be good if more than one mode
if (!tournamentIsOneModeOnly()) return true;

View File

@@ -798,6 +798,46 @@ describe("TournamentMapListGeneratorOneMode", () => {
});
describe("Recently played maps", () => {
test("One mode Bo7 avoids recently played maps when a full avoiding list exists", () => {
const team1Pool = new MapPool(
([1, 2, 3, 4, 5, 6] as const).map((stageId) => ({
mode: "SZ" as const,
stageId,
})),
);
const team2Pool = new MapPool(
([7, 8, 9, 10, 11, 12] as const).map((stageId) => ({
mode: "SZ" as const,
stageId,
})),
);
// the bo5 both teams played right before this match
const recentlyPlayedMaps = ([1, 7, 2, 8, 3] as const).map((stageId) => ({
mode: "SZ" as const,
stageId,
}));
const mapList = generateMaps({
count: 7,
seed: "1000",
teams: [
{ id: 1, maps: team1Pool },
{ id: 2, maps: team2Pool },
],
tiebreakerMaps: new MapPool([]),
modesIncluded: ["SZ"],
recentlyPlayedMaps,
});
const recentMapsInList = mapList.filter((map) =>
recentlyPlayedMaps.some(
(recent) => recent.mode === map.mode && recent.stageId === map.stageId,
),
);
expect(recentMapsInList).toEqual([]);
});
test("Avoids recently played maps when possible", () => {
const recentlyPlayedMaps = [
{ mode: "SZ" as const, stageId: 4 as const },