"""重新生成回合数据前的检查 / 快照 / 清空 / 还原(一次性脚本,用完可删)。 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]()