#mahjong #game-engine #tenhou #majsoul #game

bin+lib riichi

Japanese Riichi Mahjong game engine

1 unstable release

0.1.0 Jan 6, 2023

#722 in Game dev

LGPL-3.0

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

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, and points may be either their begin-of-game values or derived from the previous round's results.
  • wall should be shuffled, e.g. using the rand 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:

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:

  1. 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).

  2. Each other player may independently declare an reaction (model::Reaction): Chii, Pon, Daiminkan, or Ron. The resolved reaction type determines the next state.

  3. After reaction resolution, we need to check for any involuntary round-ending conditions.

  4. 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