"""麻将胡了 · 自然盘面仿真(回答"空转率/命中率到底是多少、离目标差多远")。 用法: python mahjong_simulator.py --rounds 200000 --seed 2026 它只跑**自然落牌 + 无偏构造**的局面 —— 也就是"完全不干预"时游戏长什么样: 自然 RTP(= 平均 actual_mul)、基础旋命中率、级联波数分布、FS 自然触发率、悬念盘占比。 生成端要做的事(把配比凑到目标 RTP、把高倍桶构造出来)都在这些数字之上做偏置。 """ from __future__ import annotations import argparse 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 play_round # noqa: E402 def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--rounds", type=int, default=100000) ap.add_argument("--seed", type=int, default=2026) ap.add_argument("--box", type=int, default=5, help="投注档 boxId,只影响金额不影响倍率") args = ap.parse_args() rng = random.Random(args.seed) n = args.rounds base_hit = 0 empty_rounds = 0 fs_rounds = 0 retrigger_rounds = 0 near_miss = 0 capped = 0 rotations = Counter() # 本旋波数(基础旋) fs_award_dist = Counter() bucket_dist = Counter() scatter_dist = Counter() muls = [] total = 0.0 for _ in range(n): r = play_round(rng, box_id=args.box) muls.append(r.actual_mul) total += r.actual_mul base = r.spins[0] if base.mul > 0: base_hit += 1 else: empty_rounds += 1 rotations[len(base.rotations)] += 1 scatter_dist[base.scatters] += 1 if r.has_fs: fs_rounds += 1 fs_award_dist[r.free_spins] += 1 if any(s.retrigger for s in r.spins): retrigger_rounds += 1 if r.near_miss: near_miss += 1 if r.capped: capped += 1 bucket_dist[C.bucket_of(r.actual_mul)] += 1 muls.sort() def pct(p: float) -> float: return muls[min(n - 1, int(n * p))] print(f"=== 自然仿真:{n} 局 / seed={args.seed} / 无偏构造 ===") print(f"自然 RTP(平均 actual_mul) : {total / n:.4f}") print(f"基础旋命中率(至少一波成奖) : {base_hit / n:.4%} → 空转率 {empty_rounds / n:.2%}") print(f"基础旋空转局(= 桶 1 占比) : {empty_rounds / n:.4%}") print(f"FS 自然触发率(3+ 胡) : {fs_rounds / n:.4%} (设计目标 ≈0.5%,生成端要压低)") print(f"其中重触发过的局 : {retrigger_rounds / n:.4%}") print(f"悬念盘(2 胡且第 5 轴无胡) : {near_miss / n:.4%} (设计目标 1~2%)") print(f"触 500x 封顶 : {capped / n:.4%}") print(f"actual_mul 中位 / p90 / p99 / max: {pct(0.5):.2f} / {pct(0.9):.2f} / {pct(0.99):.2f} / {muls[-1]:.2f}") print("\n基础旋波数分布(0 = 一波都不成):") for k in sorted(rotations): print(f" {k} 波: {rotations[k] / n:7.3%}") print("\n落定盘面的胡数分布:") for k in sorted(scatter_dist): print(f" {k} 个: {scatter_dist[k] / n:7.3%}") print("\nFS 赠送旋数分布(触发局内):") for k in sorted(fs_award_dist): print(f" {k} 旋: {fs_award_dist[k] / max(1, fs_rounds):7.3%}") print("\n落桶分布(桶: 占比 / 名义期望倍率):") for b in C.BUCKETS: share = bucket_dist[b.id] / n if share: print(f" {b.id:2d} {b.name:<16} {share:7.4%} expect={b.expect_mul}") if __name__ == "__main__": main()