From c275e7f9fc607307a3bdbb47197cbeb631df1c12 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Jul 2025 07:12:15 +0200 Subject: [PATCH 1/2] chore: ensure nightwave daily challenges are unique When generating a daily challenge, we now use sequentallyUniqueRandomElement with a lookbehind of 2 to ensure the 2 previous (and still active) daily challenges are not duplicated. --- src/services/rngService.ts | 53 +++++++++++++++++++++++++++++++ src/services/worldStateService.ts | 5 ++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/services/rngService.ts b/src/services/rngService.ts index d277c50e..66b6478d 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 sequentallyUniqueRandomElement = ( + 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..9e0e24ef 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, sequentallyUniqueRandomElement, 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: sequentallyUniqueRandomElement(pools.daily, day, 2, 605732938)! }; }; -- 2.47.2 From 0869a768dc3bb1d84c5f19abd6e02d6137cda120 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 6 Jul 2025 10:07:36 +0200 Subject: [PATCH 2/2] fix typo --- src/services/rngService.ts | 2 +- src/services/worldStateService.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/rngService.ts b/src/services/rngService.ts index 66b6478d..876d7584 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -159,7 +159,7 @@ export class SRng { } } -export const sequentallyUniqueRandomElement = ( +export const sequentiallyUniqueRandomElement = ( deck: readonly T[], idx: number, lookbehind: number, diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 9e0e24ef..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, sequentallyUniqueRandomElement, 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, @@ -390,7 +390,7 @@ const getSeasonDailyChallenge = (pools: IRotatingSeasonChallengePools, day: numb Daily: true, Activation: { $date: { $numberLong: dayStart.toString() } }, Expiry: { $date: { $numberLong: dayEnd.toString() } }, - Challenge: sequentallyUniqueRandomElement(pools.daily, day, 2, 605732938)! + Challenge: sequentiallyUniqueRandomElement(pools.daily, day, 2, 605732938)! }; }; -- 2.47.2