Files
dafuweng-saiadmin6.x/saiadmin-artd/src/views/plugin/dice/reward_config/utils/generateIndexByRules.ts

427 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 按与后端 DiceRewardLogic 一致的环形规则,为盘面 26 格id 025求档位 tier
* 并生成 real_ev / ui 等字段。
*
* 摇取点数为 530起点为 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/T5T4/T5 顺/逆加权条数分别等于约束中的固定值(可多条 T4/T5 格位)。
*/
export function generateTiers(input: GenerateTierInput): GenerateTierResult {
const board = computeBoardFrequencies(input.grids)
if (board === null) {
return { ok: false, message: 'grid_number 须为 530 各出现一次且共 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 }
}