initial commit
17
.editorconfig
Normal 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
@ -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
@ -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
@ -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
39
package.json
Normal 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
1
public/assets/pieces/black bishop.svg
Normal file
After Width: | Height: | Size: 7.7 KiB |
1
public/assets/pieces/black king.svg
Normal file
After Width: | Height: | Size: 8.8 KiB |
1
public/assets/pieces/black knight.svg
Normal file
After Width: | Height: | Size: 5.8 KiB |
1
public/assets/pieces/black pawn.svg
Normal 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 |
1
public/assets/pieces/black queen.svg
Normal file
After Width: | Height: | Size: 8.9 KiB |
1
public/assets/pieces/black rook.svg
Normal file
After Width: | Height: | Size: 5.4 KiB |
1
public/assets/pieces/white bishop.svg
Normal file
After Width: | Height: | Size: 7.7 KiB |
1
public/assets/pieces/white king.svg
Normal file
After Width: | Height: | Size: 8.8 KiB |
1
public/assets/pieces/white knight.svg
Normal file
After Width: | Height: | Size: 5.8 KiB |
1
public/assets/pieces/white pawn.svg
Normal 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 |
1
public/assets/pieces/white queen.svg
Normal file
After Width: | Height: | Size: 8.9 KiB |
1
public/assets/pieces/white rook.svg
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
public/assets/sounds/capture.mp3
Normal file
BIN
public/assets/sounds/castling.mp3
Normal file
BIN
public/assets/sounds/check.mp3
Normal file
BIN
public/assets/sounds/checkmate.mp3
Normal file
BIN
public/assets/sounds/incorrect-move.mp3
Normal file
BIN
public/assets/sounds/move.mp3
Normal file
BIN
public/assets/sounds/promote.mp3
Normal file
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
0
src/app/app.component.css
Normal file
1
src/app/app.component.html
Normal file
@ -0,0 +1 @@
|
||||
<app-nav-menu></app-nav-menu>
|
29
src/app/app.component.spec.ts
Normal 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
@ -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
@ -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
@ -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"},
|
||||
];
|
90
src/app/chess-logic/FENConverter.ts
Normal 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 "-";
|
||||
}
|
||||
}
|
850
src/app/chess-logic/chess-board.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
90
src/app/chess-logic/models.ts
Normal 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)[][];
|
||||
}[];
|
18
src/app/chess-logic/pieces/bishop.ts
Normal 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;
|
||||
}
|
||||
}
|
31
src/app/chess-logic/pieces/king.ts
Normal 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;
|
||||
}
|
||||
}
|
22
src/app/chess-logic/pieces/knight.ts
Normal 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;
|
||||
}
|
||||
}
|
38
src/app/chess-logic/pieces/pawn.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
20
src/app/chess-logic/pieces/piece.ts
Normal 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;
|
||||
}
|
||||
}
|
21
src/app/chess-logic/pieces/queen.ts
Normal 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;
|
||||
}
|
||||
}
|
28
src/app/chess-logic/pieces/rook.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
88
src/app/modules/chess-board/chess-board.component.css
Normal 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;
|
||||
}
|
42
src/app/modules/chess-board/chess-board.component.html
Normal 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()">
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<app-move-list
|
||||
[moveList]="moveList"
|
||||
[gameHistoryPointer]="gameHistoryPointer"
|
||||
[gameHistoryLength]="gameHistory.length"
|
||||
(showPreviousPositionEvent)="showPreviousPosition($event)">
|
||||
|
||||
</app-move-list>
|
263
src/app/modules/chess-board/chess-board.component.ts
Normal 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();
|
||||
}
|
||||
}
|
11
src/app/modules/chess-board/chess-board.service.ts
Normal 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);
|
||||
}
|
15
src/app/modules/chess-board/models.ts
Normal 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;
|
@ -0,0 +1 @@
|
||||
<p>computer-mode works!</p>
|
72
src/app/modules/computer-mode/computer-mode.component.ts
Normal 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();
|
||||
}
|
||||
}
|
35
src/app/modules/computer-mode/models.ts
Normal 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
|
||||
};
|
69
src/app/modules/computer-mode/stockfish.service.ts
Normal 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));
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
39
src/app/modules/move-list/move-list.component.css
Normal 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
|
||||
}
|
39
src/app/modules/move-list/move-list.component.html
Normal 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>
|
23
src/app/modules/move-list/move-list.component.ts
Normal 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);
|
||||
}
|
||||
}
|
0
src/app/modules/nav-menu/nav-menu.component.css
Normal file
8
src/app/modules/nav-menu/nav-menu.component.html
Normal 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 />
|
23
src/app/modules/nav-menu/nav-menu.component.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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"
|
||||
]
|
||||
}
|