1
0
Fork 0

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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jason Fraley 2026-09-08 15:57:01 -04:00
parent bf491fc9cd
commit 226cd9b2b8

View file

@ -6,6 +6,11 @@ import { CustomMinefields } from "@/collections/minigames/CustomMinefields";
import { hasPermission } from "@/utils/access-control/hasPermission"; import { hasPermission } from "@/utils/access-control/hasPermission";
import { invalidatePermissionCache } from "@/utils/access-control/loadUserPermissions"; import { invalidatePermissionCache } from "@/utils/access-control/loadUserPermissions";
import { import {
applyCreatorBank,
creatorBankIncrement,
customPlayerXp,
customXpBracket,
edgeMineIndices,
isEodUnlocked, isEodUnlocked,
minefieldFlagsError, minefieldFlagsError,
validateGridSize, validateGridSize,
@ -13,6 +18,7 @@ import {
validateMinefieldScore, validateMinefieldScore,
validateMinePositions, validateMinePositions,
} from "@/lib/custom-minigames/eod"; } from "@/lib/custom-minigames/eod";
import { creditEodXp } from "@/lib/custom-minigames/payouts";
let payload: Payload; let payload: Payload;
const RUN = `cmf-${Date.now().toString(36)}`; const RUN = `cmf-${Date.now().toString(36)}`;
@ -871,4 +877,155 @@ describe("Custom minefields", () => {
expect(await invokeAccess(readFn, otherPlayer)).toBe(true); expect(await invokeAccess(readFn, otherPlayer)).toBe(true);
}, TIMEOUT); }, 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);
});
});
}); });