initial commit

This commit is contained in:
Iain (Bill) Wiseman 2025-04-10 12:40:59 +12:00
commit 8d6befdc1f
69 changed files with 16010 additions and 0 deletions

17
.editorconfig Normal file
View File

@ -0,0 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

59
README.md Normal file
View File

@ -0,0 +1,59 @@
# ChessApp
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.3.
## Development server
To start a local development server, run:
```bash
ng serve
```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the project run:
```bash
ng build
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

101
angular.json Normal file
View File

@ -0,0 +1,101 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"chess-app": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/chess-app",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/custom-theme.scss",
"src/styles.css"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"options": {
"port": 4200,
"host": "192.168.1.70"
},
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "chess-app:build:production"
},
"development": {
"buildTarget": "chess-app:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
],
"scripts": []
}
}
}
}
}
}

13527
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View File

@ -0,0 +1,39 @@
{
"name": "chess-app",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/cdk": "^19.2.4",
"@angular/common": "^19.2.0",
"@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0",
"@angular/forms": "^19.2.0",
"@angular/material": "^19.2.4",
"@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.2.3",
"@angular/cli": "^19.2.3",
"@angular/compiler-cli": "^19.2.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.7.2"
}
}

0
public/assets/.gitkeep Normal file
View File

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="pawn" width="700pt" height="700pt" version="1.0" viewBox="0 0 933.333 933.333"><style id="style-base">.base{fill-opacity:1;fill-rule:evenodd;stroke-miterlimit:4;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1}.stroke-medium{stroke-width:20}.stroke-color{stroke:#000}</style><defs id="defs36222"><linearGradient id="fillGradient"><stop id="stop0" offset="0" style="stop-color:#7f899b;stop-opacity:1"/><stop id="stop1" offset="1" style="stop-color:#1c1c2f;stop-opacity:1"/></linearGradient><linearGradient xlink:href="#fillGradient" id="head-gradient" x1="586.526" x2="1126.022" y1="328.043" y2="534.203" gradientTransform="matrix(.73447 0 0 .736 -84.66 58.436)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#fillGradient" id="shoulders-gradient" x1="275.244" x2="862.652" y1="458.656" y2="560.747" gradientTransform="translate(36 28)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#fillGradient" id="pawn-body-gradient" x1="300.273" x2="774.046" y1="623.782" y2="764.899" gradientTransform="translate(36 28)" gradientUnits="userSpaceOnUse"/></defs><path id="boundary" d="M473.54 222.809a122.412 110.399 0 0 0-122.413 110.398 122.412 110.399 0 0 0 122.412 110.398 122.412 110.399 0 0 0 122.412-110.398A122.412 110.399 0 0 0 473.54 222.809Zm-3.72 223.765c-30.33.309-58.697 5.226-93.466 14.16-74.195 19.066-100.11 66.02-100.11 66.02h156.338c-9.007 144.948-88.584 191.253-127.13 218.502-34.193 24.17-25.465 82.341 19.903 85.31 28.113 1.84 276.615 2.363 305.653-.841 51.38-5.67 52.846-66.7 19.687-87.313-49.346-30.675-125.94-64.992-137.154-215.658H685.09s-43.433-51.835-110.026-66.684c-42.62-9.503-74.913-13.804-105.244-13.496z" class="base stroke-color" style="fill:none;stroke-width:35"/><ellipse id="head" cx="473.539" cy="333.207" class="base stroke-color stroke-medium" rx="122.412" ry="110.399" style="fill:url(#head-gradient)"/><path id="pawn-body" d="M433.417 495.01c1.469 170.175-86.733 221.098-127.967 250.246-34.192 24.17-25.463 82.342 19.906 85.311 28.113 1.84 276.613 2.361 305.651-.843 51.38-5.67 52.846-66.7 19.688-87.311-52.609-32.703-136.191-69.538-138.498-247.09-.441-33.312-79.042-30.009-78.78-.312z" class="base stroke-color stroke-medium" style="fill:url(#pawn-body-gradient)"/><path id="shoulders" d="M376.353 460.735c-74.195 19.066-100.11 66.019-100.11 66.019H685.09s-43.432-51.835-110.025-66.684c-85.24-19.007-129.173-17.204-198.712.665z" class="base stroke-color stroke-medium" style="fill:url(#shoulders-gradient)"/><path id="lower-line" d="M379.121 739.675h187.903" class="base stroke-color stroke-medium" style="fill:none"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="pawn" width="700pt" height="700pt" version="1.0" viewBox="0 0 933.333 933.333"><style id="style-base">.base{fill-opacity:1;fill-rule:evenodd;stroke-miterlimit:4;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1}.stroke-medium{stroke-width:20}.stroke-color{stroke:#000}</style><defs id="defs36222"><linearGradient id="fillGradient"><stop id="stop0" offset="0" style="stop-color:white;stop-opacity:1"/><stop id="stop1" offset="1" style="stop-color:#bfd3d7;stop-opacity:1"/></linearGradient><linearGradient xlink:href="#fillGradient" id="head-gradient" x1="586.526" x2="1126.022" y1="328.043" y2="534.203" gradientTransform="matrix(.73447 0 0 .736 -84.66 58.436)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#fillGradient" id="shoulders-gradient" x1="275.244" x2="862.652" y1="458.656" y2="560.747" gradientTransform="translate(36 28)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#fillGradient" id="pawn-body-gradient" x1="300.273" x2="774.046" y1="623.782" y2="764.899" gradientTransform="translate(36 28)" gradientUnits="userSpaceOnUse"/></defs><path id="boundary" d="M473.54 222.809a122.412 110.399 0 0 0-122.413 110.398 122.412 110.399 0 0 0 122.412 110.398 122.412 110.399 0 0 0 122.412-110.398A122.412 110.399 0 0 0 473.54 222.809Zm-3.72 223.765c-30.33.309-58.697 5.226-93.466 14.16-74.195 19.066-100.11 66.02-100.11 66.02h156.338c-9.007 144.948-88.584 191.253-127.13 218.502-34.193 24.17-25.465 82.341 19.903 85.31 28.113 1.84 276.615 2.363 305.653-.841 51.38-5.67 52.846-66.7 19.687-87.313-49.346-30.675-125.94-64.992-137.154-215.658H685.09s-43.433-51.835-110.026-66.684c-42.62-9.503-74.913-13.804-105.244-13.496z" class="base stroke-color" style="fill:none;stroke-width:35"/><ellipse id="head" cx="473.539" cy="333.207" class="base stroke-color stroke-medium" rx="122.412" ry="110.399" style="fill:url(#head-gradient)"/><path id="pawn-body" d="M433.417 495.01c1.469 170.175-86.733 221.098-127.967 250.246-34.192 24.17-25.463 82.342 19.906 85.311 28.113 1.84 276.613 2.361 305.651-.843 51.38-5.67 52.846-66.7 19.688-87.311-52.609-32.703-136.191-69.538-138.498-247.09-.441-33.312-79.042-30.009-78.78-.312z" class="base stroke-color stroke-medium" style="fill:url(#pawn-body-gradient)"/><path id="shoulders" d="M376.353 460.735c-74.195 19.066-100.11 66.019-100.11 66.019H685.09s-43.432-51.835-110.025-66.684c-85.24-19.007-129.173-17.204-198.712.665z" class="base stroke-color stroke-medium" style="fill:url(#shoulders-gradient)"/><path id="lower-line" d="M379.121 739.675h187.903" class="base stroke-color stroke-medium" style="fill:none"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

View File

@ -0,0 +1 @@
<app-nav-menu></app-nav-menu>

View File

@ -0,0 +1,29 @@
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have the 'chess-app' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('chess-app');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, chess-app');
});
});

13
src/app/app.component.ts Normal file
View File

@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { NavMenuComponent } from "./modules/nav-menu/nav-menu.component";
@Component({
selector: 'app-root',
imports: [NavMenuComponent],
providers: [],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
title = 'chess-app';
}

13
src/app/app.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(),
]
};

8
src/app/app.routes.ts Normal file
View File

@ -0,0 +1,8 @@
import { Routes } from '@angular/router';
import { ChessBoardComponent } from './modules/chess-board/chess-board.component';
import { ComputerModeComponent } from './modules/computer-mode/computer-mode.component';
export const routes: Routes = [
{ path: "against-friend" , component: ChessBoardComponent, title: "Against a friend"},
{ path: "against-computer" , component: ComputerModeComponent, title: "Computer mode"},
];

View File

@ -0,0 +1,90 @@
import { columns } from "../modules/chess-board/models";
import { Color, LastMove } from "./models";
import { King } from "./pieces/king";
import { Pawn } from "./pieces/pawn";
import { Piece } from "./pieces/piece";
import { Rook } from "./pieces/rook";
export class FENConverter {
public static readonly initalPosition: string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
public convertBoardToFEN(
board: (Piece | null)[][],
playerColor: Color,
lastMove: LastMove | undefined,
fiftyMoveRuleCounter: number,
numberOfFullMoves: number
): string {
let FEN: string = "";
for (let i = 7; i >= 0; i--) {
let FENRow: string = "";
let consecutiveEmptySquaresCounter = 0;
for (const piece of board[i]) {
if (!piece) {
consecutiveEmptySquaresCounter++;
continue;
}
if (consecutiveEmptySquaresCounter !== 0)
FENRow += String(consecutiveEmptySquaresCounter);
consecutiveEmptySquaresCounter = 0;
FENRow += piece.FENChar;
}
if (consecutiveEmptySquaresCounter !== 0)
FENRow += String(consecutiveEmptySquaresCounter);
FEN += (i === 0) ? FENRow : FENRow + "/";
}
const player: string = playerColor === Color.White ? "w" : "b";
FEN += " " + player;
FEN += " " + this.castlingAvailability(board);
FEN += " " + this.enPassantPosibility(lastMove, playerColor);
FEN += " " + fiftyMoveRuleCounter * 2;
FEN += " " + numberOfFullMoves;
return FEN;
}
private castlingAvailability(board: (Piece | null)[][]): string {
const castlingPossibilities = (color: Color): string => {
let castlingAvailability: string = "";
const kingPositionX: number = color === Color.White ? 0 : 7;
const king: Piece | null = board[kingPositionX][4];
if (king instanceof King && !king.hasMoved) {
const rookPositionX: number = kingPositionX;
const kingSideRook = board[rookPositionX][7];
const queenSideRook = board[rookPositionX][0];
if (kingSideRook instanceof Rook && !kingSideRook.hasMoved)
castlingAvailability += "k";
if (queenSideRook instanceof Rook && !queenSideRook.hasMoved)
castlingAvailability += "q";
if (color === Color.White)
castlingAvailability = castlingAvailability.toUpperCase();
}
return castlingAvailability;
}
const castlingAvailability: string = castlingPossibilities(Color.White) + castlingPossibilities(Color.Black);
return castlingAvailability !== "" ? castlingAvailability : "-";
}
private enPassantPosibility(lastMove: LastMove | undefined, color: Color): string {
if (!lastMove) return "-";
const { piece, from, to } = lastMove;
if (piece instanceof Pawn && Math.abs(to.x - from.x) === 2) {
const row: number = color === Color.White ? 6 : 3;
return columns[from.y] + String(row);
}
return "-";
}
}

View File

@ -0,0 +1,850 @@
import { columns } from "../modules/chess-board/models";
import { FENConverter } from "./FENConverter";
import { Color, Coords, FENChar, CheckState, LastMove, SafeSquares, MoveType, MoveList, GameHistory } from "./models";
import { Bishop } from "./pieces/bishop";
import { King } from "./pieces/king";
import { Knight } from "./pieces/knight";
import { Pawn } from "./pieces/pawn";
import { Piece } from "./pieces/piece";
import { Queen } from "./pieces/queen";
import { Rook } from "./pieces/rook";
// Implement ChessBoard class
export class ChessBoard {
private static chessBoardSize = 8;
private chessBoard: (Piece | null)[][];
private _playerColor = Color.White;
private _safeSquares: SafeSquares;
private _lastMove: LastMove | undefined
private _checkState: CheckState = { isInChecked: false };
private fiftyMoveRuleCounter: number = 0;
private _isGameOver: boolean = false;
private _gameOverMessage: string | undefined;
private fullNumberOfMoves: number = 1;
private _boardAsFEN: string = FENConverter.initalPosition;
private FENConverter = new FENConverter();
private _moveList: MoveList = [];
private _gameHistory: GameHistory;
// Threefold repetition
private threeFoldRepetitionDictionary = new Map<string, number>();
private threeFoldRepetitionFlag: boolean = false;
constructor() {
this.chessBoard = [
[
new Rook(Color.White),
new Knight(Color.White),
new Bishop(Color.White),
new Queen(Color.White),
new King(Color.White),
new Bishop(Color.White),
new Knight(Color.White),
new Rook(Color.White)
],
[
new Pawn(Color.White),
new Pawn(Color.White),
new Pawn(Color.White),
new Pawn(Color.White),
new Pawn(Color.White),
new Pawn(Color.White),
new Pawn(Color.White),
new Pawn(Color.White)
],
new Array(8).fill(null),
new Array(8).fill(null),
new Array(8).fill(null),
new Array(8).fill(null),
[
new Pawn(Color.Black),
new Pawn(Color.Black),
new Pawn(Color.Black),
new Pawn(Color.Black),
new Pawn(Color.Black),
new Pawn(Color.Black),
new Pawn(Color.Black),
new Pawn(Color.Black)
],
[
new Rook(Color.Black),
new Knight(Color.Black),
new Bishop(Color.Black),
new Queen(Color.Black),
new King(Color.Black),
new Bishop(Color.Black),
new Knight(Color.Black),
new Rook(Color.Black)
],
];
this._safeSquares = this.findSafeSquares();
this._gameHistory = [{
board: this.chessBoardView,
lastMove: this._lastMove,
checkState: this._checkState }];
}
public get playerColor(): Color {
return this._playerColor;
}
public get chessBoardView(): (FENChar | null)[][] {
return this.chessBoard.map(row =>
row.map(piece => piece instanceof Piece ? piece.FENChar : null)
);
}
public static isSquareDark(x: number, y: number): boolean {
return x % 2 === 0 && y % 2 === 0 || x % 2 === 1 && y % 2 === 1;
}
public get safeSquares(): SafeSquares {
return this._safeSquares;
}
public get lastMove(): LastMove | undefined {
return this._lastMove;
}
public get checkState(): CheckState {
return this._checkState;
}
public get isGameOver(): boolean {
return this._isGameOver;
}
public get gameOverMessage(): string | undefined {
return this._gameOverMessage;
}
public get boardAsFEN(): string {
return this._boardAsFEN;
}
public get moveList(): MoveList {
return this._moveList;
}
public get gameHistory(): GameHistory {
return this._gameHistory;
}
private areCoordsValid(coords: { x: number, y: number }): boolean {
return coords.x >= 0 && coords.y >= 0 && coords.x < ChessBoard.chessBoardSize && coords.y < ChessBoard.chessBoardSize;
}
public isInCheck(playerColor: Color, checkingCurrentPosition: boolean): boolean {
// Iterate over the board
for (let row = 0; row < ChessBoard.chessBoardSize; row++) {
for (let col = 0; col < ChessBoard.chessBoardSize; col++) {
// Bail if square not being occupied or the same color
const piece = this.chessBoard[row][col];
if (!piece || piece.color === playerColor) continue;
// Check the piece's moves
for (const direction of piece.directions) {
const coords = { x: row + direction.x, y: col + direction.y };
// Bail if coords are invalid
if (!this.areCoordsValid(coords)) continue;
// Check if piece instance of Pawn, King, or Knight
if (piece instanceof Pawn || piece instanceof King || piece instanceof Knight) {
// Bail if pawn and not diagonal
if (piece instanceof Pawn && direction.y === 0) continue;
const targetPiece = this.chessBoard[coords.x][coords.y];
// If the target piece is a King and not player color return true
if (targetPiece instanceof King && targetPiece.color === playerColor) {
if (checkingCurrentPosition) this._checkState = { isInChecked: true, coords: { x: coords.x, y: coords.y } };
return true;
}
}
else {
// Check if piece is a Bishop, Rook, or Queen
while (this.areCoordsValid(coords)) {
// If the target piece is a King and not player color return true
const targetPiece = this.chessBoard[coords.x][coords.y];
if (targetPiece instanceof King && targetPiece.color === playerColor) {
if (checkingCurrentPosition) this._checkState = { isInChecked: true, coords: { x: coords.x, y: coords.y } };
return true;
}
// If the target piece is not null, break
if (targetPiece !== null) break;
// Increment coords
coords.x += direction.x;
coords.y += direction.y;
}
}
}
}
}
if (checkingCurrentPosition) this._checkState = { isInChecked: false };
return false;
}
private isPositionSafeAfterMove(from: { x: number, y: number }, to: { x: number, y: number }): boolean {
const piece = this.chessBoard[from.x][from.y];
if (!piece) return false;
const newPiece: Piece | null = this.chessBoard[to.x][to.y];
// we cant put piece on a square that already contains piece of the same square
if (newPiece && newPiece.color === piece.color) return false;
// simulate position
this.chessBoard[from.x][from.y] = null;
this.chessBoard[to.x][to.y] = piece;
const isPositionSafe: boolean = !this.isInCheck(piece.color, false);
// restore position back
this.chessBoard[from.x][from.y] = piece;
this.chessBoard[to.x][to.y] = newPiece;
return isPositionSafe;
}
private findSafeSquares(): SafeSquares {
const safeSquares: SafeSquares = new Map<string, Coords[]>();
const debugPiece = undefined;
const debugX = 10;
const debugY = 4;
for (let x = 0; x < ChessBoard.chessBoardSize; x++) {
for (let y = 0; y < ChessBoard.chessBoardSize; y++) {
const piece: Piece | null = this.chessBoard[x][y];
if (x === debugX && y === debugY) console.log('=========> findSafeSquares:Piece Started for coords', x, y, piece)
if (!piece || piece.color !== this._playerColor) {
continue;
}
// console.log('findSafeSquares:Piece Started for coords', x, y, piece)
const pieceSafeSquares: Coords[] = [];
if (x === debugX && y === debugY) console.log('=========> findSafeSquares:Iterating over directions', x, y, piece)
for (const { x: dx, y: dy } of piece.directions) {
let newX: number = x + dx;
let newY: number = y + dy;
if (!this.areCoordsValid({ x: newX, y: newY })) {
if (piece.FENChar === debugPiece) console.log('findSafeSquares:Coords not valid 1', newX, newY)
if (piece.FENChar === debugPiece) console.log('findSafeSquares:Bailing 1', piece)
continue;
}
let newPiece: Piece | null = this.chessBoard[newX][newY];
if (newPiece && newPiece.color === piece.color) {
if (piece.FENChar === debugPiece) console.log('findSafeSquares:Bailing 2', piece)
continue;
}
if (piece.FENChar === debugPiece) console.log('Found a ', piece.FENChar)
// need to restrict pawn moves in certain directions
if (piece instanceof Pawn) {
// cant move pawn two squares straight if there is piece infront of him
if (dx === 2 || dx === -2) {
if (newPiece) {
if (piece.FENChar === debugPiece) console.log('findSafeSquares:Bailing 3', piece)
continue;
}
if (this.chessBoard[newX + (dx === 2 ? -1 : 1)][newY]) {
if (piece.FENChar === debugPiece) console.log('findSafeSquares:Bailing 4', piece)
continue;
}
}
// cant move pawn one square straight if piece is infront of him
if ((dx === 1 || dx === -1) && dy === 0 && newPiece) {
if (piece.FENChar === debugPiece) console.log('findSafeSquares:Bailing 5', piece)
continue;
}
// cant move pawn diagonally if there is no piece, or piece has same color as pawn
if ((dy === 1 || dy === -1) && (!newPiece || piece.color === newPiece.color)) {
if (piece.FENChar === debugPiece) console.log('findSafeSquares:Bailing 6', piece)
continue;
}
}
if (piece instanceof Pawn || piece instanceof Knight || piece instanceof King) {
if (this.isPositionSafeAfterMove({ x, y }, { x: newX, y: newY })) {
pieceSafeSquares.push({ x: newX, y: newY });
if (piece.FENChar === debugPiece) console.log('findSafeSquares:NOT Bailing 7', pieceSafeSquares.length)
if (piece.FENChar === debugPiece) console.log('findSafeSquares:NOT Bailing 7', piece)
}
else {
if (piece.FENChar === debugPiece) console.log('findSafeSquares:Bailing 7', piece)
}
}
else {
while (this.areCoordsValid({ x: newX, y: newY })) {
newPiece = this.chessBoard[newX][newY];
if (newPiece && newPiece.color === piece.color) break;
if (this.isPositionSafeAfterMove({ x, y }, { x: newX, y: newY }))
pieceSafeSquares.push({ x: newX, y: newY });
if (newPiece !== null) {
if (piece.FENChar === debugPiece) console.log('findSafeSquares:Bailing 8', piece)
break;
}
newX += dx;
newY += dy;
}
}
}
if (x === debugX && y === debugY) console.log('=========> findSafeSquares:Completed Iterating over directions', x, y, piece)
// Check if the piece is a King and can castle
if (x === debugX && y === debugY) console.log('=========> findSafeSquares:About to check for king', x, y, piece)
// Handle Castling
if (piece instanceof King) {
console.log('findSafeSquares:Checking for castling x is ', x, y)
if (this.canCastle(piece, true)) {
console.log('findSafeSquares:Pushing a', x, 6)
pieceSafeSquares.push({ x, y: 6 })
};
if (this.canCastle(piece, false)) {
console.log('findSafeSquares:Pushing b', x, 2)
pieceSafeSquares.push({ x, y: 2 });
}
}
// Handling En Passant
if (piece instanceof Pawn) {
if (this.canCaptureEnPassant(piece, { x, y })) {
console.log('findSafeSquares:Pushing a', x - 1, y + 1)
pieceSafeSquares.push({ x: x + (piece.color === Color.White ? 1 : -1), y: this._lastMove!.from.y });
}
}
if (pieceSafeSquares.length) {
if (piece.FENChar === debugPiece) console.log('Setting at ', x, y, pieceSafeSquares)
safeSquares.set(x + "," + y, pieceSafeSquares);
}
if (x === debugX && y === debugY) console.log('=========> findSafeSquares:Piece Finished for coords', x, y, piece)
}
}
return safeSquares;
}
public canCaptureEnPassant(pawn: Pawn, pawnCoords: Coords): boolean {
if (!this._lastMove) return false;
const { piece, from: lastFrom, to: LastTo } = this._lastMove;
if (
!(piece instanceof Pawn) ||
pawn.color !== this._playerColor ||
Math.abs(LastTo.x - lastFrom.x) !== 2 ||
pawnCoords.x !== LastTo.x ||
Math.abs(pawnCoords.y - LastTo.y) !== 1
) return false;
const pawnNewPositionX: number = pawnCoords.x + (pawn.color === Color.White ? 1 : -1);
const pawnNewPositionY: number = LastTo.y;
this.chessBoard[LastTo.x][LastTo.y] = null;
this.chessBoard[LastTo.x][LastTo.y] = piece;
const isPositionSafe: boolean = this.isPositionSafeAfterMove({ x: pawnCoords.x, y: pawnCoords.y }, { x: pawnNewPositionX, y: pawnNewPositionY });
return isPositionSafe;
}
public canCastle(king: King, kingSideCastle: boolean): boolean {
const debugOn = false;
const debugCollor = Color.White;
const debugKingSideCastle = true;
if (debugOn && king.color === debugCollor && kingSideCastle === debugKingSideCastle) console.log('==========>In canCastle', king, kingSideCastle)
if (king.hasMoved) {
if (debugOn && king.color === debugCollor && kingSideCastle === debugKingSideCastle) console.log('canCastle: Bailing king has moved')
return false;
}
// Set the king position based on color
const kingPositionX: number = king.color === Color.White ? 0 : 7;
const kingPositionY: number = 4;
// Set the rook position based on color
const rookPositionX: number = kingPositionX;
const rookPositionY: number = kingSideCastle ? 7 : 0;
// Bail if Piece is not a rook, or it has moved, or the player is in check
const rook: Piece | null = this.chessBoard[rookPositionX][rookPositionY];
if (!(rook instanceof Rook) || rook.hasMoved || this._checkState.isInChecked) {
if (debugOn && king.color === debugCollor && kingSideCastle === debugKingSideCastle) console.log('canCastle: Bailing. Not Rook or has Moved, or is in check Bailing 1', rook)
return false;
}
// Calculate the next king position
const firstNextKingPositionY: number = kingPositionY + (kingSideCastle ? 1 : -1);
const secondNextKingPositionY: number = kingPositionY + (kingSideCastle ? 2 : -2);
if (this.chessBoard[kingPositionX][firstNextKingPositionY] || this.chessBoard[kingPositionX][secondNextKingPositionY]) {
if (debugOn && king.color === debugCollor && kingSideCastle === debugKingSideCastle) console.log('canCastle: Bailing 2a', this.chessBoard[kingPositionX][firstNextKingPositionY])
if (debugOn && king.color === debugCollor && kingSideCastle === debugKingSideCastle) console.log('canCastle: Bailing 2b', this.chessBoard[kingPositionX][secondNextKingPositionY])
if (debugOn && king.color === debugCollor && kingSideCastle === debugKingSideCastle) console.log('canCastle: Bailing 2c', kingPositionX, firstNextKingPositionY)
if (debugOn && king.color === debugCollor && kingSideCastle === debugKingSideCastle) console.log('canCastle: Bailing 2d', kingPositionX, secondNextKingPositionY)
if (debugOn && king.color === debugCollor && kingSideCastle === debugKingSideCastle) console.log('canCastle: Bailing 2')
return false;
}
if (!kingSideCastle && this.chessBoard[kingPositionX][1]) {
if (debugOn && king.color === debugCollor) console.log('canCastle: Bailing 3')
return false;
}
const isPositionSafeA = this.isPositionSafeAfterMove({ x: kingPositionX, y: kingPositionY }, { x: kingPositionX, y: firstNextKingPositionY })
const isPositionSafeB = this.isPositionSafeAfterMove({ x: kingPositionX, y: kingPositionY }, { x: kingPositionX, y: secondNextKingPositionY });
if (debugOn && king.color === debugCollor && kingSideCastle === debugKingSideCastle) console.log('canCastle: isPositionSafeA', isPositionSafeA)
if (debugOn && king.color === debugCollor && kingSideCastle === debugKingSideCastle) console.log('canCastle: isPositionSafeB', isPositionSafeB)
return isPositionSafeA && isPositionSafeB;
}
public move(from: Coords, to: Coords, promotedPieceType: FENChar | null): void {
// Doing Move
console.log('In move', from, to, promotedPieceType)
// Check if the game is over
if (this._isGameOver) {
console.log('Game is over so bailing')
throw new Error('Game is over');
}
// Check if coords are valid
if (!this.areCoordsValid(from) || !this.areCoordsValid(to)) {
console.log('Coords invalid so bailing')
return;
}
// Check if the piece is valid
const piece = this.chessBoard[from.x][from.y];
if (!piece) {
console.log('Piece not valid so bailing')
return;
}
const piecesSafeSquares: Coords[] | undefined = this._safeSquares.get(from.x + "," + from.y);
if (!piecesSafeSquares || !piecesSafeSquares.some(coords => coords.x === to.x && coords.y === to.y)) {
console.log('Square is not safe to bailing')
throw new Error('Square is not safe');
}
// If a Pawn or King or Rook update hasMoved
if (piece instanceof Pawn || piece instanceof King || piece instanceof Rook) piece.hasMoved = true;
const moveType = new Set<MoveType>();
// Track if a piece is taken
const isPieceTaken = this.chessBoard[to.x][to.y] !== null;
if (isPieceTaken) moveType.add(MoveType.Capture);
// Increment the fifty move rule counter
if (piece instanceof Pawn || isPieceTaken) this.fiftyMoveRuleCounter = 0;
else this.fiftyMoveRuleCounter += 0.5;
// If castling, move the rook
this.handlingSpecialMoves(piece, from, to, moveType);
// Check for promotion
if (promotedPieceType) {
const promotedPiece = this.promotedPiece(promotedPieceType);
this.chessBoard[to.x][to.y] = promotedPiece;
this.chessBoard[from.x][from.y] = null;
moveType.add(MoveType.Promotion);
}
else {
// Update the board
this.chessBoard[to.x][to.y] = piece;
this.chessBoard[from.x][from.y] = null;
}
// Update the last move
this._lastMove = { piece, from, to, moveType };
// Swap the player color
console.log('=============================================', this.playerColor)
console.log('Swapping player color which is currently', this._playerColor)
console.log('=============================================', this.playerColor)
this._playerColor = this._playerColor === Color.White ? Color.Black : Color.White;
// Get the safe squares and store them
const safeSquares = this.findSafeSquares();
// Check if the player is in check
this.isInCheck(this._playerColor, true);
// Track the move type
if (this._checkState.isInChecked) {
moveType.add(!(safeSquares.size === 0) ? MoveType.CheckMate : MoveType.Check);
}
else if (!moveType.size) {
moveType.add(MoveType.BasicMove);
}
// Store the move
this.storeMove(promotedPieceType);
this.updateGameHistory();
// We need to assign the safe squares after storing the move
this._safeSquares = safeSquares;
// Increment the full move counter
if (this._playerColor === Color.White) this.fullNumberOfMoves++;
// Update the board as FEN
this._boardAsFEN = this.FENConverter.convertBoardToFEN(this.chessBoard, this._playerColor, this._lastMove, this.fiftyMoveRuleCounter, this.fullNumberOfMoves);
this.updateThreeFoldRepetitionDictionary(this._boardAsFEN);
// Check if the game is finished
this._isGameOver = this.isGameFinished();
}
private handlingSpecialMoves(piece: Piece, from: Coords, to: Coords, moveTypes: Set<MoveType>): void {
console.log('In handlingSpecialMoves', piece, from, to)
// Handle Castling
if (piece instanceof King && Math.abs(from.y - to.y) === 2) {
console.log('Handling Castling')
// Calculate rook from and to coords based on the direction of the move
const rookFrom: Coords = { x: from.x, y: to.y === 6 ? 7 : 0 };
const rookTo: Coords = { x: from.x, y: to.y === 6 ? 5 : 3 };
// Check we have a rook
const rook = this.chessBoard[rookFrom.x][rookFrom.y];
if (!(rook instanceof Rook)) return;
// Flag the rook as moved
rook.hasMoved = true;
// Move the rook
this.chessBoard[rookTo.x][rookTo.y] = rook;
this.chessBoard[rookFrom.x][rookFrom.y] = null;
// Track the move type
moveTypes.add(MoveType.Castling);
}
// Handle En Passant
if (piece instanceof Pawn && this._lastMove) {
const { piece: lastPiece, from: lastFrom, to: lastTo } = this._lastMove;
if (
lastPiece instanceof Pawn && // Last piece was a pawn
Math.abs(lastFrom.x - lastTo.x) === 2 && // Last move was a two square move
lastTo.y === to.y && // Last move was in the same column
lastTo.x === from.x // Last move was next to the current pawn
) {
this.chessBoard[lastTo.x][lastTo.y] = null;
moveTypes.add(MoveType.Capture);
}
}
}
public promotedPiece(promotedPieceType: FENChar): Rook | Knight | Bishop | Queen {
switch (promotedPieceType) {
case FENChar.WhiteRook:
case FENChar.BlackRook:
return new Rook(this._playerColor);
case FENChar.WhiteKnight:
case FENChar.BlackKnight:
return new Knight(this._playerColor);
case FENChar.WhiteBishop:
case FENChar.BlackBishop:
return new Bishop(this._playerColor);
case FENChar.WhiteQueen:
case FENChar.BlackQueen:
return new Queen(this._playerColor);
default:
throw new Error('Invalid piece type');
}
}
private isGameFinished(): boolean {
if (this.insufficientMaterial()) {
this._gameOverMessage = "Draw due insufficient material";
return true;
}
if (this._safeSquares.size === 0) {
if (this._checkState.isInChecked) {
const prevPlayer: string = this._playerColor === Color.White ? 'Black' : 'White';
this._gameOverMessage = prevPlayer + ' wins by checkmate';
}
else {
this._gameOverMessage = 'Draw by stalemate';
}
return true;
}
if (this.threeFoldRepetitionFlag) {
this._gameOverMessage = "Draw due three fold repetition rule";
return true;
}
if (this.fiftyMoveRuleCounter === 50) {
this._gameOverMessage = "Draw due fifty move rule";
return true;
}
return false;
}
private playerHasOnlyTwoKnightsAndKing(pieces: { piece: Piece, x: number, y: number }[]): boolean {
return pieces.filter(piece => piece.piece instanceof Knight).length === 2;
}
private playerHasOnlyBishopsWithSameColorAndKing(pieces: { piece: Piece, x: number, y: number }[]): boolean {
const bishops = pieces.filter(piece => piece.piece instanceof Bishop);
const areAllBishopsOfSameColor = new Set(bishops.map(bishop => ChessBoard.isSquareDark(bishop.x, bishop.y))).size === 1;
return bishops.length === pieces.length - 1 && areAllBishopsOfSameColor;
}
private insufficientMaterial(): boolean {
const whitePieces: { piece: Piece, x: number, y: number }[] = [];
const blackPieces: { piece: Piece, x: number, y: number }[] = [];
for (let x = 0; x < ChessBoard.chessBoardSize; x++) {
for (let y = 0; y < ChessBoard.chessBoardSize; y++) {
const piece: Piece | null = this.chessBoard[x][y];
if (!piece) continue;
if (piece.color === Color.White) whitePieces.push({ piece, x, y });
else blackPieces.push({ piece, x, y });
}
}
// King vs King
if (whitePieces.length === 1 && blackPieces.length === 1)
return true;
// King and Minor Piece vs King
if (whitePieces.length === 1 && blackPieces.length === 2)
return blackPieces.some(piece => piece.piece instanceof Knight || piece.piece instanceof Bishop);
else if (whitePieces.length === 2 && blackPieces.length === 1)
return whitePieces.some(piece => piece.piece instanceof Knight || piece.piece instanceof Bishop);
// both sides have bishop of same color
else if (whitePieces.length === 2 && blackPieces.length === 2) {
const whiteBishop = whitePieces.find(piece => piece.piece instanceof Bishop);
const blackBishop = blackPieces.find(piece => piece.piece instanceof Bishop);
if (whiteBishop && blackBishop) {
const areBishopsOfSameColor: boolean = ChessBoard.isSquareDark(whiteBishop.x, whiteBishop.y) && ChessBoard.isSquareDark(blackBishop.x, blackBishop.y) || !ChessBoard.isSquareDark(whiteBishop.x, whiteBishop.y) && !ChessBoard.isSquareDark(blackBishop.x, blackBishop.y);
return areBishopsOfSameColor;
}
}
if (whitePieces.length === 3 && blackPieces.length === 1 && this.playerHasOnlyTwoKnightsAndKing(whitePieces) ||
whitePieces.length === 1 && blackPieces.length === 3 && this.playerHasOnlyTwoKnightsAndKing(blackPieces)
) return true;
if (whitePieces.length >= 3 && blackPieces.length === 1 && this.playerHasOnlyBishopsWithSameColorAndKing(whitePieces) ||
whitePieces.length === 1 && blackPieces.length >= 3 && this.playerHasOnlyBishopsWithSameColorAndKing(blackPieces)
) return true;
return false;
}
private updateThreeFoldRepetitionDictionary(FEN: string): void {
const threeFoldRepetitionFENKey: string = FEN.split(" ").slice(0, 4).join("");
const threeFoldRepetionValue: number | undefined = this.threeFoldRepetitionDictionary.get(threeFoldRepetitionFENKey);
if (threeFoldRepetionValue === undefined)
this.threeFoldRepetitionDictionary.set(threeFoldRepetitionFENKey, 1);
else {
if (threeFoldRepetionValue === 2) {
this.threeFoldRepetitionFlag = true;
return;
}
this.threeFoldRepetitionDictionary.set(threeFoldRepetitionFENKey, 2);
}
}
private startingPieceCoordsNotation(): string {
const { piece: currPiece, from, to } = this._lastMove!;
console.log('In startingPieceCoordsNotation', currPiece, from, to)
// For King we only have one king. For Pawn this will be handled later
if (currPiece instanceof Pawn || currPiece instanceof King) {
console.log('Returning empty string 1')
return "";
}
// For other pieces we need to get coordinates of all pieces of the same type that made the move
const samePiecesCoords: Coords[] = [{ x: from.x, y: from.y }];
// Iterate over the board to find the piece with the same type that made the move
for (let x = 0; x < ChessBoard.chessBoardSize; x++) {
for (let y = 0; y < ChessBoard.chessBoardSize; y++) {
const piece: Piece | null = this.chessBoard[x][y];
// Continue if the piece is null or the same piece
if (!piece || (to.x === x && to.x === y)) {
console.log('Continuing')
continue;
}
if (piece.FENChar === currPiece.FENChar) {
const safeSquares: Coords[] = this._safeSquares.get(x + "," + y) || [];
const pieceHasSameTargetSquare: boolean = safeSquares.some(coords => coords.x === to.x && coords.y === to.y);
if (pieceHasSameTargetSquare) {
samePiecesCoords.push({ x, y });
}
}
}
}
if (samePiecesCoords.length === 1) {
console.log('Returning empty string 2')
return "";
}
const piecesFile = new Set(samePiecesCoords.map(coords => coords.y));
const piecesRank = new Set(samePiecesCoords.map(coords => coords.x));
// means that all of the pieces are on different files (a, b, c, ...)
if (piecesFile.size === samePiecesCoords.length) {
const result = columns[from.y];
console.log('Returning result', result)
return result
}
// means that all of the pieces are on different rank (1, 2, 3, ...)
if (piecesRank.size === samePiecesCoords.length) {
const result = String(from.x + 1);
console.log('Returning result', result)
return result;
}
// in case that there are pieces that shares both rank and a file with multiple or one piece
const result =columns[from.y] + String(from.x + 1);
console.log('Returning result', result)
return result;
}
private updateGameHistory(): void {
this._gameHistory.push({
board: [...this.chessBoardView.map(row => [...row])],
checkState: { ...this._checkState },
lastMove: this._lastMove ? { ...this._lastMove } : undefined
});
}
private storeMove(promotedPiece: FENChar | null, longNotation=false): void {
// Destructure the last move
const { piece, from, to, moveType } = this._lastMove!;
// Set name of the piece
let pieceName: string = !(piece instanceof Pawn) ? piece.FENChar.toUpperCase() : "";
let move: string;
// Check if the move is castling
if (moveType.has(MoveType.Castling)) {
// Check if the move is kingside or queenside
move = to.y - from.y === 2 ? "O-O" : "O-O-O";
console.log('Assign move 0', move)
}
else {
move = longNotation ?
pieceName + columns[from.y] + String(from.x + 1) :
pieceName + this.startingPieceCoordsNotation();
console.log('Assign move 1', move)
// If capturing, add an x
if (moveType.has(MoveType.Capture)) {
// Long notation
if(longNotation) {
move += "x";
console.log('Assign move 2', move)
}
// Short notation
else {
// Check if the piece is a pawn add y coordinate of the piece and x
if(piece instanceof Pawn) {
move += columns[from.y] + "x";
console.log('Assign move 2.1', move)
}
else {
move += "x";
console.log('Assign move 2.2', move)
}
}
}
// Add the target square to move
move += columns[to.y] + String(to.x + 1);
console.log('Assign move 3', move)
// Check if the move is a promotion and add the promoted piece
if (promotedPiece) {
move += "=" + promotedPiece.toUpperCase();
console.log('Assign move 4', move)
}
}
// Check if the move is a check append a +,
if (moveType.has(MoveType.Check)) {
move += "+";
console.log('Assign move 5', move)
}
// if checkmate append a #
else if (moveType.has(MoveType.CheckMate)) {
move += "#";
console.log('Assign move 6',move)
}
console.log('Move is', move)
console.log('fullNumberOfMoves',this.fullNumberOfMoves - 1)
if (!this._moveList[this.fullNumberOfMoves - 1])
this._moveList[this.fullNumberOfMoves - 1] = [move];
else
this._moveList[this.fullNumberOfMoves - 1].push(move);
}
}

View File

@ -0,0 +1,90 @@
import { last } from "rxjs";
import { Piece } from "./pieces/piece";
export enum Color {
White,
Black
}
export type Coords = {
x: number;
y: number;
}
export enum FENChar {
WhitePawn = "P",
WhiteKnight = "N",
WhiteBishop = "B",
WhiteRook = "R",
WhiteQueen = "Q",
WhiteKing = "K",
BlackPawn = "p",
BlackKnight = "n",
BlackBishop = "b",
BlackRook = "r",
BlackQueen = "q",
BlackKing = "k"
}
export const pieceImagePaths: Readonly<Record<FENChar, string>> = {
[FENChar.WhitePawn]: "assets/pieces/white pawn.svg",
[FENChar.WhiteKnight]: 'assets/pieces/white knight.svg',
[FENChar.WhiteBishop]: "assets/pieces/white bishop.svg",
[FENChar.WhiteRook]: "assets/pieces/white rook.svg",
[FENChar.WhiteQueen]: "assets/pieces/white queen.svg",
[FENChar.WhiteKing]: "assets/pieces/white king.svg",
[FENChar.BlackPawn]: "assets/pieces/black pawn.svg",
[FENChar.BlackKnight]: "assets/pieces/black knight.svg",
[FENChar.BlackBishop]: "assets/pieces/black bishop.svg",
[FENChar.BlackRook]: "assets/pieces/black rook.svg",
[FENChar.BlackQueen]: "assets/pieces/black queen.svg",
[FENChar.BlackKing]: "assets/pieces/black king.svg"
}
export type SafeSquares = Map<string, Coords[]>;
type SquareWithPiece = {
piece: FENChar;
coords: Coords;
}
type SquareWithoutPiece = {
piece: null;
}
export type SelectedSquare = SquareWithPiece | SquareWithoutPiece;
export enum MoveType {
Capture,
Castling,
Promotion,
Check,
CheckMate,
BasicMove
}
export type LastMove = {
piece: Piece;
from: Coords;
to: Coords;
moveType: Set<MoveType>;
}
type KingChecked = {
isInChecked: true;
coords: Coords;
}
type KingNotChecked = {
isInChecked: false;
}
export type CheckState = KingChecked | KingNotChecked;
export type MoveList = ([string, string?])[];
export type GameHistory = {
lastMove: LastMove | undefined;
checkState: CheckState;
board: (FENChar | null)[][];
}[];

View File

@ -0,0 +1,18 @@
import { Color, Coords, FENChar } from "../models";
import { Piece } from "./piece";
export class Bishop extends Piece {
protected override _FENChar: FENChar;
protected _directions: Coords[] = [
{ x: 1, y: 1 },
{ x: 1, y: -1 },
{ x: -1, y: 1 },
{ x: -1, y: -1 }
];
constructor(private pieceColor: Color) {
super(pieceColor);
this._FENChar = pieceColor === Color.White ? FENChar.WhiteBishop : FENChar.BlackBishop;
}
}

View File

@ -0,0 +1,31 @@
import { FENChar, Coords, Color } from "../models";
import { Piece } from "./piece";
export class King extends Piece {
private _hasMoved = false;
protected override _FENChar: FENChar;
protected _directions: Coords[] = [
{ x: 1, y: 1 },
{ x: 1, y: -1 },
{ x: -1, y: 1 },
{ x: -1, y: -1 },
{ x: 1, y: 0 },
{ x: -1, y: 0 },
{ x: 0, y: 1 },
{ x: 0, y: -1 }
];
constructor(private pieceColor: Color) {
super(pieceColor);
this._FENChar = pieceColor === Color.White ? FENChar.WhiteKing : FENChar.BlackKing;
}
public get hasMoved(): boolean {
return this._hasMoved;
}
public set hasMoved(_) {
this._hasMoved = true;
}
}

View File

@ -0,0 +1,22 @@
import { Color, Coords, FENChar } from "../models";
import { Piece } from "./piece";
// Implement knight piece
export class Knight extends Piece {
protected override _FENChar: FENChar;
protected _directions: Coords[] = [
{ x: 1, y: 2 },
{ x: 1, y: -2 },
{ x: -1, y: 2 },
{ x: -1, y: -2 },
{ x: 2, y: 1 },
{ x: 2, y: -1 },
{ x: -2, y: 1 },
{ x: -2, y: -1 }
];
constructor(private pieceColor: Color) {
super(pieceColor);
this._FENChar = pieceColor === Color.White ? FENChar.WhiteKnight : FENChar.BlackKnight;
}
}

View File

@ -0,0 +1,38 @@
import { FENChar, Coords, Color } from "../models";
import { Piece } from "./piece";
export class Pawn extends Piece {
private _hasMoved = false;
protected override _FENChar: FENChar;
protected _directions: Coords[] = [
{ x: 1, y: 0 },
{ x: 2, y: 0 },
{ x: 1, y: 1 },
{ x: 1, y: -1 }
];
constructor(private pieceColor: Color) {
super(pieceColor);
if (pieceColor === Color.Black) this.setBlackPawnDirection();
this._FENChar = pieceColor === Color.White ? FENChar.WhitePawn : FENChar.BlackPawn;
}
private setBlackPawnDirection(): void {
this._directions = this._directions.map(({x, y}) => ({ x: -1 * x, y }));
}
public get hasMoved(): boolean {
return this._hasMoved;
}
public set hasMoved(_) {
this._hasMoved = true;
this._directions = [
{x: 1, y: 0},
{x: 1, y: 1},
{x: 1, y: -1}
]
if(this.pieceColor === Color.Black) this.setBlackPawnDirection();
}
}

View File

@ -0,0 +1,20 @@
import { Color, Coords, FENChar } from "../models";
export abstract class Piece {
protected abstract _FENChar: FENChar;
protected abstract _directions: Coords[];
constructor(private _color: Color) {}
public get color(): Color {
return this._color;
}
public get FENChar(): FENChar {
return this._FENChar;
}
public get directions(): Coords[] {
return this._directions;
}
}

View File

@ -0,0 +1,21 @@
import { Piece } from './piece';
import { Color, Coords, FENChar } from '../models';
export class Queen extends Piece {
protected override _FENChar: FENChar;
protected _directions: Coords[] = [
{ x: 1, y: 1 },
{ x: 1, y: -1 },
{ x: -1, y: 1 },
{ x: -1, y: -1 },
{ x: 1, y: 0 },
{ x: -1, y: 0 },
{ x: 0, y: 1 },
{ x: 0, y: -1 }
];
constructor(private pieceColor: Color) {
super(pieceColor);
this._FENChar = pieceColor === Color.White ? FENChar.WhiteQueen : FENChar.BlackQueen;
}
}

View File

@ -0,0 +1,28 @@
import { FENChar, Coords, Color } from "../models";
import { Piece } from "./piece";
export class Rook extends Piece {
private _hasMoved = false;
protected override _FENChar: FENChar;
protected _directions: Coords[] = [
{ x: 1, y: 0 },
{ x: -1, y: 0 },
{ x: 0, y: 1 },
{ x: 0, y: -1 }
];
constructor(private pieceColor: Color) {
super(pieceColor);
this._FENChar = pieceColor === Color.White ? FENChar.WhiteRook : FENChar.BlackRook;
}
public get hasMoved(): boolean {
return this._hasMoved;
}
public set hasMoved(_) {
this._hasMoved = true;
}
}

View File

@ -0,0 +1,88 @@
.chess-board {
display: flex;
justify-content: center;
flex-direction: column-reverse;
width: 480px;
height: 480px;
}
.rotated {
transform: rotate(180deg);
}
.row {
display: flex;
flex-direction: row;
}
.square {
display: flex;
justify-content: center;
align-items: center;
height: 60px;
width: 60px;
cursor: pointer;
border: 1px solid white;
background-color: white;
}
.game-over-message {
color: white;
}
.promotion-dialog {
display: flex;
justify-content: center;
align-items: center;
width: 300px;
height: 100px;
}
.promotion-dialog img {
height: 70px;
cursor: pointer;
}
.close-promotion-dialog {
font-size: 45px;
cursor: pointer;
color: white;
}
.dark {
background-color: #779AAF
}
.light {
background-color: #D9E4E8;
}
.piece {
width: 50px;
}
.selected-square {
box-shadow: inset rgba(60, 70, 85, 0.5) 0px 0px 40px 0px, inset rgba(60, 70, 85, 0.5) 0px 0px 40px 0px, inset rgba(0, 0, 0, 1) 0px 0px 36px -24px;
}
.safe-square {
position: absolute;
height: 20px;
width: 20px;
background-color: #bbb;
border-radius: 50%;
z-index: 100;
}
.king-in-check {
box-shadow: inset rgb(179, 21, 0) 0px 0px 40px 0px, inset rgb(163, 11, 0) 0px 0px 40px 0px, inset rgba(0, 0, 0, 1) 0px 0px 36px -24px;
}
.last-move {
box-shadow: inset rgb(6, 179, 0) 0px 0px 40px 0px, inset rgb(6, 179, 0)0px 0px 40px 0px, inset rgba(0, 0, 0, 1) 0px 0px 36px -24px;
}
.promotion-square {
box-shadow: inset rgb(0, 98, 150) 0px 0px 40px 0px, inset rgb(0, 98, 150) 0px 0px 40px 0px, inset rgba(0, 0, 0, 1) 0px 0px 36px -24px;
}

View File

@ -0,0 +1,42 @@
<div class="chess-board" [ngClass]="{'rotated': flipMode}">
<div *ngFor="let row of chessBoardView; let x = index" class="row">
<div *ngFor="let piece of row; let y = index" class="square" [ngClass]="{
'dark': isSquareDark(x, y),
'light': !isSquareDark(x, y),
'selected': isSquareSelected(x, y),
'last-move': isSquareLastMove(x, y),
'king-in-check': isSquareChecked(x, y),
'promotion-square': isSquarePromotionSquare(x, y)
}
" (click)=" move(x, y)">
<div [ngClass]="{'safe-square': isSquareSafeForSelectedPiece(x,y)}"></div>
<img *ngIf="piece" [src]="pieceImagePaths[piece]" alt="piece" class="piece"
[ngClass]="{'rotated': flipMode}" />
</div>
</div>
</div>
<button (click)="flipBoard()">Flip</button>
<h2 *ngIf="gameOverMessage" class="game-over-message">
{{gameOverMessage}}
</h2>
<div *ngIf="isPromotionModalVisible" class="promotion-dialog">
<img *ngFor="let piece of promotionalPieces()" [src]="pieceImagePaths[piece]" (click)="promotePiece(piece)">
<span class="close-promotion-dialog" (click)="closePromotionDialog()">
&times;
</span>
</div>
<app-move-list
[moveList]="moveList"
[gameHistoryPointer]="gameHistoryPointer"
[gameHistoryLength]="gameHistory.length"
(showPreviousPositionEvent)="showPreviousPosition($event)">
</app-move-list>

View File

@ -0,0 +1,263 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ChessBoard } from '../../chess-logic/chess-board';
import { CheckState, Color, Coords, FENChar, GameHistory, LastMove, MoveList, MoveType, pieceImagePaths, SafeSquares, SelectedSquare } from '../../chess-logic/models';
import { CommonModule, NgClass, NgFor, NgIf } from '@angular/common';
import { ChessBoardService } from './chess-board.service';
import { MoveListComponent } from '../move-list/move-list.component';
import { filter, from, fromEvent, Subscription, tap } from 'rxjs';
@Component({
selector: 'app-chess-board',
standalone: true,
imports: [MoveListComponent, CommonModule],
templateUrl: './chess-board.component.html',
styleUrl: './chess-board.component.css'
})
export class ChessBoardComponent implements OnInit, OnDestroy {
public pieceImagePaths = pieceImagePaths
// The chess board is a private member of the component
protected chessBoard = new ChessBoard();
// The chess board view is a public member of the component
public chessBoardView : (FENChar|null)[][] = this.chessBoard.chessBoardView;
// The player color is a public member of the component
public get playerColor(): Color { return this.chessBoard.playerColor; };
// The safe squares is a public member
public get safeSquares(): SafeSquares { return this.chessBoard.safeSquares; }
// The game over message is a public member
public get gameOverMessage(): string | undefined { return this.chessBoard.gameOverMessage; }
private selectedSquare: SelectedSquare = {piece: null};
private pieceSafeSquares: Coords[] = [];
private lastMove:LastMove| undefined = this.chessBoard.lastMove;
private checkState: CheckState = this.chessBoard.checkState;
public get moveList(): MoveList { return this.chessBoard.moveList; }
public get gameHistory(): GameHistory { return this.chessBoard.gameHistory; };
public gameHistoryPointer: number = 0;
// Promotion properties
public isPromotionModalVisible: boolean = false;
private promotionCoords: Coords | null = null;
private promotedPiece: FENChar | null = null;
public promotionalPieces(): FENChar[] {
return (this.playerColor === Color.White ?
[FENChar.WhiteQueen, FENChar.WhiteRook, FENChar.WhiteBishop, FENChar.WhiteKnight] :
[FENChar.BlackQueen, FENChar.BlackRook, FENChar.BlackBishop, FENChar.BlackKnight]);
}
public flipMode = false;
private subscriptions$ = new Subscription();
constructor(protected chessBoardService: ChessBoardService) { }
public flipBoard(): void {
this.flipMode = !this.flipMode;
this.chessBoardService.chessBoardState$.next(this.chessBoard.boardAsFEN);
console.log('flipMode', this.flipMode);
}
public isSquareDark(row: number, col: number): boolean {
return ChessBoard.isSquareDark(row, col);
}
public isSquareSelected(row: number, col: number): boolean {
if(!this.selectedSquare.piece) return false
return this.selectedSquare.coords.x === row && this.selectedSquare.coords.y === col;
}
public isSquareSafeForSelectedPiece(row: number, col: number): boolean {
return this.pieceSafeSquares.some(coords => coords.x === row && coords.y === col);
}
public isSquareLastMove(row: number, col: number): boolean {
if (!this.lastMove) return false;
return this.lastMove.to.x === row && this.lastMove.to.y === col ||
this.lastMove.from.x === row && this.lastMove.from.y === col;
}
public isSquareChecked(row: number, col: number): boolean {
const result = this.checkState.isInChecked && this.checkState.coords.x === row && this.checkState.coords.y === col;
if(this.checkState.isInChecked && result) {
console.log('---------------------------------')
console.log('incoming row', row);
console.log('incoming col', col);
console.log('checkState', this.checkState);
console.log('checkState.isInChecked', this.checkState.isInChecked);
console.log('checkState coords', this.checkState.coords);
console.log('row', this.checkState.coords.x);
console.log('col', this.checkState.coords.y);
console.log('result', result);
}
// console.log('result', result);
return result
}
public isSquarePromotionSquare(row: number, col: number): boolean {
if (!this.promotionCoords) return false;
return this.promotionCoords.x === row && this.promotionCoords.y === col;
}
public unmarkingPreviouslySelectedAndfSafeSquares(): void {
this.selectedSquare = {piece: null};
this.pieceSafeSquares = [];
if(this.isPromotionModalVisible) {
this.isPromotionModalVisible = false;
this.promotedPiece = null;
this.promotionCoords = null;
}
}
public selectingPiece(row: number, col: number): void {
if(this.gameOverMessage) return;
console.log('Selecting', row, col);
const piece :FENChar | null = this.chessBoardView[row][col];
if (!piece) return
if (this.isWrongPieceSelected(piece)) return;
const isSameSquareClicked: boolean = !!this.selectedSquare.piece && this.selectedSquare.coords.x === row && this.selectedSquare.coords.y === col;
if (isSameSquareClicked) {
this.unmarkingPreviouslySelectedAndfSafeSquares();
return;
}
this.selectedSquare = {piece: piece, coords: {x: row, y: col}};
this.pieceSafeSquares = this.safeSquares.get(row + "," + col) || [];
}
private placingPiece(row: number, col: number): void {
if (!this.selectedSquare.piece) return;
if (!this.isSquareSafeForSelectedPiece(row, col)) return;
// Pawn promotion
const shouldOpenDialog =
(this.selectedSquare.piece === FENChar.WhitePawn && row === 7 ||
this.selectedSquare.piece === FENChar.BlackPawn && row === 0)
if(shouldOpenDialog) {
// Clear the selected square and safe squares if we are promoting a pawn
this.pieceSafeSquares = [];
// Open the promotion modal
this.isPromotionModalVisible = true;
// Set the promotion coords
this.promotionCoords = {x: row, y: col};
return;
}
this.updateBoard(this.selectedSquare.coords, {x: row, y: col}, this.promotedPiece);
}
protected updateBoard(from: Coords, to: Coords, promotedPiece: FENChar| null): void {
this.chessBoard.move(from, to, promotedPiece);
this.chessBoardView = this.chessBoard.chessBoardView;
this.checkState = this.chessBoard.checkState;
this.lastMove = this.chessBoard.lastMove;
this.markLastMoveAndCheckState(this.lastMove, this.chessBoard.checkState);
this.unmarkingPreviouslySelectedAndfSafeSquares();
console.log('=============================================', this.playerColor)
console.log('Updating next FEN position', this.chessBoard.boardAsFEN);
this.chessBoardService.chessBoardState$.next(this.chessBoard.boardAsFEN);
this.gameHistoryPointer++;
}
public promotePiece(piece: FENChar): void {
if (!this.promotionCoords) return; // Check we have a promotion coords
if (!this.selectedSquare.piece) return; // Check we have a selected piece
this.promotedPiece = piece;
this.updateBoard(this.selectedSquare.coords, this.promotionCoords, this.promotedPiece);
}
public closePromotionDialog():void {
this.unmarkingPreviouslySelectedAndfSafeSquares();
}
private markLastMoveAndCheckState(lastMove: LastMove | undefined, checkState: CheckState): void {
this.lastMove = lastMove;
this.checkState = checkState;
if (this.lastMove) {
this.moveSound(this.lastMove.moveType);
}
else {
this.moveSound(new Set<MoveType>([MoveType.BasicMove ]));
}
}
public move(row: number, col: number): void {
this.selectingPiece(row, col);
this.placingPiece(row, col);
}
private isWrongPieceSelected(piece: FENChar): boolean {
const isWhitePieceSelected: boolean = piece === piece.toUpperCase();
return isWhitePieceSelected && this.playerColor === Color.Black ||
!isWhitePieceSelected && this.playerColor === Color.White;
}
public showPreviousPosition(moveIndex: number): void {
const { board, checkState, lastMove } = this.gameHistory[moveIndex];
this.chessBoardView = board;
this.markLastMoveAndCheckState(lastMove, checkState);
this.gameHistoryPointer = moveIndex;
}
private moveSound(moveType: Set<MoveType>): void {
const moveSound = new Audio("/assets/sounds/move.mp3");
if (moveType.has(MoveType.Promotion)) moveSound.src = "assets/sounds/promotion.mp3";
else if (moveType.has(MoveType.Capture)) moveSound.src = "assets/sounds/capture.mp3";
else if (moveType.has(MoveType.Castling)) moveSound.src = "assets/sounds/castling.mp3";
if (moveType.has(MoveType.CheckMate)) moveSound.src = "assets/sounds/checkmate.mp3";
else if (moveType.has(MoveType.Check)) moveSound.src = "assets/sounds/check.mp3";
console.log('Playing sound', moveSound.src);
moveSound.play();
}
ngOnInit(): void {
const keyboardSubscription$ = fromEvent<KeyboardEvent>(document, 'keyup')
.pipe(
filter((event: KeyboardEvent) => event.key === 'ArrowLeft' || event.key === 'ArrowRight'),
tap((event: KeyboardEvent) => {
switch(event.key) {
case 'ArrowRight':
if (this.gameHistoryPointer === this.gameHistory.length - 1) return;
this.gameHistoryPointer++ ;
break;
case 'ArrowLeft':
if (this.gameHistoryPointer === 0) return;
this.gameHistoryPointer--;
break;
default:
break;
}
this.showPreviousPosition(this.gameHistoryPointer);
})
).subscribe();
this.subscriptions$.add(keyboardSubscription$);
}
ngOnDestroy(): void {
this.subscriptions$.unsubscribe();
}
}

View File

@ -0,0 +1,11 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { FENConverter } from '../../chess-logic/FENConverter';
@Injectable({
providedIn: 'root'
})
export class ChessBoardService {
public chessBoardState$ = new BehaviorSubject<string>(FENConverter.initalPosition);
}

View File

@ -0,0 +1,15 @@
import { FENChar } from "../../chess-logic/models";
type SquareWithPiece = {
piece: FENChar;
x: number;
y: number;
}
type SquareWithoutPiece = {
piece: null;
}
export type SelectedSquare = SquareWithPiece | SquareWithoutPiece;
export const columns = ["a", "b", "c", "d", "e", "f", "g", "h"] as const;

View File

@ -0,0 +1 @@
<p>computer-mode works!</p>

View File

@ -0,0 +1,72 @@
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
import { ChessBoardComponent } from '../chess-board/chess-board.component';
import { StockfishService } from './stockfish.service';
import { ChessBoardService } from '../chess-board/chess-board.service';
import { Subscription, firstValueFrom } from 'rxjs';
import { Color } from '../../chess-logic/models';
import { MoveListComponent } from '../move-list/move-list.component';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-computer-mode',
imports: [MoveListComponent, CommonModule],
templateUrl: '../chess-board/chess-board.component.html',
styleUrls: ['../chess-board/chess-board.component.css']
})
export class ComputerModeComponent extends ChessBoardComponent implements OnInit, OnDestroy {
private computerSubscriptions$ = new Subscription();
constructor(private stockfishService: StockfishService) {
super(inject(ChessBoardService));
}
public override ngOnInit(): void {
super.ngOnInit();
const computerConfiSubscription$: Subscription = this.stockfishService.computerConfiguration$.subscribe({
next: (computerConfiguration) => {
if (computerConfiguration.color === Color.White) this.flipBoard();
}
});
const chessBoardStateSubscription$: Subscription = this.chessBoardService.chessBoardState$.subscribe({
next: async (FEN: string) => {
if (this.chessBoard.isGameOver) {
chessBoardStateSubscription$.unsubscribe();
return;
}
const player: Color = FEN.split(" ")[1] === "w" ? Color.White : Color.Black;
console.log("FEN contains :%s", player === 0 ? "White" : "Black");
console.log("Computer Configuration :%s", this.stockfishService.computerConfiguration$.value.color === 0 ? "White" : "Black");
// if (this.stockfishService.computerConfiguration$.value.color === Color.White) {
if ( player !== this.stockfishService.computerConfiguration$.value.color) {
// Not the computer's turn
console.log("Not the computer's turn", this.stockfishService.computerConfiguration$.value.color);
return;
}
// }
// else {
// if (player === this.stockfishService.computerConfiguration$.value.color) {
// // Not the computer's turn
// console.log("Not the computer's turn", this.stockfishService.computerConfiguration$.value.color);
// return;
// }
// }
const { prevX, prevY, newX, newY, promotedPiece } = await firstValueFrom(this.stockfishService.getBestMove(FEN));
this.updateBoard({x: prevX,y: prevY}, {x: newX, y: newY}, promotedPiece);
}
});
this.computerSubscriptions$.add(chessBoardStateSubscription$);
this.computerSubscriptions$.add(computerConfiSubscription$);
}
public override ngOnDestroy(): void {
super.ngOnDestroy();
this.computerSubscriptions$.unsubscribe();
}
}

View File

@ -0,0 +1,35 @@
import { Color, FENChar } from "../../chess-logic/models";
export type StockfishQueryParams = {
depth: number;
fen: string;
}
export type ChessMove = {
prevX: number;
prevY: number;
newX: number;
newY: number;
promotedPiece: FENChar |null;
}
export type StockfishResponse = {
success: boolean;
evaulatuion: number | null;
mate: number | null;
bestmove: string;
continuation: string;
}
export type ComputerConfguration = {
color: Color
level: number;
}
export const stockfishLevels: Readonly<Record<number,number>> = {
1:1,
2:4,
3:7,
4:10,
5:13
};

View File

@ -0,0 +1,69 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ChessMove, ComputerConfguration, StockfishQueryParams, StockfishResponse } from './models';
import { BehaviorSubject, Observable, of, switchMap } from 'rxjs';
import { Color, FENChar } from '../../chess-logic/models';
@Injectable({
providedIn: 'root',
})
export class StockfishService {
// private readonly api = 'https://stockfish.online/api/s/v2.php';
private readonly api = 'http://192.168.1.70:31701/getBestMove';
public computerConfiguration$ = new BehaviorSubject<ComputerConfguration>({ color: Color.Black, level: 1 });
constructor(private http: HttpClient) { }
private convertColumnLetterToYCoord = (string: string) => string.charCodeAt(0) - "a".charCodeAt(0);
private promotedPiece(piece: string | undefined): FENChar | null {
if (piece === undefined) return null;
const computerColor = this.computerConfiguration$.value.color;
if (computerColor === Color.White) {
switch (piece.toLowerCase()) {
case 'q': return FENChar.WhiteQueen;
case 'r': return FENChar.WhiteRook;
case 'b': return FENChar.WhiteBishop;
case 'n': return FENChar.WhiteKnight;
default: return null;
}
}
else {
switch (piece.toLowerCase()) {
case 'q': return FENChar.BlackQueen;
case 'r': return FENChar.BlackRook;
case 'b': return FENChar.BlackBishop;
case 'n': return FENChar.BlackKnight;
default: return null;
}
}
}
private moveFromStockfishString(move: string): ChessMove {
console.log('moveFromStockfishString: Got move', move);
const prevY: number = this.convertColumnLetterToYCoord(move[0]);
const prevX: number = Number(move[1]) - 1;
const newY: number = this.convertColumnLetterToYCoord(move[2]);
const newX: number = Number(move[3]) - 1;
const promotedPiece = this.promotedPiece(move[4]);
const newChessMove = { prevX, prevY, newX, newY, promotedPiece };
return newChessMove;
}
public getBestMove(fen: string): Observable<ChessMove> {
const queryParams: StockfishQueryParams = {
fen,
depth: this.computerConfiguration$.value.level
};
return this.http.post<StockfishResponse>(this.api, queryParams)
.pipe(
switchMap((response: StockfishResponse) => {
const bestmove: string = response.bestmove.split(' ')[1];
return of(this.moveFromStockfishString(bestmove));
})
)
}
}

View File

@ -0,0 +1,39 @@
.move-list {
max-height: 100px;
overflow-y: scroll;
display: flex;
flex-direction: column-reverse;
}
.move-list::-webkit-scrollbar {
display: none;
}
.move-list {
-ms-overflow-style: none;
scrollbar-width: none;
}
.row {
width: 250px;
display: flex;
color: white
}
.move-number {
width: 50px;
}
.current-move {
background-color: rgb(0, 143, 168);
}
.move {
cursor: pointer;
width: 100px;
}
.move:hover {
background-color: rgb(0, 143, 168);
opacity: 0.7
}

View File

@ -0,0 +1,39 @@
<p>move-list works!</p>
<div>
<button mat-icon-button color="primary" [disabled]="gameHistoryPointer === 0" (click)="showPreviousPosition(0)">
<mat-icon>first_page</mat-icon>
</button>
<button mat-icon-button color="primary" [disabled]="gameHistoryPointer === 0"
(click)="showPreviousPosition(gameHistoryPointer - 1)">
<mat-icon>navigate_before</mat-icon>
</button>
<button mat-icon-button color="primary" [disabled]="gameHistoryPointer === gameHistoryLength - 1"
(click)="showPreviousPosition(gameHistoryPointer + 1)">
<mat-icon>navigate_next</mat-icon>
</button>
<button mat-icon-button color="primary" [disabled]="gameHistoryPointer === gameHistoryLength - 1"
(click)="showPreviousPosition(gameHistoryLength - 1)">
<mat-icon>last_page</mat-icon>
</button>
</div>
<div class="move-list">
<div>
<div *ngFor="let move of moveList; let moveNumber = index" class="row">
<div class="move-number">{{moveNumber + 1}}.</div>
<div class="move" [ngClass]="{'current-move': moveNumber * 2 + 1 === gameHistoryPointer}"
(click)="showPreviousPosition(moveNumber * 2 + 1)">
{{move[0]}}
</div>
<div *ngIf="move[1]" class="move" [ngClass]="{'current-move': moveNumber * 2 + 2 === gameHistoryPointer}"
(click)="showPreviousPosition(moveNumber * 2 + 2)">
{{move[1]}}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from "@angular/material/icon";
import { MoveList } from '../../chess-logic/models';
@Component({
selector: 'app-move-list',
templateUrl: './move-list.component.html',
styleUrls: ['./move-list.component.css'],
standalone: true,
imports: [CommonModule, MatButtonModule, MatIconModule]
})
export class MoveListComponent {
@Input({ required: true }) public moveList!: MoveList;
@Input({ required: true }) public gameHistoryPointer: number = 0;
@Input({ required: true }) public gameHistoryLength: number = 1;
@Output() public showPreviousPositionEvent = new EventEmitter<number>();
public showPreviousPosition(moveIndex: number): void {
this.showPreviousPositionEvent.emit(moveIndex);
}
}

View File

@ -0,0 +1,8 @@
<mat-toolbar>
<button mat-button routerLink="/against-friend">Play Against Friend</button>
<button mat-button (click)="playAgainstComputer()">
Play Against Computer
</button>
</mat-toolbar>
<router-outlet />

View File

@ -0,0 +1,23 @@
import { Component } from '@angular/core';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { PlayAgainstComputerDialogComponent } from '../play-against-computer-dialog/play-against-computer-dialog.component';
@Component({
selector: 'app-nav-menu',
standalone: true,
imports: [MatToolbarModule, MatButtonModule, RouterModule, MatDialogModule],
templateUrl: './nav-menu.component.html',
styleUrl: './nav-menu.component.css'
})
export class NavMenuComponent {
constructor(private dialog: MatDialog) { }
public playAgainstComputer(): void {
this.dialog.open(PlayAgainstComputerDialogComponent);
}
}

View File

@ -0,0 +1,28 @@
.stockfish-strength-container {
display: flex;
justify-content: space-evenly;
}
.strength-div {
border: 1px solid white;
cursor: pointer;
width: 40px;
height: 40px;
text-align: center;
font-size: 20px;
line-height: 40px;
}
.choose-side {
display: flex;
justify-content: space-evenly;
}
.king-img {
width: 50px;
cursor: pointer;
}
.selected {
background-color: green;
}

View File

@ -0,0 +1,19 @@
<mat-dialog-content>
<h2 mat-dialog-title>Stockfish Strength</h2>
<div class="stockfish-strength-container">
<div *ngFor="let level of stockfishLevels" class="strength-div" (click)="selectStockfishLevel(level)"
[ngClass]="{'selected': level === stockfishLevel}">
{{level}}
</div>
</div>
<div class="choose-side">
<img src="assets/pieces/white king.svg" class="king-img" draggable="false" (click)="play('w')">
<img src="assets/pieces/black king.svg" class="king-img" draggable="false" (click)="play('b')">
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close (click)="closeDialog()">Cancel</button>
</mat-dialog-actions>

View File

@ -0,0 +1,43 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { StockfishService } from '../computer-mode/stockfish.service';
import { Router } from '@angular/router';
import { Color } from '../../chess-logic/models';
@Component({
selector: 'app-play-against-computer-dialog',
imports: [MatDialogModule, MatButtonModule, CommonModule],
templateUrl: './play-against-computer-dialog.component.html',
styleUrl: './play-against-computer-dialog.component.css'
})
export class PlayAgainstComputerDialogComponent {
public stockfishLevels: readonly number[] = [1, 2, 3, 4, 5];
public stockfishLevel: number = 1;
constructor(
private stockfishService: StockfishService,
private dialog: MatDialog,
private router: Router
) { }
public selectStockfishLevel(level: number): void {
this.stockfishLevel = level;
}
public play(color: "w" | "b"): void {
this.dialog.closeAll();
console.log('We got ', color);
this.stockfishService.computerConfiguration$.next({
color: color === "w" ? Color.Black : Color.White,
level: this.stockfishLevel
});
this.router.navigate(["against-computer"]);
}
public closeDialog(): void {
this.router.navigate(["against-friend"]);
}
}

16
src/custom-theme.scss Normal file
View File

@ -0,0 +1,16 @@
// Custom Theming for Angular Material
// For more information: https://material.angular.io/guide/theming
@use '@angular/material' as mat;
html {
@include mat.theme((
color: (
theme-type: light,
primary: mat.$azure-palette,
tertiary: mat.$blue-palette,
),
typography: Roboto,
density: 0,
));
}

15
src/index.html Normal file
View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ChessApp</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<app-root></app-root>
</body>
</html>

6
src/main.ts Normal file
View File

@ -0,0 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

29
src/styles.css Normal file
View File

@ -0,0 +1,29 @@
/* You can add global styles to this file, and also import other style files */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
background-color: black;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }

15
tsconfig.app.json Normal file
View File

@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}

27
tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

15
tsconfig.spec.json Normal file
View File

@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}