427 lines
12 KiB
TypeScript
427 lines
12 KiB
TypeScript
/**
|
||
* 按与后端 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<number>
|
||
leopardLandCcw: Set<number>
|
||
leopardLandUnion: Set<number>
|
||
}
|
||
|
||
/**
|
||
* 条数约束: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<number, number> = {}
|
||
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<number>(BOARD_SIZE).fill(0)
|
||
const freqCcw = new Array<number>(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<number>()
|
||
const leopardLandCcw = new Set<number>()
|
||
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<number>()
|
||
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<IndexTier, number>; ccw: Record<IndexTier, number> } {
|
||
const cw: Record<IndexTier, number> = { T1: 0, T2: 0, T3: 0, T4: 0, T5: 0 }
|
||
const ccw: Record<IndexTier, number> = { 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 }
|
||
}
|