fix: ensure nightwave daily challenges are unique (#2422)
Some checks failed
Build Docker image / docker-arm64 (push) Waiting to run
Build / build (push) Has been cancelled
Build Docker image / docker-amd64 (push) Has been cancelled

When generating a daily challenge, we now use sequentiallyUniqueRandomElement with a lookbehind of 2 to ensure the 2 previous (and still active) daily challenges are not duplicated.

Re #2411

Reviewed-on: #2422
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
This commit is contained in:
Sainan 2025-07-06 20:13:32 -07:00 committed by Sainan
parent 773f96ebbc
commit 3d8c1d036a
2 changed files with 55 additions and 3 deletions

View File

@ -151,4 +151,57 @@ export class SRng {
arr[lastIdx] = tmp;
}
}
shuffledArray<T>(inarr: readonly T[]): T[] {
const arr = [...inarr];
this.shuffleArray(arr);
return arr;
}
}
export const sequentiallyUniqueRandomElement = <T>(
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];
};

View File

@ -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)!
};
};