100 lines
3.8 KiB
Python
100 lines
3.8 KiB
Python
"""麻将胡了 · 自然盘面仿真(回答"空转率/命中率到底是多少、离目标差多远")。
|
||
|
||
用法:
|
||
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()
|