Skip to content

a2life/shogi_engine_api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Shogi AI API Server

Built with AI assistance — This project was developed with the help of Claude, an AI assistant made by Anthropic. The architecture, code, and documentation were produced through an iterative human–AI collaboration.


⚠️ Concurrency notice — USI Shogi engines are inherently single-threaded in terms of position state: the engine holds exactly one position at a time and processes commands sequentially. This server serialises all requests through a single queue, so concurrent clients must wait their turn. This design is well-suited for personal tools, game clients, or light automation, but is ill-fitted for high-concurrency usage. If you need to serve many simultaneous users, consider running multiple engine instances behind a load balancer.


A lightweight Node.js HTTP server that wraps any USI-compatible Shogi engine as a child process and exposes it over a REST API. No npm packages required — pure Node.js standard library.

Requirements

  • Node.js 20+
  • A USI-compatible Shogi engine binary (YaneuraOu, Apery, Fairy-Stockfish with shogi variant, etc.)

Usage

# Pass engine path as a command-line argument
node shogi-api-server.js /path/to/engine

# Or use an environment variable
SHOGI_ENGINE=/path/to/engine node shogi-api-server.js

# Change the port (default: 3000)
PORT=8080 SHOGI_ENGINE=./engine node shogi-api-server.js

On startup the server automatically loads engine options from engine_options.json, then performs the USI handshake (usiusiok, setoption × N, isreadyreadyok) before accepting any API requests. You do not need to send these commands yourself.


Project Layout

project-root/
├── engine_options.json      ← engine setoption settings (edit freely)
├── engine/
│   └── engine.exe           ← USI engine binary
│   └── eval/           ← USI engine binary
│      └── bin.nn           ← evaluation function binary
├── src/
│   ├── engine.ts
│   ├── router.ts
│   ├── usi.ts
│   ├── types.ts
│   └── config.ts
└── package.json

Engine Options

Engine startup options are loaded from engine_options.json in the project root. Edit this file to tune engine behaviour without touching any source code. The server issues a setoption name <N> value <V> command for each entry during the initialization sequence, between usiok and isready.

{
  "FV_SCALE": "24",
  "BookFile": "no_book",
  "USI_Hash": "256000",
  "USI_Threads":2
}

If the file is missing or malformed, a warning is logged and the server continues with engine defaults.


API Endpoints

GET / — Health check

Returns the engine's current status and a list of available endpoints.

curl http://localhost:3000/
{
  "status": "ready",
  "engineReady": true,
  "enginePath": "./engine/engine.exe",
  "restartCount": 0,
  "pendingRequests": 0,
  "waitingRequests": 0,
  "endpoints": ["..."]
}

status is "initializing" while the handshake is in progress, and "ready" once the engine is accepting commands.


POST /api/analyze — Atomic position + go (recommended)

Sends position and go as a single atomic operation through the engine queue. This is the preferred way to request analysis because it eliminates the race condition that arises when separate clients send position and go as independent requests.

curl -X POST http://localhost:3000/api/analyze \
  -H "Content-Type: application/json" \
  -d '{
    "sfen":  "startpos",
    "moves": "7g7f 3c3d",
    "go":    "go movetime 3000"
  }'

Request body:

Field Required Description
sfen Yes SFEN board string or the literal startpos.
moves No Space-separated USI move list.
go No Full go command string. Defaults to go infinite.
timeoutMs No Hard timeout override in milliseconds.
mateMaxMoves No Mate constraint: stop as soon as a mate-in-N with N ≤ this value is found. Only meaningful when go contains mate.
mateTimeSec No Mate constraint: keep searching for this many seconds; return the best (shortest) mate found when time expires. Only meaningful when go contains mate.

Mate search with /api/analyze

Pass "go": "go mate <ms>" to start a mate search, and use mateMaxMoves and/or mateTimeSec to control when the server stops collecting:

# Stop as soon as any mate is found (no constraints)
curl -X POST http://localhost:3000/api/analyze \
  -H "Content-Type: application/json" \
  -d '{"sfen": "...", "go": "go mate 60000"}'

# Stop as soon as a mate-in-11 or shorter is found
curl -X POST http://localhost:3000/api/analyze \
  -H "Content-Type: application/json" \
  -d '{"sfen": "...", "go": "go mate 60000", "mateMaxMoves": 11}'

# Search for 60 seconds; return the shortest mate found by then
curl -X POST http://localhost:3000/api/analyze \
  -H "Content-Type: application/json" \
  -d '{"sfen": "...", "go": "go mate 60000", "mateTimeSec": 60}'

# Stop early if mate-in-11 found, otherwise return best after 60 seconds
curl -X POST http://localhost:3000/api/analyze \
  -H "Content-Type: application/json" \
  -d '{"sfen": "...", "go": "go mate 60000", "mateMaxMoves": 11, "mateTimeSec": 60}'

POST /api/command — Raw command

Send any USI command as a raw string in a JSON body. Most flexible option; no URL encoding required. Also supports mate-search constraints when the command is go mate.

# Standard command
curl -X POST http://localhost:3000/api/command \
  -H "Content-Type: application/json" \
  -d '{"command": "go movetime 2000"}'

# Mate search — stop on first mate found
curl -X POST http://localhost:3000/api/command \
  -H "Content-Type: application/json" \
  -d '{"command": "go mate 60000"}'

# Mate search — stop when mate-in-11 or shorter is found
curl -X POST http://localhost:3000/api/command \
  -H "Content-Type: application/json" \
  -d '{"command": "go mate 60000", "mateMaxMoves": 11}'

# Mate search — search for 60 seconds, return best mate seen
curl -X POST http://localhost:3000/api/command \
  -H "Content-Type: application/json" \
  -d '{"command": "go mate 60000", "mateTimeSec": 60}'

# Mate search — stop early on mate-in-11, otherwise return best after 60 seconds
curl -X POST http://localhost:3000/api/command \
  -H "Content-Type: application/json" \
  -d '{"command": "go mate 60000", "mateMaxMoves": 11, "mateTimeSec": 60}'

Mate constraint fields (only for go mate commands):

Field Description
moves Stop as soon as a mate-in-N with N ≤ this value is found.
time Wall-clock time limit in seconds. Returns best mate seen when time expires.

If neither moves nor time is provided, the server stops on the very first engine output line that contains mate.


GET /api/command/:command — Simple commands

Send any USI command that has no parameters, or whose parameter contains no slash characters.

curl http://localhost:3000/api/command/usinewgame
curl http://localhost:3000/api/command/stop
curl http://localhost:3000/api/command/quit

GET /api/command/:command/:parameter — Commands with a parameter

Append the parameter as an additional path segment. Spaces must be encoded as %20.

# go movetime 1000
curl "http://localhost:3000/api/command/go/movetime%201000"

# go depth 10
curl "http://localhost:3000/api/command/go/depth%2010"

# setoption name Hash value 256
curl "http://localhost:3000/api/command/setoption/name%20Hash%20value%20256"

Limitation: parameters that contain / characters cannot be sent this way. SFEN strings always contain / (rank separators), so position must use /api/analyze, /api/position, or POST /api/command.


GET /api/position — Position command with SFEN

SFEN strings contain / (rank separators) and + (promoted pieces), both reserved characters in URL paths. This endpoint accepts SFEN as a query parameter where slashes can be safely percent-encoded as %2F.

Note: for a full analysis workflow, prefer POST /api/analyze which sends position and go atomically. Use this endpoint only when you need to set the position independently.

GET /api/position?sfen=<encoded-sfen>[&moves=<moves>]
Parameter Required Description
sfen Yes SFEN board string or the literal startpos. Encode / as %2F. The sfen keyword prefix is optional and stripped automatically.
moves No Space-separated move list in USI notation.
curl "http://localhost:3000/api/position?sfen=startpos"
curl "http://localhost:3000/api/position?sfen=startpos&moves=7g7f+3c3d"
curl "http://localhost:3000/api/position?sfen=lr6l%2F7k1%2F4S4%2F3p1S%2BSbp%2FPnP2g1p1%2F1Bn5P%2FLP1P1G3%2F2GSR4%2F1K6L+b+G9P2np+1"

Response format

All endpoints return JSON:

{
  "command": "position startpos | go movetime 3000",
  "response": [
    "info depth 1 seldepth 1 score cp 30 nodes 100 pv 7g7f",
    "info depth 5 seldepth 7 score cp 45 nodes 4200 pv 7g7f 3c3d 2g2f",
    "bestmove 7g7f ponder 3c3d"
  ]
}

Error responses include an error field and an appropriate HTTP status code.


Typical Game Session

# 1. Check the engine is ready (optional — startup is automatic)
curl http://localhost:3000/

# 2. Start a new game
curl http://localhost:3000/api/command/usinewgame

# 3. Request best move from starting position
curl -X POST http://localhost:3000/api/analyze \
  -H "Content-Type: application/json" \
  -d '{"sfen": "startpos", "go": "go movetime 1000"}'
# → bestmove 7g7f ponder 3c3d

# 4. After both sides move, request the next best move
curl -X POST http://localhost:3000/api/analyze \
  -H "Content-Type: application/json" \
  -d '{"sfen": "startpos", "moves": "7g7f 3c3d", "go": "go movetime 1000"}'

# 5. Repeat step 4 for each move pair

How It Works

Startup sequence

  1. Spawn the engine process
  2. Send usi → wait for usiok
  3. Read engine_options.json and send setoption name … value … for each entry
  4. Send isready → wait for readyok

Any API requests that arrive during this sequence are held in a waiting queue and replayed in order once readyok is received.

Multi-client serialisation

All commands pass through a single queue — only one command is in flight to the engine at any time. POST /api/analyze fuses position and go into one atomic queue entry, preventing a second client from injecting a different position between them. Clients that arrive while the engine is busy wait their turn.

Mate-search monitoring

The server implements a four-mode state machine for go mate commands:

moves time Behaviour
Stop on the first engine output line containing mate N
Stop immediately when mate-in-N with N ≤ moves is found
Run for time seconds; return the shortest mate seen at expiry
Stop early if N ≤ moves is found; otherwise return best mate at timeout

When time expires without finding a mate within the move limit, the best (shortest) mate seen during the search window is returned. If no mate was found at all, all collected engine output lines are returned.

Response collection (non-mate commands)

  1. Sentinel detection — well-known commands have a defined terminator: usiok for usi, readyok for isready, bestmove for go. Response is returned immediately when that line appears.
  2. Idle flush — for other commands, output is collected and returned 200 ms after the last line from the engine.
  3. Hard timeout — a 10-second safety net resolves the request with whatever has been collected so far.

Engine restart

Scenario Exit code Signal Restart?
Engine responded to quit 0 No
Engine crashed / assertion failed non-zero Yes
Killed by SIGTERM SIGTERM Yes
OOM / external kill -9 SIGKILL Yes
Server shutting down (Ctrl-C) any any No

Up to 5 restart attempts with a 1-second delay between each. A successful readyok resets the counter. If the limit is reached the server exits with code 1.

Graceful shutdown

Ctrl-C sends quit to the engine, gives it 2 seconds to exit cleanly, then force-kills it and closes the HTTP server.


Configuration

Environment variables / command-line:

Variable Default Description
SHOGI_ENGINE ./engine/engine.exe Path to the USI engine binary. Can also be passed as the first CLI argument.
PORT 3000 HTTP port to listen on.
ENGINE_OPTIONS_PATH ./engine_options.json Path to the engine options JSON file.

Constants in config.ts:

Constant Default Description
MAX_RESTARTS 5 Maximum automatic restart attempts after a crash.
RESTART_DELAY_MS 1000 Milliseconds to wait before each restart attempt.
DEFAULT_TIMEOUT_MS 10000 Per-command hard timeout in milliseconds.
IDLE_FLUSH_MS 200 Milliseconds of engine silence before flushing a response.

License

MIT

About

shogi_engine_api proof of concept

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published