Local Testing

How to simulate games locally with riichienv before joining ranked play

Overview

RiichiLab's game server uses riichienv's core engine (riichienv-core) to run games. You can use the Python package riichienv to simulate games locally without running the game server. This lets you test and debug your bot logic before connecting to the online server.

Prerequisites

  • Python 3.12+
  • pip install riichienv — game engine with simulation and observation APIs
  • pip install websockets — only needed if connecting to the online server

Implementing an Agent

Your bot is an Agent class that implements an act(obs: Observation) -> Action method. The Observation provides the current game state visible to your player, and legal_actions() returns the list of valid actions you can take.

from riichienv import Action, ActionType, Observationclass MyAgent:    """Your bot agent. Implement act() to return an Action."""    def act(self, obs: Observation) -> Action:        actions = obs.legal_actions()        # Win if possible        for a in actions:            if a.action_type == ActionType.Tsumo or a.action_type == ActionType.Ron:                return a        # Declare riichi if possible        for a in actions:            if a.action_type == ActionType.Riichi:                return a        # Default: discard the drawn tile (tsumogiri)        for a in actions:            if a.action_type == ActionType.Discard:                return a        # Pass on claims        return Action(ActionType.Pass)

Running a Local Simulation

Use riichienv.RiichiEnv to run a complete game locally. No game server, no WebSocket — just pure Python:

import jsonfrom riichienv import RiichiEnvagent = MyAgent()# Create a game environment#   game_mode: 1 = East-only, 2 = East-South (hanchan)#   seed: fixed seed for reproducibility (optional)env = RiichiEnv(game_mode=2, seed=42)# Get initial observations for all playersobservations = env.get_observations()while not env.done():    # Find the player who needs to act    for pid in range(4):        obs = observations[pid]        actions = obs.legal_actions()        if actions:            # Your agent decides the action            action = agent.act(obs)            observations = env.step(action)            break# Game finished — check resultsprint("Scores:", env.scores())print("Ranks:", env.ranks())# Full MJAI log is availablefor event in env.mjai_log:    print(json.dumps(event, ensure_ascii=False))

This runs a full hanchan (East-South) game with your agent controlling all 4 players. You can also assign different agents to different seats.

Observation & request_action

When connecting to RiichiLab's online game server, your bot receives request_action messages via WebSocket. This is the only event type your bot needs to handle — all other events (like tsumo, dahai, etc.) are informational. When replying, echo the request's request_id so the server can bind your reply to the request deterministically (see the MJAI Protocol page).

Each request_action includes an observation key containing a base64-encoded serialized Observation object. Deserialize it with Observation.deserialize_from_base64() to get a full riichienv Observation with legal actions, hand tiles, and game state:

# In a request_action message from the server:# {#   "type": "request_action",#   "request_id": 42,#   "time": {"grace_ms": 3000, "bank_ms": 15000, "deadline_ms": 18000},#   "possible_actions": [...],#   "observation": "base64-encoded-string..."# }from riichienv import Observation# Deserialize the observationobs = Observation.deserialize_from_base64(msg["observation"])# Available methods:obs.legal_actions()         # -> list[Action]  — valid actions to choose fromobs.new_events()            # -> list[str]     — new MJAI events (JSON strings)obs.hand                    # -> list[int]     — current hand tiles (136-format)obs.player_id               # -> int           — your seat number (0-3)obs.to_dict()               # -> dict          — full observation as a dictionary# For neural network bots:obs.encode_extended_win_projection()   # -> feature encodingobs.encode_discard_history_decay()     # -> discard history features

Note: For 3-player games, use Observation3P instead of Observation. The API is the same — just replace Observation.deserialize_from_base64() with Observation3P.deserialize_from_base64().

WebSocket Bot Example

Here's how to connect the same Agent to the online game server via WebSocket:

import asyncioimport jsonimport websocketsfrom riichienv import Observationasync def bot(url: str):    async with websockets.connect(url, additional_headers=HEADERS) as ws:        my_seat = None        agent = MyAgent()        while True:            msg = json.loads(await ws.recv())            match msg["type"]:                case "start_game":                    my_seat = msg.get("id", 0)                case "request_action":                    # Deserialize the observation from the server                    obs = Observation.deserialize_from_base64(                        msg["observation"]                    )                    action = agent.act(obs)                    resp = json.loads(action.to_mjai())                    # Echo request_id for deterministic binding                    if "request_id" in msg:                        resp["request_id"] = msg["request_id"]                    await ws.send(json.dumps(resp))                case "end_game":                    breakTOKEN = "your-bot-token-here"HEADERS = {"Authorization": f"Bearer {TOKEN}"}asyncio.run(bot("wss://game.riichi.dev/ws/validate"))

The key difference from local simulation: instead of calling env.get_observations() directly, you deserialize the observation field from the server's request_action message. The same Agent.act() logic works in both modes.

Use your bot token with wss://game.riichi.dev/ws/validate when testing this flow against the live server. See Bot Validation for the activation requirements.

Architecture Note

RiichiLab's game server is built on riichienv-core (the Rust crate). The Python package riichienv wraps the same Rust core via PyO3, so local simulation behavior matches the online server exactly. By testing locally first, you can iterate quickly without needing network connectivity or waiting for matchmaking.