mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-12 06:06:28 -05:00
SQ new map algorithm fix unintended maps appearing
This commit is contained in:
@@ -50,6 +50,8 @@ export function* generate(args: {
|
||||
considerGuaranteed?: boolean;
|
||||
/** Initial weights for specific stage-mode combinations. Key format: `${mode}-${stageId}` (generate via `MapList.stageModeKey`). Negative weights can be used to deprioritize certain maps. */
|
||||
initialWeights?: Map<string, number>;
|
||||
/** Skip the ensureMinimumCandidates check that inflates weights to ensure half the pool is available. Useful when initial weights already define the desired selection. */
|
||||
skipEnsureMinimumCandidates?: boolean;
|
||||
}): Generator<Array<ModeWithStage>, Array<ModeWithStage>, GenerateNext> {
|
||||
if (args.mapPool.isEmpty()) {
|
||||
while (true) yield [];
|
||||
@@ -84,11 +86,13 @@ export function* generate(args: {
|
||||
);
|
||||
}
|
||||
|
||||
ensureMinimumCandidates({
|
||||
mapPool: args.mapPool,
|
||||
stageWeights,
|
||||
stageModeWeights,
|
||||
});
|
||||
if (!args.skipEnsureMinimumCandidates) {
|
||||
ensureMinimumCandidates({
|
||||
mapPool: args.mapPool,
|
||||
stageWeights,
|
||||
stageModeWeights,
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < amount; i++) {
|
||||
const mode = currentModeOrder[i % currentModeOrder.length];
|
||||
@@ -139,12 +143,15 @@ function initializeWeights(
|
||||
const stageWeights = new Map<StageId, number>();
|
||||
const stageModeWeights = new Map<string, number>();
|
||||
|
||||
const hasInitialWeights = initialWeights && initialWeights.size > 0;
|
||||
|
||||
for (const mode of modes) {
|
||||
const stageIds = mapPool[mode];
|
||||
for (const stageId of stageIds) {
|
||||
stageWeights.set(stageId, 0);
|
||||
const key = modeStageKey(mode, stageId);
|
||||
const initialWeight = initialWeights?.get(key) ?? 0;
|
||||
const initialWeight =
|
||||
initialWeights?.get(key) ?? (hasInitialWeights ? -1000 : 0);
|
||||
stageModeWeights.set(key, initialWeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import type { StageId } from "~/modules/in-game-lists/types";
|
||||
import * as Test from "~/utils/Test";
|
||||
import {
|
||||
mapModePreferencesToModeList,
|
||||
matchMapList,
|
||||
normalizeAndCombineWeights,
|
||||
} from "./match.server";
|
||||
|
||||
vi.mock("~/features/sendouq/core/default-maps.server", () => ({
|
||||
getDefaultMapWeights: vi.fn(),
|
||||
}));
|
||||
|
||||
import { getDefaultMapWeights } from "~/features/sendouq/core/default-maps.server";
|
||||
import { SENDOUQ_BEST_OF } from "~/features/sendouq/q-constants";
|
||||
|
||||
describe("mapModePreferencesToModeList()", () => {
|
||||
test("returns default list if no preferences", () => {
|
||||
const modeList = mapModePreferencesToModeList([], []);
|
||||
@@ -218,3 +227,103 @@ describe("normalizeAndCombineWeights()", () => {
|
||||
expect(result.get("map2-TC")).toBe(40);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matchMapList()", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("maps not in team preferences or defaults should not be preferred over default maps", async () => {
|
||||
// Note stage 23 (Lemuria Hub) is NOT in defaults
|
||||
const defaultStageIds: StageId[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
|
||||
const mockDefaults = new Map<string, number>();
|
||||
|
||||
for (const stageId of defaultStageIds) {
|
||||
mockDefaults.set(`SZ-${stageId}`, -1);
|
||||
}
|
||||
|
||||
vi.mocked(getDefaultMapWeights).mockResolvedValue(mockDefaults);
|
||||
|
||||
// Both teams have empty preferences (no map pools set)
|
||||
// This forces the system to rely entirely on defaults
|
||||
const emptyPreferences = {
|
||||
modes: [{ mode: "SZ" as const, preference: "PREFER" as const }],
|
||||
pool: [],
|
||||
};
|
||||
|
||||
const result = await matchMapList(
|
||||
{
|
||||
preferences: [{ userId: 1, preferences: emptyPreferences }],
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
preferences: [{ userId: 2, preferences: emptyPreferences }],
|
||||
id: 2,
|
||||
},
|
||||
);
|
||||
|
||||
const szMaps = result.filter((m) => m.mode === "SZ");
|
||||
|
||||
for (const map of szMaps) {
|
||||
expect(defaultStageIds).toContain(map.stageId);
|
||||
}
|
||||
});
|
||||
|
||||
test("user selected maps should be preferred over default maps even with small pool", async () => {
|
||||
// User selected stages - just 7 stages per mode (less than half of 25)
|
||||
// Note: stages 1 (EELTAIL_ALLEY) and 9 (STURGEON_SHIPYARD) are banned for SZ
|
||||
const userSelectedStageIds: StageId[] = [0, 2, 3, 4, 5, 6, 7];
|
||||
|
||||
// Default stages include many more maps that users did NOT select
|
||||
const defaultStageIds: StageId[] = [
|
||||
8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||
];
|
||||
|
||||
const mockDefaults = new Map<string, number>();
|
||||
for (const stageId of defaultStageIds) {
|
||||
mockDefaults.set(`SZ-${stageId}`, -SENDOUQ_BEST_OF);
|
||||
}
|
||||
|
||||
vi.mocked(getDefaultMapWeights).mockResolvedValue(mockDefaults);
|
||||
|
||||
// Both teams have selected the same 5 stages
|
||||
const teamPreferences = {
|
||||
modes: [{ mode: "SZ" as const, preference: "PREFER" as const }],
|
||||
pool: [
|
||||
{
|
||||
mode: "SZ" as const,
|
||||
stages: userSelectedStageIds,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = await matchMapList(
|
||||
{
|
||||
preferences: [
|
||||
{ userId: 1, preferences: teamPreferences },
|
||||
{ userId: 2, preferences: teamPreferences },
|
||||
{ userId: 3, preferences: teamPreferences },
|
||||
{ userId: 4, preferences: teamPreferences },
|
||||
],
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
preferences: [
|
||||
{ userId: 5, preferences: teamPreferences },
|
||||
{ userId: 6, preferences: teamPreferences },
|
||||
{ userId: 7, preferences: teamPreferences },
|
||||
{ userId: 8, preferences: teamPreferences },
|
||||
],
|
||||
id: 2,
|
||||
},
|
||||
);
|
||||
|
||||
const szMaps = result.filter((m) => m.mode === "SZ");
|
||||
|
||||
// All selected maps should come from user preferences, not defaults
|
||||
for (const map of szMaps) {
|
||||
expect(userSelectedStageIds).toContain(map.stageId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -195,6 +195,7 @@ export async function matchMapList(
|
||||
),
|
||||
),
|
||||
initialWeights: weights,
|
||||
skipEnsureMinimumCandidates: true,
|
||||
});
|
||||
generator.next();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { db } from "~/db/sql";
|
||||
import type { UserMapModePreferences } from "~/db/tables";
|
||||
import type { StageId } from "~/modules/in-game-lists/types";
|
||||
import { dbInsertUsers, dbReset } from "~/utils/Test";
|
||||
import { SENDOUQ_BEST_OF } from "../q-constants";
|
||||
import {
|
||||
clearCacheForTesting,
|
||||
getDefaultMapWeights,
|
||||
@@ -178,7 +179,7 @@ describe("getDefaultMapWeights()", () => {
|
||||
expect(tcMaps.length).toBe(7);
|
||||
});
|
||||
|
||||
test("assigns weight of -1 to all selected maps", async () => {
|
||||
test("assigns weight of -SENDOUQ_BEST_OF to all selected maps", async () => {
|
||||
mockSeasonCurrent.mockReturnValue({
|
||||
nth: 1,
|
||||
starts: new Date("2023-01-01"),
|
||||
@@ -201,7 +202,7 @@ describe("getDefaultMapWeights()", () => {
|
||||
const result = await getDefaultMapWeights();
|
||||
|
||||
for (const weight of result.values()) {
|
||||
expect(weight).toBe(-1);
|
||||
expect(weight).toBe(-SENDOUQ_BEST_OF);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import * as Seasons from "~/features/mmr/core/Seasons";
|
||||
import { modesShort } from "~/modules/in-game-lists/modes";
|
||||
import { logger } from "~/utils/logger";
|
||||
import * as QRepository from "../QRepository.server";
|
||||
import { SENDOUQ_BEST_OF } from "../q-constants";
|
||||
|
||||
let cachedDefaults: Map<string, number> | null = null;
|
||||
|
||||
const ONE_WEEK_IN_DAYS = 7;
|
||||
const DEFAULT_MAP_WEIGHT = -1;
|
||||
const DEFAULT_MAP_WEIGHT = -SENDOUQ_BEST_OF;
|
||||
const TOP_MAPS_PER_MODE = 7;
|
||||
|
||||
export function clearCacheForTesting() {
|
||||
|
||||
Reference in New Issue
Block a user