Skip to main content

Core Engine Overview

The Core engine (RabiRiichi.csproj, namespace RabiRiichi.Core and friends) is a headless, deterministic implementation of riichi mahjong rules. It has no networking and no rendering. You configure a game, tell it how players make decisions, and it runs the hand to completion, emitting events along the way.

Mental model

A running game is organized around a handful of collaborating pieces:

PieceTypeResponsibility
GameCore/Game.csThe root object and DI container. Owns players, wall, info, the event bus, and the main queue.
Game infoCore/GameInfo.csDynamic per-round state: round, dealer, honba, riichi sticks, current player, game id.
Players & handsCore/Player.cs, Core/Hand.csEach seat's points, hand tiles, melds, discards, riichi/furiten/agari state.
WallCore/Wall.csThe live wall, dead wall, dora/ura indicators, rinshan draws.
ConfigCore/Config/GameConfig.csEvery rule tunable and the pluggable rule "setup".
EventsEvents/**The FIFO event queue + priority-ordered listeners that drive the game.
ActionsActions/**How players are offered choices (inquiries) and respond.
PatternsPatterns/**Hand decomposition, yaku recognition, and scoring.

How a game runs

  1. You construct a GameConfig and set its actionCenter (an IActionCenter — your bridge for answering player inquiries).
  2. new Game(config) builds a dependency-injection container, instantiates the players and wall, runs the pluggable setup (which registers all the yaku patterns and event listeners), and queues the bootstrap InitGameEvent.
  3. await game.Start() pumps the event queue. Listeners handle each event and may queue more events. Whenever a player must choose, a WaitPlayerActionEvent presents an inquiry and blocks the queue until the answer (or a timeout) arrives.
  4. Wins and draws run the scoring pipeline, points move, the dealer rotates (or repeats), and either the next hand begins or the match ends.

The complete event graph is documented in The event system.

Determinism and the DI container

Game is built on Microsoft.Extensions.DependencyInjection. During construction it registers itself, the RNG, config, event bus, wall, game info, the PatternResolver, and more as singletons, then calls the ruleset's setup to register patterns and listeners. Anywhere in the engine you can resolve a service with:

var resolver = game.Get<PatternResolver>();

Shuffling uses a RabiRand seeded from config.seed (or the current time when unset). Providing a seed makes a game fully reproducible — this is what the scenario tests and simulations rely on.

The pluggable ruleset (BaseSetup / RiichiSetup)

Rules aren't hard-coded into Game; they're assembled by a BaseSetup. GameConfig.setup defaults to RiichiSetup, which:

  • registers the three base patterns (standard hand, seven pairs, thirteen orphans) and the full standard pattern list (every yaku, fu calculator, and bonus like dora),
  • registers all action resolvers (who may chii/pon/kan/ron/riichi/…), and
  • registers every event listener into the event bus.

Because it's just a class, you can subclass it to add, remove, or restrict yaku — which is exactly how the server offers per-room custom rulesets.

Where to go next