Skip to content

Commit

Permalink
Big fix 4 (#13)
Browse files Browse the repository at this point in the history
- Added iterative deepening and aspiration windows, removed fixed depth searching. Info at each depth will now be logged out as the engine searches.
- Added simple time control: fixed time limit of 1/30 of the remaining time. Catto will now search infinitely if wtime, btime or movetime are not provided.
- Small optimizations which removes duplicated function calls.
- Fixed "position fen" command not working properly with "moves".
  • Loading branch information
nguyenphuminh authored Jul 23, 2024
1 parent cb2e8af commit bc190a0
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 84 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,8 @@ There are several configurations for Catto that you can change in `catto.config.

```js
module.exports = {
// Search depth
searchDepth: 4,
// The starting position represented as a FEN string
fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
// Current version to show in UCI
version: "v0.6.0",
version: "v0.7.0",
// Late move reduction config
lmrFullDepth: 4, // Number of moves to be searched in full depth
lmrMaxReduction: 3, // Only apply LMR above this depth
Expand All @@ -61,6 +57,8 @@ module.exports = {
}
```

Note that the config file is compiled with the engine itself, so if you are using the built version, like `catto.exe`, creating a `catto.config.js` file will have no effect.


## What do we currently have?

Expand All @@ -84,6 +82,10 @@ module.exports = {
* PeSTO evaluation.
* Pawn structure.
* Checkmate and draw detection.
* Iterative deepening.
* Aspiration windows.
* Time control:
* Fixed time per move: 1/30 of the remaining time.
* UCI.


Expand Down
6 changes: 1 addition & 5 deletions catto.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
module.exports = {
// Search depth
searchDepth: 4,
// The starting position represented as a FEN string
fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
// Current version to show in UCI
version: "v0.6.1",
version: "v0.7.0",
// Late move reduction config
lmrFullDepth: 4, // Number of moves to be searched in full depth
lmrMaxReduction: 3, // Only apply LMR above this depth
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "catto",
"version": "0.6.1",
"version": "0.7.0",
"description": "The Catto chess engine",
"main": "index.js",
"scripts": {
Expand Down
92 changes: 56 additions & 36 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export class Engine {
public prevMove?: Move;
public bestMove?: Move;
public uci: boolean = false;

// Used for engine termination
public startTime: number = 0;
public timeout: number = 99999999999;
public stopped: boolean = false;

// Used for killer move heuristic
public killerMove = [ new Array(64).fill(null), new Array(64).fill(null) ];
// Used for counter move heuristic
Expand All @@ -59,8 +65,8 @@ export class Engine {
public pvTable: string[];

constructor(options: EngineOptions) {
this.fen = options.fen;
this.searchDepth = options.searchDepth;
this.fen = options.fen || "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
this.searchDepth = options.searchDepth || 64;
this.chess = new Chess(this.fen);
this.uci = options.uci;
this.lmrMaxReduction = options.lmrMaxReduction;
Expand Down Expand Up @@ -115,12 +121,11 @@ export class Engine {
}

// Move ordering
getMovePrio(move: Move): number {
getMovePrio(move: Move, currentBoardHash: string): number {
let priority = 0;

// Hash Move
// const currentBoardHash = this.chess.fen();
const currentBoardHash = genZobristKey(this.chess).toString();
if (
this.hashTable[currentBoardHash] &&
this.hashTable[currentBoardHash].move &&
Expand Down Expand Up @@ -162,13 +167,10 @@ export class Engine {
}

sortMoves(moves: Move[]) {
const scoredMoves: { move: Move, priority: number }[] = [];

for (const move of moves) {
scoredMoves.push({ move, priority: this.getMovePrio(move) });
}
const currentBoardHash = genZobristKey(this.chess).toString();

return scoredMoves
return moves
.map(move => ({ move, priority: this.getMovePrio(move, currentBoardHash) }))
.sort((moveA, moveB) => moveB.priority - moveA.priority)
.map(scoredMove => scoredMove.move);
}
Expand Down Expand Up @@ -203,6 +205,9 @@ export class Engine {
this.ply--;

this.chess.undo(); // Take back move

// Return 0 if engine is forced to stop
if (this.stopped || Date.now() - this.startTime > this.timeout) return 0;

// fail-hard beta cutoff
if (score >= beta) {
Expand All @@ -221,11 +226,11 @@ export class Engine {
}

// Calculate extensions
calculateExtensions() {
calculateExtensions(moves: number, inCheck: boolean) {
let extensions = 0;

// One reply extension and check extension
if (this.chess.moves().length === 1 || this.chess.inCheck()) {
if (moves === 1 || inCheck) {
extensions = 1;
}

Expand All @@ -234,8 +239,7 @@ export class Engine {

// The main negamax search algorithm
negamax(depth: number, alpha: number, beta: number, extended: number): number {
// Calculate extensions
const extensions = extended < this.maxExtensions ? this.calculateExtensions() : 0;
const inCheck = this.chess.inCheck();

this.nodes++;

Expand All @@ -257,7 +261,7 @@ export class Engine {
if (depth === 0) return this.quiescence(alpha, beta);

// Null move pruning
if (this.ply && depth >= 3 && !this.chess.inCheck()) {
if (this.ply && depth >= 3 && !inCheck) {
// Preserve old moves to reconstruct chess obj
const oldMoves = this.chess.history();

Expand Down Expand Up @@ -287,7 +291,7 @@ export class Engine {

// Detecting checkmates and stalemates
if (possibleMoves.length === 0) {
if (this.chess.inCheck()) {
if (inCheck) {
return -49000 + this.ply; // Checkmate

// Ply is added because:
Expand All @@ -298,6 +302,9 @@ export class Engine {
return 0; // Stalemate
}

// Calculate extensions
const extensions = extended < this.maxExtensions ? this.calculateExtensions(possibleMoves.length, inCheck) : 0;

// Sort moves
possibleMoves = this.sortMoves(possibleMoves);
let searchedMoves = 0, bestMoveSoFar: Move;
Expand Down Expand Up @@ -349,6 +356,9 @@ export class Engine {

searchedMoves++;

// Return 0 if engine is forced to stop
if (this.stopped || Date.now() - this.startTime > this.timeout) return 0;

// Fail-hard beta cutoff
if (score >= beta) {
// Store move in the case of a fail-hard beta cutoff
Expand Down Expand Up @@ -397,25 +407,35 @@ export class Engine {
return alpha;
}

findMove(): {
bestMove: Move | undefined;
time: number;
depth: number;
nodes: number;
pvTable: string[];
evaluation: number;
} {
const start = Date.now();

const evaluation = this.negamax(this.searchDepth, -50000, 50000, 0);

return {
bestMove: this.bestMove,
time: Date.now() - start,
depth: this.searchDepth,
nodes: this.nodes,
pvTable: this.pvTable,
evaluation
};
findMove() {
// Iterative deepening with aspiration windows
this.startTime = Date.now();

let alpha = -50000, beta = 50000, score = 0, currentBestMove = null;

for (let depth = 1; depth <= this.searchDepth; depth++) {
// Stop searching if forced to stop
if (this.stopped || Date.now() - this.startTime > this.timeout) {
break;
}

score = this.negamax(depth, alpha, beta, 0);

if (score <= alpha || score >= beta) {
alpha = -50000;
beta = 50000;
depth--;
continue;
}

alpha = score - 50;
beta = score + 50;

console.log(`info depth ${depth} score cp ${Math.round(score)} time ${Date.now() - this.startTime} nodes ${this.nodes} pv ${this.pvTable.join(" ").trim()}`);

currentBestMove = this.bestMove;
}

console.log(`bestmove ${currentBestMove?.lan}`);
}
}
44 changes: 19 additions & 25 deletions src/evaluate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,79 +25,73 @@ export function evaluateBoard(chessObj: Chess) {

const color = pcolor(board[x][y]!.color);

// Count material
mg[color] += mgMaterial[PIECE_NUM[board[x][y]!.type]];
eg[color] += egMaterial[PIECE_NUM[board[x][y]!.type]];

// Count square value
mg[color] += mgTable[board[x][y]!.color + board[x][y]!.type][x][y];
eg[color] += egTable[board[x][y]!.color + board[x][y]!.type][x][y];
// Count material and square value
mg[color] += mgMaterial[PIECE_NUM[board[x][y]!.type]] + mgTable[board[x][y]!.color + board[x][y]!.type][x][y];
eg[color] += egMaterial[PIECE_NUM[board[x][y]!.type]] + egTable[board[x][y]!.color + board[x][y]!.type][x][y];

// Guess game phase based on material
gamePhase += gamephaseInc[PIECE_NUM[board[x][y]!.type]];

// Count pawns in a file
if (board[x][y]!.type === "p") {
file[pcolor(board[x][y]!.color)][y] += 1;
file[color][y] += 1;
}
}
}

// Pawn structure eval
let pawnDeficit = 0;
let pawnDeficit = 0, us = pcolor(side), enemy = pcolor(side) ^ 1;

for (let index = 0; index < 8; index++) {
// Doubled pawns of us
pawnDeficit -= file[pcolor(side)][index] >= 1 ? (file[pcolor(side)][index] - 1) * 20 : 0;
// Doubled pawns of the opponent
pawnDeficit += file[pcolor(side) ^ 1][index] >= 1 ? (file[pcolor(side) ^ 1][index] - 1) * 20 : 0;

pawnDeficit -= (file[us][index] >= 1 ? (file[us][index] - 1) * 20 : 0) - (file[enemy][index] >= 1 ? (file[enemy][index] - 1) * 20 : 0);

let isolatedPawnScore = 0;

// Isolated pawns of us
if (file[pcolor(side)][index] >= 1) {
if (file[us][index] >= 1) {
if (
// If pawn is from b to g file
(
index > 0 &&
index < 7 &&
file[pcolor(side)][index - 1] === 0 &&
file[pcolor(side)][index + 1] === 0
file[us][index - 1] === 0 &&
file[us][index + 1] === 0
) ||
// If pawn is from a file
(
index === 0 &&
file[pcolor(side)][index + 1] === 0
file[us][index + 1] === 0
) ||
// If pawn is from h file
(
index === 7 &&
file[pcolor(side)][index - 1] === 0
file[us][index - 1] === 0
)
) {
isolatedPawnScore -= 10;
}
}

// Isolated pawns of the opponent
if (file[pcolor(side) ^ 1][index] >= 1) {
if (file[enemy][index] >= 1) {
if (
// If pawn is from b to g file
(
index > 0 &&
index < 7 &&
file[pcolor(side) ^ 1][index - 1] === 0 &&
file[pcolor(side) ^ 1][index + 1] === 0
file[enemy][index - 1] === 0 &&
file[enemy][index + 1] === 0
) ||
// If pawn is from a file
(
index === 0 &&
file[pcolor(side) ^ 1][index + 1] === 0
file[enemy][index + 1] === 0
) ||
// If pawn is from h file
(
index === 7 &&
file[pcolor(side) ^ 1][index - 1] === 0
file[enemy][index - 1] === 0
)
) {
isolatedPawnScore += 10;
Expand All @@ -108,8 +102,8 @@ export function evaluateBoard(chessObj: Chess) {
}

// Tapered eval
let mgScore = mg[pcolor(side)] - mg[pcolor(side) ^ 1];
let egScore = eg[pcolor(side)] - eg[pcolor(side) ^ 1];
let mgScore = mg[us] - mg[enemy];
let egScore = eg[us] - eg[enemy];

let mgPhase = gamePhase;

Expand Down
Loading

0 comments on commit bc190a0

Please sign in to comment.