「死ねば助かるのに………」 - 赤木しげる
Real-time mahjong AI assistant for Mahjong Soul, Tenhou, and more.
Akagi V3: A single-binary Rust + Tauri rewrite of Akagi and AkagiNG.
Ask anything on Discord · Report Bug · Request Feature · DeepWiki
Other branches:
The purpose of this project is to provide a convenient way to understand your performance in mahjong matches in real time and to learn from it. This project is intended for educational purposes only. The author is not responsible for any actions taken by users. Game developers and publishers reserve the right to act against users who violate their terms of service; any consequences (account suspension, etc.) are the user's responsibility.
Akagi watches your Mahjong Soul / Tenhou game over a local proxy or a built-in browser, mirrors the game state, and shows shanten, waits, agari rate, tenpai rate, per-opponent deal-in risk, and a recommended discard in a draggable HUD. A built-in AI model ships inside the app — nothing to install — and its suggestion appears each turn; point it at the cloud inference API when you want a stronger, hosted model.
https://github.com/user-attachments/assets/42812e85-ccf0-49fd-b825-adbb5b7b58b0
https://github.com/user-attachments/assets/2ce7cb71-8b25-4895-a12b-0a638665dcab
https://github.com/user-attachments/assets/d5bc6ff6-6560-4365-ae55-660c9a522790
For users
For developers
Live HUD — shanten, waits, agari rate, tenpai rate, per-opponent deal-in risk, suggested attack/defence discard. Draggable, resizable UI layout.
Two capture modes
Two bot backends
Per-mode routing throughout: bot.active_4p and bot.active_3p swap automatically based on the table's player count.
Game history — every completed match is auto-recorded. The History tab shows a rank pie chart, a cumulative PT line chart with selectable scoring rules (Mahjong Soul tiers / Tenhou ranks / Custom uma), and detailed stats (win rate, deal-in rate, riichi rate, fuuro rate, ryukyoku rate, average winning / deal-in points, average winning turn, yakuman / nagashi-mangan counts).
Simple first-run setup — language → platform → capture mode → CA trust / Chromium pick → bot settings → done.
Internationalization — English, 日本語, 繁體中文, 简体中文. Live switch from Setup or Settings.
Sanma (3-player) — fully supported: AI analysis, per-mode bot routing, history stats, 3p uma tables.
In-app updates — checks for new releases on launch and on demand from Settings → Updates; one click downloads the update, applies it in place, and restarts. Read-only installs (e.g. AppImage) fall back to the release page.
| Platform | 4-Player | 3-Player | AutoPlay |
|---|---|---|---|
| Mahjong Soul (Majsoul) | ✓ | ✓ | ✓ |
| Tenhou | ✓ | ✓ | ✗ |
| Riichi City | ✓ | ✓ | ✗ |
| Amatsuki | (planned) | (planned) | ✗ |
Akagi ships as a portable zip — one self-contained folder per platform. Download the file for your OS from Releases, unzip anywhere you have write permission (e.g. ~/Apps/, Desktop), and run akagi inside. Configuration, logs, history, the CA cert, and bots are all created right next to it, so moving / backing up / uninstalling is just moving / copying / deleting the folder.
| OS | File | Notes |
|---|---|---|
| Windows | akagi-<version>-windows-x64.zip |
x86_64. Requires WebView2 (preinstalled on Win10 1803+ / Win11). SmartScreen will warn — More info → Run anyway. |
| macOS | akagi-<version>-macos-arm64.zip |
Apple Silicon. Unsigned: run xattr -cr <unzipped folder> once, or right-click → Open the first time. |
| Linux | akagi-<version>-linux-x64.zip |
Built on ubuntu-22.04 (glibc 2.35+). Requires WebKit2GTK 4.1 (apt install libwebkit2gtk-4.1-0 / dnf install webkit2gtk4.1 / pacman -S webkit2gtk-4.1). |
On first launch the Setup wizard walks you through language, platform, capture mode, bot settings, and CA trust (only if you choose MITM mode). There is no bot to install — the built-in one is already there.
The simplest path. After Setup, Akagi finds Chrome / Edge / Brave / Chromium automatically and launches it with its own separate profile; log in to Mahjong Soul and play.
Frames are intercepted via the Chrome DevTools Protocol — no system proxy, no certificate.
System-wide proxy with a self-signed root CA at ./ca/:
./ca/akagi-ca.crt (or .cer / .pem / .der).127.0.0.1:23410. Health probe: GET /ping → pong.localhost, 127.0.0.1 and ::1 direct, never through Akagi.[!IMPORTANT] Step 4 is not optional. Games talk to themselves over loopback for internal bookkeeping, and a redirector rule that matches the game for any target host will sweep those sockets into Akagi too. Akagi refuses them (you will see a
refusing CONNECT to loopbackwarning in the log), but the game may still misbehave — so exclude loopback at the source.In Proxifier: Profile → Proxification Rules, enable the built-in Localhost rule (Action: Direct) and drag it above your game rule. Order matters — Proxifier takes the first rule that matches, so a Localhost rule sitting below the game rule never fires.
Akagi ships a built-in, pure-Rust bot. It's the default for both modes (bot.active_4p = "akagi-native", bot.active_3p = "akagi-native3p") and appears at the top of the Bots tab, always "ready".
It's a small neural net trained by behavior cloning (weights are embedded in the binary), so its strength is modest by design — a sensible default, not a top-tier engine.
The built-in bot can optionally hand its decisions to a remote inference server instead of running its embedded model — a stronger, hosted model reached over the network. The embedded local model stays loaded as an automatic fallback: if the server is unreachable, rate-limited, or the key is invalid, the bot plays the local model's move so a live game never stalls.
Three ways:
bot.active_4p and bot.active_3p are independent. Akagi picks the right one when the game starts, based on the table's player count.
Beyond these two backends, Akagi can also run external mjai bots as subprocesses. That's an extension point for developers rather than a step anyone needs — see mjai Bots (plugin interface).
Every cleanly-ended match (one that produced an end_game mjai event) is persisted under <config_root>/history/:
<config_root>/history/
├── index.jsonl # one GameRecord per line (ULID-keyed)
└── games/
└── <ulid>.mjai.jsonl # full event-stream copy
Mid-game disconnects leave an unfinalised buffer and are silently dropped — only complete games make it to disk.
The frontend's History tab shows:
場次 (銅 / 銀 / 金 / 玉 / 王座) and 段位 (初心 1 星 → 魂天).段位 (新人 → 天鳳位 across 21 ranks)..mjai.jsonl.PT-rule and filter selections persist to localStorage. Records load from the backend on bridge boot and stay current via the history-recorded Tauri event.
See src/history/README.md for the math, the storage schema, and how to add a new platform / stat field / filter dimension.
Per-session logs land under <log_dir>/<YYYYMMDD-HHMMSS>/:
<log_dir>/<session>/
├── all.log # combined tracing output
├── <target>.log # per-module filtered logs
├── proxy.binlog # raw binary WS frames
├── majsoul/<flow_id>.log # per-WebSocket flow JSON log
├── majsoul/<flow_id>.mjai.jsonl # per-game mjai event stream
└── inspector.jsonl # frames seen by the Inspector
The frontend's Logs route has two tabs:
Filterable application log. Filter by level (trace / debug / info / warn / error) and by module. Live-tail or browse past sessions; click a row to see source location + raw structured fields. An Open Folder button reveals the session directory in the OS file manager.
Protocol-level frame viewer. Three entry types:
meta field (confidence / q-values / whatever the bot emits).Frame counts show how many mjai events each WS frame produced. Useful when debugging a bot or a bridge issue.
[!TIP] Reproduce the problem, then save the session folder under
<log_dir>/<session>/— it has everything (app log, raw frames, mjai events, bot meta) needed to file a useful bug report.
./ca/akagi-ca.crt is trusted in your OS store. Verify the proxy is running: curl http://127.0.0.1:23410/ping should reply pong. Check your proxy redirector (Proxifier / system proxy) is sending the game client to the right host:port.refusing CONNECT to loopback in the log, then exclude localhost, 127.0.0.1 and ::1 — see step 4 of the MITM setup above.capture.chromium.executable manually in Settings or config.toml. If the launched browser starts but no frames flow, check that --remote-debugging-port was not blocked by another extension.bot.active_3p in Settings → Bot — it is independent of bot.active_4p.Done in alpha.8:
Planned:
Detailed bug tracking lives in GitHub Issues.
Single Rust binary. Subsystems own only their bus handles, never each other. src/event_bus.rs is the single source of truth for channel types.
┌────────────────────────┐
Game client ─│ capture (mitm | cdp) │── CA at ./ca (mitm only)
WebSocket └─────────┬──────────────┘
▼
┌────────────────────────┐
│ bridge::<platform> │ wire bytes → MjaiEvent
└─────────┬──────────────┘
▼ MjaiBus
┌──────────────────┼──────────────────┐
▼ ▼ ▼
game_state::tracker bot::manager ipc forwarder
│ │ │
▼ PostBus ▼ BotResponseBus ▼ app.emit
analysis::runner built-in NN (in-proc) Tauri webview
│ | cloud API
▼ AnalysisBus | mjai subprocess
└──► ipc forwarder ──► app.emit
src/lib.rs wires the buses on boot. The frontend talks to the backend over push events (mjai-event, bot-response, bot-status, …) and a set of pull commands, both documented in src/ipc/README.md. With AutoPlay on, the autoplay manager consumes the bot's decisions and clicks the table through the Chromium capture backend (CDP).
| Layer | Tech |
|---|---|
| Shell | Tauri 2 |
| Backend | Rust (edition 2021), tokio, tracing, clap |
| MITM | hudsucker 0.24 (rcgen-ca, rustls-client) |
| CDP capture | chromiumoxide 0.9 |
| Mahjong engine | riichienv-core 0.4 |
| Built-in bot | candle 0.9 (pure-Rust NN inference; weights embedded) |
| Cloud inference | reqwest 0.13 (rustls) |
| Protobuf | prost 0.14 + prost-reflect 0.16 |
| Frontend | React 19, TypeScript, Vite 8 |
| Styling | Tailwind CSS v4, shadcn/ui (Radix Nova preset) |
| State | Zustand |
| Charts | Recharts |
| Tile rendering | <mah-gen> Web Component |
| i18n | react-i18next |
| mjai bot runtime | python-build-standalone 3.12 + uv (bundled per platform; plugin bots only — the built-in bot needs none of it) |
.
├── src/
│ ├── analysis/ Shanten / waits / agari-rate / risk / discard search
│ ├── autoplay/ Bot decisions → table clicks via CDP (AutoPlay)
│ ├── bot/ Bot manager: built-in bot, cloud API client, mjai subprocess runner
│ ├── bridge/ Per-platform protocol → MjaiEvent
│ │ ├── majsoul/ Mahjong Soul (liqi protobuf)
│ │ ├── riichi_city/ Riichi City (MITM only)
│ │ └── tenhou/ Tenhou (JSON tag stream, observe-only)
│ ├── capture/ Capture backends abstraction (mitm | chromium)
│ ├── config/ AppConfig (TOML) sections + resolution
│ ├── event_bus.rs Broadcast channels between subsystems
│ ├── game_state/ riichienv-driven mirror, snapshot, mahgen view
│ ├── github/ GitHub Releases client (bot install, self-update)
│ ├── history/ Game replay storage + index
│ ├── inspector/ Frame / event / bot-reaction broadcaster
│ ├── ipc/ Tauri commands, app state, capture supervisor
│ ├── logger/ Per-session log dir + per-target file appenders
│ ├── proxy/ MITM HTTP/HTTPS/WS via hudsucker; CA at ./ca
│ ├── schema/ MjaiEvent enum + IPC payload types
│ ├── updater/ In-app self-update (check + apply)
│ └── lib.rs Boot / wiring
├── native_bot/ Built-in bot crate: obs/action codec, candle CNN, embedded weights
├── mjai_bot/
│ └── example/ Rule-based shanten optimizer (ships in tree)
├── frontend/ React + Vite + Tailwind + shadcn UI
│ └── src/
│ ├── routes/ Overview / GameDashboard / Bots / History / Logs / Settings / Setup / InspectorView / DiagnosticView
│ ├── tiles/ Dashboard tiles (header, hands, opponents, analysis, …)
│ ├── stores/ Zustand stores, one per domain (game, bot, config, theme, …)
│ └── i18n/ en / ja / zh-TW / zh-CN
├── tests/ Integration tests
├── capabilities/ Tauri permissions
├── icons/ App icons
├── tauri.conf.json Window + bundle config
└── Cargo.toml
Per-module developer guides live in each src/*/README.md.
Optional, and aimed at developers. The built-in bot is the default and needs none of this — you only come here to run a different engine under Akagi.
Besides its own bot, Akagi can drive any engine that speaks the mjai protocol. Such a bot is a standalone subprocess talking JSONL over stdin/stdout: Akagi feeds it the game as mjai events, and it replies with an action plus optional HUD data.
The full guide lives in mjai_bot/README.md: the I/O protocol, the mjai event stream, the reaction and meta HUD format, toast notifications, and manifest.toml settings. mjai_bot/example/ is a working rule-based bot you can copy.
For local development, drop your bot folder under mjai_bot/<name>/ and click Install environment on its row in the Bots tab to build its venv — no need to repackage and reinstall on every change. The activation toggle stays disabled until the environment is ready.
The Bots tab installs a bot from a GitHub release or a local ZIP.
The IPC command install_bot_from_github(repo, asset_glob?, name?) fetches the latest release zip, extracts it under mjai_bot/<name>/, validates bot.py, and runs uv sync once. Subsequent launches are fast — the sync is gated by a stamp at mjai_bot/<name>/.akagi/synced.stamp.
Install from ZIP is the offline equivalent: click Browse… to pick a .zip (or paste its path). It runs the exact same extract / validate / uv sync pipeline; your source .zip is left untouched.
Bots run as a separate OS subprocess spawned by Akagi. Communication is strictly JSONL over stdin / stdout — no in-process linking, no shared address space, no FFI. This is an intentional license boundary: an AGPL-licensed bot (e.g. Mortal, which links libriichi) stays inside its own process, so dropping it under mjai_bot/<name>/ does not make Akagi a derived work of the bot.
Prerequisites
libwebkit2gtk-4.1-dev, libgtk-3-dev, libayatana-appindicator3-dev, librsvg2-dev, protobuf-compilerRun / build
# Debug — launches the GUI; Vite dev-server proxied by Tauri
cargo run
# Pass a custom config path
cargo run -- --config ./my-config.toml
# Build a portable zip for the current target
cargo install tauri-cli --locked # if not already installed
bash scripts/fetch-runtime.sh # populate runtime/<triple>/
cargo tauri build --no-bundle # writes target/<triple>/release/akagi
bash scripts/package-zip.sh <target-triple>
# → dist/akagi-<version>-<os>-<arch>.zip
# Frontend dev only (Vite on :1420)
cd frontend && npm ci && npm run dev
Bundled runtime
scripts/fetch-runtime.sh <target-triple> downloads python-build-standalone 3.12 + uv for the target and stages them under runtime/. scripts/package-zip.sh then copies that tree next to the binary inside the zip; src/bot/runtime.rs finds it exe-adjacent at runtime, so the shipped app works without a system Python install.
Integration tests live in tests/:
| File | Covers |
|---|---|
analysis_pipeline.rs |
End-to-end analysis (events → shanten → discard recommendation) |
analysis_bench.rs |
Hot-path performance |
bot_lifecycle.rs |
Install → sync → spawn → roundtrip |
example_bot.rs |
Rule-based reference bot driving a synthetic game |
mortal_zip_layout.rs |
Validates the Mortal release-zip layout |
cargo test # all tests, incl. integration
cargo test --release # for the perf bench
GitHub Actions release.yml builds on tag push (v3.*) or manual dispatch. One portable zip per target:
| OS runner | Target | Artifact |
|---|---|---|
ubuntu-22.04 (glibc 2.35) |
x86_64-unknown-linux-gnu |
akagi-<version>-linux-x64.zip |
macos-14 |
aarch64-apple-darwin |
akagi-<version>-macos-arm64.zip |
windows-latest |
x86_64-pc-windows-msvc |
akagi-<version>-windows-x64.zip |
Repository admins can build the current head commit of an open PR by posting an exact /build-artifacts comment on that PR. The pr-build.yml workflow replies with links to the three portable artifacts when the build finishes; artifacts are retained for 14 days. Comments from users without repository admin permission are ignored.
Each zip ships python-build-standalone 3.12 + uv next to the binary, so bots run without a system Python install.
Tags must be on the v3 branch.
| Source | Used in | What for |
|---|---|---|
| mjai JSONL spec (Gimite) | src/schema/mjai/ |
MjaiEvent enum + bot wire contract — 15 event types, tile-string format, state-machine rules. |
EndlessCheng/mahjong-helper (Go analysis CLI) |
src/analysis/ |
Direct Rust port of util/ — shanten, waits, agari-rate, tenpai-rate, risk model, discard search. |
Xerxes-2/MajsoulMax-rs (Rust MITM proxy, GPL-3.0) |
src/proxy/handler.rs, src/bridge/majsoul/parser.rs, src/bridge/majsoul/proto/liqi.proto |
Reference for the 5-layer Mahjong Soul WS wire format (type byte → Wrapper → inner message → action protobuf). Format only — no code copied. |
smly/RiichiEnv (Rust RL env w/ Python bindings) |
Cargo.toml (riichienv-core dep), src/analysis/, src/game_state/ |
Tile / hand / shanten / yaku / score primitives + game-state model. The analysis engine and game tracker are built on this. |
eric200203/mahgen (mahjong-tile rendering DSL) |
src/game_state/mahgen_view.rs, frontend <mah-gen> |
DSL syntax for pre-encoding hand / meld / river strings backend-side. |
smly/mjai.app (mahjong AI competition platform) |
mjai_bot/, src/bot/ |
Bot subprocess convention — JSONL stdin/stdout, argv python bot.py <player_id>, AKAGI_PLAYER_ID env, end-of-batch flush points. |
shinkuan/Akagi |
Architecture / behaviour parity | The original feature set we are reproducing: MITM proxy, mjai bridge, pluggable bots, recommendation HUD. |
Akagi v3 is licensed under the Apache License 2.0. Copyright 2026 Shinkuan. Third-party attributions live in NOTICE — read it alongside the license. Per Apache-2.0 §4(d), redistributions must include both files.
Bundled / linked sources
src/analysis/ is a Rust port of util/.<mah-gen> custom element.Reference-only (no code copied; listed in NOTICE for credit)