first commit
This commit is contained in:
@@ -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]()
|
||||
Reference in New Issue
Block a user