sl-wukong-engine
v1.1.0
Published
Wukong - A powerful TypeScript Xiangqi (Chinese Chess) engine with advanced AI search algorithms
Maintainers
Readme
🐵 SL Wukong Engine
A powerful TypeScript Xiangqi (Chinese Chess) engine with advanced AI search algorithms.
🚀 Installation
npm install sl-wukong-engine📖 Usage
Basic Example
const engine = require('sl-wukong-engine').default;
// Initialize with starting position
engine.setBoard(engine.START_FEN);
// Print the board
engine.printBoard();
// Generate legal moves
const moves = engine.generateMoves();
console.log(`Legal moves: ${moves.length}`);
// Make a move
const move = moves[0].move;
if (engine.makeMove(move)) {
console.log(`Move made: ${engine.moveToString(move)}`);
engine.printBoard();
}
// Take back the move
engine.takeBack();TypeScript Example
import engine from 'sl-wukong-engine';
// Set custom position using FEN notation
engine.setBoard('rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1');
// Evaluate position
const score = engine.evaluate();
console.log(`Position score: ${score}`);
// Get board state
const state = engine.getBoardState();
console.log(`Current side: ${state.side === 0 ? 'Red' : 'Black'}`);Draw & Repetition Example
engine.makeMove(engine.moveFromString('b0a0'));
const draw = engine.getDrawStatus();
if (draw.isDraw) console.log('Hòa:', draw.reasons.join(', '));
if (draw.claimable.length) console.log('Có thể xin hòa:', draw.claimable.join(', '));Repetition Rules Example
const move = engine.moveFromString('b0a0');
engine.makeMove(move);
try {
// Perpetual check is checked first, then perpetual chase
engine.detectPerpetualRules();
} catch (error) {
// PerpetualCheckError and PerpetualChaseError are named exports
console.log(error.name, error.message);
}🧪 Tests
npm test # unit tests + FEN audit
npm run audit:fen # FEN audit onlyThe suite in tests/ covers attack/defence detection, what counts as a
chase, full perpetual check / perpetual chase cycles, and the draw rules. Add a
case there for every rule change — each test is a FEN plus a move list, so new
rule scenarios are cheap to express.
scripts/audit-fen.js then checks every FEN used by the
tests against the rules a FEN parser cannot enforce: 9 squares per rank, both
kings present and inside their palace, advisors and elephants only on squares
they can actually reach, no pawn behind its starting rank, the side not to move
not left in check (this covers two facing kings), and getFen() round-tripping
the position. A position that is deliberately impossible must be marked with
fen-audit: illegal-ok, and a deliberately malformed FEN with
fen-audit: skip-start / fen-audit: skip-end.
🎮 API Reference
Board Management
setBoard(fen: string)- Set board position from FEN notation (also resets the game history; passSTART_FENto start a new game)getPiece(square: number)- Piece standing on a squareprintBoard()- Print board to consolegetBoardState()- Get a copy of the complete board stategetSide()/getSixty()/getRepetitions()- Side to move, moves since the last capture, how many times the current position occurred before
Move Generation & Execution
generateMoves(onlyCaptures?: number)- Pseudo-legal moves (0 = all, 1 = captures only)generateLegalMoves()- Legal moves onlymakeMove(move: number)- Make a move (returns 1 if legal, 0 if not)takeBack()- Undo last movemoveFromString(notation: string)/moveToString(move: number)- Convert between"b0a0"notation and the encoded movemoveStack()- Copy of the move historygetMyLastMoveBySide(side: number)- Most recent move played by that sideisSquareAttacked(square: number, side: number)- Check if square is attacked
Search
search(depth: number)- Search the position, returns the best moveperft(depth: number)- Performance test from the current positionsetTimeControl(timeControl)/resetTimeControl()/clearSearch()
Evaluation
evaluate()- Evaluate current position (positive = Red advantage)inCheck(side: number)- Check if side is in check
Draw Rules
getDrawStatus({ maxRepeats?, moveLimit? })- One call for the whole picture:{ isDraw, reasons, claimable }.reasonsare draws that already stand,claimableare the ones that only become a draw when a player asks for it.agreeDraw(agreed = true)/isDrawAgreed()- Draw by agreement. Cleared bysetBoard().isDrawByInsufficientMaterial()- Neither side has a piece able to mate.hasMatingMaterial(side)andgetAttackingMaterial(side)expose the per-side view; advisors and elephants never count as attacking material. Finer endgame judgements (a lone knight or lone cannon that cannot force a win) are left to the application — use the material counts to implement them.isDrawByRepetition(maxRepeats = 3)- The position repeated and neither side broke a repetition prohibition. A side that is perpetually checking or perpetually chasing loses instead, so this returnsfalsethere.canClaimDrawByMoveLimit(moveLimit = 50)/getMovesWithoutCapture()- The 50-move rule, counted per side (100 plies). Claimable, never automatic; the search uses the same threshold to score a line as a draw.
Repetition Rules (perpetual check / perpetual chase)
Call these right after makeMove() — they judge the side that just moved and
throw when it breaks a repetition rule.
detectPerpetualRules(maxRepeats = 3)- Preferred entry point. Runs the perpetual check test first, then the perpetual chase test, because perpetual check is the heavier offence and must be reported as such.detectPerpetualCheck(maxChecks = 3)- ThrowsPerpetualCheckErrorwhen the position has repeatedmaxCheckstimes and every move by that side inside the repetition cycle was a check. Draws (both sides checking) do not throw.detectPerpetualChase(maxChases = 3)- ThrowsPerpetualChaseErrorwhen the position has repeatedmaxChasestimes and every move by that side inside the cycle chased the same piece. Chasing with a king or a pawn, chasing a pawn that has not crossed the river, chasing with a pinned piece and chasing a defended piece when the exchange loses material are all exempt. A side that is checking throughout the cycle is never reported as chasing — that case belongs todetectPerpetualCheck, so calling the two in the wrong order still yields the right error.getChaseTargets(side: number)- Pieces the given side is currently chasing.getLastMoveChases()- Chase records created by the last move.getAttackers(square: number, side: number)- Every piece ofsidethat can legally capture on that square.getRepetitions()- How many times the current position occurred before.setRuleTracking(enabled: boolean)- Turn the per-move rule bookkeeping off (search and perft do this automatically); returns the previous value.
FEN & Notation
getFen()- Get current position as FEN stringSTART_FEN- Starting position FEN constant
Move Encoding
Moves are encoded as 32-bit integers containing:
- Source square
- Target square
- Piece type
- Captured piece (if any)
- Special flags
Use moveToString() to convert to human-readable format.
🎯 Features
- ✅ Full Xiangqi rules implementation
- ✅ Legal move generation
- ✅ Position evaluation
- ✅ FEN notation support
- ✅ Move validation
- ✅ Check detection
- ✅ Perpetual check/chase detection
- ✅ Draw rules: agreement, insufficient material, repetition, 50-move limit
- ✅ TypeScript type definitions
📝 FEN Notation
FEN (Forsyth-Edwards Notation) format for Xiangqi:
rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1
│ │ │ │ │ │ └ fullmove number (ignored)
│ │ │ │ │ └── halfmove clock → 50-move rule
│ │ │ │ └──── en passant placeholder (unused)
│ │ │ └────── castling placeholder (unused)
│ │ └──────── side to move: w = Red, b = Black
└ 10 ranks, rank 9 (Black's back rank) first down to rank 0, files a→i inside each rank- Uppercase = Red pieces (R=Rook, N=Knight, B=Bishop, A=Advisor, K=King, C=Cannon, P=Pawn)
- Lowercase = Black pieces
- Digits = that many empty squares; every rank must add up to 9
- The halfmove clock counts plies since the last capture, so
setBoard()on a mid-game FEN restores the 50-move counter (getMovesWithoutCapture())
setBoard() rejects a FEN that is not 10 ranks of 9 squares, has no side field,
non-numeric counters, unknown piece letters, or does not have exactly one king per
side — a malformed rank used to load silently as a different position.
🔒 License
MIT License - See LICENSE file for details
👨💻 Author
Created with ❤️ for Xiangqi enthusiasts
🐛 Issues
Report issues at: https://github.com/yourusername/sl-wukong-engine/issues
