Skip to content

Commit

Permalink
feat(ai): init ai package
Browse files Browse the repository at this point in the history
  • Loading branch information
Neosoulink committed Oct 18, 2024
1 parent 4a7395e commit 716d3c8
Show file tree
Hide file tree
Showing 20 changed files with 462 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .changeset/two-hats-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@chess-d/ai": minor
---

# Logs

- feat(ai): init `ai` package
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"queenside",
"timestep",
"tsyringe",
"turborepo"
"turborepo",
"Zeyu"
],
"editor.formatOnSave": true
}
26 changes: 26 additions & 0 deletions packages/ai/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@chess-d/ai",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "tsc --watch",
"build": "tsc"
},
"main": "./dist/main.js",
"types": "./dist/main.d.ts",
"devDependencies": {
"@chess-d/configs": "workspace:*",
"reflect-metadata": "^0.2.0",
"tsyringe": "^4.8.0",
"typescript": "^5.5.3"
},
"peerDependencies": {
"chess.js": "1.0.0-beta.8",
"@quick-threejs/utils": "^0.1.9"
},
"keywords": [
"ai",
"chess"
]
}
26 changes: 26 additions & 0 deletions packages/ai/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { isObject } from "@quick-threejs/utils";
import { Chess } from "chess.js";
import { container } from "tsyringe";

import { AiModel, SupportedAiModel } from "../shared";
import { ZeyuModule } from "./zeyu/zeyu.module";

/**
* @description
*/
export const register = (
model: SupportedAiModel,
app: Chess
): AiModel | undefined => {
if (!isObject(app))
throw new Error("Unable to retrieve the application context.");

container.register(Chess, {
useValue: app
});

if (model === SupportedAiModel.zeyu)
return container.resolve<AiModel>(ZeyuModule);

return undefined;
};
24 changes: 24 additions & 0 deletions packages/ai/src/core/zeyu/zeyu.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { inject, singleton } from "tsyringe";
import { Color } from "chess.js";

import { AiModel } from "../../shared";
import { ZeyuService } from "./zeyu.service";

@singleton()
export class ZeyuModule implements AiModel {
public sum = 0;
constructor(@inject(ZeyuService) private readonly service: ZeyuService) {}

public getMove(color: Color) {
const [bestMove] = this.service.minimax(
3,
Number.NEGATIVE_INFINITY,
Number.POSITIVE_INFINITY,
true,
this.sum,
color
);

return bestMove;
}
}
197 changes: 197 additions & 0 deletions packages/ai/src/core/zeyu/zeyu.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { inject, singleton } from "tsyringe";
import { Chess, Color, Move } from "chess.js";

import {
PIECES_WEIGHTS,
PST_KE_OPPONENT,
PST_KE_SELF,
PST_OPPONENT,
PST_SELF
} from "../../shared";

@singleton()
export class ZeyuService {
private positionCount = 0;

constructor(@inject(Chess) private readonly game: Chess) {}

/**
* @description
*
* Evaluates the board at this point in time,
* using the material weights and piece square tables.
*/
evaluateBoard = (move: Move, prevSum: number, color: Color) => {
if (this.game.isCheckmate()) {
// Opponent is in checkmate (good for us)
if (move.color === color) return 10 ** 10;

// Our king's in checkmate (bad for us)
return -(10 ** 10);
}

if (
this.game.isDraw() ||
this.game.isThreefoldRepetition() ||
this.game.isStalemate()
)
return 0;

if (this.game.isCheck()) {
// Opponent is in check (good for us)
if (move.color === color) prevSum += 50;
// Our king's in check (bad for us)
else prevSum -= 50;
}

const from = [
8 - parseInt(move.from[1] as string),
move.from.charCodeAt(0) - "a".charCodeAt(0)
] as const;
const to = [
8 - parseInt(move.to[1] as string),
move.to.charCodeAt(0) - "a".charCodeAt(0)
] as const;

let isKingEndgame = false;

// Change endgame behavior for kings
if (prevSum < -1500 && move.piece === "k") isKingEndgame = true;

if (move.captured) {
const weight = PIECES_WEIGHTS[move.captured];
const pstOpponent = isKingEndgame
? PST_KE_OPPONENT[move.color]
: PST_OPPONENT[move.color][move.captured];
const pstSelf = PST_SELF[move.color][move.captured];

// Opponent piece was captured (good for us)
if (move.color === color)
prevSum += weight + (pstOpponent[to[0]]?.[to[1]] ?? 0);
// Our piece was captured (bad for us)
else prevSum -= weight + (pstSelf[to[0]]?.[to[1]] ?? 0);
}

const selfWeight = PIECES_WEIGHTS[move.piece];
const pstSelf = isKingEndgame
? PST_KE_SELF[move.color]
: PST_SELF[move.color][move.piece];

if (move.promotion) {
const promotionWeight = PIECES_WEIGHTS[move.promotion];
const pstPromotion = isKingEndgame
? PST_KE_SELF[move.color]
: PST_SELF[move.color][move.promotion];

// Our piece was promoted (good for us)
if (move.color === color) {
prevSum -= selfWeight + (pstSelf[from[0]]?.[from[1]] ?? 0);
prevSum += promotionWeight + (pstPromotion[to[0]]?.[to[1]] ?? 0);
}
// Opponent piece was promoted (bad for us)
else {
prevSum +=
PIECES_WEIGHTS[move.piece] + (pstSelf[from[0]]?.[from[1]] ?? 0);
prevSum -=
PIECES_WEIGHTS[move.promotion] + (pstPromotion[to[0]]?.[to[1]] ?? 0);
}
} else {
// The moved piece still exists on the updated board, so we only need to update the position value
if (move.color !== color) {
prevSum += pstSelf[from[0]]?.[from[1]] ?? 0;
prevSum -= pstSelf[to[0]]?.[to[1]] ?? 0;
} else {
prevSum -= pstSelf[from[0]]?.[from[1]] ?? 0;
prevSum += pstSelf[to[0]]?.[to[1]] ?? 0;
}
}

return prevSum;
};

/**
* @description
*
* Performs the minimax algorithm to choose the best move: https://en.wikipedia.org/wiki/Minimax (pseudocode provided)
* Recursively explores all possible moves up to a given depth, and evaluates the game board at the leaves.
*
* Basic idea: maximize the minimum value of the position resulting from the opponent's possible following moves.
* Optimization: alpha-beta pruning: https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning (pseudocode provided)
*
* @inputs
* - game: the game object.
* - depth: the depth of the recursive tree of all possible moves (i.e. height limit).
* - isMaximizingPlayer: true if the current layer is maximizing, false otherwise.
* - sum: the sum (evaluation) so far at the current layer.
* - color: the color of the current player.
*
* @output The best move at the root of the current subtree.
*/
minimax(
depth: number,
alpha: number,
beta: number,
isMaximizingPlayer: boolean,
sum: number,
color: Color
): [Move | null, number] {
this.positionCount++;
const children = this.game.moves({ verbose: true });

// Sort moves randomly, so the same move isn't always picked on ties
children.sort(function (a, b) {
return 0.5 - Math.random();
});

// Maximum depth exceeded or node is a terminal node (no children)
if (depth === 0 || children.length === 0) return [null, sum];

// Find maximum/minimum from list of 'children' (possible moves)
let maxValue = Number.NEGATIVE_INFINITY;
let minValue = Number.POSITIVE_INFINITY;
let currMove: Move | undefined | null = null;
let bestMove: Move | undefined | null = null;

for (let i = 0; i < children.length; i++) {
currMove = children[i];

if (currMove) {
// Note: in our case, the 'children' are simply modified game states
const currPrettyMove = this.game.move(currMove);
const newSum = this.evaluateBoard(currPrettyMove, sum, color);
const [_childBestMove, childValue] = this.minimax(
depth - 1,
alpha,
beta,
!isMaximizingPlayer,
newSum,
color
);

this.game.undo();

if (isMaximizingPlayer) {
if (childValue > maxValue) {
maxValue = childValue;
bestMove = currPrettyMove;
}

if (childValue > alpha) alpha = childValue;
} else {
if (childValue < minValue) {
minValue = childValue;
bestMove = currPrettyMove;
}

if (childValue < beta) beta = childValue;
}

// Alpha-beta pruning
if (alpha >= beta) break;
}
}

if (isMaximizingPlayer) return [bestMove, maxValue];
else return [bestMove, minValue];
}
}
4 changes: 4 additions & 0 deletions packages/ai/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import "reflect-metadata";

export * from "./core";
export * from "./shared";
1 change: 1 addition & 0 deletions packages/ai/src/shared/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./piece.constant";
Loading

0 comments on commit 716d3c8

Please sign in to comment.