Skip to content

chenxingyuu/maj

Repository files navigation

Mahjong RL - 日本麻将强化学习

日本麻将(Riichi Mahjong)游戏引擎与 Gymnasium 环境,用于训练强化学习智能体。

架构

  • C++ 核心引擎src/):高性能游戏逻辑,实现摸打流程、手牌评估、向听计算、役种判定、得点结算以及完整对局流程。
  • Python 绑定src/bindings.cpp):通过 pybind11 将 C++ 引擎导出为 Python 模块 mahjong_cpp
  • Gymnasium 环境mahjong/env.py):封装为标准 Gymnasium 接口,提供观测编码与动作掩码。
  • 训练流水线mahjong/train.py):基于自博弈的 PPO 训练与评估脚本。

特性

  • 完整的 4 人立直麻将规则实现
  • 40+ 役/役满检测
  • 标准符数 / 番数得点系统
  • 东南战(半庄),带庄家轮转与本场数
  • 包含荒牌流局、九种九牌等流局处理
  • 支持立直、一发、Dora / Ura Dora
  • 支持赤宝牌(Akadora)
  • 动作掩码保证只输出合法动作
  • 约 161 维离散动作空间,约 7000+ 维观测空间

规则说明(C++ 引擎 src/

这一节简要说明当前 C++ 核心引擎实现的主要规则与已知简化点,方便和现有日麻客户端 / 规则书对照。

  • 基本设置

    • 人数与起始分:4 人对战,每人起始分 25000 点。
    • 场次结构:固定为一东一南半庄(Hanchan),风场顺序为 东场 → 南场,共 8 局(含连庄局数)。
    • 庄家与连庄
      • 当前局的庄家为 round_number % 4
      • 和了后如果 庄家和了 或特定流局条件满足,则庄家连庄,本场数 honba +1。
    • 游戏结束条件
      • round_wind >= 2(南场打一圈结束);
      • 或任意一名玩家分数 < 0。
  • 牌与牌山

    • 牌组:使用 34 种牌、共 136 张(每种 4 张),含 3 张赤宝牌(赤5万 / 赤5筒 / 赤5索)
    • 赤宝牌(赤ドラ)
      • 使用 tile_id = tile_type * 4 + tile_index 表示单张牌;
      • tile_index == 0tile_type ∈ {5m, 5p, 5s}(即 t == 4, 13, 22)时视为赤宝牌。
    • 牌山生成与布局
      • 初始牌山为 [0, 1, ..., 135],经 std::shuffle 随机洗牌。
      • 可摸牌山tiles[0..121](共 122 张),按顺序自前向后摸牌。
      • 王牌(Dead wall)tiles[122..135](共 14 张),布局为:
        • 表 Dora 指示牌tiles[130], 128, 126, 124, 122(最多 5 张);
        • 里 Dora 指示牌tiles[131], 129, 127, 125, 123(最多 5 张,对应表 Dora 张数);
        • 岭上牌tiles[135], 134, 133, 132,从 135 向前依次摸。
    • 起手配牌
      • 庄家开始,按 4 张一组轮流发 3 圈 + 每人补 1 张,庄家最终 14 张,其余 13 张。
  • 立直与一发

    • 立直条件can_riichi):
      • 未立直、门前清、分数 ≥ 1000,牌山剩余牌数 ≥ 4;
      • 自家手牌张数满足听牌结构(去任意一张后向听数为 0)。
    • 立直流程
      • 选择一张手牌打出并声宣立直,当次打出的牌记录为立直舍牌;
      • 即时支付 1000 点立直棒,riichi_sticks +1;
      • 若立直发生在 东 1 局前 1 巡内(含起手和第一轮摸打),则记作 双立直
    • 一发(Ippatsu)
      • 立直后自家获得 ippatsu 标记;
      • 当发生任何吃 / 碰 / 杠 时,全员 ippatsu 标记被清除;
      • 引擎中未对 “自家下一次摸打后自动失效” 做完全精细化处理,逻辑有一定简化。
  • 杠相关规则

    • 支持 暗杠(Ankan) / 大明杠(Minkan) / 加杠(Kakan),并在杠后:
      • 翻开新 Dora 指示牌(最多 5 张);
      • 从岭上摸 1 张牌(岭上没牌则直接荒牌流局)。
    • 四开杠途中流局(Suukaikan,四杠流局)
      • 引擎维护全场所有副露中的杠数(暗 / 明 / 加杠均计入)。
      • 当场上累计杠数 达到或超过 4 个 时:
        • 仅有 1 名玩家 拥有全部 4 个以上的杠(即可以冲「四槓子」役满),则 本局继续
        • 否则立即触发途中流局,draw_type = "四開槓",不再进行岭上补牌。
    • 未实现 / 简化点
      • 抢杠荣和(槓放銃 / 槍槓)当前未实现:加杠时不会触发他家“抢杠和”,逻辑中直接跳过该检查。
      • 暗杠目前不会取消他家一发标记(与正式规则有出入)。
  • 流局与途中流局

    • 荒牌流局(墙牌摸完)
      • 当可摸牌山或岭上牌耗尽时触发;
      • 进行听牌 / 不听罚分(3000 点在听者与非听者之间流动);
      • 庄家听牌则连庄,本场数 +1,否则换庄、本场数按规则更新。
    • 九种九牌流局(Kyuushukyuhai)
      • 起手 14 张中有 9 种及以上幺九牌(含字牌与断幺外数字端张),且尚未进行鸣牌;
      • 玩家可主动宣告“九种九牌”,触发途中流局,draw_type = "九种九牌",庄家连庄,本场数 +1。
    • 其他常见途中流局(四风连打、四家立直、三家和等)目前未实现
  • 役种与计分

    • 役种检测由 yaku.cpp 完成,包含 40+ 个役 / 役满,包括但不限于:
      • 一般役:立直、一发、门前清自摸和、平和、断幺九、一杯口、三色同顺、三色同刻、一气通贯、对对和、三暗刻、三杠子、小三元等;
      • 门前限定役:两杯口、七对子等;
      • 役满:国士无双、国士无双十三面、四暗刻、四暗刻单骑、字一色、大三元、小四喜、大四喜、清老头、绿一色、九莲宝灯、纯正九莲宝灯、天和、地和、四槓子 等。
    • ** Dora 计分**:
      • 根据当前 表 Dora 指示牌 计算 Dora 种类;
      • 里 Dora 指示牌数量与表 Dora 数量一致,在立直和了时生效;
      • 赤宝牌根据 is_red_dora(tile_id) 单独+1 翻,与普通 Dora 累加。
    • 点棒结算
      • 采用标准符数 / 番数计算方式,区分庄家 / 闲家的自摸与荣和支付;
      • 本场数影响放铳方 / 非和方额外支付(每本 300 点总计拆分)。
  • 其他注意事项

    • 振听(Furiten)判定有简化:通过“把自己舍牌视作摸进来再加一张看能否和牌”的近似方式来判定,有可能与正式规则边界情况略有差异。
    • 客户端 / 强化学习侧 目前并不区分赤宝牌的 tile_type(仍为 5m/5p/5s),赤宝信息只在 C++ 引擎中通过 tile_idis_red_dora 体现,用于计役与计分。

以上为当前实现状态的概要说明;如需严格对齐某一特定赛事 / 平台的详细规则,可在此基础上进一步调整 src/game.cppsrc/yaku.cpp 等文件中的逻辑。

模型与训练(mahjong/train.py

这一节简要描述当前用于强化学习的模型结构 MahjongNet 以及 PPO 训练流程。

  • 观测与动作空间

    • 观测(Observation) 来源于 MahjongEnv,由三部分组成:
      • spatial:形状为 (40, 34, 4) 的张量,40 个通道分别编码自家手牌、4 家河牌、4 家副露以及 dora 指示牌位置;
      • global_features:长度为 18 的向量,包含场风 / 自风、墙牌剩余、本场数、立直棒数、4 家点数与立直状态、巡目、当前是否摸牌阶段、摸入牌种、自家是否门前、有无副露等信息;
      • action_mask:长度为 161 的 0/1 向量,标记当前时刻哪些离散动作是 合法 的。
    • 动作空间(Action space):长度为 161 的离散动作:
      • 0–33:按牌种打出对应牌(弃牌);
      • 34–54:吃(不同花色与起点的顺子模板);
      • 55–88:碰(按牌种);
      • 89–122:杠(暗杠 / 大明杠 / 加杠,按牌种编码);
      • 123–156:立直并打出对应牌种;
      • 157:自摸;158:荣和;159:过;160:九种九牌流局宣言。
  • 网络结构:MahjongNet

    • 使用一个共享干路的 策略‑价值网络(policy‑value network)
      • 空间编码器 spatial_encoder
        • 两层 2D 卷积:Conv2d(OBS_CHANNELS, 64, kernel_size=(3,1), padding=(1,0)) ×2,均接 ReLU;
        • 展平后接 Linear(64 * NUM_TILE_TYPES * 4, hidden=256) + ReLU;
        • 负责从 40×34×4 的局面平面中抽取牌型与结构化信息。
      • 全局特征编码器 global_encoder
        • Linear(OBS_GLOBAL_SIZE, 64) + ReLU,将 18 维全局特征压缩到 64 维。
      • 共享层 shared
        • 将空间特征(256 维)与全局特征(64 维)拼接成 320 维;
        • Linear(320, 256) + ReLU 得到共享隐藏表示。
      • 策略头 policy_head
        • Linear(256, ACTION_SPACE_N) 输出每个动作的 logits;
        • 前向时若提供 action_mask,会对非法动作位置填充 -inf,从而在 softmax / Categorical 中强制屏蔽非法动作。
      • 价值头 value_head
        • Linear(256, 1) 输出标量状态价值 (V(s)),形状为 (batch,)
  • 训练算法:PPO 自博弈

    • 自博弈设置
      • 训练时,自家(玩家 0)使用可学习的 MahjongNet
      • 其余 3 家使用规则驱动的 RuleAgent,形成固定对手环境。
    • 数据收集 self_play_collect
      • 在一个环境中从起局打到局终:
        • 轮到自家时,用模型前向得到带掩码的策略分布 Categorical(logits),采样动作、执行一步环境,并记录观测、动作、即时回报、价值估计、log‑prob、动作掩码与终止标志;
        • 轮到对手时,由 RuleAgent 选择动作,不记录到训练缓冲区。
      • 样本缓存在 PPOBuffer 中,直至达到设定容量(默认 buffer_size=4096)。
    • 优势估计与损失
      • 使用 GAE‑Lambda(gamma=0.99, lambda=0.95)计算优势 advantages 与回报 returns
      • 训练前对优势做标准化(零均值、单位方差);
      • 优化目标为标准 PPO:
        • 策略损失:裁剪的 PPO 目标,裁剪阈值 clip_eps=0.2
        • 价值损失:((V(s)-R)^2) 的均方误差,系数 vf_coef=0.5
        • 熵正则:鼓励策略多样性,系数 ent_coef=0.01
    • 优化细节
      • 每次更新时按最多 256 的 batch size 打乱取 mini‑batch,迭代 epochs=4 轮;
      • 优化器为 Adam(lr=3e-4)(可通过命令行参数 --lr 调整);
      • 使用 clip_grad_norm_(model.parameters(), 0.5) 做梯度裁剪。
    • 奖励设计
      • 在单局结束时,将该局点数变化(相对本局开始时)除以 1000 作为局内累积 reward;
      • 在整场对战结束时,以最终点数相对 25000 的增减比值(除以 25000)作为终局奖励;
      • 中间步骤 reward 默认为 0。
  • 评估与模型保存

    • 每隔 --eval-interval 轮(默认 50)会对当前模型进行评估:
      • 与同样的规则对手打 --eval-games(默认 20)局,统计平均回报 avg_reward 和自家第一名率 win_rate
      • 若新的 win_rate 超过历史最佳,则保存为 checkpoints/best_model.pt
    • 训练过程中还会周期性写入 checkpoint(每 100 轮一次,含模型与优化器状态),训练结束时额外保存 final_model.pt

Quick Start

环境要求

  • uv(用于创建虚拟环境与安装依赖)。安装示例:

    curl -LsSf https://astral.sh/uv/install.sh | sh

Build

# 使用 uv 创建虚拟环境并安装依赖(含训练与开发依赖)
uv venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
uv sync --extra train --extra dev

# 编译 C++ 引擎
cmake -S . -B build -DBUILD_PYTHON=ON -DBUILD_TESTS=ON \
  -Dpybind11_DIR=$(python -c "import pybind11; print(pybind11.get_cmake_dir())")
cmake --build build -j$(nproc)

# 复制编译好的模块
cp build/src/mahjong_cpp*.so .

Run Tests

# C++ tests
./build/tests/cpp/mahjong_tests

# Python tests
PYTHONPATH=. pytest tests/python/ -v

Train

PYTHONPATH=. python -m mahjong.train \
  --iterations 1000 \
  --games-per-iter 10 \
  --eval-interval 50 \
  --device cpu

CLI Game (1 Human + 3 AI)

# 从项目根目录运行
uv run maj-game --seed 42

# 使用固定牌山(便于复现)
uv run maj-game --seed 42 --wall-file apps/cli_game/examples/wall.txt

Use as Environment

from mahjong.env import MahjongEnv
from mahjong.agents.random_agent import RandomAgent

env = MahjongEnv(seed=42)
agent = RandomAgent()

obs, info = env.reset()
done = False
while not done:
    action = agent.act(obs)
    obs, reward, terminated, truncated, info = env.step(action)
    done = terminated or truncated

print(f"Final scores: {info['scores']}")

Project Structure

maj/
├── CMakeLists.txt            # Top-level CMake
├── pyproject.toml            # Python package config
├── src/                      # C++ core engine
│   ├── tile.h/cpp            # Tile representation
│   ├── hand.h/cpp            # Hand management + shanten
│   ├── meld.h/cpp            # Open melds (chi/pon/kan)
│   ├── wall.h/cpp            # Tile wall + dead wall
│   ├── player.h/cpp          # Player state
│   ├── action.h/cpp          # Action types
│   ├── yaku.h/cpp            # Yaku evaluation (40+ yaku)
│   ├── score.h/cpp           # Scoring calculation
│   ├── game.h/cpp            # Game engine
│   └── bindings.cpp          # pybind11 bindings
├── mahjong/                  # Python package
│   ├── env.py                # Gymnasium environment
│   ├── train.py              # PPO training pipeline
│   └── agents/               # Agent implementations
│       ├── random_agent.py
│       └── rule_agent.py
├── apps/                     # Application entry modules
│   └── cli_game/             # CLI game app (migrated from maj-game)
│       ├── main.py
│       ├── client/
│       └── examples/wall.txt
└── tests/
    ├── cpp/                  # C++ unit tests (Google Test)
    └── python/               # Python tests (pytest)

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages