Files
2026-09-22 15:39:53 +08:00

100 lines
3.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""麻将胡了 · 自然盘面仿真(回答"空转率/命中率到底是多少、离目标差多远")。
用法:
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()