Chip service and actions, fixed ChipBankCard with delta popup, sounds, spinner, and arena integration; chip bank migration and tests. Arena diffs are the chip-card mounts and join/leave spinner dispatches. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
69 lines
2.3 KiB
TypeScript
69 lines
2.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
CHIP_PER_XP,
|
|
CHIP_TOP_UP_COOLDOWN_MS,
|
|
CHIP_TOP_UP_THRESHOLD,
|
|
chipTopUpState,
|
|
chipsForXp,
|
|
} from "@/lib/minigames/chips";
|
|
|
|
describe("chipsForXp", () => {
|
|
it("converts XP to chips at the fixed rate", () => {
|
|
expect(CHIP_PER_XP).toBe(10);
|
|
expect(chipsForXp(1)).toBe(10);
|
|
expect(chipsForXp(5)).toBe(50);
|
|
expect(chipsForXp(50)).toBe(500);
|
|
});
|
|
|
|
it("rounds fractional XP to whole chips", () => {
|
|
expect(chipsForXp(0.5)).toBe(5);
|
|
expect(chipsForXp(1.25)).toBe(13);
|
|
});
|
|
|
|
it("never returns negative chips", () => {
|
|
expect(chipsForXp(0)).toBe(0);
|
|
expect(chipsForXp(-3)).toBe(0);
|
|
expect(chipsForXp(Number.NaN)).toBe(0);
|
|
expect(chipsForXp(Number.POSITIVE_INFINITY)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("chipTopUpState", () => {
|
|
const NOW = 1_700_000_000_000;
|
|
|
|
it("is claimable for a first-time broke player (no refill timestamp)", () => {
|
|
const state = chipTopUpState(0, null, NOW);
|
|
expect(state.topUpAvailable).toBe(true);
|
|
expect(state.nextTopUpAt).toBeNull();
|
|
});
|
|
|
|
it("is not claimable while the pool holds the threshold or more", () => {
|
|
expect(chipTopUpState(CHIP_TOP_UP_THRESHOLD, null, NOW).topUpAvailable).toBe(false);
|
|
expect(chipTopUpState(1500, null, NOW).topUpAvailable).toBe(false);
|
|
});
|
|
|
|
it("is claimable below the threshold even with a stale refill", () => {
|
|
const stale = new Date(NOW - CHIP_TOP_UP_COOLDOWN_MS - 1).toISOString();
|
|
const state = chipTopUpState(499, stale, NOW);
|
|
expect(state.topUpAvailable).toBe(true);
|
|
expect(state.nextTopUpAt).toBeNull();
|
|
});
|
|
|
|
it("is locked during the cooldown window and reports when it unlocks", () => {
|
|
const recent = new Date(NOW - 60_000).toISOString();
|
|
const state = chipTopUpState(0, recent, NOW);
|
|
expect(state.topUpAvailable).toBe(false);
|
|
expect(state.nextTopUpAt).toBe(new Date(NOW - 60_000 + CHIP_TOP_UP_COOLDOWN_MS).toISOString());
|
|
});
|
|
|
|
it("unlocks the moment the cooldown expires", () => {
|
|
const boundary = new Date(NOW - CHIP_TOP_UP_COOLDOWN_MS).toISOString();
|
|
expect(chipTopUpState(0, boundary, NOW).topUpAvailable).toBe(true);
|
|
});
|
|
|
|
it("treats an unparseable refill timestamp as never refilled", () => {
|
|
const state = chipTopUpState(0, "not-a-date", NOW);
|
|
expect(state.topUpAvailable).toBe(true);
|
|
expect(state.nextTopUpAt).toBeNull();
|
|
});
|
|
});
|