A Discord bot that parses maj games
Find a file
2026-07-30 17:53:37 -07:00
docs now it should stop failing on the first try 2026-07-29 03:32:09 -07:00
emotes now dora/ura indicator is shown. fixed tileback 2026-07-29 04:07:46 -07:00
scripts initial commit 2026-07-28 00:31:20 -07:00
src/mahjong_analyze_bot oopsie 2026-07-30 17:53:37 -07:00
tests oopsie 2026-07-30 17:53:37 -07:00
.env.example initial commit 2026-07-28 00:31:20 -07:00
.gitignore initial commit 2026-07-28 00:31:20 -07:00
LICENSE initial commit 2026-07-28 00:31:20 -07:00
pyproject.toml initial commit 2026-07-28 00:31:20 -07:00
README.md oopsie 2026-07-30 17:53:37 -07:00
test.bin initial commit 2026-07-28 00:31:20 -07:00
uv.lock initial commit 2026-07-28 00:31:20 -07:00

mahjong-analyze-bot

An offline-first, version-tolerant framework and Discord bot for downloading, parsing, and summarizing Mahjong Soul replays. Strategic play advice is intentionally outside the current scope.

share URL or paipu token
        |
        v
validated ReplayReference
        |
        v
ReplaySource -> FetchedReplay -> ReplayDecoder -> DecodedReplay
                                                  |
                                                  v
                                      MahjongSoulNormalizer
                                                  |
                                                  v
                                ParseResult + diagnostics
                                                  |
                                                  v
                                  ReplayAnalyzer -> GameAnalysis
                                                  |
                                                  v
                                      JSON or Markdown report

Provider transport and protobuf details are kept outside the canonical game model. Unknown records become UnknownEvent values in lenient mode, so a protocol update degrades a replay to partial instead of silently discarding new data.

What works

  • Strict local validation and canonicalization of official replay links, including anonymized IDs and highlighted-player suffixes.
  • Actual account-backed replay downloading through current route discovery, the required route handshake, multiplexed WebSocket RPCs, authentication, heartbeats, and the fetchGameRecord/readGameRecord recovery.
  • EN/global Yostar UID + login-token authentication, direct OAuth access-token authentication, and CN account/password payloads.
  • A built-in, independently maintained minimal protobuf descriptor containing the live and replay fields used by this project; no generated Mahjong Soul source or client assets are bundled.
  • Optional hash-pinned external FileDescriptorSet loading for deployments that have separately reviewed a broader schema.
  • Legacy records, current actions[*].result, and mixed replay layouts.
  • Normalized rounds, draws, discards, riichi, calls, kans, sanma kita, dora, wins, exhaustive/abortive draws, and final results.
  • Round-by-round score recaps, 12,000-point notable hands with yaku and bonus breakdowns, and deterministic game highlights such as dealer runs, streaks, lead changes, comebacks, thrown leads, busts, rare wins, extreme dora, and unusual call/kan sequences.
  • A Discord client which accepts official replay links through /analyze, DMs, or messages that explicitly mention the bot, then presents overview, rounds, highlights, and crazy hands as bounded embeds with public navigation buttons.
  • A persistent, integrity-checked raw replay cache for the Discord bot. Cached replays have no time-based expiry and avoid another Mahjong Soul download.
  • Bounded replay/archive downloads and structured safe-to-display errors.

The project does not yet calculate strategic advice, rate-limit Discord users, or restore interactive message state across process restarts.

Install

Python 3.11 or newer is required.

python -m pip install -e ".[live,dev]"

For the Discord bot and development tools:

python -m pip install -e ".[bot,dev]"

For offline JSON parsing only:

python -m pip install -e ".[dev]"

Command line

Validate a link without opening it or making a network request:

mahjong-replay reference "https://mahjongsoul.game.yo-star.com/?paipu=260720-8b3acfb1-7547-4267-9e2d-e4d1e9e98d7a_a877807605"

Parse an already-decoded JSON export:

mahjong-replay parse-json replay.json --reference "260720-8b3acfb1-7547-4267-9e2d-e4d1e9e98d7a_a877807605"

Copy .env.example to .env and configure the account, or provide the same values through the process environment. The CLI automatically loads .env from its current working directory for live commands without overriding existing environment values. Then download the raw protobuf replay:

mahjong-replay download "https://mahjongsoul.game.yo-star.com/?paipu=..." --output replay.bin

Normalize it directly from the live service:

mahjong-replay parse-live "https://mahjongsoul.game.yo-star.com/?paipu=..."

Generate a readable game recap, or request the same analysis as structured JSON:

mahjong-replay analyze-live "https://mahjongsoul.game.yo-star.com/?paipu=..."
mahjong-replay analyze-live "https://mahjongsoul.game.yo-star.com/?paipu=..." --format json

download refuses to replace a file unless --force is explicit. Commands write results to standard output, expected failures to standard error, and use exit code 2 for expected errors.

The Discord bot stores raw protobuf replays and the associated replay header in var/replay-cache by default. Set MAJSOUL_REPLAY_CACHE_DIR to use another directory. Entries do not expire automatically. Clearing them is deliberately destructive and requires an explicit confirmation:

mahjong-replay clear-cache --force

See docs/LIVE.md for credential setup and version configuration. Secrets are accepted only through environment variables; there are deliberately no password/token command-line flags.

Discord bot

Create a Discord application and bot, invite it with the bot and applications.commands scopes and permission to view channels, send messages, embed links, and read message history, then configure DISCORD_BOT_TOKEN alongside the Mahjong Soul account values in .env. The bot only reads DMs and guild messages that explicitly mention it, so the privileged Message Content intent is not required.

Run it from the directory containing .env:

mahjong-discord

Use /analyze with one official replay link, send the bot a DM containing the link, or mention it in a server message containing the link. Its response starts on an overview page; the Overview, Rounds, Highlights, and Big hands buttons jump between sections. A multi-page section button shows both its page count, such as Rounds 1/2, while the embed footer explains that clicking the same section button again cycles to its next page. Anyone who can see the message can use its controls. They have no timer and remain active while the bot process that created the view is running; process restarts currently end that interaction session without deleting or editing the message.

Library use

The offline model and JSON decoder have no runtime dependencies:

from pathlib import Path

from mahjong_analyze_bot.replays import (
    FetchedReplay,
    JsonReplayDecoder,
    MahjongSoulNormalizer,
    parse_replay_reference,
)

reference = parse_replay_reference(
    "https://mahjongsoul.game.yo-star.com/"
    "?paipu=260720-8b3acfb1-7547-4267-9e2d-e4d1e9e98d7a_a877807605"
)
artifact = FetchedReplay(
    reference=reference,
    payload=Path("replay.json").read_bytes(),
    payload_type="decoded-json",
    source="local-file",
)
decoded = JsonReplayDecoder().decode(artifact)
result = MahjongSoulNormalizer().normalize(decoded)
print(result.to_json(indent=2, ensure_ascii=False))

Analyze and render any normalized replay without depending on Mahjong Soul's transport:

from mahjong_analyze_bot import ReplayAnalyzer, render_markdown

analysis = ReplayAnalyzer().analyze(result.require_replay())
print(render_markdown(analysis))

The live connection owns one authenticated session and is reusable across requests:

from mahjong_analyze_bot.replays import connect_live_from_env

async with await connect_live_from_env() as connection:
    artifact = await connection.fetch(replay_url)
    result = await connection.parse(another_replay_url)

A Discord process should keep a small number of these sessions alive rather than logging in for every command. Replay retrieval is initially serialized per source because upstream rate limits are not documented.

Security and compatibility

The submitted share-page URL is always parsed locally and never fetched. Only the canonical game UUID is sent to the replay RPC; the _a... suffix is a highlighted-player hint, not an access token.

Archive data_url responses are accepted only over HTTPS from an exact operator-configured hostname allowlist. Redirects are disabled and the size cap is enforced while streaming.

Mahjong Soul exposes a legacy manifest version, a Unity product version, and a separate RPC resource/code version. The last value cannot currently be discovered reliably without the running client, so it is a required deployment setting rather than a guessed constant. Login error 151 reports this explicitly.

This is an unofficial integration against a private protocol that may change without notice. Automation may violate service rules or lead to account action. Use a dedicated account, do not use it simultaneously in the game client, and never send its credentials through Discord or commit them.

See docs/PROTOCOL.md for the wire boundary and docs/SCHEMA_UPDATES.md for external-schema promotion.

Development

pytest
ruff check .
ruff format --check .

Normal tests never contact Mahjong Soul or need credentials. An account-backed smoke test should remain opt-in and outside ordinary CI.

Acknowledgements

The independently written implementation was informed by behavior documented in public projects including MajsoulData, mahjong-paipu-assistant, majsoulmanager, Soulless, and auto-naga. They are references, not runtime dependencies; their source and generated schemas are not vendored.

The MIT license covers this project's own code only. It does not grant rights to Mahjong Soul assets, protocol extracts, replay data, or third-party code.

License

MIT