@og-nav/expo-stockfish
v0.1.0
Published
Stockfish chess engine for React Native + Expo (iOS)
Maintainers
Readme
@og-nav/expo-stockfish
Expo native module wrapping the Stockfish 17 C++ chess engine for iOS. Runs Stockfish with NNUE neural networks on-device via a four-layer bridge (Swift -> ObjC++ -> C++ -> vendored Stockfish source), exposing a clean React hook and low-level UCI API.
import { useStockfish, useStockfishInfo } from "@og-nav/expo-stockfish";
const { isReady, bestMove, setPosition, search, stop } = useStockfish();
const info = useStockfishInfo(); // streaming info, call in a leaf componentInstall
npm install @og-nav/expo-stockfish
# or
pnpm add @og-nav/expo-stockfishThen download the NNUE neural network files (~88 MB):
npx expo-stockfish download-nnueThe NNUE files are not included in the npm package to keep it small. They are bundled into the iOS app as a resource bundle at build time.
Peer dependencies
The module requires Expo and React Native but does not bundle them:
npx expo install expo react react-nativeQuickstart
React hook (recommended)
import { Chessboard, type ChessboardRef } from "@og-nav/expo-chessboard";
import { Chess } from "chess.ts";
import { useStockfish } from "@og-nav/expo-stockfish";
import { useRef, useState, useCallback, useEffect } from "react";
export function PlayScreen() {
const [chess] = useState(() => new Chess());
const boardRef = useRef<ChessboardRef>(null);
const { isReady, bestMove, setPosition, search, stop } = useStockfish({
skill: 10, // 0-20 (default 20)
throttleMs: 150, // throttle info-line state updates
});
// React to engine's best move
useEffect(() => {
if (bestMove) {
boardRef.current?.animateMove(bestMove.from, bestMove.to, bestMove.promotion);
}
}, [bestMove]);
const onPlayerMove = useCallback(() => {
if (chess.turn() === "b" && !chess.gameOver()) {
stop();
setPosition(chess.fen());
search({ depth: 15 });
}
}, [chess, stop, setPosition, search]);
if (!isReady) return <Text>Loading Stockfish...</Text>;
return (
<Chessboard
ref={boardRef}
chess={chess}
boardSize={360}
playerSide="white"
onMove={onPlayerMove}
/>
);
}One-shot helper
For cases where you just need a single best move:
import { findBestMove } from "@og-nav/expo-stockfish";
const move = await findBestMove(
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
{ depth: 18, skill: 10 }
);
console.log(move.uci); // "e7e5"Low-level API
Full control over the UCI protocol:
import {
startEngine,
stopEngine,
sendCommand,
addOutputListener,
isRunning,
getProcessStats,
} from "@og-nav/expo-stockfish";
await startEngine();
const sub = addOutputListener((line) => {
console.log("engine:", line);
});
sendCommand("position startpos moves e2e4");
sendCommand("go depth 20");
// Later:
sendCommand("stop");
await stopEngine();
sub.remove();API Reference
useStockfish(options?)
React hook that manages the engine lifecycle.
Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| autoStart | boolean | true | Start engine on mount |
| threads | number | 1 | Number of search threads |
| hashMB | number | 32 | Hash table size in MB |
| skill | number | 20 | Skill Level (0-20) |
| throttleMs | number | 100 | Throttle info-line state updates |
| onError | (err: Error) => void | — | Error callback |
Returns:
| Field | Type | Description |
|-------|------|-------------|
| isReady | boolean | Engine initialized and ready |
| bestMove | BestMove \| null | Latest bestmove result |
| analyze | (opts?) => Promise<AnalyzeResult> | Promise-returning search (idiomatic for game loops) |
| setPosition | (fen: string) => void | Send position command |
| search | (opts?) => void | Start search (depth/movetime/infinite) |
| stop | () => void | Stop current search |
| sendRaw | (cmd: string) => void | Send raw UCI command |
| start | () => Promise<void> | Manually start engine |
| shutdown | () => Promise<void> | Manually stop engine |
Streaming info lines are exposed separately via useStockfishInfo() so
consumers can subscribe in a leaf component. This keeps the ~50/sec info
stream from re-rendering the parent subtree (e.g. a chessboard).
useStockfishInfo()
React hook that subscribes to the shared info store and returns the latest
SearchInfo | null. Call it in the component that actually renders the
info strip, not in a parent that also renders the board:
function EngineInfoBar() {
const info = useStockfishInfo();
if (!info) return null;
return <Text>depth {info.depth} · {info.nps} nps</Text>;
}For imperative reads (e.g. polling from a timer), use getInfoSnapshot()
instead — it returns the current snapshot without subscribing.
findBestMove(fen, options?)
One-shot async helper. Starts the engine if needed, sends position + search, resolves on bestmove. 60-second safety timeout.
Parsers
import { parseInfoLine, parseBestMove, parseUCIMove } from "@og-nav/expo-stockfish";
const info = parseInfoLine("info depth 20 nodes 1234567 nps 500000 pv e2e4 e7e5");
// { depth: 20, nodes: 1234567, nps: 500000, pv: ["e2e4", "e7e5"], ... }
const best = parseBestMove("bestmove e2e4 ponder e7e5");
// { from: "e2", to: "e4", uci: "e2e4", ponder: "e7e5" }
const move = parseUCIMove("e7e8q");
// { from: "e7", to: "e8", promotion: "q" }getProcessStats()
Returns current process stats (thermal state, memory). Useful for monitoring engine performance on device.
const stats = getProcessStats();
// { thermalState: 0, residentBytes: 123456789, virtualBytes: 987654321 }NNUE Setup
The Stockfish 17 neural networks (~88 MB total) are required for full engine strength. They are excluded from the npm package to keep it under npm's size limit.
# Download NNUE files
npx expo-stockfish download-nnue
# Force re-download
npx expo-stockfish download-nnue --forceThe files are placed in cpp/Stockfish/src/ and bundled into the iOS
app as StockfishNNUE.bundle at build time.
If the automatic download fails, you can download manually:
curl -L -o cpp/Stockfish/src/nn-5227780996d3.nnue \
https://tests.stockfishchess.org/api/nn/nn-5227780996d3.nnue
curl -L -o cpp/Stockfish/src/nn-37f18f62d772.nnue \
https://tests.stockfishchess.org/api/nn/nn-37f18f62d772.nnueExample App
The example/ directory contains a full Expo app with two tabs:
- Play — Play against Stockfish using
@og-nav/expo-chessboardfor the board - Test — Device-side test harness (position runner, bench, mate-in-N suite, leak detector)
cd example
npm install
npx expo-stockfish download-nnue # from parent dir
npx pod-install
npx expo run:iosArchitecture
Four-layer bridge from JS to C++:
JS/TS (useStockfish hook)
-> ExpoStockfishModule.swift (Expo Modules definition)
-> StockfishBridge.mm (ObjC++ singleton)
-> StockfishEngine.cpp (C++ wrapper)
-> Vendored Stockfish 17 sourceThe C++ layer replaces std::cin/std::cout with custom streambufs to
route UCI protocol through the bridge. Engine output is dispatched to the
main thread for safe React state updates.
Known Limitations
- iOS only. Android module is a stub.
- Single engine instance. The bridge is a singleton — only one Stockfish process at a time.
- Stockfish 17. Upgrade to SF 18 is planned for a future release.
- No
std::cin/std::coutwhile engine runs. The bridge redirects these streams.
License
GPL-3.0-or-later (required by Stockfish's license).
