麻将胡了
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""麻将胡了 · 自然盘面仿真(回答"空转率/命中率到底是多少、离目标差多远")。
|
||||
|
||||
用法:
|
||||
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()
|
||||
Reference in New Issue
Block a user