From e8b1d0fd7120a7f6412910de6069df8e00dc6823 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 18 Dec 2025 21:41:32 -0600 Subject: [PATCH] [Bug] Fix evil team admin randomization (#6830) --- src/utils/random.ts | 21 ++++++---------- test/utils/random.test.ts | 51 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 test/utils/random.test.ts diff --git a/src/utils/random.ts b/src/utils/random.ts index 530ee394c70..3a3d3ad0280 100644 --- a/src/utils/random.ts +++ b/src/utils/random.ts @@ -12,7 +12,7 @@ import { globalScene } from "#app/global-scene"; import type { Mutable } from "#types/type-helpers"; -import { randSeedItem } from "#utils/common"; +import { randSeedItem, randSeedShuffle } from "#utils/common"; /** * Select a random element using an offset such that the chosen element is @@ -21,9 +21,10 @@ import { randSeedItem } from "#utils/common"; * @remarks * If the seed offset is greater than the number of choices, this will just choose a random element * - * @param arr - The array of items to choose from + * @param choices - The array of items to choose from + * @param seedOffset - The offset into the array * @param scene - (default {@linkcode globalScene}); The scene to use for random seeding - * @returns A random item from the array that is guaranteed to be different from the + * @returns A random item from the array that is guaranteed to be different from the previous result * @typeParam T - The type of items in the array * * @example @@ -38,8 +39,7 @@ import { randSeedItem } from "#utils/common"; * ``` */ export function randSeedUniqueItem(choices: readonly T[], seedOffset: number, scene = globalScene): T { - if (seedOffset === 0 || choices.length <= seedOffset) { - // cast to mutable is safe because randSeedItem does not actually modify the array + if (choices.length <= seedOffset) { return randSeedItem(choices as Mutable); } @@ -47,14 +47,7 @@ export function randSeedUniqueItem(choices: readonly T[], seedOffset: number, let choice: T; scene.executeWithSeedOffset(() => { - const curChoices = choices.slice(); - for (let i = 0; i < seedOffset; i++) { - const previousChoice = randSeedItem(curChoices); - curChoices.splice(curChoices.indexOf(previousChoice), 1); - } - choice = randSeedItem(curChoices); - }, seedOffset); - - // Bang is safe since there are at least `seedOffset` choices, so the method above is guaranteed to set `choice` + choice = randSeedShuffle(choices.slice())[seedOffset]; + }, 0); return choice!; } diff --git a/test/utils/random.test.ts b/test/utils/random.test.ts new file mode 100644 index 00000000000..62ddeadf4f8 --- /dev/null +++ b/test/utils/random.test.ts @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Pagefault Games + * SPDX-FileContributor: SirzBenjie + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { GameManager } from "#test/test-utils/game-manager"; +import { randSeedUniqueItem } from "#utils/random"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Utils - Random", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + describe("randSeedUniqueItem", () => { + // TODO: Remove `initialization of game` once `randSeedUniqueItem` stops using `executeWithSeedOffset` + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + }); + + it("should prevent duplicates when provided with different offsets", async () => { + const choices = ["a", "b", "c", "d"]; + const choice1 = randSeedUniqueItem(choices, 0); + const choice2 = randSeedUniqueItem(choices, 1); + const choice3 = randSeedUniqueItem(choices, 2); + expect(choice2).not.toEqual(choice1); + expect(choice2).not.toEqual(choice3); + expect(choice1).not.toEqual(choice3); + }); + + it("should gracefully handle an offset larger than the choices", () => { + const choices = ["a", "b", "c"]; + // 1) the function must not throw + // 2) The output must be one of the choices + const choice = randSeedUniqueItem(choices, 5); + expect(choices).toContain(choice); + }); + }); +});