/** * 按与后端 DiceRewardLogic 一致的环形规则,为盘面 26 格(id 0~25)求档位 tier, * 并生成 real_ev / ui 等字段。 * * 摇取点数为 5~30:起点为 grid_number==摇取点数 的格位下标,顺时针落点 (start+摇取)%26, * 逆时针落点 start-摇取(<0 则 +26)。 */ export type IndexTier = 'T1' | 'T2' | 'T3' | 'T4' | 'T5' /** 豹子摇取点数:这些摇取下落点不得为 T4/T5(与 dice_reward 表中「点数」列一致) */ export const LEOPARD_ROLLS: readonly number[] = [5, 10, 15, 20, 25, 30] const BOARD_SIZE = 26 const GRID_MIN = 5 const GRID_MAX = 30 const T_ALL: IndexTier[] = ['T1', 'T2', 'T3', 'T4', 'T5'] const T_NO_LEOPARD: IndexTier[] = ['T1', 'T2', 'T3'] export interface BoardFrequencies { grids: number[] freqCw: number[] freqCcw: number[] leopardLandCw: Set leopardLandCcw: Set leopardLandUnion: Set } /** * 条数约束:T1/T4/T5 为「恰好」;T2 为「不少于」(加权条数,与 dice_reward 中顺/逆各 26 条摇取结果一致) */ export interface TierCountConstraints { t1FixedCw: number t2MinCw: number /** 顺时针方向 T4 加权条数固定为该值 */ t4FixedCw: number /** 顺时针方向 T5 加权条数固定为该值 */ t5FixedCw: number t1FixedCcw: number t2MinCcw: number /** 逆时针方向 T4 加权条数固定为该值 */ t4FixedCcw: number /** 逆时针方向 T5 加权条数固定为该值 */ t5FixedCcw: number } /** 各档位统一 real_ev 标准(生成 DiceRewardConfig 时使用;细则可再到表格里改) */ export interface TierRealEvStandards { T1: number T2: number T3: number T4: number T5: number } /** 默认标准(与规则弹窗说明一致) */ export const DEFAULT_TIER_REAL_EV_STANDARDS: TierRealEvStandards = { T1: 300, T2: 150, T3: 50, T4: -40, T5: 0 } /** * 校验档位与 real_ev 区间是否一致;通过返回 null,否则返回 i18n 键名(不含 page.configPage. 前缀) */ export function validateTierRealEvStandards(s: TierRealEvStandards): string | null { if (!Number.isFinite(s.T1) || !(s.T1 > 200)) { return 'ruleGenInvalidT1RealEv' } if (!Number.isFinite(s.T2) || !(s.T2 > 100 && s.T2 < 200)) { return 'ruleGenInvalidT2RealEv' } if (!Number.isFinite(s.T3) || !(s.T3 > 0 && s.T3 < 100)) { return 'ruleGenInvalidT3RealEv' } if (!Number.isFinite(s.T4) || !(s.T4 < 0)) { return 'ruleGenInvalidT4RealEv' } if (!Number.isFinite(s.T5) || s.T5 !== 0) { return 'ruleGenInvalidT5RealEv' } return null } export interface GenerateTierInput { grids: number[] constraints: TierCountConstraints } export interface GenerateTierResultOk { ok: true tiers: IndexTier[] } export interface GenerateTierResultFail { ok: false message: string } export type GenerateTierResult = GenerateTierResultOk | GenerateTierResultFail export function computeBoardFrequencies(grids: number[]): BoardFrequencies | null { if (grids.length !== BOARD_SIZE) { return null } const gridToPos: Record = {} for (let pos = 0; pos < BOARD_SIZE; pos++) { const g = grids[pos] if (g < GRID_MIN || g > GRID_MAX) { return null } if (gridToPos[g] !== undefined) { return null } gridToPos[g] = pos } const freqCw = new Array(BOARD_SIZE).fill(0) const freqCcw = new Array(BOARD_SIZE).fill(0) for (let roll = GRID_MIN; roll <= GRID_MAX; roll++) { const startPos = gridToPos[roll] const endCw = (startPos + roll) % BOARD_SIZE const endCcw = startPos - roll >= 0 ? startPos - roll : BOARD_SIZE + startPos - roll freqCw[endCw]++ freqCcw[endCcw]++ } const leopardLandCw = new Set() const leopardLandCcw = new Set() for (let di = 0; di < LEOPARD_ROLLS.length; di++) { const d = LEOPARD_ROLLS[di] const sp = gridToPos[d] leopardLandCw.add((sp + d) % BOARD_SIZE) const eccw = sp - d >= 0 ? sp - d : BOARD_SIZE + sp - d leopardLandCcw.add(eccw) } const leopardLandUnion = new Set() leopardLandCw.forEach((x) => leopardLandUnion.add(x)) leopardLandCcw.forEach((x) => leopardLandUnion.add(x)) return { grids: [...grids], freqCw, freqCcw, leopardLandCw, leopardLandCcw, leopardLandUnion } } function sumWeighted(freq: number[], tiers: IndexTier[], tier: IndexTier): number { let s = 0 for (let i = 0; i < BOARD_SIZE; i++) { if (tiers[i] === tier) { s += freq[i] } } return s } function meetsConstraints( tiers: IndexTier[], freqCw: number[], freqCcw: number[], c: TierCountConstraints ): boolean { const cw1 = sumWeighted(freqCw, tiers, 'T1') const cw2 = sumWeighted(freqCw, tiers, 'T2') const cw4 = sumWeighted(freqCw, tiers, 'T4') const cw5 = sumWeighted(freqCw, tiers, 'T5') const cc1 = sumWeighted(freqCcw, tiers, 'T1') const cc2 = sumWeighted(freqCcw, tiers, 'T2') const cc4 = sumWeighted(freqCcw, tiers, 'T4') const cc5 = sumWeighted(freqCcw, tiers, 'T5') return ( cw1 === c.t1FixedCw && cw2 >= c.t2MinCw && cw4 === c.t4FixedCw && cw5 === c.t5FixedCw && cc1 === c.t1FixedCcw && cc2 >= c.t2MinCcw && cc4 === c.t4FixedCcw && cc5 === c.t5FixedCcw ) } function nonLeopardTierChoices(c: TierCountConstraints): IndexTier[] { const out: IndexTier[] = ['T1', 'T2', 'T3'] if (c.t4FixedCw > 0 || c.t4FixedCcw > 0) { out.push('T4') } if (c.t5FixedCw > 0 || c.t5FixedCcw > 0) { out.push('T5') } return out } function shuffleIndices(rand: () => number): number[] { const a: number[] = [] for (let i = 0; i < BOARD_SIZE; i++) { a.push(i) } for (let i = BOARD_SIZE - 1; i > 0; i--) { const j = Math.floor(rand() * (i + 1)) const t = a[i] a[i] = a[j] a[j] = t } return a } function mulberry32(seed: number): () => number { return () => { let t = (seed += 0x6d2b79f5) t = Math.imul(t ^ (t >>> 15), t | 1) t ^= t + Math.imul(t ^ (t >>> 7), t | 61) return ((t ^ (t >>> 14)) >>> 0) / 4294967296 } } /** * 随机搜索可行档位:豹子落点禁 T4/T5;T4/T5 顺/逆加权条数分别等于约束中的固定值(可多条 T4/T5 格位)。 */ export function generateTiers(input: GenerateTierInput): GenerateTierResult { const board = computeBoardFrequencies(input.grids) if (board === null) { return { ok: false, message: 'grid_number 须为 5~30 各出现一次且共 26 条' } } const { freqCw, freqCcw, leopardLandUnion } = board const c = input.constraints let seed = 0 for (let i = 0; i < BOARD_SIZE; i++) { seed = (seed + input.grids[i] * 31 + i) | 0 } const rand = mulberry32(seed === 0 ? 0x9e3779b9 : seed) const needT5Cell = c.t5FixedCw > 0 || c.t5FixedCcw > 0 if (needT5Cell) { let hasNonLeopard = false for (let i = 0; i < BOARD_SIZE; i++) { if (!leopardLandUnion.has(i)) { hasNonLeopard = true break } } if (!hasNonLeopard) { return { ok: false, message: '无可用 T5 格位(豹子摇取落点占满全盘,请调整 grid_number 排布)' } } } const nonLeopardChoices = nonLeopardTierChoices(c) const maxAttempts = 400000 for (let attempt = 0; attempt < maxAttempts; attempt++) { const tiers: IndexTier[] = new Array(BOARD_SIZE).fill('T3') const order = shuffleIndices(rand) for (let oi = 0; oi < order.length; oi++) { const pos = order[oi] if (leopardLandUnion.has(pos)) { tiers[pos] = T_NO_LEOPARD[Math.floor(rand() * T_NO_LEOPARD.length)] } else { tiers[pos] = nonLeopardChoices[Math.floor(rand() * nonLeopardChoices.length)] } } if (meetsConstraints(tiers, freqCw, freqCcw, c)) { return { ok: true, tiers } } } return { ok: false, message: '在当前盘面与约束下未找到可行解,请调整 T1/T4/T5 固定条数或放宽 T2 下限后重试' } } function uiTextByTierWhenStandards( tier: IndexTier, realEv: number ): { ui_text: string; ui_text_en: string } { if (tier === 'T5') { return { ui_text: '再来一次', ui_text_en: 'Once again' } } const value = String(realEv) return { ui_text: value, ui_text_en: value } } /** 展示文案:直接使用真实结算值(中英文相同) */ function uiTextFromRealEv(realEv: number): { ui_text: string; ui_text_en: string } { const value = String(realEv) return { ui_text: value, ui_text_en: value } } /** * 按 tier 生成展示字段。 * @param standards 若传入,则各档位统一使用对应 real_ev 标准;不传则使用内置随机占位(兼容脚本/旧逻辑) */ export function buildRowsFromTiers( grids: number[], tiers: IndexTier[], standards?: TierRealEvStandards ): Array<{ id: number grid_number: number ui_text: string ui_text_en: string real_ev: number tier: IndexTier remark: string }> { const rows: Array<{ id: number grid_number: number ui_text: string ui_text_en: string real_ev: number tier: IndexTier remark: string }> = [] let t4Seq = 0 for (let id = 0; id < BOARD_SIZE; id++) { const tier = tiers[id] const grid_number = grids[id] let ui_text = '' let ui_text_en = '' let real_ev = 0 let remark = '' if (standards !== undefined) { if (tier === 'T1') { real_ev = standards.T1 const f = uiTextByTierWhenStandards(tier, real_ev) ui_text = f.ui_text ui_text_en = f.ui_text_en remark = '大奖格' } else if (tier === 'T2') { real_ev = standards.T2 const f = uiTextByTierWhenStandards(tier, real_ev) ui_text = f.ui_text ui_text_en = f.ui_text_en remark = standards.T2 <= 1 ? '完美回本' : '小赚' } else if (tier === 'T3') { real_ev = standards.T3 const f = uiTextByTierWhenStandards(tier, real_ev) ui_text = f.ui_text ui_text_en = f.ui_text_en remark = '抽水' } else if (tier === 'T4') { real_ev = standards.T4 const f = uiTextByTierWhenStandards(tier, real_ev) ui_text = f.ui_text ui_text_en = f.ui_text_en remark = '惩罚' } else { real_ev = standards.T5 const f = uiTextByTierWhenStandards(tier, real_ev) ui_text = f.ui_text ui_text_en = f.ui_text_en remark = '前端需要在播放一次动画(特殊)' } } else if (tier === 'T1') { real_ev = 101 + ((id * 17 + grid_number * 3) % 398) if (real_ev >= 500) { real_ev = 498 } const f = uiTextFromRealEv(real_ev) ui_text = f.ui_text ui_text_en = f.ui_text_en remark = '大奖格' } else if (tier === 'T2') { if (id % 3 === 0) { real_ev = 1 remark = '完美回本' } else { real_ev = 20 + ((id * 11) % 75) if (real_ev <= 0) { real_ev = 50 } remark = '小赚' } const f = uiTextFromRealEv(real_ev) ui_text = f.ui_text ui_text_en = f.ui_text_en } else if (tier === 'T3') { real_ev = -72 - (id % 15) const f = uiTextFromRealEv(real_ev) ui_text = f.ui_text ui_text_en = f.ui_text_en remark = '抽水' } else if (tier === 'T4') { t4Seq++ real_ev = -101 - t4Seq * 15 const f = uiTextFromRealEv(real_ev) ui_text = f.ui_text ui_text_en = f.ui_text_en remark = '惩罚' } else { real_ev = 0 const f = uiTextFromRealEv(real_ev) ui_text = f.ui_text ui_text_en = f.ui_text_en remark = '前端需要在播放一次动画(特殊)' } rows.push({ id, grid_number, ui_text, ui_text_en, real_ev, tier, remark }) } return rows } export function summarizeCounts( board: BoardFrequencies, tiers: IndexTier[] ): { cw: Record; ccw: Record } { const cw: Record = { T1: 0, T2: 0, T3: 0, T4: 0, T5: 0 } const ccw: Record = { T1: 0, T2: 0, T3: 0, T4: 0, T5: 0 } for (let k = 0; k < T_ALL.length; k++) { const t = T_ALL[k] cw[t] = sumWeighted(board.freqCw, tiers, t) ccw[t] = sumWeighted(board.freqCcw, tiers, t) } return { cw, ccw } }