swagger 推广后台 优化对战百佳历史接口
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
"""麻将胡了 · 回合池生成器:把引擎跑出的回合写进 `game_mahjong_round` / `game_mahjong_step`。
|
||||
|
||||
两种模式:
|
||||
--mode natural (默认)自然形态:不干预,直接采样落库。**RTP≈5.25,只能测链路,不能当验收**。
|
||||
--mode quota 配额构造:先标定每桶池内实测均值 → 二分解出 16 桶配额(空转率锁 65%、目标 RTP 0.80)
|
||||
→ 按配额定量填充。**这才是能上线/能做 RTP 验收的池子**。
|
||||
|
||||
用法(在 mahjong-py 目录下):
|
||||
python generate_rounds.py --mode quota --per-box 1000 --dry-run
|
||||
python generate_rounds.py --mode quota --per-box 1000 --write
|
||||
python generate_rounds.py --mode natural --per-box 100 --dry-run
|
||||
python generate_rounds.py --boxes 1,2 --per-box 100 --write --mode quota
|
||||
|
||||
写库内容(两种模式一样):
|
||||
1. `game_mahjong_round`:每局一行(bucket_id / actual_mul / 旋数 / 波数 / has_fs / near_miss)
|
||||
2. `game_mahjong_step`:每帧一行(逐波;含 grid_state / ways_detail / 下落补位 / 阶梯 / 触发信息)
|
||||
3. `game_mahjong_box_bucket`:**配额就是 count**(3 个身份各一套,used=0)
|
||||
4. `game_mahjong_box_template`:每箱每身份一条生效模板(= 配额快照,供抽干补桶)
|
||||
5. `game_mahjong_box.total_rounds`:回写成本次生成的回合数
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from mahjong import config as C # noqa: E402
|
||||
from mahjong.engine import GRID_COLS, GRID_ROWS, play_round # noqa: E402
|
||||
|
||||
DB = dict(host="127.0.0.1", port=23306, user="henan", password="iTSJSPPZM3LSGAPC", database="henan")
|
||||
EMPTY_SHARE = 0.65 # 空转率(桶 1 配额占比,见设计文档 §3.9);未按箱分档时的统一值
|
||||
|
||||
# 按档调波动(用户 2026-09-22 定):**所有箱子 RTP 都锁 0.80**,但波动不同 ——
|
||||
# 低档(1~20):空转低 → 剩余配额必须更"平" → 小奖更多、手感热;
|
||||
# 高档(200~500):空转高 → 剩余配额更"陡" → 中大奖更多、博性更强。
|
||||
# 机制:`solve_quotas` 会把"剩余部分"的加权均值凑到 `0.80/(1-空转率)`,所以
|
||||
# **只改空转率就能同时改形状**,不用再单独调 power。
|
||||
VOLATILITY_LADDER = (
|
||||
(20, 0.62, "低档:空转 62%,小奖更多(热)"),
|
||||
(100, 0.65, "中档:空转 65%(设计基准)"),
|
||||
(10 ** 9, 0.68, "高档:空转 68%,中大奖更多(陡)"),
|
||||
)
|
||||
|
||||
|
||||
def empty_share_for(box_id: int) -> float:
|
||||
bet = C.bet_of(box_id)
|
||||
for upper, share, _ in VOLATILITY_LADDER:
|
||||
if bet <= upper:
|
||||
return share
|
||||
return EMPTY_SHARE
|
||||
TARGET_RTP = 0.80
|
||||
CALIB_ROUNDS = 6000 # 每箱标定样本(用来量每个桶的**池内实测**均值)
|
||||
MAX_TRIES = 400_000 # 单个桶的采样上限(防低概率桶把时间吃光)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 行构造
|
||||
def grid_rows(flat) -> list:
|
||||
"""扁平 20 格 → 4 行 × 5 列(接口用的 gridState 形状)。"""
|
||||
return [list(flat[r * GRID_COLS:(r + 1) * GRID_COLS]) for r in range(GRID_ROWS)]
|
||||
|
||||
|
||||
def build_steps(round_, round_id: int):
|
||||
"""把一局摊成逐帧行。"""
|
||||
rows, step_seq = [], 0
|
||||
spin_win = 0.0
|
||||
for spin in round_.spins:
|
||||
spin_win_before = spin_win
|
||||
acc_sym = Counter()
|
||||
winners = []
|
||||
rotations = spin.rotations
|
||||
frames = rotations or (None,) # 空旋也要发一帧(落定盘面),否则客户端无从渲染
|
||||
for i, rot in enumerate(frames, start=1):
|
||||
step_seq += 1
|
||||
if rot is None:
|
||||
grid_flat = spin.landing
|
||||
ways, removed, drops, fills, conv, gold = [], [], [], [], [], []
|
||||
base_mul, rot_mul = 0.0, C.ladder_mul(1, spin.is_fs)
|
||||
else:
|
||||
grid_flat = rot.grid_before
|
||||
gold = [{"pos": p, "symbol": rot.grid_before[p - 1]} for p in rot.golden_before]
|
||||
conv = [{"pos": p, "fromSymbol": s} for p, s in rot.converted]
|
||||
ways = [{"symbol": w.symbol, "reelCount": w.run, "counts": list(w.counts),
|
||||
"loc": list(w.loc), "ways": w.ways, "payBase": w.pay_base,
|
||||
"rotationMul": rot.rotation_mul,
|
||||
"win": round(w.pay_base * rot.rotation_mul, 4)}
|
||||
for w in rot.wins]
|
||||
seen = set()
|
||||
removed = [p for w in rot.wins for p in w.loc if not (p in seen or seen.add(p))]
|
||||
drops = [{"from": a, "to": b, "symbol": s} for a, b, s in rot.drops]
|
||||
fills = [{"pos": p, "symbol": s} for p, s in rot.fills]
|
||||
base_mul, rot_mul = rot.base_mul, rot.rotation_mul
|
||||
for w in rot.wins:
|
||||
acc_sym[w.symbol] += len(w.loc)
|
||||
winners.append({"symbol": w.symbol, "count": w.run,
|
||||
"win": round(w.pay_base * rot.rotation_mul, 4)})
|
||||
spin_win = spin_win_before + sum(r.win_mul for r in rotations[:i])
|
||||
is_last = (i == len(frames))
|
||||
scat = ({"scatterCount": spin.scatters, "freeSpins": spin.fs_awarded, "extraSpins": 0}
|
||||
if spin.index == 0 and i == 1 and spin.fs_awarded else None)
|
||||
retri = ({"scatterCount": spin.scatters, "extraSpins": C.fs_award(spin.scatters)}
|
||||
if spin.index > 0 and i == 1 and spin.retrigger else None)
|
||||
rows.append(dict(
|
||||
id=round_id * 100 + step_seq,
|
||||
round_id=round_id,
|
||||
reel_spin_id=spin.index,
|
||||
rotation_seq=i,
|
||||
grid_state=json.dumps(grid_rows(grid_flat), ensure_ascii=False) if grid_flat else None,
|
||||
golden_detail=json.dumps(gold, ensure_ascii=False) if gold else None,
|
||||
wild_converted=json.dumps(conv, ensure_ascii=False) if conv else None,
|
||||
ways_detail=json.dumps(ways, ensure_ascii=False) if ways else None,
|
||||
accumulated_symbols=json.dumps(dict(acc_sym), ensure_ascii=False),
|
||||
spin_winners=json.dumps(winners, ensure_ascii=False),
|
||||
removed_positions=json.dumps(removed),
|
||||
drop_data=json.dumps(drops, ensure_ascii=False) if drops else None,
|
||||
fill_data=json.dumps(fills, ensure_ascii=False) if fills else None,
|
||||
scatter_trigger=json.dumps(scat, ensure_ascii=False) if scat else None,
|
||||
retrigger_info=json.dumps(retri, ensure_ascii=False) if retri else None,
|
||||
base_score=round(base_mul, 4),
|
||||
win_amount=round(sum(r.win_mul for r in rotations[:i]), 4),
|
||||
rotation_mul=rot_mul,
|
||||
spin_win=round(spin_win, 4),
|
||||
win_capped=1 if (round_.capped and is_last and spin is round_.spins[-1]) else 0,
|
||||
bigwin_level=None,
|
||||
is_final=1 if is_last else 0,
|
||||
))
|
||||
spin_win = spin_win_before + sum(r.win_mul for r in rotations)
|
||||
return rows
|
||||
|
||||
|
||||
def make_round(box_id: int, seq: int, rng: random.Random):
|
||||
r = play_round(rng, box_id=box_id)
|
||||
round_id = box_id * 1_000_000 + seq
|
||||
steps = build_steps(r, round_id=round_id)
|
||||
row = dict(round_id=round_id, box_id=box_id, bucket_id=C.bucket_of(r.actual_mul),
|
||||
actual_mul=round(r.actual_mul, 4), free_spin_count=r.free_spins,
|
||||
total_rotations=sum(len(s.rotations) for s in r.spins),
|
||||
has_fs=1 if r.has_fs else 0, near_miss=1 if r.near_miss else 0)
|
||||
return row, steps
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 配额构造
|
||||
def calibrate(box_id: int, n: int, seed: int):
|
||||
"""小样本标定:每个桶的**池内实测均值**(自然均值与名义 expect_mul 差很多,必须实测)。"""
|
||||
rng = random.Random(seed)
|
||||
tot, cnt = Counter(), Counter()
|
||||
for _ in range(n):
|
||||
r = play_round(rng, box_id=box_id)
|
||||
b = C.bucket_of(r.actual_mul)
|
||||
tot[b] += r.actual_mul
|
||||
cnt[b] += 1
|
||||
means = {}
|
||||
for b in C.BUCKETS:
|
||||
means[b.id] = (tot[b.id] / cnt[b.id]) if cnt[b.id] else b.expect_mul
|
||||
return means, cnt
|
||||
|
||||
|
||||
def _quota_from_power(means, power: float, empty_share: float):
|
||||
"""w_b = 30×0.6^((b-1)×power)(b≥2),桶 1 锁 empty_share;返回 16 个配额(下标 1..16)。"""
|
||||
ws = {b.id: 30.0 * (0.6 ** ((b.id - 1) * power)) for b in C.BUCKETS if b.id >= 2}
|
||||
s = sum(ws.values())
|
||||
q = {b.id: 0.0 for b in C.BUCKETS}
|
||||
q[1] = empty_share
|
||||
for b_id, w in ws.items():
|
||||
q[b_id] = (1.0 - empty_share) * w / s
|
||||
return q
|
||||
|
||||
|
||||
def solve_quotas(means, target_rtp=TARGET_RTP, empty_share=EMPTY_SHARE):
|
||||
"""二分 power,使 Σ q_b × mean_b = target_rtp。power 越大越偏向低倍桶(期望越低)。"""
|
||||
def expected(power):
|
||||
q = _quota_from_power(means, power, empty_share)
|
||||
return sum(q[b.id] * means[b.id] for b in C.BUCKETS)
|
||||
lo, hi = 0.05, 8.0
|
||||
# expected(power) 随 power **递减**:power 越大越偏向低倍桶
|
||||
for _ in range(80):
|
||||
mid = (lo + hi) / 2
|
||||
if expected(mid) > target_rtp:
|
||||
lo = mid # 期望偏高 → 还要更大的 power
|
||||
else:
|
||||
hi = mid
|
||||
q = _quota_from_power(means, (lo + hi) / 2, empty_share)
|
||||
return q, expected((lo + hi) / 2)
|
||||
|
||||
|
||||
def _fill(box_id: int, targets: dict, seed: int, max_tries=MAX_TRIES):
|
||||
"""按桶目标条数填充:落进目标桶才收。返回 (pool, bucket_cnt, short)。"""
|
||||
rng = random.Random(seed)
|
||||
pool, tries, short = [], Counter(), []
|
||||
for b in C.BUCKETS:
|
||||
got = 0
|
||||
while got < targets[b.id] and tries[b.id] < max_tries:
|
||||
tries[b.id] += 1
|
||||
row, steps = make_round(box_id, len(pool) + 1, rng)
|
||||
if row["bucket_id"] == b.id:
|
||||
pool.append((row, steps))
|
||||
got += 1
|
||||
if got < targets[b.id]:
|
||||
short.append(f"桶{b.id} {got}/{targets[b.id]}")
|
||||
for i, (row, steps) in enumerate(pool, start=1):
|
||||
rid = box_id * 1_000_000 + i
|
||||
row["round_id"] = rid
|
||||
for j, st in enumerate(steps, start=1):
|
||||
st["round_id"] = rid
|
||||
st["id"] = rid * 100 + j
|
||||
cnt = Counter(row["bucket_id"] for row, _ in pool)
|
||||
return pool, cnt, short
|
||||
|
||||
|
||||
def quota_targets(per_box: int, quotas: dict):
|
||||
targets = {b.id: int(round(per_box * quotas[b.id])) for b in C.BUCKETS}
|
||||
diff = per_box - sum(targets.values())
|
||||
if diff:
|
||||
targets[1] += diff
|
||||
return targets
|
||||
|
||||
|
||||
def _sample_bucket(box_id: int, bucket_id: int, rng: random.Random, max_tries=MAX_TRIES):
|
||||
"""采一条落进指定桶的回合(低概率桶会多试几次)。"""
|
||||
for _ in range(max_tries):
|
||||
row, steps = make_round(box_id, 0, rng)
|
||||
if row["bucket_id"] == bucket_id:
|
||||
return row, steps
|
||||
return None
|
||||
|
||||
|
||||
def _tune(box_id: int, pool: list, target_rtp: float, per_box: int, seed: int, verbose=True):
|
||||
"""整数可执行的定向微调:配额算出来是小数,四舍五入会把「0.4 条」的尾部桶抹成 0 条,
|
||||
于是它本该贡献的期望就丢了(实测池内均值会系统性偏低 5~8pt)。
|
||||
|
||||
做法:拿空转局(桶 1,倍率 0)去换中高桶的回合,每换一条按**真实倍率**结账,
|
||||
直到池内均值落进目标 ±0.005 以内。空转率只会动几条(<0.5pt)。
|
||||
"""
|
||||
rng = random.Random(seed + 31337)
|
||||
cur = sum(r["actual_mul"] for r, _ in pool)
|
||||
need = target_rtp * per_box - cur # 还差多少(单位:倍率总和)
|
||||
empties = [i for i, (r, _) in enumerate(pool) if r["bucket_id"] == 1]
|
||||
ordered = sorted((x for x in C.BUCKETS if x.id >= 2), key=lambda x: -x.expect_mul)
|
||||
swaps = 0
|
||||
while need > 0.6 and empties and ordered:
|
||||
cand = next((b for b in ordered if b.expect_mul <= need), None)
|
||||
if cand is None:
|
||||
break
|
||||
got = None
|
||||
for _ in range(300): # 只收"不超过缺口"的回合,保证不会冲过头
|
||||
s = _sample_bucket(box_id, cand.id, rng)
|
||||
if s and s[0]["actual_mul"] <= need:
|
||||
got = s
|
||||
break
|
||||
if got is None: # 这一档试不出来,退一档
|
||||
ordered = [b for b in ordered if b.id != cand.id]
|
||||
continue
|
||||
row, steps = got
|
||||
pool[empties.pop()] = (row, steps)
|
||||
need -= row["actual_mul"]
|
||||
swaps += 1
|
||||
rng = random.Random(rng.random()) # 让后续采样换流
|
||||
if verbose and swaps:
|
||||
print(f" 微调:换入 {swaps} 条中高桶回合(空转率随之动 {swaps / per_box:.2%})")
|
||||
cnt = Counter(r["bucket_id"] for r, _ in pool)
|
||||
rtp = sum(r["actual_mul"] for r, _ in pool) / max(1, len(pool))
|
||||
if verbose:
|
||||
print(f" 微调后池内实测 RTP {rtp:.4f}(与目标差 {abs(rtp - target_rtp):.4f})")
|
||||
return pool, cnt
|
||||
|
||||
|
||||
def generate_quota(box_id: int, per_box: int, seed: int, empty_share: float, target_rtp: float,
|
||||
verbose=True):
|
||||
"""解配额 → 定条数 → 填充 → 整数微调 → 重排 id。
|
||||
|
||||
`empty_share`:桶 1 的配额占比。CLI 不传时按 `empty_share_for(box_id)` 取**按档波动**的值
|
||||
(低档 62% / 中档 65% / 高档 68%),这样各箱形状不同、但 RTP 都锁在 `target_rtp`。
|
||||
|
||||
为什么填完还要微调:运行时按库存消耗(一个 cycle 每条回合正好播一次)⇒ `cycle RTP = 池内均值`,
|
||||
而配额是连续解出来的小数,四舍五入会抹掉尾部桶(见 `_tune` 的注释)。
|
||||
"""
|
||||
means, _ = calibrate(box_id, CALIB_ROUNDS, seed)
|
||||
quotas, exp_rtp = solve_quotas(means, target_rtp, empty_share)
|
||||
targets = quota_targets(per_box, quotas)
|
||||
if verbose:
|
||||
print(f" 标定:配额理论期望 {exp_rtp:.4f}(目标 {target_rtp});桶1 配额 {quotas[1]:.2%};"
|
||||
f"桶条数(1/2/3/12/16) = {targets[1]}/{targets[2]}/{targets[3]}/{targets[12]}/{targets[16]}")
|
||||
pool, cnt, short = _fill(box_id, targets, seed + 999 + box_id * 7919)
|
||||
pool, cnt = _tune(box_id, pool, target_rtp, per_box, seed + box_id, verbose)
|
||||
for i, (row, steps) in enumerate(pool, start=1): # 重排 id(采样与微调都打乱了顺序)
|
||||
rid = box_id * 1_000_000 + i
|
||||
row["round_id"] = rid
|
||||
for j, st in enumerate(steps, start=1):
|
||||
st["round_id"] = rid
|
||||
st["id"] = rid * 100 + j
|
||||
return pool, quotas, means, exp_rtp, short
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 写库
|
||||
def write(conn, pool_by_box, per_box, audiences=C.AUDIENCES):
|
||||
cur = conn.cursor()
|
||||
total_r = total_s = 0
|
||||
for box, (pool, quotas, bucket_cnt) in pool_by_box.items():
|
||||
rounds = [row for row, _ in pool]
|
||||
steps = [st for _, sts in pool for st in sts]
|
||||
lo, hi = box * 1_000_000 + 1, box * 1_000_000 + per_box
|
||||
cur.execute(f"DELETE FROM game_mahjong_step WHERE round_id BETWEEN {lo} AND {hi}")
|
||||
cur.execute(f"DELETE FROM game_mahjong_round WHERE box_id={box}")
|
||||
cur.executemany(
|
||||
"INSERT INTO game_mahjong_round (round_id,box_id,bucket_id,actual_mul,free_spin_count,"
|
||||
"total_rotations,has_fs,near_miss) VALUES (%(round_id)s,%(box_id)s,%(bucket_id)s,"
|
||||
"%(actual_mul)s,%(free_spin_count)s,%(total_rotations)s,%(has_fs)s,%(near_miss)s)", rounds)
|
||||
cols = ("id", "round_id", "reel_spin_id", "rotation_seq", "grid_state", "golden_detail",
|
||||
"wild_converted", "ways_detail", "accumulated_symbols", "spin_winners",
|
||||
"removed_positions", "drop_data", "fill_data", "scatter_trigger", "retrigger_info",
|
||||
"base_score", "win_amount", "rotation_mul", "spin_win", "win_capped",
|
||||
"bigwin_level", "is_final")
|
||||
cur.executemany(
|
||||
f"INSERT INTO game_mahjong_step ({','.join(cols)}) "
|
||||
f"VALUES ({','.join('%(' + c + ')s' for c in cols)})", steps)
|
||||
for aud in audiences:
|
||||
cur.execute("DELETE FROM game_mahjong_box_bucket WHERE box_id=%s AND audience=%s", (box, aud))
|
||||
cur.executemany(
|
||||
"INSERT INTO game_mahjong_box_bucket (box_id,bucket_id,audience,count,used) "
|
||||
"VALUES (%s,%s,%s,%s,0)",
|
||||
[(box, b.id, aud, bucket_cnt.get(b.id, 0)) for b in C.BUCKETS])
|
||||
items = json.dumps([{"bucketId": b.id, "count": bucket_cnt.get(b.id, 0)} for b in C.BUCKETS],
|
||||
ensure_ascii=False)
|
||||
cur.execute("DELETE FROM game_mahjong_box_template WHERE box_id=%s AND audience=%s", (box, aud))
|
||||
cur.execute(
|
||||
"INSERT INTO game_mahjong_box_template (box_id,audience,name,total_rounds,items_json,is_active) "
|
||||
"VALUES (%s,%s,%s,%s,%s,1)",
|
||||
(box, aud, f"配额池-{per_box}局", per_box, items))
|
||||
cur.execute("UPDATE game_mahjong_box SET total_rounds=%s WHERE box_id=%s", (per_box, box))
|
||||
conn.commit()
|
||||
total_r += len(rounds)
|
||||
total_s += len(steps)
|
||||
rtp = sum(r["actual_mul"] for r in rounds) / max(1, len(rounds))
|
||||
print(f" 箱 {box:2d}(赌注 {C.bet_of(box):>3}):回合 {len(rounds):>5} 帧 {len(steps):>6} "
|
||||
f"**池内实测 RTP {rtp:.4f}** 空转 {bucket_cnt.get(1,0)/max(1,len(rounds)):.1%} "
|
||||
f"FS 局 {sum(r['has_fs'] for r in rounds)}")
|
||||
print(f"合计:回合 {total_r},帧 {total_s}")
|
||||
return total_r, total_s
|
||||
|
||||
|
||||
def build_natural(box_id, per_box, seed):
|
||||
rng = random.Random(seed + box_id)
|
||||
pool, cnt = [], Counter()
|
||||
for i in range(1, per_box + 1):
|
||||
row, steps = make_round(box_id, i, rng)
|
||||
pool.append((row, steps))
|
||||
cnt[row["bucket_id"]] += 1
|
||||
return pool, cnt
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--mode", choices=("natural", "quota"), default="natural")
|
||||
ap.add_argument("--per-box", type=int, default=1000)
|
||||
ap.add_argument("--boxes", default="")
|
||||
ap.add_argument("--seed", type=int, default=20260922)
|
||||
ap.add_argument("--empty-share", type=float, default=None,
|
||||
help="空转率(桶 1 配额占比);不传=按档取(低档 0.62 / 中档 0.65 / 高档 0.68)")
|
||||
ap.add_argument("--target-rtp", type=float, default=TARGET_RTP)
|
||||
ap.add_argument("--write", action="store_true")
|
||||
ap.add_argument("--dry-run", action="store_true")
|
||||
ap.add_argument("--host", default=DB["host"]); ap.add_argument("--port", type=int, default=DB["port"])
|
||||
ap.add_argument("--user", default=DB["user"]); ap.add_argument("--password", default=DB["password"])
|
||||
ap.add_argument("--database", default=DB["database"])
|
||||
args = ap.parse_args()
|
||||
|
||||
boxes = [int(x) for x in args.boxes.split(",") if x.strip()] or list(C.BOX_IDS)
|
||||
print(f"==== {args.mode} 模式:{len(boxes)} 箱 × {args.per_box} 局"
|
||||
f"{'' if args.write and not args.dry_run else '(干跑,不写库)'} ====")
|
||||
pool_by_box = {}
|
||||
for box in boxes:
|
||||
if args.mode == "natural":
|
||||
pool, cnt = build_natural(box, args.per_box, args.seed)
|
||||
quotas = None
|
||||
pool_by_box[box] = (pool, quotas, cnt)
|
||||
else:
|
||||
share = args.empty_share if args.empty_share is not None else empty_share_for(box)
|
||||
pool, quotas, means, exp_rtp, short = generate_quota(
|
||||
box, args.per_box, args.seed, share, args.target_rtp)
|
||||
cnt = Counter(row["bucket_id"] for row, _ in pool)
|
||||
if short:
|
||||
print(f" 未填满:{';'.join(short)}")
|
||||
pool_by_box[box] = (pool, quotas, cnt)
|
||||
if not (args.write and not args.dry_run):
|
||||
rows = [r for r, _ in pool_by_box[box][0]]
|
||||
rtp = sum(r["actual_mul"] for r in rows) / max(1, len(rows))
|
||||
print(f" 箱 {box:2d}(赌注 {C.bet_of(box):>3}):回合 {len(rows):>5} "
|
||||
f"**池内实测 RTP {rtp:.4f}** 空转 {cnt.get(1,0)/max(1,len(rows)):.1%} "
|
||||
f"FS 局 {sum(r['has_fs'] for r in rows)}")
|
||||
if not (args.write and not args.dry_run):
|
||||
return
|
||||
import pymysql
|
||||
conn = pymysql.connect(host=args.host, port=args.port, user=args.user,
|
||||
password=args.password, database=args.database, charset="utf8mb4")
|
||||
write(conn, pool_by_box, args.per_box)
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user