diff --git a/app/features/map-list-generator/core/MapList.test.ts b/app/features/map-list-generator/core/MapList.test.ts index ef819e728..164894251 100644 --- a/app/features/map-list-generator/core/MapList.test.ts +++ b/app/features/map-list-generator/core/MapList.test.ts @@ -700,16 +700,15 @@ describe("MapList.resume()", () => { ).toBe("SZ"); }); - it("exclusion is keyed on (mode, stage), not stage alone", () => { + it("avoids the just-played stage when alternatives exist, even across modes", () => { const sharedPool = new MapPool({ TW: [], - SZ: [1, 2], - TC: [1, 2], + SZ: [1, 2, 3, 4, 5], + TC: [1, 2, 3, 4, 5], RM: [], CB: [], }); - const seenForTC = new Set(); for (let i = 0; i < 50; i++) { const gen = MapList.resume({ mapPool: sharedPool, @@ -718,10 +717,10 @@ describe("MapList.resume()", () => { gen.next(); const next = gen.next({ amount: 1 }).value![0]; expect(next.mode).toBe("TC"); - seenForTC.add(next.stageId); + expect( + next.stageId, + "same stage picked back-to-back across modes", + ).not.toBe(1); } - - expect(seenForTC.has(1)).toBe(true); - expect(seenForTC.has(2)).toBe(true); }); }); diff --git a/app/features/map-list-generator/core/MapList.ts b/app/features/map-list-generator/core/MapList.ts index 6ffb18695..69ce9bea6 100644 --- a/app/features/map-list-generator/core/MapList.ts +++ b/app/features/map-list-generator/core/MapList.ts @@ -54,6 +54,8 @@ export function* generate(args: { skipEnsureMinimumCandidates?: boolean; /** Fixed mode order — when set, skips the random `modeOrders` shuffle and uses only this order. Intended for `resume`. */ modeOrder?: ModeShort[]; + /** Initial weights for stages (mode-agnostic). Used by `resume` to carry over stage-level penalties from history. */ + initialStageWeights?: Map; }): Generator, Array, GenerateNext> { if (args.mapPool.isEmpty()) { while (true) yield []; @@ -65,6 +67,7 @@ export function* generate(args: { modes, args.mapPool.parsed, args.initialWeights, + args.initialStageWeights, ); const orderedModes = args.modeOrder ? [args.modeOrder] : modeOrders(modes); let currentOrderIndex = 0; @@ -164,13 +167,28 @@ export function resume(args: { initialWeights.set(modeStageKey(pair.mode, pair.stageId), 0); } for (const { mode, stageId } of args.history) { - initialWeights.set(modeStageKey(mode, stageId), -1000); + initialWeights.set(modeStageKey(mode, stageId), -100); + } + + const STAGE_PENALTY = -20; + const initialStageWeights = new Map(); + for (const pair of args.mapPool.stageModePairs) { + if (!initialStageWeights.has(pair.stageId)) { + initialStageWeights.set(pair.stageId, 0); + } + } + for (const { stageId } of args.history) { + for (const [key, value] of initialStageWeights.entries()) { + initialStageWeights.set(key, value + 1); + } + initialStageWeights.set(stageId, STAGE_PENALTY); } return generate({ mapPool: args.mapPool, modeOrder, initialWeights: initialWeights.size > 0 ? initialWeights : undefined, + initialStageWeights, skipEnsureMinimumCandidates: true, }); } @@ -179,6 +197,7 @@ function initializeWeights( modes: ModeShort[], mapPool: ReadonlyMapPoolObject, initialWeights?: Map, + initialStageWeights?: Map, ) { const stageWeights = new Map(); const stageModeWeights = new Map(); @@ -188,7 +207,7 @@ function initializeWeights( for (const mode of modes) { const stageIds = mapPool[mode]; for (const stageId of stageIds) { - stageWeights.set(stageId, 0); + stageWeights.set(stageId, initialStageWeights?.get(stageId) ?? 0); const key = modeStageKey(mode, stageId); const initialWeight = initialWeights?.get(key) ?? (hasInitialWeights ? -1000 : 0);