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:
parent
bf491fc9cd
commit
226cd9b2b8
1 changed files with 157 additions and 0 deletions
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue