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

167 lines
6.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.
"""麻将胡了 · 唯一事实来源(规则参数)。
数值全部取自 docs/麻将胡了设计方案.md(§2 规则 / §3 桶与 RTP / §2.8 投注档)。
改规则改这里;生成器、引擎、单测、仿真都从这里读。
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, Mapping, Tuple
# ============================================================
# 盘面:5 轴 × 4 行 = 20 格,ways = 4^5 = 1024
# ============================================================
GRID_COLS = 5
GRID_ROWS = 4
GRID_SIZE = GRID_ROWS * GRID_COLS
WAYS = GRID_ROWS ** GRID_COLS
WILD = "W" # 百搭(元宝):只能由金色符号转化产生,卷轴永不落
SCATTER = "S" # 胡(夺宝):只触发免费旋转,不赔现金
# 格位约定:接口/落库用 1 基、行优先(1~20);引擎内部用 0 基。
# 引擎内 index = row * GRID_COLS + col;pos() 负责换算(见 engine.py)。
@dataclass(frozen=True)
class SymbolRule:
code: str
name: str
payouts: Mapping[int, float] # 连轴数(3/4/5) → 倍投注总额
# 赔付表:原版"每条基础投注点数"已 ÷20 内化,表中数值**直接是倍率**
SYMBOL_RULES: Dict[str, SymbolRule] = {
"A": SymbolRule("A", "發", {3: 0.75, 4: 3.0, 5: 5.0}),
"B": SymbolRule("B", "中", {3: 0.5, 4: 2.0, 5: 4.0}),
"C": SymbolRule("C", "白板", {3: 0.4, 4: 1.0, 5: 3.0}),
"D": SymbolRule("D", "八萬", {3: 0.3, 4: 0.75, 5: 2.0}),
"E": SymbolRule("E", "花筒", {3: 0.2, 4: 0.5, 5: 1.0}),
"F": SymbolRule("F", "索红绿", {3: 0.2, 4: 0.5, 5: 1.0}),
"G": SymbolRule("G", "筒", {3: 0.1, 4: 0.25, 5: 0.5}),
"H": SymbolRule("H", "索绿", {3: 0.1, 4: 0.25, 5: 0.5}),
}
PAYING: Tuple[str, ...] = ("A", "B", "C", "D", "E", "F", "G", "H")
PAYTABLE: Dict[str, Mapping[int, float]] = {code: r.payouts for code, r in SYMBOL_RULES.items()}
MIN_CHAIN = 3 # 前缀链至少 3 连才成奖(只有 3/4/5 三档)
# 卷轴出现权重:只有 8 个付费符号 + 胡(**没有 W**,原生盘面永不落百搭)
SYMBOL_WEIGHTS: Dict[str, int] = {
"A": 5, "B": 6, "C": 8, "D": 9, "E": 11, "F": 11, "G": 13, "H": 13, "S": 2,
}
# 金色麻将符号:只可能出现在第 2/3/4 轴(0 基列 1..3)
GOLDEN_COLS: Tuple[int, ...] = (1, 2, 3)
GOLDEN_CELL_PROB = 0.10 # 逐格独立;新补入的符号也可以带金
# ============================================================
# 乘数阶梯(本波阶梯倍率):每旋从第一档重新开始,不跨旋累计
# ============================================================
LADDER_BASE: Tuple[int, ...] = (1, 2, 3, 5)
LADDER_FS: Tuple[int, ...] = (2, 4, 6, 10)
def ladder_mul(rotation_seq: int, is_fs: bool) -> int:
"""第 n 波(1 基)的阶梯倍率;第 4 波及以后取最后一档。"""
if rotation_seq < 1:
raise ValueError("波序从 1 开始")
ladder = LADDER_FS if is_fs else LADDER_BASE
return ladder[min(rotation_seq, len(ladder)) - 1]
# ============================================================
# 免费旋转:3+ 胡 → 12 旋 + 每多 1 个 +2;一屏最多 5 个胡;上限 36 旋
# "发不满就不发":已发放 + 本次应赠 > 36 时,本旋绝不落 3+ 胡
# ============================================================
SCATTER_TRIGGER = 3
FS_BASE_SPINS = 12
FS_EXTRA_PER_SCATTER = 2
MAX_SCATTER_PER_SCREEN = 5
MAX_FREE_SPINS = 36
def fs_award(scatter_count: int) -> int:
return FS_BASE_SPINS + FS_EXTRA_PER_SCATTER * max(0, scatter_count - SCATTER_TRIGGER)
# 单局封顶:总倍率到达即截断,剩余旋作废
MAX_WIN_CAP_MUL = 500
# 悬念盘:基础旋恰好 2 个胡且第 5 轴无胡(生成端配额,客户端播第 5 列放大)
NEARMISS_SCATTER_COUNT = 2
NEARMISS_2SCATTER_RATE = 0.015
# ============================================================
# 投注档:11 档纯投注档,无投注倍数(betMul 仅预留、服务端只接受 1)
# price = cost = box.bet;boxId = 序号(1~11)
# ============================================================
BET_LADDER: Tuple[int, ...] = (1, 3, 5, 10, 20, 30, 50, 100, 200, 300, 500)
BOX_IDS: Tuple[int, ...] = tuple(range(1, len(BET_LADDER) + 1))
def bet_of(box_id: int) -> int:
return BET_LADDER[box_id - 1]
# ============================================================
# 饰品桶(16 档,0x~500x):沿用玫瑰女郎 17 档结构、删掉 1000x 那档
# 期望倍率 = (区间下限 + 上限) / 2;区间连续无缝
# 桶 16 区间上界故意开到 700 作缓冲,但生成端"先按 500 截断再落桶"
# ============================================================
@dataclass(frozen=True)
class Bucket:
id: int
name: str
expect_mul: float
range_low: float
range_high: float
BUCKETS: Tuple[Bucket, ...] = (
Bucket(1, "0倍饰品", 0.0, 0.0, 0.0),
Bucket(2, "0.5倍饰品", 0.5, 0.01, 1.0),
Bucket(3, "1.5倍饰品", 1.5, 1.0, 2.0),
Bucket(4, "2.5倍饰品", 2.5, 2.0, 3.0),
Bucket(5, "4倍饰品", 4.0, 3.0, 5.0),
Bucket(6, "6.5倍饰品", 6.5, 5.0, 8.0),
Bucket(7, "10倍饰品", 10.0, 8.0, 12.0),
Bucket(8, "16倍饰品", 16.0, 12.0, 20.0),
Bucket(9, "25倍饰品", 25.0, 20.0, 30.0),
Bucket(10, "40倍饰品", 40.0, 30.0, 50.0),
Bucket(11, "60倍饰品", 60.0, 50.0, 70.0),
Bucket(12, "100倍饰品", 100.0, 70.0, 130.0),
Bucket(13, "150倍饰品", 150.0, 130.0, 170.0),
Bucket(14, "200倍饰品", 200.0, 170.0, 230.0),
Bucket(15, "300倍饰品", 300.0, 230.0, 370.0),
Bucket(16, "500倍饰品(封顶)", 500.0, 370.0, 700.0),
)
FIXED_CAP_BUCKET_ID = 16 # 封顶桶
def bucket_of(mul: float) -> int:
"""按**已截断**的倍率落桶(顺序:先 min(mul, 500) 再落桶)。<0.01 视为空转。"""
mul = min(float(mul), MAX_WIN_CAP_MUL)
if mul < 0.01:
return 1
for b in BUCKETS:
if b.range_low <= mul <= b.range_high:
return b.id
return FIXED_CAP_BUCKET_ID
# ============================================================
# RTP 与身份
# ============================================================
RTP = 0.80 # 目标返奖率(后台可按箱改)
CONVERGE_RATIO = 0.15 # 收敛阶段占比(一轮 N = fixedSum / (1 - cr))
AUDIENCE_COMMON = "common" # 其余全部(**含 user_type='03' 机器人**)
AUDIENCE_ANCHOR = "anchor" # user_type = '01'
AUDIENCE_WHITE = "white" # is_blank_account = 1(最高优先)
# 只有 3 个身份维度:robot 维度已按用户要求去掉(2026-09-22),机器人归 common
AUDIENCES: Tuple[str, ...] = (AUDIENCE_COMMON, AUDIENCE_ANCHOR, AUDIENCE_WHITE)
# 生成器/池子规模(先小规模验证,再放大到线上)
ALLOC_K = 1.5 # 池子 = 配比 × 1.5
ALLOC_FLOOR = 60 # 每桶保底条数
SMOKE_ROUNDS_PER_BUCKET = 20 # `--per-bucket 20` 那种冒烟规模