2026-05-29 12:55:44 +08:00

131 lines
5.9 KiB
JavaScript

import { describe, expect, test } from "vitest";
import {
addTierToStage,
correctStageTierProbabilities,
emptyLuckyGiftForm,
payloadFromLuckyGiftForm,
stageExpectedRTP,
stageProbabilityTotal,
updateStageTier,
} from "@/features/lucky-gift/configModel.js";
describe("lucky gift config payload", () => {
test("submits business units required by the v2 admin contract", () => {
const payload = payloadFromLuckyGiftForm(emptyLuckyGiftForm());
expect(payload.target_rtp_percent).toBe(95);
expect(payload.pool_rate_percent).toBe(96);
expect(payload.control_band_percent).toBe(1);
expect(payload.gift_price_reference).toBe(100);
expect(payload.novice_max_equivalent_draws).toBe(2000);
expect(payload.normal_max_equivalent_draws).toBe(20000);
expect(payload.max_single_payout).toBe(50000);
expect(payload.user_hourly_payout_cap).toBe(34200);
expect(payload.user_daily_payout_cap).toBe(615600);
expect(payload.device_daily_payout_cap).toBe(1026000);
expect(payload.room_hourly_payout_cap).toBe(684000);
expect(payload.anchor_daily_payout_cap).toBe(12312000);
expect(payload).not.toHaveProperty("target_rtp_ppm");
expect(payload).not.toHaveProperty("multiplier_ppms");
expect(payload.stages.map((stage) => stage.stage)).toEqual(["novice", "normal", "advanced"]);
});
test("keeps each default stage probability at 100 percent", () => {
const form = emptyLuckyGiftForm();
expect(form.stages.map(stageProbabilityTotal)).toEqual([100, 100, 100]);
});
test("keeps each default stage expected RTP at target percent", () => {
const form = emptyLuckyGiftForm();
expect(form.stages.map(stageExpectedRTP)).toEqual([95, 95, 95]);
});
test("updates a single tier without mutating other stages", () => {
const form = emptyLuckyGiftForm();
const nextStages = updateStageTier(form.stages, "normal", 0, { probabilityPercent: "39.5" });
expect(nextStages.find((stage) => stage.stage === "normal").tiers[0].probabilityPercent).toBe("39.5");
expect(nextStages.find((stage) => stage.stage === "novice").tiers[0].probabilityPercent).toBe(10);
});
test("adds explicit editable tier to requested stage", () => {
const form = emptyLuckyGiftForm();
const nextStages = addTierToStage(form.stages, "advanced");
const advanced = nextStages.find((stage) => stage.stage === "advanced");
expect(advanced.tiers).toHaveLength(5);
expect(advanced.tiers[4]).toMatchObject({ broadcastLevel: "none", rewardSource: "base_rtp" });
});
test("corrects a stage probability to requested RTP without zeroing enabled tiers", () => {
const form = emptyLuckyGiftForm();
const nextStages = correctStageTierProbabilities(form.stages, "normal", 95);
const normal = nextStages.find((stage) => stage.stage === "normal");
expect(stageProbabilityTotal(normal)).toBeCloseTo(100, 6);
expect(stageExpectedRTP(normal)).toBeCloseTo(95, 6);
expect(enabledProbabilityPPMTotal(normal)).toBe(1_000_000);
expect(normal.tiers.map((tier) => Number(tier.multiplier))).toEqual([0, 0.5, 1, 2]);
expect(normal.tiers.every((tier) => Number(tier.probabilityPercent) > 0)).toBe(true);
});
test("clamps correction to closest reachable RTP when multiplier range cannot cover target", () => {
const form = emptyLuckyGiftForm();
const nextStages = correctStageTierProbabilities(form.stages, "novice", 250);
const novice = nextStages.find((stage) => stage.stage === "novice");
expect(stageProbabilityTotal(novice)).toBeCloseTo(100, 6);
expect(stageExpectedRTP(novice)).toBeCloseTo(199.955, 6);
expect(novice.tiers.every((tier) => Number(tier.probabilityPercent) > 0)).toBe(true);
});
test("balances RTP contribution across high multipliers including 100x", () => {
const form = emptyLuckyGiftForm();
const advanced = form.stages.find((stage) => stage.stage === "advanced");
advanced.tiers = [
luckyTier("advanced_none", 0),
luckyTier("advanced_1x", 1),
luckyTier("advanced_2x", 2),
luckyTier("advanced_5x", 5, { highWaterOnly: true }),
luckyTier("advanced_10x", 10, { highWaterOnly: true }),
luckyTier("advanced_100x", 100, { highWaterOnly: true }),
];
const nextStages = correctStageTierProbabilities(form.stages, "advanced", 95);
const corrected = nextStages.find((stage) => stage.stage === "advanced");
const positiveTiers = corrected.tiers.filter((tier) => Number(tier.multiplier) > 0);
const contributions = positiveTiers.map((tier) => Number(tier.multiplier) * Number(tier.probabilityPercent));
const tier100x = corrected.tiers.find((tier) => tier.tierId === "advanced_100x");
expect(stageProbabilityTotal(corrected)).toBeCloseTo(100, 6);
expect(stageExpectedRTP(corrected)).toBeCloseTo(95, 6);
expect(enabledProbabilityPPMTotal(corrected)).toBe(1_000_000);
expect(corrected.tiers.every((tier) => Number(tier.probabilityPercent) > 0)).toBe(true);
expect(Number(tier100x.probabilityPercent)).toBeGreaterThan(0.01);
expect(Math.max(...contributions) - Math.min(...contributions)).toBeLessThan(1.1);
});
});
function luckyTier(tierId, multiplier, options = {}) {
return {
broadcastLevel: "none",
enabled: true,
highWaterOnly: Boolean(options.highWaterOnly),
multiplier,
probabilityPercent: 1,
rewardSource: "base_rtp",
tierId,
};
}
function enabledProbabilityPPMTotal(stage) {
return stage.tiers.reduce((sum, tier) => {
if (tier.enabled === false) {
return sum;
}
return sum + Math.round(Number(tier.probabilityPercent) * 10_000);
}, 0);
}