1 个不稳定版本
0.1.0 | 2023年1月6日 |
---|
#633 在 游戏开发
390KB
8K SLoC
Riichi Mahjong 游戏引擎
本库实现了一个以库的形式存在的标准日本立直麻将游戏引擎,基于 riichi-elements 和 riichi-decomp 的基础。
目录
model
--- 整个游戏的数据结构- State(包括
StateCore
)、Action、Reaction RoundBegin
、RoundEnd
、...AgariResult
、Scoring
、...RoundHistory
、RoundHistoryLite
、...
- State(包括
engine
--- 游戏引擎 Engine。rules
--- 可配置的引擎 Ruleset。yaku
--- 所有已知的 Yaku 和工具。interop
--- 与其他日本立直麻将游戏实现的模型数据一起工作。
快速示例
请参阅 engine::Engine
的文档。
use riichi::prelude::*; // includes `Engine` and `riichi_elements::prelude::*`
let mut engine = Engine::new();
engine.begin_round(RoundBegin {
ruleset: Default::default(),
round_id: RoundId { kyoku: 0, honba: 0 }, // east 1 kyoku, 0 honba (first round in game)
wall: wall::make_sorted_wall([1, 1, 1]), // 1111m2222m3333m4444m0555m...
pot: 0,
points: [25000, 25000, 25000, 25000],
});
assert_eq!(engine.state().core.seq, 0);
assert_eq!(engine.state().core.actor, P0);
engine.register_action(Action::Discard(Discard {
tile: t!("1m"), ..Discard::default()}))?;
// use `engine.register_reaction` for Chii/Pon/Daiminkan/Ron
let step = engine.step();
assert_eq!(step.action_result, ActionResult::Pass);
assert_eq!(engine.state().core.seq, 1);
assert_eq!(engine.state().core.actor, P1);
/* ... */
# Ok::<(), riichi::engine::ActionError>(())
在更真实的环境中
round_id
、pot
和points
可能是其游戏开始时的值或从前一轮的结果推导而来。wall
应该被打乱,例如使用rand
包。- 引擎的 State 应在每个步骤中被玩家观察。
- 动作和反应应来自玩家的输入。
我们如何建模游戏
游戏设置
每场游戏(半间、吨筒等)由4名玩家进行,至少包含1轮(局)。目前不支持3人变体。
每一轮以一个初始状态开始
- "宝-局-本家"组合,即"东1局,0本家",表示为
model::RoundId
。 - 每轮开始时每个玩家有多少分。
- 当前桌上剩余多少立直棒。
- 本轮将使用的完整的洗好的墙(34 x 4 = 136张牌)(见
riichi_elements::wall
)。
一轮的状态机
一轮内的游戏流程可以建模为以下状态机
┌──────┐
│ Deal │
└─┬────┘
│
│ ┌────────────────────────────────────────────────────────────────┐
│ │ │
▼ ▼ #1 #2 │
┌────────┐ Draw=Y ┌────────────┐ ┌─────────────┐ Nothing │
│DrawHead├──────────►│ │ │ ├───────────┤
└────────┘ Meld=N │ │ Discard │ │ │
#4 │ ├──────────►│ │ #3 ▼
│ │ Riichi │ │ ┌─────────────────┐
│ In-turn │ │ Resolved │ │ Forced abortion │
│ player's │ │ declaration │ └─────────────────┘
┌────────┐ Draw=Y │ decision │ │ from │ ▲
┌─►│DrawTail├──────────►│ │ │ out-of-turn │ │
│ └────────┘ Meld=Y │ (Action) │ │ players │ Daiminkan │
│ #4 │ │ │ ├───────────┤
│ │ │ │ (Reaction) │ │
│ │ │ Kakan │ │ │
│ ┌────────┐ Draw=N │ ├──────────►│ │ Chii │
│ │Chii/Pon├──────────►│ │ Ankan │ ├─────────┐ │
│ └────────┘ Meld=Y └──┬───────┬─┘ └──────┬──────┘ Pon │ │
│ #4 ▲ │ │ │ │ │
│ │ NineKinds│ │Tsumo │Ron │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌──────────┐ ┌─────┐ ┌─────┐ │ │
│ │ #3│ Abortion │ │ Win │#3 │ Win │#3 │ │
│ │ └──────────┘ └─────┘ └─────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
一轮中的一个逻辑回合内存在多个状态
-
当前回合的玩家准备采取行动(
model::Action
),在抓牌和/或报听之后。这个行动可能是终止的(九种方式中断或自摸赢牌)。 -
其他玩家可以独立声明一个反应(
model::Reaction
):吃、碰、大明杠或立直。已解决的反应类型决定了下一个状态。 -
反应解决后,我们需要检查是否有任何非自愿的回合结束条件。
-
所有完成,然后下一个玩家根据之前发生的事情获得抓牌和/或报听,标志着下一个回合的开始。
并不是所有行动在所有时间都是有效的;有效性通常取决于状态变量,这些变量在状态机图中没有展示。
每个回合一个状态简化
可以通过仅显式地建模一个状态(每个回合)来简化,即玩家在回合中做出决定之前的状态(在抓牌或吃/碰之后)。这在状态机图中基本上表示为#1
,表示为model::State
。
图中其他所有状态都可以从这个状态推导出来
- 标记为
#2
的状态基本上是动作前的状态(#1
)加上采取的动作。 - 标记为
#3
的状态是终端(中断/赢牌)。它们可以在正常游戏流程之外单独处理。 - 标记为
#4
的状态是引擎在没有任何玩家输入的情况下跳过的内部暂态状态。
这一关键简化使得可以将一局游戏的正常流程表示为一个三元组的序列: 状态 + 动作 + 反应(可选)。
可选特性
serde
(默认:启用)
定义了针对大多数常见数据结构的以JSON为中心的序列化格式,包括riichi-elements包中的那些。
这简化了与外部程序(机器人、客户端-服务器、分析、数据处理)的互操作性,游戏状态的持久化等。
请参阅每个类型的具体文档以获取详细格式。
tenhou-log-json
(默认:启用)
定义了Tenhou JSON格式日志的中间反序列化数据模型,以及将每轮的先决条件、动作-反应和结束条件重构为我们自己的数据模型。
有关详细信息,请参阅interop::tenhou_log_json
模块级别的文档。
static-lut
(默认:禁用)
启用riichi-decomp包中的对应特性,该特性静态构建了其手牌分析算法所需的查找表。如果禁用,则查找表将在riichi_decomp::Decomposer
首次实例化时生成。
参考
依赖项
~3–12MB
~124K SLoC