1 unstable release
0.1.0 | Jan 6, 2023 |
---|
#722 in Game dev
390KB
8K
SLoC
Riichi Mahjong Game Engine
This crate implements a game engine of standard Japanese Riichi Mahjong in the form of a library, building upon the foundation of riichi-elements and riichi-decomp.
Table of Contents
model
--- Data structures for the entire game:- State (including
StateCore
), Action, Reaction RoundBegin
,RoundEnd
, ...AgariResult
,Scoring
, ...RoundHistory
,RoundHistoryLite
, ...
- State (including
engine
--- The game Engine.rules
--- Configurable Ruleset for the engine.yaku
--- All known Yaku's and utils.interop
--- Working with data models from other implementations of the Japanese Riichi Mahjong game.
Quick Example
See docs on 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>(())
In a more realistic setting:
round_id
,pot
, andpoints
may be either their begin-of-game values or derived from the previous round's results.wall
should be shuffled, e.g. using therand
crate.- The State of the engine should be observed by the players at each step.
- Actions and Reactions should be from players' inputs.
How We Model the Game
Game Setup
Each game (Hanchan, Tonpuu, ...) is played by 4 players and consists of at least 1 round (Kyoku). The 3-player variant is currently not supported.
Each round starts with an initial state:
- The "Ba-Kyoku-Honba" triplet, i.e. "East 1 Kyoku, 0 Honba", represented as
model::RoundId
. - How many points each player has at the beginning of the round.
- How many "riichi sticks" currently remains on the table.
- The complete shuffled wall (34 x 4 = 136 tiles) to be used in this round (see
riichi_elements::wall
).
State Machine of a Round
The game flow within a round can be modeled as the following state machine:
┌──────┐
│ 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 │ │
│ │ └──────────┘ └─────┘ └─────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
There are multiple states within one logical turn of a round:
-
The player in turn is ready to take an action (
model::Action
), after incoming draw and/or meld. This action might be terminal (abortion by nine kinds, or win by self draw). -
Each other player may independently declare an reaction (
model::Reaction
): Chii, Pon, Daiminkan, or Ron. The resolved reaction type determines the next state. -
After reaction resolution, we need to check for any involuntary round-ending conditions.
-
All done, then the next player gains draw and/or meld depending on what has happened so far, marking the beginning of the next turn.
Not all actions are valid at all times; the validity often depends on state variables not illustrated in the state machine diagram.
One-state-per-turn Simplification
It is possible to simplify by only explicitly modeling one state (per turn), namely the one before the in-turn
player makes a decision (after taking a draw or a Chii/Pon). This is basically #1
in the state machine diagram,
represented by model::State
.
All other states in the diagram can be derived from this:
- The state marked
#2
is basically the pre-action state (#1
) + the action taken. - The states marked
#3
are terminal (abortion / win). They can be handled separately outside the normal game flow. - The states marked
#4
are internal transitory states skipped over by the engine without any player input.
This key simplification enables a regular representation of the normal game flow of a round as a sequence of triplets: State + Action + Reaction (optional).
Optional features
serde
(Default: enabled)
Defines a JSON-centric serialization format for most of the common data structures, including those in the riichi-elements crate.
This simplifies interop with external programs (bots, client-server, analysis, data processing), persistence of game states, etc..
See each individual type's docs for the detailed format.
tenhou-log-json
(Default: enabled)
Defines an intermediate de/serialization data model for Tenhou's JSON-formatted logs, and reconstruction of each round's preconditions, action-reactions, and end conditions into our own data model.
See interop::tenhou_log_json
mod-level docs for details.
static-lut
(Default: disabled)
Enables the corresponding feature in the riichi-decomp crate, which builds the lookup tables required by its hand
analysis algorithms statically. If disabled, the lookup tables will be generated upon first instantiation of
riichi_decomp::Decomposer
.
References
Dependencies
~3–11MB
~122K SLoC