159 lines
5.6 KiB
JavaScript
159 lines
5.6 KiB
JavaScript
import { stageOptions } from "@/features/lucky-gift/constants.js";
|
|
import {
|
|
applyProbabilityMap,
|
|
correctRTPProbabilities,
|
|
decimal,
|
|
expectedRTPPercent,
|
|
formatDecimal,
|
|
probabilityTotal
|
|
} from "@/shared/utils/rtpProbability.js";
|
|
import { DEFAULT_POOL_ID } from "./api.js";
|
|
|
|
// 幸运礼物配置表单的纯领域逻辑:配置 <-> 表单换算、概率闭合校正、阶段摘要。
|
|
// 与展示层分离,弹窗组件只负责渲染和事件绑定。
|
|
|
|
export function configToForm(config = {}) {
|
|
const targetRTPPercent = formNumber(config.target_rtp_percent ?? config.targetRtpPercent);
|
|
return {
|
|
app_code: config.app_code || config.appCode || "",
|
|
control_band_percent: formNumber(config.control_band_percent ?? config.controlBandPercent),
|
|
enabled: Boolean(config.enabled),
|
|
gift_price_reference: formNumber(config.gift_price_reference ?? config.giftPriceReference),
|
|
pool_id: config.pool_id || config.poolId || DEFAULT_POOL_ID,
|
|
pool_rate_percent: formNumber(config.pool_rate_percent ?? config.poolRatePercent),
|
|
settlement_window_wager: formNumber(config.settlement_window_wager ?? config.settlementWindowWager),
|
|
stages: normalizeFormStages(config.stages, targetRTPPercent),
|
|
target_rtp_percent: targetRTPPercent
|
|
};
|
|
}
|
|
|
|
export function formToConfig(config, form) {
|
|
return {
|
|
...config,
|
|
app_code: form.app_code,
|
|
control_band_percent: numberFromForm(form.control_band_percent),
|
|
enabled: Boolean(form.enabled),
|
|
gift_price_reference: numberFromForm(form.gift_price_reference),
|
|
pool_id: form.pool_id,
|
|
pool_rate_percent: numberFromForm(form.pool_rate_percent),
|
|
settlement_window_wager: numberFromForm(form.settlement_window_wager),
|
|
stages: form.stages,
|
|
target_rtp_percent: numberFromForm(form.target_rtp_percent)
|
|
};
|
|
}
|
|
|
|
// 概率列必须整体闭合到 100% 且期望 RTP 逼近目标值,所以任何倍率/目标 RTP 改动后都重算全阶段概率,
|
|
// 不允许运营手工填概率绕过服务端的闭合校验。
|
|
export function correctAllStageProbabilities(form) {
|
|
return {
|
|
...form,
|
|
stages: (form.stages || []).map((stage) => {
|
|
const probabilityByIndex = correctRTPProbabilities(stage.tiers, form.target_rtp_percent, {
|
|
multiplier: (item) => item.multiplier
|
|
});
|
|
return {
|
|
...stage,
|
|
tiers: probabilityByIndex ? applyProbabilityMap(stage.tiers, probabilityByIndex, formatDecimal) : stage.tiers
|
|
};
|
|
})
|
|
};
|
|
}
|
|
|
|
export function stageSummary(stage, form) {
|
|
const probability = probabilityTotal(stage.tiers);
|
|
const expected = expectedRTPPercent(stage.tiers, {
|
|
multiplier: (item) => item.multiplier
|
|
});
|
|
const target = Number(form.target_rtp_percent || 0);
|
|
const band = Number(form.control_band_percent || 0);
|
|
const probabilityClosed = Math.abs(probability - 100) < 0.0001;
|
|
const rtpInBand = expected >= target - band && expected <= target + band;
|
|
const fullLabel = stageOptions.find(([key]) => key === stage.stage)?.[1] || stage.stage;
|
|
|
|
return {
|
|
expected,
|
|
ok: probabilityClosed && rtpInBand,
|
|
probability,
|
|
probabilityClosed,
|
|
rtpInBand,
|
|
stageKey: stage.stage,
|
|
stageLabel: fullLabel,
|
|
// 发布按钮不做硬拦截(服务端仍会校验),这里的状态只用于提示运营当前阶段是否可安全发布。
|
|
status: probabilityClosed && rtpInBand ? "可发布" : probabilityClosed ? "RTP 偏离" : "概率未闭合",
|
|
tabLabel: stage.stage === "normal" ? "普通" : fullLabel.replace(/阶段$/, "")
|
|
};
|
|
}
|
|
|
|
export function nextStageMultiplier(tiers = []) {
|
|
const positive = tiers.map((tier) => decimal(tier.multiplier)).filter((value) => value > 0);
|
|
if (!positive.length) {
|
|
return 1;
|
|
}
|
|
const max = Math.max(...positive);
|
|
if (max >= 5) {
|
|
return max * 2;
|
|
}
|
|
if (max >= 2) {
|
|
return 5;
|
|
}
|
|
return max + 1;
|
|
}
|
|
|
|
function normalizeFormStages(stages, targetRTPPercent = 95) {
|
|
const byStage = new Map((Array.isArray(stages) ? stages : []).map((stage) => [stage.stage, stage]));
|
|
return correctAllStageProbabilities({
|
|
stages: stageOptions.map(([stageKey]) => {
|
|
const source = byStage.get(stageKey);
|
|
const sourceTiers = Array.isArray(source?.tiers) && source.tiers.length ? source.tiers : defaultStageTiers(stageKey);
|
|
return {
|
|
stage: stageKey,
|
|
tiers: sourceTiers.map((tier) => ({
|
|
enabled: tier.enabled !== false,
|
|
highWaterOnly: Boolean(tier.high_water_only ?? tier.highWaterOnly),
|
|
multiplier: formNumber(tier.multiplier),
|
|
probabilityPercent: formNumber(tier.probability_percent ?? tier.probabilityPercent),
|
|
tierId: tier.tier_id || tier.tierId || ""
|
|
}))
|
|
};
|
|
}),
|
|
target_rtp_percent: targetRTPPercent
|
|
}).stages;
|
|
}
|
|
|
|
function defaultStageTiers(stage) {
|
|
if (stage === "advanced") {
|
|
return [
|
|
defaultTier("advanced_none", 0, 27),
|
|
defaultTier("advanced_1x", 1, 60),
|
|
defaultTier("advanced_2x", 2, 10),
|
|
defaultTier("advanced_5x", 5, 3, { high_water_only: true })
|
|
];
|
|
}
|
|
return [
|
|
defaultTier(`${stage}_none`, 0, 10),
|
|
defaultTier(`${stage}_0_5x`, 0.5, 20),
|
|
defaultTier(`${stage}_1x`, 1, 55),
|
|
defaultTier(`${stage}_2x`, 2, 15)
|
|
];
|
|
}
|
|
|
|
function defaultTier(tierID, multiplier, probabilityPercent, options = {}) {
|
|
return {
|
|
enabled: true,
|
|
highWaterOnly: Boolean(options.high_water_only),
|
|
multiplier,
|
|
probabilityPercent,
|
|
tierId: tierID
|
|
};
|
|
}
|
|
|
|
export function formNumber(value) {
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? String(parsed) : "0";
|
|
}
|
|
|
|
export function numberFromForm(value) {
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|