- Python 100%
| docs | ||
| emotes | ||
| scripts | ||
| src/mahjong_analyze_bot | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| test.bin | ||
| uv.lock | ||
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/readGameRecordrecovery. - 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
FileDescriptorSetloading for deployments that have separately reviewed a broader schema. - Legacy
records, currentactions[*].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.