first commit

This commit is contained in:
2026-09-21 19:21:35 +08:00
commit 6c944c4ffd
2733 changed files with 296956 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
"""重新生成回合数据前的检查 / 快照 / 清空 / 还原(一次性脚本,用完可删)。
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]()