diff --git a/src/services/rngService.ts b/src/services/rngService.ts index d277c50e..876d7584 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -151,4 +151,57 @@ export class SRng { arr[lastIdx] = tmp; } } + + shuffledArray(inarr: readonly T[]): T[] { + const arr = [...inarr]; + this.shuffleArray(arr); + return arr; + } } + +export const sequentiallyUniqueRandomElement = ( + deck: readonly T[], + idx: number, + lookbehind: number, + seed: number = 0 +): T | undefined => { + // This algorithm may modify a shuffle up to index `lookbehind + 1`. It assumes that the last `lookbehind` cards are not adjusted. + if (lookbehind + 1 >= deck.length - lookbehind) { + throw new Error( + `this algorithm cannot guarantee ${lookbehind} unique cards in a row with a deck of size ${deck.length}` + ); + } + + const iteration = Math.trunc(idx / deck.length); + const card = idx % deck.length; + const currentShuffle = new SRng(mixSeeds(new SRng(iteration).randomInt(0, 100_000), seed)).shuffledArray(deck); + if (card < currentShuffle.length - lookbehind) { + // We are indexing before the end of the deck, so adjustments may be needed to achieve uniqueness. + const window: T[] = []; + { + const previousShuffle = new SRng( + mixSeeds(new SRng(iteration - 1).randomInt(0, 100_000), seed) + ).shuffledArray(deck); + for (let i = previousShuffle.length - lookbehind; i != previousShuffle.length; ++i) { + window.push(previousShuffle[i]); + } + } + // From this point on, `window.length == lookbehind` should hold. + for (let i = 0; i != lookbehind; ++i) { + if (window.indexOf(currentShuffle[i]) != -1) { + for (let j = i; ; ++j) { + // `j < currentShuffle.length - lookbehind` should hold. + if (window.indexOf(currentShuffle[j]) == -1) { + const tmp = currentShuffle[j]; + currentShuffle[j] = currentShuffle[i]; + currentShuffle[i] = tmp; + break; + } + } + } + window.splice(0, 1); + window.push(currentShuffle[i]); + } + } + return currentShuffle[card]; +}; diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index cd4139ac..423bd776 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -9,7 +9,7 @@ import darvoDeals from "@/static/fixed_responses/worldState/darvoDeals.json"; import { buildConfig } from "@/src/services/buildConfigService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; -import { getRandomElement, getRandomInt, SRng } from "@/src/services/rngService"; +import { getRandomElement, getRandomInt, sequentiallyUniqueRandomElement, SRng } from "@/src/services/rngService"; import { eMissionType, ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus"; import { ICalendarDay, @@ -385,13 +385,12 @@ const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallenge const getSeasonDailyChallenge = (pools: IRotatingSeasonChallengePools, day: number): ISeasonChallenge => { const dayStart = EPOCH + day * 86400000; const dayEnd = EPOCH + (day + 3) * 86400000; - const rng = new SRng(new SRng(day).randomInt(0, 100_000)); return { _id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") }, Daily: true, Activation: { $date: { $numberLong: dayStart.toString() } }, Expiry: { $date: { $numberLong: dayEnd.toString() } }, - Challenge: rng.randomElement(pools.daily)! + Challenge: sequentiallyUniqueRandomElement(pools.daily, day, 2, 605732938)! }; };