A cute Go-playing model.
Moka v2.1 is a 244,529-parameter policy and value network for 9×9 Go. Its 260 KB INT8 weights run entirely in the browser. It has the same architecture and size as Moka v2, with weights trained on more on-policy data.
With 64-visit full-symmetry search on fresh openings:
| Opponent | Moka v1 | Moka v2 | Moka v2.1 |
|---|---|---|---|
| KataGo b6c96, raw policy, 200 games | 98 | 159 | 152 |
| KataGo b18, raw policy, 200 games | — | 43 | 44 |
| Moka v1, 100 games | — | 85 | 93 |
| Moka v2, 200 games | — | — | 133 |
The KataGo opponents play the top move of their raw policy. They do not search. Moka v2.1 is about 120 Elo stronger than Moka v2 (95% CI +70 to +170). It scores 22% against the raw policy of the native 9×9 b18 network, which is about 220 Elo weaker.
Moka v2.1 was distilled from the native 9×9 KataGo b18 network. The training data has three parts: b18 self-play games, games of Moka against KataGo b6c96, and games of Moka against the raw policy of KataGo b18. KataGo b18 labelled each position in all three parts.
| Path | Weights | Runtime | Total load |
|---|---|---|---|
| Moka v2.1 · INT8 | 234 KB | 4 KB | 238 KB |
| Moka v1 · INT8 | 100 KB | 4 KB | 103 KB |
| KataGo b6c96 · ONNX | 4.1 MB | 12.8 MB | 17.0 MB |
Weights sizes are gzipped. The Moka v2 browser path is about 70× smaller than the teacher path. The point is not to replace KataGo. It is to put a learned Go player inside an ordinary webpage.
Moka runs inference in a Web Worker, which keeps model execution off the main thread.
| Metric | Latency |
|---|---|
| Initialization | 10.1 ms |
| Mean inference | 9.0 ms |
| p50 inference | 8.8 ms |
| p95 inference | 9.6 ms |
These are the Moka v1 medians of five Chromium runs on Apple Silicon. Each run measured 100 positions after 10 warmups. Latency includes Worker messaging; inference stays off the main thread.
Moka v2 needs more compute for each inference. In Node.js on an Apple M5 Max, the same runtime took 10.8 ms for Moka v2 and 4.1 ms for Moka v1 (mean of 100 positions, median of five runs). Expect browser latency for Moka v2 to be approximately 2.6× the values in the table.
Embed Moka by serving its browser client, worker, manifest, and weights with your site. The example below runs without a framework or bundler.
Build Moka from this repository, then copy its four runtime files into your site’s public directory.
ni
nr build
mkdir -p ../your_site/public/moka
cp dist/index.js ../your_site/public/moka/index.js
cp dist/worker.js ../your_site/public/moka/worker.js
cp model/go-model.json ../your_site/public/moka/go-model.json
cp model/go-model.bin ../your_site/public/moka/go-model.binThe deployed site should serve these files from /moka. Use different URLs in the initialization options if you choose another directory.
Run this code from a JavaScript module, such as an app.js file loaded with type="module".
import {
GoModelWorkerClient,
createGameState,
encodeStudentFeatures,
getAutomaticallyDeadMoves,
getAreaScore,
playMove,
removeDeadStones,
selectHighestLegalMove,
} from "/moka/index.js";
const mokaWorker = new Worker("/moka/worker.js", {
type: "module",
});
const moka = new GoModelWorkerClient(mokaWorker);
await moka.initialize({
manifestUrl: "/moka/go-model.json",
weightsUrl: "/moka/go-model.bin",
});
let gameState = createGameState();initialize downloads the manifest and weights, verifies the model digest, and loads the model inside the worker.
Encode the current game state, run inference, and select the highest-rated legal move.
const inference = await moka.infer(encodeStudentFeatures(gameState));
const mokaMove = selectHighestLegalMove(gameState, inference.policyLogits);
const nextGameState = playMove(gameState, mokaMove);
if (nextGameState) {
gameState = nextGameState;
}
const currentPlayerWinProbability = (inference.value + 1) / 2;The value result ranges from -1 to 1 for the player who was next to move. The final line converts it to a probability from 0 to 1.
Map board intersections to move indexes with row * 9 + column. Indexes 0 through 80 represent board points in row-major order, starting at the top-left. Index 81 represents a pass. playMove returns null when a move is illegal.
Call moka.dispose() when your page or component no longer needs the model. This terminates the worker and rejects pending requests.
Two passes stop play, but dead stones must be removed before area scoring. Moka combines conservative pass-alive analysis with a capture aftermath to identify them automatically:
const deadMoves = getAutomaticallyDeadMoves(gameState);
const adjudicatedState = removeDeadStones(gameState, deadMoves);
const blackLead = getAreaScore(adjudicatedState);Pass-alive groups and unresolved seki remain on the board. removeDeadStones returns a copy and leaves the played position unchanged. Positive scores lead for Black; negative scores lead for White, including 7 points of komi.
Serve the assets over HTTP or HTTPS because browsers do not start module workers from file:// pages. A Content Security Policy (CSP) must allow the worker URL through worker-src. Cross-origin asset URLs also need Cross-Origin Resource Sharing (CORS) headers.
Install the JavaScript and Python dependencies before training or building Moka.
uv sync
niPlace KataGo b6c96 at teachers/katago-b6c96.onnx.
Moka v2 needs KataGo with a Metal or CUDA backend and the native 9×9 b18 network at teachers/kata9x9-native/kata9x9-b18c384nbt-20231025.bin.gz.
-
Generate b18 self-play positions:
uv run python -m go_model.selfplay_generate --games 3000 --visits 64
-
Generate on-policy positions. Moka plays KataGo b6c96, and b18 labels each position. Add
--opponent-katago-model teachers/kata9x9-native/kata9x9-b18c384nbt-20231025.bin.gzto make Moka play the raw policy of b18 instead:uv run python -m go_model.onpolicy_generate \ --moka-checkpoints checkpoints/moka-global-soup-exact-q50-int8-roundtrip.safetensors \ --games 3000 --visits 32
-
Train the network:
uv run python -m go_model.scaled_train \ --data "data/selfplay/*.npz" "data/onpolicy/*.npz" \ --checkpoint checkpoints/moka-v2.safetensors --epochs 10
-
Round the weights to INT8 and export the browser weights:
uv run python -m go_model.quantization \ --checkpoint checkpoints/moka-v2.safetensors \ --output checkpoints/moka-v2-int8.safetensors uv run go-export --checkpoint checkpoints/moka-v2-int8.safetensors
To get more on-policy data, do step 2 again with the new checkpoint added to --moka-checkpoints. Moka v2 used two rounds against b6c96. Moka v2.1 added one more round against b6c96 and one round against b18.
Compare two checkpoints in direct games:
uv run python -m go_model.headtohead --first NEW.safetensors --second OLD.safetensors \
--games 100 --simulations 64 --opening-offset 20100000Play a checkpoint against the KataGo analysis engine. --visits 1 makes KataGo play its raw policy:
uv run python -m go_model.katago_match --checkpoint NEW.safetensors \
--games 100 --visits 1 --opening-offset 20040000Build the browser client and worker into dist.
nr buildRun the TypeScript checks and Python test suite before publishing changes.
nr check
nr testMIT