TanyaoDojo β€” Mahjong AI checkpoints

JAX/Flax checkpoints from TanyaoDojo, a from-scratch mahjong AI stack (vectorized env + behavior cloning + self-play RL). Every number below comes from the same duplicate 1v3 protocol: the challenger plays all four seats over identical walls against three copies of a strong open-source baseline (Mortal v4), seed_key=20260711, seeds from 10000, placement points [90, 45, 0, -135].

avg_pt is the challenger's mean placement points relative to the baseline; 0 would mean parity. Higher is better.

The headline checkpoint has a 100k-game milestone measurement (-4.66 +/- 0.535, 16.0M decisions, zero fallbacks) β€” not a small-sample estimate.

Checkpoints

File Arch Obs Training avg_pt vs baseline
bc_v2_g186.pkl 256ch x 10blk (12.4M) v2 (34x36 + 32) BC on 10y logs + LR-1e-4 refine -4.66 +/- 0.535 (100k games)
bc_lean_g402.pkl 256ch x 10blk (12.4M) lean (34x20 + 26) BC on 14y logs + LR-1e-4 refine -5.07 +/- 1.54 (12k games)
bc_lean_w192_ep2.pkl 192ch x 8blk (5.3M) lean BC on 6y logs, 2 epochs -8.87 +/- 2.70 (4k games)
rl_oracle_800m.pkl 256ch x 10blk lean (actor) oracle-critic PPO league, 0.8B steps -8.38 +/- 2.65 (4k) β€” negative result, wrong objective (see below)

The last row is published deliberately, and its interpretation was corrected on 2026-08-23 β€” the correction is more useful than the checkpoint.

An asymmetric actor-critic whose critic sees all four hands fit the value function ~100x better (v_loss 0.102 -> 0.001) yet lost 3.2pt of external strength. It was the fourth consecutive negative RL result here. We originally read this as "oracle critics don't transfer". That reading was wrong.

The real cause: the training objective was not the evaluation objective. All four RL runs used round_mode="single", which ends the episode after a single hand, and the reward is mahjong's raw point transfer. The arena scores an entire hanchan by placement points [90, 45, 0, -135]. Measured directly, not inferred from code: 600 steps x 256 envs produced 1541 episodes (one hand each); rewards ranged [-120, 130] with rows like [+30, -10, -10, -10] summing to ~0 (point transfers); and mahjax writes order_points only into the final score, never into rewards, in any round_mode.

A single-hand point maximizer has no concept of 4th place β€” falling to last costs it nothing extra, so it should be maximally aggressive. This predicts that more RL steps make the arena result worse (observed), and it explains why a better critic hurt more: it converged faster onto the wrong objective.

So this checkpoint is not evidence against oracle critics. It is evidence that we optimized the wrong function for ~10^10 environment steps. The fix β€” terminal placement-aligned reward over full hanchan β€” is in the repo as jax_rl/reward_placement.py, and all four RL results above are being redone on it.

First result after the fix (1B steps from the -4.66 base, 11 arena evals of 1600 games each along the way): -4.88 +/- 0.81 β€” statistically flat versus the base. The previous four runs lost 3.2 to 10 points each and got worse the longer they ran. The degradation is gone; the gains are not there yet, and the metrics say why β€” approx_kl about 4e-5 per update and only 0.011 KL of drift from the base after a billion steps, i.e. a tight trust region plus roughly 0.75 hanchan terminals per rollout left the policy almost unmoved. The next step is therefore signal density, not a different algorithm: GRP potential shaping (a 7.5M-sample "position -> expected final placement points" potential, reward becomes phi(s') - phi(s) with the terminal remainder), which is policy-invariant by the shaping theorem β€” verified numerically β€” and raises the fraction of non-zero-reward steps from 0.1% to 1.10%.

If you are porting an RL recipe (ours or anyone's): check that its episode boundary and reward definition are the same function your benchmark scores.

Format

Plain pickle of a Flax parameter pytree for LeanACNet(channels, blocks) (see jax_rl/net_lean.py in the repo). Load and run:

import pickle, jax
from net_lean import LeanACNet          # from the TanyaoDojo repo
from obs_v2 import observe_v2           # or obs_lean.observe_lean

params = pickle.load(open("bc_v2_g186.pkl", "rb"))
net = LeanACNet(channels=256, blocks=10)
logits, value = net.apply(params, observe_v2(state))   # state: Mahjax red_mahjong State

Observation must match the checkpoint: bc_v2_* needs obs_v2 (36 planes), everything else needs obs_lean (20 planes). Planes are stored as uint8 * scale in datasets (scale 24 for v2, 4 for lean) and divided back at train/eval time.

Evaluation harness: jax_rl/mjai_bot/run_eval.py in the repo (--obs v2 for the v2 checkpoint).

Training data β€” not distributed

These models were trained on Tenhou houou-level game logs. Neither the logs nor the derived datasets are redistributed here, per Tenhou's terms. The repo ships the full builder (jax_rl/data_bridge/make_bc_dataset.py) so you can rebuild equivalent datasets from logs you obtain yourself.

License

MIT for these weights and the core training code. Note that the repo's evaluation bridge (jax_rl/mjai_bot/) links libriichi and is AGPL-3.0; see the repo's LICENSING.md.

Downloads last month

-

Downloads are not tracked for this model. How to track
Video Preview
loading