Files
henan-backend/olympus-py/_regen.py
T
2026-09-21 19:21:35 +08:00

330 lines
16 KiB
Python
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.
"""重新生成回合数据前的检查 / 快照 / 清空 / 还原(一次性脚本,用完可删)。
python _regen.py check # 查看箱配置、桶配比、回合池构成
python _regen.py snapshot # 备份 box / box_ornament / box_template 到 JSON
python _regen.py clear # 清空 game_olympus_step + game_olympus_round
python _regen.py restore # 从备份还原 box_ornament 的 count(used 归 0)
python _regen.py verify # 校验:buy_fs 触发盘 4S0M、FS 内仍有炸弹、池子每桶 77
python _regen.py fsstats [box_id] # 免费旋转观感统计:每局几次消除、几次炸弹并入、累计倍数
"""
import json
import sys
from datetime import datetime
from pathlib import Path
import olympus_db as db
SNAP = Path(__file__).resolve().parent / '_backup_box_config.json'
def check():
with db.get_conn() as conn:
cur = conn.cursor()
cur.execute('''SELECT box_id, box_type, bet, total_ornaments, rtp
FROM game_olympus_box ORDER BY box_id''')
boxes = cur.fetchall()
print('boxes=%d' % len(boxes))
for b in boxes[:3] + boxes[-2:]:
print(' ', b)
cur.execute('''SELECT audience, COUNT(DISTINCT box_id), SUM(count), SUM(used)
FROM game_olympus_box_ornament GROUP BY audience''')
print('box_ornament:', cur.fetchall())
cur.execute('''SELECT box_id, audience, COUNT(*) n, SUM(count) total, SUM(used) used
FROM game_olympus_box_ornament WHERE box_id IN (1,21,30)
GROUP BY box_id, audience ORDER BY box_id''')
for r in cur.fetchall():
print(' cfg', r)
for box_id in (1, 21, 30):
cur.execute('''SELECT ornament_id, COUNT(*) FROM game_olympus_round
WHERE box_id=%s GROUP BY ornament_id ORDER BY ornament_id''',
(box_id,))
print(' pool box%d:' % box_id, cur.fetchall())
cur.execute('''SELECT ornament_id, count FROM game_olympus_box_ornament
WHERE box_id=21 AND audience='common' ORDER BY ornament_id''')
print(' cfg box21 common:', cur.fetchall())
cur.execute('''SELECT ornament_id, count FROM game_olympus_box_ornament
WHERE box_id=30 AND audience='common' ORDER BY ornament_id''')
print(' cfg box30 common:', cur.fetchall())
def snapshot():
data = {'ts': datetime.now().isoformat(timespec='seconds')}
with db.get_conn() as conn:
cur = conn.cursor(db.pymysql.cursors.DictCursor)
for t in ('game_olympus_box', 'game_olympus_box_ornament', 'game_olympus_box_template'):
cur.execute('SELECT * FROM ' + t)
data[t] = [{k: (str(v) if v is not None else None) for k, v in row.items()}
for row in cur.fetchall()]
SNAP.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding='utf-8')
print('snapshot ->', SNAP,
{k: len(v) for k, v in data.items() if k != 'ts'})
def clear():
with db.get_conn() as conn:
cur = conn.cursor()
for t in ('game_olympus_step', 'game_olympus_round'):
cur.execute('TRUNCATE TABLE ' + t)
print('truncated', t)
print('max_round_id =', db.get_max_round_id(), 'max_step_id =', db.get_max_step_id())
def restore():
data = json.loads(SNAP.read_text(encoding='utf-8'))
rows = data['game_olympus_box_ornament']
with db.get_conn() as conn:
cur = conn.cursor()
cur.execute('SELECT COUNT(*) FROM game_olympus_box_ornament')
before = cur.fetchone()[0]
cur.execute('DELETE FROM game_olympus_box_ornament')
cur.executemany('''INSERT INTO game_olympus_box_ornament
(box_id, ornament_id, audience, count, used) VALUES (%s,%s,%s,%s,0)''',
[(int(r['box_id']), int(r['ornament_id']), r['audience'],
int(r['count'])) for r in rows])
print('box_ornament: %d -> %d rows (used=0)' % (before, len(rows)))
# box 行对比:库里现值 vs 备份值
dc = conn.cursor(db.pymysql.cursors.DictCursor)
dc.execute('''SELECT box_id, box_type, bet, total_ornaments, rtp, price, status, sort
FROM game_olympus_box ORDER BY box_id''')
old = {int(r['box_id']): r for r in data['game_olympus_box']}
for r in dc.fetchall():
o = old.get(int(r['box_id']))
if o is None:
print(' box %s: 新增' % r['box_id'])
continue
diff = {k: (o[k], str(r[k])) for k in r
if str(o[k]) != str(r[k])}
if diff:
print(' box %s 变化:' % r['box_id'], diff)
def verify():
"""grid_state 里数 '"S"'/'"M"' 出现次数(短码是独立 JSON 字符串,不会误匹配符号名)。"""
cnt = ("(CHAR_LENGTH(s.grid_state)-CHAR_LENGTH(REPLACE(s.grid_state,'\"%s\"','')))/3")
with db.get_conn() as conn:
cur = conn.cursor()
cur.execute('SELECT COUNT(*) FROM game_olympus_round')
rounds = cur.fetchone()[0]
cur.execute('SELECT COUNT(*) FROM game_olympus_step')
steps = cur.fetchone()[0]
cur.execute('SELECT box_id, COUNT(*) FROM game_olympus_round GROUP BY box_id ORDER BY box_id')
per_box = {r[0]: r[1] for r in cur.fetchall()}
print('rounds=%d steps=%d 每箱回合=%s' % (rounds, steps, sorted(set(per_box.values()))))
cur.execute('''SELECT COUNT(*) FROM game_olympus_round WHERE box_id NOT BETWEEN 1 AND 30''')
print('越界 box_id 回合:', cur.fetchone()[0])
# buy_fs 触发盘:每步 4 个 S、0 个 M
cur.execute(f'''SELECT COUNT(*), SUM({cnt % 'S'}), SUM({cnt % 'M'}), MAX(s.cascade_seq)
FROM game_olympus_step s JOIN game_olympus_round r USING(round_id)
WHERE r.box_id BETWEEN 21 AND 30 AND s.free_spin_id=0''')
n, sc, bm, maxseq = cur.fetchone()
print('buy_fs 触发盘: 步数=%s S总数=%s(应=%s) M总数=%s(应=0) 最大级联页=%s'
% (n, sc, 4 * (n or 0), bm, maxseq))
# 免费旋转内仍然有炸弹(累计器机制没被误伤)
cur.execute(f'''SELECT COUNT(*), SUM({cnt % 'M'}) FROM game_olympus_step s
JOIN game_olympus_round r USING(round_id)
WHERE s.free_spin_id > 0''')
fn, fm = map(float, cur.fetchone())
fm = fm or 0.0
cur.execute(f'''SELECT COUNT(*), SUM({cnt % 'M'}) FROM game_olympus_step s
JOIN game_olympus_round r USING(round_id)
WHERE r.box_id <= 10 AND s.free_spin_id = 0''')
nn, nm = map(float, cur.fetchone())
nm = nm or 0.0
print('FS 步数=%s 带炸弹格=%s (%.1f%%)' % (fn, fm, 100.0 * (fm or 0) / (30 * fn)))
print('normal 基础盘步数=%s 带炸弹格=%s (%.1f%%)' % (nn, nm, 100.0 * (nm or 0) / (30 * nn)))
# ante 基础旋同样不许有炸弹;normal/ante 的 100x+ 桶(基础旋无炸弹赔不出来)应全是 FS 回合
cur.execute(f'''SELECT COUNT(*), SUM({cnt % 'M'}) FROM game_olympus_step s
JOIN game_olympus_round r USING(round_id)
WHERE r.box_id BETWEEN 11 AND 20 AND s.free_spin_id = 0''')
an, am = map(float, cur.fetchone())
am = am or 0.0
print('ante 基础盘步数=%s 带炸弹格=%s (%.1f%%)' % (an, am, 100.0 * am / (30 * an)))
cur.execute('''SELECT r.ornament_id, COUNT(*) 回合, SUM(r.free_spin_count>0) FS回合
FROM game_olympus_round r WHERE r.box_id <= 20 AND r.ornament_id >= 12
GROUP BY r.ornament_id ORDER BY r.ornament_id''')
print('normal/ante 100x+ 桶(应 回合=FS回合):', cur.fetchall())
cur.execute('''SELECT r.box_id, COUNT(*) 回合, SUM(r.free_spin_count>0) FS回合
FROM game_olympus_round r WHERE r.box_id <= 20
GROUP BY r.box_id ORDER BY r.box_id''')
print('normal/ante 每箱 FS 回合数:', cur.fetchall())
# 爆奖档位:只有小/中/大,且只落在免费旋转的旋末帧
cur.execute('''SELECT bigwin_level, COUNT(*) FROM game_olympus_step
GROUP BY bigwin_level ORDER BY bigwin_level''')
print('爆奖档位分布:', cur.fetchall())
cur.execute('''SELECT COUNT(*) FROM game_olympus_step
WHERE bigwin_level IS NOT NULL AND (is_final <> 1 OR free_spin_id = 0)''')
print('爆奖档位落在基础旋/非末帧的行数(应=0):', cur.fetchone()[0])
# 盘面绝不能有空格(前端会显示成空白格子)
cur.execute('''SELECT COUNT(*) FROM game_olympus_step WHERE grid_state LIKE '%""%' ''')
print('盘面空白格行数(应=0):', cur.fetchone()[0])
# 池子构成(每桶应≈77)
cur.execute('''SELECT box_id, MIN(c), MAX(c) FROM
(SELECT box_id, ornament_id, COUNT(*) c FROM game_olympus_round
GROUP BY box_id, ornament_id) t GROUP BY box_id ORDER BY box_id''')
print('每箱桶内回合数 min/max:', sorted({(r[1], r[2]) for r in cur.fetchall()}))
# 配比是否为后台调好的曲线(非均分)
cur.execute('''SELECT box_id, SUM(count) FROM game_olympus_box_ornament
GROUP BY box_id, audience ORDER BY box_id LIMIT 3''')
print('box_ornament 前3箱×(身份) sum(count):', cur.fetchall())
def fsstats():
"""免费旋转观感统计(按桶):每局"有消除的旋数"、"炸弹并入累计器的次数"、局末累计倍数中位数。
玩家真正感受到的是"按后台配比加权"后的那一行 —— 低倍桶占比高时观感就被低倍桶决定。"""
import statistics
from collections import defaultdict, Counter
from olympus.config import ORNAMENTS
_orn = {o['id']: o['name'] for o in ORNAMENTS}
box_id = int(sys.argv[2]) if len(sys.argv) > 2 else 21
with db.get_conn() as conn:
cur = conn.cursor()
cur.execute('''SELECT r.round_id, r.ornament_id, s.free_spin_id, s.base_score,
s.accumulator_before, s.accumulator_after
FROM game_olympus_step s JOIN game_olympus_round r USING(round_id)
WHERE r.box_id=%s AND s.free_spin_id>0 ORDER BY s.round_id, s.id''', (box_id,))
rows = cur.fetchall()
cur.execute('''SELECT ornament_id, count FROM game_olympus_box_ornament
WHERE box_id=%s AND audience=%s''', (box_id, 'common'))
cfg = {int(a): int(b) for a, b in cur.fetchall()}
cur.execute('SELECT ornament_id, AVG(actual_mul) FROM game_olympus_round WHERE box_id=%s GROUP BY ornament_id', (box_id,))
avg = {int(a): float(b) for a, b in cur.fetchall()}
spins = {}
for rid, orn, fs, base, ab, aa in rows:
d = spins.setdefault((rid, orn, fs), {'elim': False, 'ab': 0.0, 'aa': 0.0})
if float(base or 0) > 0:
d['elim'] = True
d['ab'] = float(ab or 0)
d['aa'] = float(aa or 0)
per = defaultdict(Counter)
rounds_of = defaultdict(set)
acc_of = {}
for (rid, orn, fs), d in spins.items():
c = per[orn]
c['spins'] += 1
rounds_of[orn].add(rid)
acc_of[rid] = max(acc_of.get(rid, 0.0), d['aa'])
if d['elim']:
c['elim'] += 1
if d['aa'] > d['ab']:
c['merges'] += 1
total = sum(cfg.values()) or 1
print('box%d 免费旋转观感(桶 抽中率 | 每局消除旋数 | 每局炸弹并入 | 局末累计中位 | 该桶实际均值)' % box_id)
w_elim = w_merges = 0.0
for orn in sorted(per):
c = per[orn]
n = len(rounds_of[orn]) or 1
elim_per, merge_per = c['elim'] / n, c['merges'] / n
accs = [acc_of[r] for r in rounds_of[orn]]
w = cfg.get(orn, 0) / total
w_elim += w * elim_per
w_merges += w * merge_per
print(' %2d %6sx %5.1f%% | %6.1f 旋 | %6.2f 次 | %8.0f | %8.2f' % (
orn, _orn.get(orn, '?'), 100 * w, elim_per, merge_per,
statistics.median(accs) if accs else 0, avg.get(orn, 0)))
print(' ── 玩家加权(按配比):每局 %.1f 旋有消除、%.2f 次炸弹并入' % (w_elim, w_merges))
def fsdry():
"""免费旋转"连续空转"统计:一局里最长连续几旋一个符号都没消(玩家观感=一直白转)。
dry = 该旋所有步的 base_score 都是 0(scatter 赔付不算消除:S 不参与消除)。
只统计有 FS 的回合;1000x 封顶截断的回合里被丢掉的旋按缺失处理,不参与连击计数。
"""
from collections import Counter, defaultdict
from olympus.config import ORNAMENTS
_orn = {o['id']: o['name'] for o in ORNAMENTS}
box_id = int(sys.argv[2]) if len(sys.argv) > 2 else None
sql = '''SELECT r.round_id, r.box_id, r.ornament_id, s.free_spin_id,
SUM(s.base_score > 0) elim
FROM game_olympus_step s JOIN game_olympus_round r USING(round_id)
WHERE s.free_spin_id > 0'''
args = ()
if box_id:
sql += ' AND r.box_id=%s'
args = (box_id,)
sql += ''' GROUP BY r.round_id, r.box_id, r.ornament_id, s.free_spin_id
ORDER BY r.round_id, s.free_spin_id'''
with db.get_conn() as conn:
cur = conn.cursor()
cur.execute(sql, args)
rows = cur.fetchall()
cur.execute('SELECT box_id, box_type FROM game_olympus_box')
btype = {int(a): str(b) for a, b in cur.fetchall()}
cur.execute("""SELECT box_id, ornament_id, count FROM game_olympus_box_ornament
WHERE audience='common'""")
cfg = defaultdict(dict)
for b, o, c in cur.fetchall():
cfg[int(b)][int(o)] = int(c)
per_round, meta = defaultdict(list), {}
for rid, b, o, fs, elim in rows:
per_round[int(rid)].append((int(fs), float(elim or 0) > 0))
meta[int(rid)] = (int(b), int(o))
# rid -> (box, orn, 旋数, 有消除旋数, 最长连空)
stat = {}
for rid, seq in per_round.items():
run = mx = 0
for _, e in sorted(seq):
run = 0 if e else run + 1
mx = max(mx, run)
stat[rid] = (meta[rid][0], meta[rid][1], len(seq), sum(e for _, e in seq), mx)
def show(tag, rids, weight_by_cfg=False):
"""weight_by_cfg:≥4 连空的概率按后台配比 count 加权(= 玩家实际遇到的概率)"""
if not rids:
return []
dist, wsum, p4 = Counter(), 0.0, 0.0
spins = elim = 0
worst = []
for rid in rids:
b, o, n, e, mx = stat[rid]
w = (cfg.get(b, {}).get(o, 0) or 1) if weight_by_cfg else 1
wsum += w
spins += n
elim += e
dist[min(mx, 6)] += 1 # 6 及以上并作一格,否则 >6 的局不进任何格、占比会莫名少一截
if mx >= 4:
p4 += w
worst.append((mx, rid))
n = len(rids)
worst.sort(reverse=True)
print('%-12s 局=%5d 旋/局=%5.1f 有消除=%4.1f/局(%4.1f%%) | 最长连空 0~6+ : %s | ≥4连空 %5.1f%%'
% (tag, n, spins / n, elim / n, 100.0 * elim / max(1, spins),
' '.join('%d%s:%.0f%%' % (k, '+' if k == 6 else '', 100.0 * dist[k] / n)
for k in range(7)),
100.0 * p4 / wsum))
return worst
by_mode = defaultdict(list)
for rid, (b, o) in meta.items():
by_mode[btype.get(b, '?')].append(rid)
print('=== 免费旋转"连续空转"(dry = 该旋 base_score 全 0;≥4连空 一列买 FS 按配比加权)===')
all_worst = []
for mode in ('normal', 'ante', 'buy_fs'):
rids = by_mode.get(mode) or []
if not rids:
continue
all_worst += [(mx, rid, mode, meta[rid]) for mx, rid in show(mode, rids)[:5]]
by_orn = defaultdict(list) # 同一模式的 10 个箱合并看桶
for rid in rids:
by_orn[meta[rid][1]].append(rid)
for o in sorted(by_orn):
show(' %s %s' % (mode, _orn.get(o, o)), by_orn[o], weight_by_cfg=True)
all_worst.sort(reverse=True)
print('--- 最差局(最长连空 / round_id / 模式 / 桶)---')
for mx, rid, mode, (b, o) in all_worst[:12]:
print(' 连空%d旋 round=%d %s box%d 桶%s' % (mx, rid, mode, b, _orn.get(o, o)))
if __name__ == '__main__':
cmd = sys.argv[1] if len(sys.argv) > 1 else 'check'
globals()[cmd]()