npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sl-wukong-engine

v1.1.0

Published

Wukong - A powerful TypeScript Xiangqi (Chinese Chess) engine with advanced AI search algorithms

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 only

The 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; pass START_FEN to start a new game)
  • getPiece(square: number) - Piece standing on a square
  • printBoard() - Print board to console
  • getBoardState() - Get a copy of the complete board state
  • getSide() / 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 only
  • makeMove(move: number) - Make a move (returns 1 if legal, 0 if not)
  • takeBack() - Undo last move
  • moveFromString(notation: string) / moveToString(move: number) - Convert between "b0a0" notation and the encoded move
  • moveStack() - Copy of the move history
  • getMyLastMoveBySide(side: number) - Most recent move played by that side
  • isSquareAttacked(square: number, side: number) - Check if square is attacked

Search

  • search(depth: number) - Search the position, returns the best move
  • perft(depth: number) - Performance test from the current position
  • setTimeControl(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 }. reasons are draws that already stand, claimable are the ones that only become a draw when a player asks for it.
  • agreeDraw(agreed = true) / isDrawAgreed() - Draw by agreement. Cleared by setBoard().
  • isDrawByInsufficientMaterial() - Neither side has a piece able to mate. hasMatingMaterial(side) and getAttackingMaterial(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 returns false there.
  • 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) - Throws PerpetualCheckError when the position has repeated maxChecks times and every move by that side inside the repetition cycle was a check. Draws (both sides checking) do not throw.
  • detectPerpetualChase(maxChases = 3) - Throws PerpetualChaseError when the position has repeated maxChases times 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 to detectPerpetualCheck, 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 of side that 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 string
  • START_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