From 226cd9b2b88417bfca1bc5c21695a1143e382253 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Tue, 8 Sep 2026 15:57:01 -0400 Subject: [PATCH] test(eod): cover custom payout math, loss reveal, and exposure rule Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- tests/int/custom-minefields.int.spec.ts | 157 ++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/tests/int/custom-minefields.int.spec.ts b/tests/int/custom-minefields.int.spec.ts index 9f1aaf8..751e163 100644 --- a/tests/int/custom-minefields.int.spec.ts +++ b/tests/int/custom-minefields.int.spec.ts @@ -6,6 +6,11 @@ import { CustomMinefields } from "@/collections/minigames/CustomMinefields"; import { hasPermission } from "@/utils/access-control/hasPermission"; import { invalidatePermissionCache } from "@/utils/access-control/loadUserPermissions"; import { + applyCreatorBank, + creatorBankIncrement, + customPlayerXp, + customXpBracket, + edgeMineIndices, isEodUnlocked, minefieldFlagsError, validateGridSize, @@ -13,6 +18,7 @@ import { validateMinefieldScore, validateMinePositions, } from "@/lib/custom-minigames/eod"; +import { creditEodXp } from "@/lib/custom-minigames/payouts"; let payload: Payload; const RUN = `cmf-${Date.now().toString(36)}`; @@ -871,4 +877,155 @@ describe("Custom minefields", () => { expect(await invokeAccess(readFn, otherPlayer)).toBe(true); }, TIMEOUT); }); + + // --------------------------------------------------------------------------- + // Custom XP payouts, loss reveal rules, and exposure toggle + // --------------------------------------------------------------------------- + + describe("Custom XP payouts and loss reveal rules", () => { + describe("payout math", () => { + it("maps mine counts to standard difficulty brackets", () => { + expect(customXpBracket(10)).toEqual({ baseScore: 100, parSeconds: 60 }); + expect(customXpBracket(15)).toEqual({ baseScore: 100, parSeconds: 60 }); + expect(customXpBracket(16)).toEqual({ baseScore: 300, parSeconds: 180 }); + expect(customXpBracket(50)).toEqual({ baseScore: 300, parSeconds: 180 }); + expect(customXpBracket(51)).toEqual({ baseScore: 600, parSeconds: 420 }); + }); + + it("pays nothing on losses", () => { + expect(customPlayerXp({ mineCount: 10, seconds: 30, won: false })).toBe(0); + }); + + it("pays 1 XP on training-bracket wins regardless of pace", () => { + expect(customPlayerXp({ mineCount: 10, seconds: 60, won: true })).toBe(1); + expect(customPlayerXp({ mineCount: 10, seconds: 1, won: true })).toBe(1); + }); + + it("pays 2 XP only on fast supply-road-bracket clears", () => { + expect(customPlayerXp({ mineCount: 100, seconds: 1, won: true })).toBe(2); + expect(customPlayerXp({ mineCount: 100, seconds: 420, won: true })).toBe(1); + expect(customPlayerXp({ mineCount: 60, seconds: 120, won: true })).toBe(1); + }); + + it("banks the creator share fractionally and credits whole points at 1", () => { + const first = applyCreatorBank(0, 1); + expect(first.bank).toBeCloseTo(0.05); + expect(first.wholeXpToCredit).toBe(0); + + const second = applyCreatorBank(0.96, 1); + expect(second.bank).toBeCloseTo(0.01); + expect(second.wholeXpToCredit).toBe(1); + + const third = applyCreatorBank(1.98, 2); + expect(third.bank).toBeCloseTo(0.08); + expect(third.wholeXpToCredit).toBe(2); + + expect(creatorBankIncrement(2)).toBeCloseTo(0.1); + }); + }); + + describe("edge-mine loss reveal", () => { + it("returns nothing when nothing is exposed", () => { + expect( + edgeMineIndices({ rows: 9, cols: 9, mines: [0, 40, 80], revealed: [] }), + ).toEqual([]); + }); + + it("picks mines adjacent to the exposed area, diagonals included", () => { + // 9x9: revealed cell index 1 (row 0 col 1); mine at 0 (row 0 col 0) touches it. + expect(edgeMineIndices({ rows: 9, cols: 9, mines: [0], revealed: [1] })).toEqual([0]); + // Mine at 40 (row 4 col 4) touches revealed 48 (row 5 col 3) diagonally. + expect(edgeMineIndices({ rows: 9, cols: 9, mines: [40], revealed: [48] })).toEqual([40]); + // Mine at 80 (row 8 col 8) is far from the exposed area and stays hidden. + expect(edgeMineIndices({ rows: 9, cols: 9, mines: [40, 80], revealed: [48] })).toEqual([ + 40, + ]); + }); + }); + + describe("exposure rule", () => { + it("defaults exposeEdgeMines to true on new minefields", async () => { + const field = await payload.create({ + collection: "custom-minefields", + data: { name: "Exposure Default", author: authorId, rows: 9, cols: 9, mines: validMines, mineCount: 3 }, + overrideAccess: true, + depth: 0, + }); + minefieldIds.push(field.id); + expect(field.exposeEdgeMines).toBe(true); + }, TIMEOUT); + + it("persists an author-chosen exposure rule of false", async () => { + const field = await payload.create({ + collection: "custom-minefields", + data: { + name: "Exposure Off", + author: authorId, + rows: 9, + cols: 9, + mines: validMines, + mineCount: 3, + exposeEdgeMines: false, + }, + overrideAccess: true, + depth: 0, + }); + minefieldIds.push(field.id); + expect(field.exposeEdgeMines).toBe(false); + }, TIMEOUT); + }); + + describe("profile credit", () => { + let xpPlayer: User; + + beforeAll(async () => { + xpPlayer = (await payload.create({ + collection: "users", + data: { + username: `${RUN}-xp-player`, + discordUsername: `${RUN}-xp-player`, + displayName: "XP PLAYER", + steamId: `7656119${Math.floor(Math.random() * 1e9)}`, + password: "Test123", + roles: ["user"], + }, + overrideAccess: true, + depth: 0, + })) as unknown as User; + }); + + afterAll(async () => { + await payload + .delete({ collection: "profiles", where: { user: { equals: xpPlayer.id } }, overrideAccess: true }) + .catch(() => {}); + await payload.delete({ collection: "users", id: xpPlayer.id, overrideAccess: true }).catch(() => {}); + }); + + it("credits EOD minigame XP cumulatively on the profile", async () => { + const first = await creditEodXp(payload, xpPlayer.id, 1); + expect(first).not.toBeNull(); + expect(first?.xpEarned).toBe(1); + + const second = await creditEodXp(payload, xpPlayer.id, 1); + expect(second?.xpEarned).toBe(2); + expect(second?.profileId).toBe(first?.profileId); + + const profileRes = await payload.find({ + collection: "profiles", + where: { user: { equals: xpPlayer.id } }, + limit: 1, + depth: 0, + overrideAccess: true, + }); + const profile = profileRes.docs[0]; + expect(profile).toBeDefined(); + expect(profile?.progression?.minigames?.eodMinesweeper?.xpEarned).toBe(2); + }, TIMEOUT); + + it("returns null for users without a profile", async () => { + // User id 999999999 has no profile doc in this database. + expect(await creditEodXp(payload, 999999999, 1)).toBeNull(); + }, TIMEOUT); + }); + }); });