weiqi-level-bots
v3.1.2
Published
Go (Baduk/Weiqi) bots graded by playing strength — 15 measured levels from absolute beginner to low amateur dan, driving Pachi, GNU Go, AmiGo and Brown over GTP
Maintainers
Readme
weiqi-level-bots
Go (Baduk / Weiqi) bots graded by playing strength — 15 levels, from complete beginner to low amateur dan.
Finding an opponent that plays like a person and is genuinely weak is surprisingly hard. This package cuts playing strength into 15 rungs, measured rather than guessed: pick a level and start playing.
Install
npm install weiqi-level-botsRequires Node.js 18 or later, plus a Go engine installed separately (see Installing an engine).
Quick start
import { runBotWithSlot } from 'weiqi-level-bots';
const res = await runBotWithSlot('lv6', {
moves: [['B', 'D4'], ['W', 'Q16']],
boardSize: 19
});
console.log(res.move); // 'Q4'moves is the game record so far, an array of [colour, coordinate] pairs; coordinates
use standard GTP notation (which skips the letter I). The returned move is the bot's
move, and PASS means it passes.
Strength levels
15 levels, weakest first:
| Levels | Suited to |
|---|---|
| lv1 … lv6 | Absolute beginners who have just learned the rules |
| lv7 lv8 lv9 | Novices who can finish a game |
| lv10 lv11 lv12 | Improvers who know basic life and death |
| lv13 lv14 lv15 | Intermediate and above |
The ordering comes from measurement, not estimation: 3,611 games over 489 handicap matchups on a 19x19 board, with the upper half measured twice to check reproducibility.
Two deliberate quirks worth knowing:
lv5andlv6overlaplv3andlv4in raw strength. They play their first six moves with GNU Go before handing over to a weak engine. Six moves out of ~300 barely changes the result, but it keeps the opening from looking random. For beginners that matters more than a rating gap — winning games is itself the lesson, so several weak-but-different opponents serve better than one strictly ordered ladder.lv12andlv13sit close together. GNU Go's level slider compresses almost its whole 0~12 range into one rung; they are kept apart because they feel different to play against, not because the measurement separates them.
To build a level picker, use SLOTS — it carries the rung number and intended audience,
but never the recipe behind a level:
import { SLOTS } from 'weiqi-level-bots';
SLOTS[0]; // { id: 'lv1', order: 1, tier: 'beginner' }
SLOTS.filter((s) => s.tier === 'beginner'); // every beginner levelInstalling an engine
No engine binaries ship with this package. The 15 levels are drawn from four engines, because no single engine covers the whole range:
| Engine | Levels | Install |
|---|---|---|
| Pachi | 1, 2, 4, 6, 8, 10, 11, 14, 15 | build from pasky/pachi with make DCNN=0, keeping its data files beside the binary |
| GNU Go | 12, 13 (and the opening of 5, 6) | brew install gnu-go / apt install gnugo |
| AmiGo | 7, 9 | build from karino2/AmigoGtpFork with CFLAGS="-std=gnu89" |
| Brown | 3, 5 | build Brown 1.0, adding #include <string.h> to interface.c |
Levels whose engine is missing throw an error naming it; the rest keep working.
Docker: docker/engines.Dockerfile ships with the package — after install it sits
at node_modules/weiqi-level-bots/docker/engines.Dockerfile. It builds all three
source-built engines into /engines, with the patches and build flags already worked
out. Use it as a build stage and install GNU Go from apt in your runtime image:
FROM node:20-bookworm-slim
# Debian installs gnugo into /usr/games, which is not on PATH in slim images —
# link it, or levels 12, 13 and the openings of 5 and 6 fail with ENOENT.
RUN apt-get update && apt-get install -y --no-install-recommends gnugo \
&& ln -s /usr/games/gnugo /usr/local/bin/gnugo \
&& rm -rf /var/lib/apt/lists/*
COPY --from=engines /engines /opt/go-engines
ENV WEIQI_ENGINE_DIR=/opt/go-engines
# Fail the build if the link above is missing. Without this the image builds and passes
# a naive health check, then serves with levels 12, 13 and the openings of 5, 6 dead.
RUN gnugo --versionIf the executable is not on PATH, there are two ways to point at it.
① Environment variable (easiest when several engines live in one directory):
export WEIQI_ENGINE_DIR=/opt/go-engines② Per call:
import { runBot } from 'weiqi-level-bots';
await runBot({
moves: [],
boardSize: 9,
enginePath: '/usr/local/bin/gnugo'
});Lookup order is enginePath → WEIQI_ENGINE_DIR → PATH. When an engine is missing,
the thrown error carries installation instructions.
API
| Export | Description |
|---|---|
| runBotWithSlot(slotId, options) | Play one move at a given slot (recommended) |
| runBot(options) | Play one move with parameters of your choosing |
| SLOTS | Every level as { id, order, tier }, for building a level picker |
| SLOT_IDS | Every level id (lv1 … lv15) |
Commonly used options fields:
| Field | Default | Description |
|---|---|---|
| moves | required | The game record so far |
| boardSize | 9 | Board size |
| komi | 6.5 | Komi |
| color | alternates | Colour to play |
| timeoutMs | 30000 | Per-call timeout |
| includeCandidates | false | Also return the candidate moves (topMoves) |
Ending a game
This package only produces moves. Detecting that a game has ended, and scoring it, are the caller's job.
Do not rely on the bot to pass. Weak engines pass at the wrong moments and keep playing
when nothing is left — lv4 reached the move cap in 77 of 81 measured games. Treat only
two passes in a row, or RESIGN, as an ending, and run your own end-of-game check past
a move threshold.
Notes
- One engine process is spawned per move. If you expose this over a network, add your own authentication, concurrency limits and rate limiting.
- The game record you pass in is validated; an invalid coordinate or colour throws.
- A
PASSearly in the game is usually an engine artefact rather than a real pass. The package already suppresses some of these, but not all.
Licence
MIT. The engines themselves carry their own licences (GNU Go is GPL); this package neither bundles nor links them, and only communicates with them as separate processes.
