js-chess-engine
v2.4.3
Published
Simple and fast Node.js chess engine with configurable AI and no dependencies
Maintainers
Readme
JS-CHESS-ENGINE
Complete TypeScript chess engine without dependencies for Node.js >=24 and browsers. Includes configurable AI with difficulty levels 1-5.
⚠️ Upgrading from v1? See Breaking Changes section at the end of this document
Install
Install with npm
npm i js-chess-engine --saveor install with yarn
yarn add js-chess-engineNode.js Requirement: Node.js >=24 is required for v2.
Import
TypeScript / ESM
// Import Game class and stateless functions
import { Game, moves, status, move, ai, getFen } from 'js-chess-engine'
// Import types for TypeScript
import type { BoardConfig, PieceSymbol, MovesMap, AILevel } from 'js-chess-engine'
const game = new Game()CommonJS (Node.js)
const { Game, moves, status, move, ai, getFen } = require('js-chess-engine')
const game = new Game()Examples
Two full example applications are available:
--> Server — js-chess-engine-app — React + Node.js REST API, engine runs on the server — LIVE DEMO
->> Browser — js-chess-engine-fe-app — Pure frontend React app, engine runs entirely in the browser, no server needed — LIVE DEMO
Documentation
You have two options for using this engine:
Both options use the same transposition table cache for AI performance. The difference is that the Game class stores the board state internally and tracks move history, while stateless functions require a board configuration on each call.
Option 1 - Game class (stateful)
Use the Game class to manage chess game state. All methods use the internally stored board — no need to pass configuration each time. The class also tracks full move history.
import { Game } from 'js-chess-engine'
import type { BoardConfig, MovesMap } from 'js-chess-engine'
const game = new Game()You can export your game to JSON or FEN at any time and use these formats to restore your game later.
API Description
constructor
new Game(configuration) - Create a new game with optional initial configuration.
Params:
configurationBoardConfig | string (optional) - Chess board configuration (JSON object or FEN string). Default is standard starting position.
import { Game } from 'js-chess-engine'
import type { BoardConfig } from 'js-chess-engine'
// New game with starting position
const game1 = new Game()
// From FEN string
const game2 = new Game('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1')
// From BoardConfig object
const config: BoardConfig = { /* ... */ }
const game3 = new Game(config)move
game.move(from, to) - Perform a move on the chessboard and recalculate game state. Returns full BoardConfig object (breaking change from v1).
Params:
fromstring (mandatory) - Starting square (case-insensitive, e.g., 'E2' or 'e2')tostring (mandatory) - Destination square (case-insensitive, e.g., 'E4' or 'e4')
Returns: BoardConfig - Full board configuration after the move
import type { BoardConfig } from 'js-chess-engine'
const newConfig: BoardConfig = game.move('E2', 'E4')
console.log(newConfig.pieces) // {"E4": "P", "E1": "K", ...}moves
game.moves(from?) - Get all legal moves for the current player. Optionally filter by a specific square.
Params:
fromstring (optional) - Square to filter moves from (case-insensitive, e.g., 'E2'). If omitted, returns moves for all pieces.
Returns: MovesMap - Object mapping from-squares to arrays of to-squares
import type { MovesMap } from 'js-chess-engine'
// Get all moves (no parameter)
const allMoves: MovesMap = game.moves()
// {"E2": ["E3", "E4"], "B1": ["A3", "C3"], ...}
// Get moves for specific square
const pawnMoves: MovesMap = game.moves('E2')
// {"E2": ["E3", "E4"]}setPiece
game.setPiece(location, piece) - Add or replace a chess piece at the specified location.
Params:
locationstring (mandatory) - Square location (case-insensitive, e.g., 'E2')piecePieceSymbol (mandatory) - Piece symbol using FEN notation (K, Q, R, B, N, P for white; k, q, r, b, n, p for black)
import type { PieceSymbol } from 'js-chess-engine'
const piece: PieceSymbol = 'Q'
game.setPiece('E5', piece)removePiece
game.removePiece(location) - Remove a piece from the specified location.
Params:
locationstring (mandatory) - Square location (case-insensitive, e.g., 'E2')
game.removePiece('E5')aiMove
⚠️ DEPRECATED: This method will be removed in v3.0.0. Use
ai()instead, which returns both the move and board state.
game.aiMove(level) - Calculate and perform the best move for the current player using AI. Returns only the move (v1 API compatible).
Params:
levelAILevel (optional) - AI difficulty level (1-5). See Computer AI section. Default: 3
Returns: HistoryEntry - The played move (e.g., {"E2": "E4"})
import type { HistoryEntry, AILevel } from 'js-chess-engine'
const level: AILevel = 4
const move: HistoryEntry = game.aiMove(level)
console.log(move) // {"E2": "E4"}
// To get board state after move, use exportJson()
const board = game.exportJson()
// RECOMMENDED: Use ai() instead
const result = game.ai({ level: 4 })
console.log(result.move) // {"E2": "E4"}
console.log(result.board) // Full board stateai
game.ai(options?) - Calculate the best move using AI. Returns both the move and board state.
Params:
optionsobject (optional) - Configuration options:levelnumber (optional) - AI difficulty level (1-5). See Computer AI section. Default:3playboolean (optional) - Whether to apply the move to the game. Default:true. Iffalse, returns the move without modifying the game state, andboardwill contain the current state (before the move).analysisboolean (optional) - Iftrue, also returns ananalysispayload containing all root legal moves scored by the engine's search (sorted best → worst). Default:false.ttSizeMBnumber (optional) - Transposition table size in MB (0 to disable, min 0.25 MB). Default: auto-scaled by AI level. See Auto-Scaling Transposition Table for details.randomnessnumber (optional) - Centipawn threshold for move variety. The engine picks randomly among all moves scoring within this many centipawns of the best move. Makes games less predictable without playing blunders. Default:0(fully deterministic). Reference values:10very subtle ·30slight variety ·80noticeable ·200chaotic.depthobject (optional) - Override AI search depth parameters. Omitted fields fall back to the level's defaults (see Computer AI table).basenumber (optional) - Base search depth. Integer > 0.extendednumber (optional) - Max adaptive extension depth. Integer 0-3.checkboolean (optional) - Enable check extensions.quiescencenumber (optional) - Quiescence search depth. Integer >= 0.
Returns: { move: HistoryEntry, board: BoardConfig, analysis?: Array<{ move: HistoryEntry, score: number }>, depth?: number, nodesSearched?: number, bestScore?: number } - Object containing the move and board state (current state if play=false, updated state if play=true). Extra fields are returned only when analysis: true.
import type { HistoryEntry, BoardConfig } from 'js-chess-engine'
// Play the move (default behavior)
const result1 = game.ai({ level: 4 })
console.log(result1.move) // {"E2": "E4"}
console.log(result1.board.turn) // "black" (updated after move)
// Analysis mode: get move without applying it
const result2 = game.ai({ level: 4, play: false })
console.log(result2.move) // {"E2": "E4"}
console.log(result2.board.turn) // "white" (current state, before move)
// Root move scoring (debug/inspection)
const result2b = game.ai({ level: 4, play: false, analysis: true })
console.log(result2b.analysis?.slice(0, 5)) // [{ move: {"E2": "E4"}, score: 12 }, ...]
console.log(result2b.bestScore)
console.log(result2b.depth)
console.log(result2b.nodesSearched)
// Use default level 3
const result3 = game.ai()
console.log(result3.move) // AI move with level 3
// TT size auto-scales by level (see Auto-Scaling Transposition Table section)
const result4 = game.ai({ level: 5 })
console.log(result4.move) // Level 5: 40 MB Node.js / 20 MB browser (auto)
// Override TT size manually if needed
const result5 = game.ai({ level: 3, ttSizeMB: 128 })
console.log(result5.move) // Force 128MB cache
// Ultra-lightweight mode for low-end devices
const result6 = game.ai({ level: 2, ttSizeMB: 0.5 })
console.log(result6.move) // Force 512KB cache
// Custom depth overrides (use level 3 defaults, but increase base depth)
const result7 = game.ai({ level: 3, depth: { base: 5 } })
// Full depth control: deep search, no extensions, no quiescence
const result8 = game.ai({ level: 1, depth: { base: 4, extended: 0, check: false, quiescence: 0 } })
// Randomness: vary move selection among nearly-equal scoring moves (default: 0)
const result9 = game.ai({ level: 3 }) // fully deterministic (default)
const result10 = game.ai({ level: 3, randomness: 10 }) // very subtle variety
const result11 = game.ai({ level: 3, randomness: 80 }) // noticeable variety, casual playgetHistory
game.getHistory() - Get all played moves with board states.
Returns: Array of objects containing move and resulting board configuration
const history = game.getHistory()
// [
// { move: {"E2": "E4"}, pieces: {...}, turn: "black", ... },
// { move: {"E7": "E5"}, pieces: {...}, turn: "white", ... }
// ]printToConsole
game.printToConsole() - Print an ASCII representation of the chessboard to console.
game.printToConsole()
// +---+---+---+---+---+---+---+---+
// 8 | r | n | b | q | k | b | n | r |
// +---+---+---+---+---+---+---+---+
// 7 | p | p | p | p | p | p | p | p |
// +---+---+---+---+---+---+---+---+
// ...exportJson
game.exportJson() - Export current game state as a JSON BoardConfig object.
Returns: BoardConfig - Full board configuration
import type { BoardConfig } from 'js-chess-engine'
const config: BoardConfig = game.exportJson()exportFEN
game.exportFEN() - Export current game state as a FEN string.
Returns: string - FEN notation
const fen: string = game.exportFEN()
// "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"Option 2 - Stateless functions
Call functions directly without creating a Game object. Each function takes a board configuration as its first parameter. Ideal for serverless environments or when you manage state externally (REST APIs, Redux, etc.).
import { move, moves, status, ai, aiMove, getFen } from 'js-chess-engine'
import type { BoardConfig, MovesMap } from 'js-chess-engine'The stateless functions are wrappers around the same engine used by the Game class — the only signature difference is the boardConfiguration first parameter.
API Description
moves
moves(boardConfiguration) - Get all legal moves for the current player.
Params:
boardConfigurationBoardConfig | string (mandatory) - Board configuration (JSON object or FEN string)
Returns: MovesMap
import { moves } from 'js-chess-engine'
import type { MovesMap, BoardConfig } from 'js-chess-engine'
const config: BoardConfig = { /* ... */ }
const allMoves: MovesMap = moves(config)
// {"E2": ["E3", "E4"], "B1": ["A3", "C3"], ...}
// From FEN string
const fenMoves: MovesMap = moves('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1')status
status(boardConfiguration) - Get calculated board configuration with current game status. Useful for converting FEN to JSON.
Params:
boardConfigurationBoardConfig | string (mandatory) - Board configuration
Returns: BoardConfig - Full board configuration with status
import { status } from 'js-chess-engine'
import type { BoardConfig } from 'js-chess-engine'
// Convert FEN to JSON
const config: BoardConfig = status('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1')
console.log(config.turn) // "black"
console.log(config.check) // false
console.log(config.checkMate) // falsegetFen
getFen(boardConfiguration) - Convert board configuration to FEN string.
Params:
boardConfigurationBoardConfig | string (mandatory) - Board configuration
Returns: string - FEN notation
import { getFen } from 'js-chess-engine'
import type { BoardConfig } from 'js-chess-engine'
const config: BoardConfig = { /* ... */ }
const fen: string = getFen(config)move
move(boardConfiguration, from, to) - Perform a move and get the new board state. Returns full BoardConfig object (breaking change from v1).
Params:
boardConfigurationBoardConfig | string (mandatory) - Board configurationfromstring (mandatory) - Starting square (case-insensitive)tostring (mandatory) - Destination square (case-insensitive)
Returns: BoardConfig - Full board configuration after the move
import { move } from 'js-chess-engine'
import type { BoardConfig } from 'js-chess-engine'
// Move from FEN string
const config1: BoardConfig = move('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', 'E2', 'E4')
// Chain moves
const config2: BoardConfig = move(config1, 'E7', 'E5')aiMove
⚠️ DEPRECATED: This function will be removed in v3.0.0. Use
ai()instead, which returns both the move and board state.
aiMove(boardConfiguration, level) - Calculate and return the best move using AI. Returns only the move (v1 API compatible).
Params:
boardConfigurationBoardConfig | string (mandatory) - Board configurationlevelAILevel (optional) - AI difficulty level (1-5). Default: 3
Returns: HistoryEntry - The played move (e.g., {"E2": "E4"})
import { aiMove, ai } from 'js-chess-engine'
const fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
// Deprecated
const played = aiMove(fen, 4) // {"E2": "E4"}
// RECOMMENDED: Use ai() instead
const result = ai(fen, { level: 4 })
console.log(result.move) // {"E2": "E4"}
console.log(result.board) // Full board stateai
ai(boardConfiguration, options?) - Stateless equivalent of game.ai(). Same options and return type — see the Game class ai() docs for full parameter documentation.
import { ai } from 'js-chess-engine'
import type { HistoryEntry, BoardConfig } from 'js-chess-engine'
const fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
// Play the move (default behavior)
const result1 = ai(fen, { level: 4 })
console.log(result1.move) // {"E2": "E4"}
console.log(result1.board.turn) // "black" (updated after move)
// Analysis mode: get move without applying it
const result2 = ai(fen, { level: 4, play: false, analysis: true })
console.log(result2.analysis?.slice(0, 5)) // [{ move: {"E2": "E4"}, score: 12 }, ...]
console.log(result2.bestScore)
console.log(result2.depth)
console.log(result2.nodesSearched)
// Custom depth overrides
const result3 = ai(fen, { level: 3, depth: { base: 5, quiescence: 3 } })
// Randomness: deterministic by default, opt in for variety
const result4 = ai(fen, { level: 3 }) // fully deterministic (default)
const result5 = ai(fen, { level: 3, randomness: 10 }) // very subtle varietyBoard Configuration
Board configuration can be represented as either a JSON object (BoardConfig) or a FEN string.
JSON Format
The JSON format is convenient for modern applications where state is represented as objects (React, Redux, etc.).
import type { BoardConfig } from 'js-chess-engine'
const config: BoardConfig = {
"turn": "black",
"pieces": {
"E1": "K",
"C1": "B",
"E8": "k"
},
"isFinished": false,
"check": false,
"checkMate": false,
"staleMate": false,
"castling": {
"whiteLong": true,
"whiteShort": true,
"blackLong": true,
"blackShort": true
},
"enPassant": "E6",
"halfMove": 0,
"fullMove": 1
}turn - Player to move next. Values: "white" (default) or "black"
isFinished - true when the game is over (checkmate or stalemate). Default: false
check - true when the current player is in check. Default: false
checkMate - true when the current player is checkmated. Default: false
staleMate - true when the current player is stalemated (no legal moves but not in check). Default: false
castling - Castling availability for each side. true means castling is still possible. Default: all true
whiteLong(queenside) - White king moves from E1 to C1whiteShort(kingside) - White king moves from E1 to G1blackLong(queenside) - Black king moves from E8 to C8blackShort(kingside) - Black king moves from E8 to G8
enPassant - If a pawn just made a two-square move, this is the square "behind" the pawn for en passant capture. Default: null
halfMove - Number of halfmoves since the last capture or pawn advance. Used for the fifty-move rule. Default: 0
fullMove - Full move number. Starts at 1 and increments after Black's move. Default: 1
pieces - Pieces on the board using FEN notation:
| Piece | White | Black | | :----: | :---: | :---: | | Pawn | P | p | | Knight | N | n | | Bishop | B | b | | Rook | R | r | | Queen | Q | q | | King | K | k |
FEN Format
You can also use Forsyth–Edwards Notation (FEN):
import { move } from 'js-chess-engine'
const fen = 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1'
const newConfig = move(fen, 'H7', 'H5')
console.log(newConfig)
// BoardConfig object with updated positionComputer AI
The engine includes a sophisticated AI based on the Minimax algorithm with alpha-beta pruning, enhanced with advanced performance optimizations. There are five difficulty levels:
| Level | Alias | Description | Base Depth | Extended Depth | Check Ext. | Quiescence | Total Max | | :---: | :---------------- | :----------------------------------- | :--------: | :------------: | :--------: | :--------: | :-------: | | 1 | Beginner | Very weak play, minimal lookahead | 1 ply | +0 ply | +0 ply | +0 ply | 1 ply | | 2 | Easy | Suitable for new chess players | 2 ply | +0 ply | +1 ply | +0 ply | 3 ply | | 3 | Intermediate | Balanced difficulty (default) | 2 ply | +1 ply | +1 ply | +1 ply | 5 ply | | 4 | Advanced | Strong play with deeper search | 3 ply | +2 ply | +1 ply | +2 ply | 8 ply | | 5 | Expert | Very strong play, deep search | 4 ply | +3 ply | +1 ply | +4 ply | 12 ply |
Depth Components:
- Base Depth: Minimum search depth for the AI level
- Extended Depth: Additional adaptive depth added in simplified positions (endgames, constrained moves)
- Check Extension: Extra ply when a move gives check (prevents tactical oversights)
- Quiescence Search: Additional plies to evaluate forcing moves (captures/promotions) at leaf nodes, preventing horizon effect blunders
- Total Max: Maximum possible search depth in tactical positions (all extensions combined)
Performance: Response time increases with level (deeper search + larger transposition table). Exact timings vary a lot by CPU, position complexity, and cache size, so the repo includes a benchmark script—run npm run benchmark to measure performance on your machine.
import { Game } from 'js-chess-engine'
const game = new Game()
// Different difficulty levels
game.ai({ level: 1 }) // Beginner
game.ai({ level: 2 }) // Easy
game.ai({ level: 3 }) // Intermediate (default)
game.ai({ level: 4 }) // Advanced
game.ai({ level: 5 }) // ExpertImplementation Highlights:
- Alpha-beta pruning with transposition table (auto-scales by AI level for optimal memory usage)
- Principal Variation Search (PVS) for faster node pruning
- Advanced move ordering (PV moves, MVV-LVA captures, killer moves)
- Iterative deepening for optimal move ordering
- Position evaluation with material balance and piece-square tables
- Smart environment detection and level-based memory scaling for optimal performance across platforms
Auto-Scaling Transposition Table (Smart Memory Management) {#transposition-table}
The engine automatically adjusts cache size based on AI level and environment:
| AI Level | Node.js Cache | Browser Cache | Use Case | | :------: | :-----------: | :-----------: | :--------------------------- | | 1 | 0.5 MB | 0.25 MB | Lightweight, fast responses | | 2 | 1 MB | 0.5 MB | Mobile-friendly performance | | 3 | 4 MB | 2 MB | Balanced (default) | | 4 | 16 MB | 8 MB | Strong tactical play | | 5 | 40 MB | 20 MB | Maximum strength |
Lower levels use less memory for faster responses, higher levels use more for better move quality. Browser cache sizes are appropriate for modern devices (2024+). Override with ttSizeMB option if needed.
Note: TT size directly affects AI strength. Larger cache = better move ordering and fewer redundant searches, resulting in stronger play.
📚 Complete AI Implementation Guide →
For comprehensive technical documentation including algorithms, data structures, optimization techniques, and developer guides, see the AI implementation documentation.
Hints
Pawn Promotion: When a pawn reaches the opposite end of the board, it is automatically promoted to a Queen. If you want the player to choose the promotion piece in your application, use the
setPiece()method to replace the queen with the desired piece.Castling: Castling moves are included in the moves returned by
moves(). When a king moves two squares (castling), the rook automatically moves as well.Fifty-Move Rule: The
halfMovecounter is calculated automatically, but the fifty-move rule is not enforced by the engine. You can implement this rule in your application if needed.
TypeScript Support
Version 2.0 is written entirely in TypeScript and exports all necessary types:
import { Game } from 'js-chess-engine'
import type {
// Board types
BoardConfig,
PieceSymbol,
Square,
Color,
MovesMap,
CastlingRights,
HistoryEntry,
// AI types
AILevel,
// Piece types
PieceType
} from 'js-chess-engine'See /src/types/board.types.ts and /src/types/ai.types.ts for complete type definitions.
Collaboration
Collaborators are welcome. Please ensure your code passes TypeScript type checking and all tests before submitting a pull request:
npm run typecheck # TypeScript type checking
npm run test # Run test suiteIf possible, use commit message prefixes like feat: or fix: - the changelog is generated from these.
TODO
- FEN validation
- Additional endgame tablebase support
CHANGELOG
Changelog can be found HERE.
In conclusion - why another chess engine?
I am not a chess pro. My father is.
When I was ten, I had an Atari (with Turbo Basic), and I was hoping for a new PC. My father told me: "Make me a computer program which beats me in chess, and I'll buy you a new PC."

Obviously, it was a trap and I failed. Twenty years later, it came back to my mind, and I decided to finish what I started. This is version 2.0 - a complete TypeScript rewrite with improved performance and architecture.
Breaking Changes from v1
Version 2.0 is a complete TypeScript rewrite with significant API changes. While method names remain the same, several return types have changed:
1. moves(square) Return Type Changed
v1 Behavior:
const game = new Game()
game.moves('E2') // Returns array: ["E3", "E4"]v2 Behavior:
const game = new Game()
game.moves('E2') // Returns object: {"E2": ["E3", "E4"]}2. move() Return Type Changed
v1 Behavior:
game.move('E2', 'E4') // Returns move object: {"E2": "E4"}v2 Behavior:
game.move('E2', 'E4') // Returns full BoardConfig object3. aiMove() API - Restored for Migration (Deprecated)
The aiMove() function has been restored to v1 API compatibility to ease migration, but is deprecated and will be removed in v3.0.0.
v1 Behavior:
aiMove(config, 2) // Returns move object: {"E2": "E4"}v2 Behavior (Current):
aiMove(config, 3) // Returns move object: {"E2": "E4"} ✅ v1 compatibleNew ai() function - For users who need both move and board state:
ai(config, { level: 3 }) // Returns: { move: {"E2": "E4"}, board: {...} }⚠️ DEPRECATION NOTICE:
aiMove()is deprecated and will be removed in v3.0.0. Migrate toai()for better functionality.Migration:
// Old (deprecated) const move = game.aiMove(3) const board = game.exportJson() // New (recommended) const result = game.ai({ level: 3 }) console.log(result.move) // Same move object console.log(result.board) // Board state included
4. AI Difficulty Levels Changed
- v1: Levels 0-4 (0=easiest, 4=hardest)
- v2: Levels 1-5 (1=easiest, 5=hardest)
Migration:
- Level 0 → Level 1 (Beginner)
- Level 1 → Level 2 (Easy)
- Level 2 → Level 3 (Intermediate, default)
- Level 3 → Level 4 (Advanced)
- Level 4 → Level 5 (Expert)
The default level has changed from 2 to 3 to maintain similar difficulty in the middle range.
5. New staleMate Field
v2 adds explicit staleMate: boolean field to BoardConfig. In v1, stalemate was inferred from isFinished && !checkMate. isFinished remains for backward compatibility but can be removed in future versions.
6. Node.js Version Requirement
- v1: Node.js >=20 <21
- v2: Node.js >=24
What Stayed the Same
- ✅ Game class constructor signature
- ✅ Method names (move, moves, aiMove, etc.)
- ✅ Board formats (JSON and FEN)
- ✅ Chess rules (castling, en passant, promotions)
- ✅ Square notation (case-insensitive: 'E2' or 'e2')
