# 🐵 SL Wukong Engine

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

## 🚀 Installation

```bash
npm install sl-wukong-engine
```

## 📖 Usage

### Basic Example

```javascript
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

```typescript
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

```javascript
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

```javascript
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

```bash
npm test          # unit tests + FEN audit
npm run audit:fen # FEN audit only
```

The suite in [`tests/`](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`](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
