Work in progress Riichi Mahjong engine in pure Elixir.
The documentation can be found at https://riichi.hexdocs.pm.
Installation
The package can be installed by adding riichi to your list of dependencies in mix.exs:
def deps do
[
{:riichi, "~> 0.2"}
]
endQuick start
- Create a ruleset. You can use one of the
default_*helpers or construct%Riichi.Rules{}directly.
rules = Riichi.Rules.default_four_player()- Initialize the engine.
{events, state} = Riichi.new(rules)This will create a new game with a random wall.
A second argument can be passed to new to initialize the game with a pre-generated wall (see Riichi.Wall).
The engine is pure, so you'll need to keep the updated state after every action.
For usage in actual games you'll want to wrap it in a GenServer.
- Choose an action to perform.
The events are a list of Riichi.Event.t() structs representing what happened in the game.
The engine will progress the state until a player input is needed, so the output will frequently contain more than one event.
The last event in the list will have a valid_actions field containing a map of %{Actor.t() => [Riichi.Action.t()]}.
Each player that has a corresponding entry in valid_actions must choose one action and send it back.
last_event = Enum.at(events, -1)
# suppose an event like this actually exists in the output
[%Riichi.Action.Discard{} = discard_action | _] = last_event.valid_actions[:actor1]
{:ok, {new_events, new_state}} = Riichi.process_action(state, discard_action)- Personalize the event list.
This event list contains complete information and isn't meant for sending to players directly. When using this library for implementing an actual game, you need to be careful to not accidentally send information that's meant to be hidden. For that, first split the event list into multiple, obscured views, one per player, like so:
# note that this doesn't need access to the full game state - only the rules
views = Riichi.personalize_events(rules, events)
# views is %{Actor.t() => [Event.t()]}- Repeat action submission until the game is over.
The Riichi.Event.GameEnd event marks the end of the game.
You may deal with clients that take too long to respond by calling either:
Riichi.force_continue(state)which sends the highest priority action for each actor who hasn't submitted an action. This prefers skipping, then calling ron/tsumo when available, then discarding the drawn tile.Riichi.skip(state)which sendsRiichi.Action.Skipfor actors that can perform it.
Other functionality
Most of the intended public API surface lives in the Riichi and Riichi.Rules modules.
There are however other modules you may find useful:
Riichi.Tile- tile parsing, sorting and comparisonRiichi.HandandRiichi.Hand.Meld- mahjong hand data structures and operationsRiichi.Decomposer- hand interpretation and wait detectionRiichi.Wall- wall generation, shuffling and utilities for operating on the wallRiichi.Scoring- hand scoring
Scoring example
import Riichi.Tile, only: :sigils
hand =
[ ~t"1m", ~t"1m", ~t"3m", ~t"3m", ~t"5m", ~t"5m",
~t"7m", ~t"7m", ~t"9m", ~t"9m", ~t"2p", ~t"2p", ~t"4p" ]
|> Riichi.Hand.new()
input = %Riichi.Scoring.Input{
rules: Riichi.Rules.default_four_player(),
event: %Riichi.Event.Discard{actor: :actor2, tile: ~t"4p"},
decompositions: Riichi.Decomposer.decompose(hand),
dora_indicators: [],
ura_dora_indicators: [],
dealer?: true,
riichi: %Riichi.Player.RiichiFlags{ippatsu?: true},
first_chance?: false,
last_turn?: true,
after_a_kan?: false,
round_wind: :east,
seat_wind: :east
}
%Riichi.Scoring{han: 5, fu: 25, value: {:ron, 12000}} = Riichi.Scoring.calculate(input)Design and implementation goals
- Pure Elixir, no NIFs.
- Immutable state machines with no side-effects.
- Simple algorithms with no binary blobs or precomputed tables.
- Rich output format by default. Game clients should not need to reproduce the engine.
- [WIP] Compatible with a multitude of other log formats.
License
AGPL-3.0-only.