├── .editorconfig ├── .gitattributes ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── chess-logic │ │ ├── FENConverter.ts │ │ ├── chess-board.ts │ │ ├── models.ts │ │ └── pieces │ │ │ ├── bishop.ts │ │ │ ├── king.ts │ │ │ ├── knight.ts │ │ │ ├── pawn.ts │ │ │ ├── piece.ts │ │ │ ├── queen.ts │ │ │ └── rook.ts │ ├── modules │ │ ├── chess-board │ │ │ ├── chess-board.component.css │ │ │ ├── chess-board.component.html │ │ │ ├── chess-board.component.ts │ │ │ ├── chess-board.service.ts │ │ │ └── models.ts │ │ ├── computer-mode │ │ │ ├── computer-mode.component.ts │ │ │ ├── models.ts │ │ │ └── stockfish.service.ts │ │ ├── move-list │ │ │ ├── move-list.component.css │ │ │ ├── move-list.component.html │ │ │ └── move-list.component.ts │ │ ├── nav-menu │ │ │ ├── nav-menu.component.css │ │ │ ├── nav-menu.component.html │ │ │ └── nav-menu.component.ts │ │ └── play-against-computer-dialog │ │ │ ├── play-against-computer-dialog.component.css │ │ │ ├── play-against-computer-dialog.component.html │ │ │ └── play-against-computer-dialog.component.ts │ └── routes │ │ └── app-routing.module.ts ├── assets │ ├── .gitkeep │ ├── pieces │ │ ├── black bishop.svg │ │ ├── black king.svg │ │ ├── black knight.svg │ │ ├── black pawn.svg │ │ ├── black queen.svg │ │ ├── black rook.svg │ │ ├── white bishop.svg │ │ ├── white king.svg │ │ ├── white knight.svg │ │ ├── white pawn.svg │ │ ├── white queen.svg │ │ └── white rook.svg │ └── sound │ │ ├── capture.mp3 │ │ ├── castling.mp3 │ │ ├── check.mp3 │ │ ├── checkmate.mp3 │ │ ├── incorrect-move.mp3 │ │ ├── move.mp3 │ │ └── promote.mp3 ├── custom-theme.scss ├── favicon.ico ├── index.html ├── main.ts └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChessGame 2 | 3 | The chess application consists of two modes: playing against a friend in the same browser and playing against the computer, which utilizes the Stockfish REST API from https://stockfish.online/ 4 | 5 | Images for pieces used from Lichess official repo: https://github.com/lichess-org 6 | 7 | Live version: https://awsomecstutorials.github.io/chess-game/ 8 | 9 | Tutorial can be found on FreeCodeCamp youtube channel: https://youtu.be/fJIsqZmQVZQ 10 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "chess-game": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "docs", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": [ 20 | "zone.js" 21 | ], 22 | "tsConfig": "tsconfig.app.json", 23 | "assets": [ 24 | "src/favicon.ico", 25 | "src/assets" 26 | ], 27 | "styles": [ 28 | "@angular/material/prebuilt-themes/purple-green.css", 29 | "src/styles.css" 30 | ], 31 | "scripts": [] 32 | }, 33 | "configurations": { 34 | "production": { 35 | "budgets": [ 36 | { 37 | "type": "initial", 38 | "maximumWarning": "5mb", 39 | "maximumError": "4mb" 40 | }, 41 | { 42 | "type": "anyComponentStyle", 43 | "maximumWarning": "2kb", 44 | "maximumError": "4kb" 45 | } 46 | ], 47 | "outputHashing": "all" 48 | }, 49 | "development": { 50 | "buildOptimizer": false, 51 | "optimization": false, 52 | "vendorChunk": true, 53 | "extractLicenses": false, 54 | "sourceMap": true, 55 | "namedChunks": true 56 | } 57 | }, 58 | "defaultConfiguration": "production" 59 | }, 60 | "serve": { 61 | "builder": "@angular-devkit/build-angular:dev-server", 62 | "configurations": { 63 | "production": { 64 | "browserTarget": "chess-game:build:production" 65 | }, 66 | "development": { 67 | "browserTarget": "chess-game:build:development" 68 | } 69 | }, 70 | "defaultConfiguration": "development" 71 | }, 72 | "extract-i18n": { 73 | "builder": "@angular-devkit/build-angular:extract-i18n", 74 | "options": { 75 | "browserTarget": "chess-game:build" 76 | } 77 | }, 78 | "test": { 79 | "builder": "@angular-devkit/build-angular:karma", 80 | "options": { 81 | "polyfills": [ 82 | "zone.js", 83 | "zone.js/testing" 84 | ], 85 | "tsConfig": "tsconfig.spec.json", 86 | "assets": [ 87 | "src/favicon.ico", 88 | "src/assets" 89 | ], 90 | "styles": [ 91 | "@angular/material/prebuilt-themes/purple-green.css", 92 | "src/styles.css" 93 | ], 94 | "scripts": [] 95 | } 96 | } 97 | } 98 | } 99 | }, 100 | "cli": { 101 | "analytics": false 102 | } 103 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chess-game", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^16.2.0", 14 | "@angular/cdk": "^16.2.14", 15 | "@angular/common": "^16.2.0", 16 | "@angular/compiler": "^16.2.0", 17 | "@angular/core": "^16.2.0", 18 | "@angular/forms": "^16.2.0", 19 | "@angular/material": "^16.2.14", 20 | "@angular/platform-browser": "^16.2.0", 21 | "@angular/platform-browser-dynamic": "^16.2.0", 22 | "@angular/router": "^16.2.0", 23 | "rxjs": "~7.8.0", 24 | "tslib": "^2.3.0", 25 | "zone.js": "~0.13.0" 26 | }, 27 | "devDependencies": { 28 | "@angular-devkit/build-angular": "^16.2.0", 29 | "@angular/cli": "~16.2.0", 30 | "@angular/compiler-cli": "^16.2.0", 31 | "@types/jasmine": "~4.3.0", 32 | "jasmine-core": "~4.6.0", 33 | "karma": "~6.4.0", 34 | "karma-chrome-launcher": "~3.2.0", 35 | "karma-coverage": "~2.2.0", 36 | "karma-jasmine": "~5.1.0", 37 | "karma-jasmine-html-reporter": "~2.1.0", 38 | "typescript": "~5.1.3" 39 | } 40 | } -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awsomeCStutorials/chess-game/87420327bd8eea68a6d462eac60372daee263441/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(() => TestBed.configureTestingModule({ 6 | declarations: [AppComponent] 7 | })); 8 | 9 | it('should create the app', () => { 10 | const fixture = TestBed.createComponent(AppComponent); 11 | const app = fixture.componentInstance; 12 | expect(app).toBeTruthy(); 13 | }); 14 | 15 | it(`should have as title 'chess-game'`, () => { 16 | const fixture = TestBed.createComponent(AppComponent); 17 | const app = fixture.componentInstance; 18 | expect(app.title).toEqual('chess-game'); 19 | }); 20 | 21 | it('should render title', () => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.nativeElement as HTMLElement; 25 | expect(compiled.querySelector('.content span')?.textContent).toContain('chess-game app is running!'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'chess-game'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { HttpClientModule } from "@angular/common/http"; 5 | import { AppComponent } from './app.component'; 6 | import { ChessBoardComponent } from './modules/chess-board/chess-board.component'; 7 | import { ComputerModeComponent } from './modules/computer-mode/computer-mode.component'; 8 | import { NavMenuComponent } from './modules/nav-menu/nav-menu.component'; 9 | import { AppRoutingModule } from './routes/app-routing.module'; 10 | import { PlayAgainstComputerDialogComponent } from './modules/play-against-computer-dialog/play-against-computer-dialog.component'; 11 | import { MoveListComponent } from './modules/move-list/move-list.component'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | AppComponent, 16 | ChessBoardComponent, 17 | ComputerModeComponent, 18 | ], 19 | imports: [ 20 | BrowserModule, 21 | HttpClientModule, 22 | AppRoutingModule, 23 | NavMenuComponent, 24 | PlayAgainstComputerDialogComponent, 25 | MoveListComponent 26 | ], 27 | providers: [], 28 | bootstrap: [AppComponent] 29 | }) 30 | export class AppModule { } 31 | -------------------------------------------------------------------------------- /src/app/chess-logic/FENConverter.ts: -------------------------------------------------------------------------------- 1 | import { columns } from "../modules/chess-board/models"; 2 | import { Color, LastMove } from "./models"; 3 | import { King } from "./pieces/king"; 4 | import { Pawn } from "./pieces/pawn"; 5 | import { Piece } from "./pieces/piece"; 6 | import { Rook } from "./pieces/rook"; 7 | 8 | export class FENConverter { 9 | public static readonly initalPosition: string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; 10 | 11 | public convertBoardToFEN( 12 | board: (Piece | null)[][], 13 | playerColor: Color, 14 | lastMove: LastMove | undefined, 15 | fiftyMoveRuleCounter: number, 16 | numberOfFullMoves: number 17 | ): string { 18 | let FEN: string = ""; 19 | 20 | for (let i = 7; i >= 0; i--) { 21 | let FENRow: string = ""; 22 | let consecutiveEmptySquaresCounter = 0; 23 | 24 | for (const piece of board[i]) { 25 | if (!piece) { 26 | consecutiveEmptySquaresCounter++; 27 | continue; 28 | } 29 | 30 | if (consecutiveEmptySquaresCounter !== 0) 31 | FENRow += String(consecutiveEmptySquaresCounter); 32 | 33 | consecutiveEmptySquaresCounter = 0; 34 | FENRow += piece.FENChar; 35 | } 36 | 37 | if (consecutiveEmptySquaresCounter !== 0) 38 | FENRow += String(consecutiveEmptySquaresCounter); 39 | 40 | FEN += (i === 0) ? FENRow : FENRow + "/"; 41 | } 42 | 43 | const player: string = playerColor === Color.White ? "w" : "b"; 44 | FEN += " " + player; 45 | FEN += " " + this.castlingAvailability(board); 46 | FEN += " " + this.enPassantPosibility(lastMove, playerColor); 47 | FEN += " " + fiftyMoveRuleCounter * 2; 48 | FEN += " " + numberOfFullMoves; 49 | return FEN; 50 | } 51 | 52 | private castlingAvailability(board: (Piece | null)[][]): string { 53 | const castlingPossibilities = (color: Color): string => { 54 | let castlingAvailability: string = ""; 55 | 56 | const kingPositionX: number = color === Color.White ? 0 : 7; 57 | const king: Piece | null = board[kingPositionX][4]; 58 | 59 | if (king instanceof King && !king.hasMoved) { 60 | const rookPositionX: number = kingPositionX; 61 | const kingSideRook = board[rookPositionX][7]; 62 | const queenSideRook = board[rookPositionX][0]; 63 | 64 | if (kingSideRook instanceof Rook && !kingSideRook.hasMoved) 65 | castlingAvailability += "k"; 66 | 67 | if (queenSideRook instanceof Rook && !queenSideRook.hasMoved) 68 | castlingAvailability += "q"; 69 | 70 | if (color === Color.White) 71 | castlingAvailability = castlingAvailability.toUpperCase(); 72 | } 73 | return castlingAvailability; 74 | } 75 | 76 | const castlingAvailability: string = castlingPossibilities(Color.White) + castlingPossibilities(Color.Black); 77 | return castlingAvailability !== "" ? castlingAvailability : "-"; 78 | } 79 | 80 | private enPassantPosibility(lastMove: LastMove | undefined, color: Color): string { 81 | if (!lastMove) return "-"; 82 | const { piece, currX: newX, prevX, prevY } = lastMove; 83 | 84 | if (piece instanceof Pawn && Math.abs(newX - prevX) === 2) { 85 | const row: number = color === Color.White ? 6 : 3; 86 | return columns[prevY] + String(row); 87 | } 88 | return "-"; 89 | } 90 | } -------------------------------------------------------------------------------- /src/app/chess-logic/chess-board.ts: -------------------------------------------------------------------------------- 1 | import { columns } from "../modules/chess-board/models"; 2 | import { FENConverter } from "./FENConverter"; 3 | import { CheckState, Color, Coords, FENChar, GameHistory, LastMove, MoveList, MoveType, SafeSquares } from "./models"; 4 | import { Bishop } from "./pieces/bishop"; 5 | import { King } from "./pieces/king"; 6 | import { Knight } from "./pieces/knight"; 7 | import { Pawn } from "./pieces/pawn"; 8 | import { Piece } from "./pieces/piece"; 9 | import { Queen } from "./pieces/queen"; 10 | import { Rook } from "./pieces/rook"; 11 | 12 | export class ChessBoard { 13 | private chessBoard: (Piece | null)[][]; 14 | private readonly chessBoardSize: number = 8; 15 | private _playerColor = Color.White; 16 | private _safeSquares: SafeSquares; 17 | private _lastMove: LastMove | undefined; 18 | private _checkState: CheckState = { isInCheck: false }; 19 | private fiftyMoveRuleCounter: number = 0; 20 | 21 | private _isGameOver: boolean = false; 22 | private _gameOverMessage: string | undefined; 23 | 24 | private fullNumberOfMoves: number = 1; 25 | private threeFoldRepetitionDictionary = new Map(); 26 | private threeFoldRepetitionFlag: boolean = false; 27 | 28 | private _boardAsFEN: string = FENConverter.initalPosition; 29 | private FENConverter = new FENConverter(); 30 | 31 | private _moveList: MoveList = []; 32 | private _gameHistory: GameHistory; 33 | 34 | constructor() { 35 | this.chessBoard = [ 36 | [ 37 | new Rook(Color.White), new Knight(Color.White), new Bishop(Color.White), new Queen(Color.White), 38 | new King(Color.White), new Bishop(Color.White), new Knight(Color.White), new Rook(Color.White) 39 | ], 40 | [ 41 | new Pawn(Color.White), new Pawn(Color.White), new Pawn(Color.White), new Pawn(Color.White), 42 | new Pawn(Color.White), new Pawn(Color.White), new Pawn(Color.White), new Pawn(Color.White) 43 | ], 44 | [null, null, null, null, null, null, null, null], 45 | [null, null, null, null, null, null, null, null], 46 | [null, null, null, null, null, null, null, null], 47 | [null, null, null, null, null, null, null, null], 48 | [ 49 | new Pawn(Color.Black), new Pawn(Color.Black), new Pawn(Color.Black), new Pawn(Color.Black), 50 | new Pawn(Color.Black), new Pawn(Color.Black), new Pawn(Color.Black), new Pawn(Color.Black) 51 | ], 52 | [ 53 | new Rook(Color.Black), new Knight(Color.Black), new Bishop(Color.Black), new Queen(Color.Black), 54 | new King(Color.Black), new Bishop(Color.Black), new Knight(Color.Black), new Rook(Color.Black) 55 | ], 56 | ]; 57 | this._safeSquares = this.findSafeSqures(); 58 | this._gameHistory = [{ board: this.chessBoardView, lastMove: this._lastMove, checkState: this._checkState }]; 59 | } 60 | 61 | public get playerColor(): Color { 62 | return this._playerColor; 63 | } 64 | 65 | public get chessBoardView(): (FENChar | null)[][] { 66 | return this.chessBoard.map(row => { 67 | return row.map(piece => piece instanceof Piece ? piece.FENChar : null); 68 | }) 69 | } 70 | 71 | public get safeSquares(): SafeSquares { 72 | return this._safeSquares; 73 | } 74 | 75 | public get lastMove(): LastMove | undefined { 76 | return this._lastMove; 77 | } 78 | 79 | public get checkState(): CheckState { 80 | return this._checkState; 81 | } 82 | 83 | public get isGameOver(): boolean { 84 | return this._isGameOver; 85 | } 86 | 87 | public get gameOverMessage(): string | undefined { 88 | return this._gameOverMessage; 89 | } 90 | 91 | public get boardAsFEN(): string { 92 | return this._boardAsFEN; 93 | } 94 | 95 | public get moveList(): MoveList { 96 | return this._moveList; 97 | } 98 | 99 | public get gameHistory(): GameHistory { 100 | return this._gameHistory; 101 | } 102 | 103 | public static isSquareDark(x: number, y: number): boolean { 104 | return x % 2 === 0 && y % 2 === 0 || x % 2 === 1 && y % 2 === 1; 105 | } 106 | 107 | private areCoordsValid(x: number, y: number): boolean { 108 | return x >= 0 && y >= 0 && x < this.chessBoardSize && y < this.chessBoardSize; 109 | } 110 | 111 | public isInCheck(playerColor: Color, checkingCurrentPosition: boolean): boolean { 112 | for (let x = 0; x < this.chessBoardSize; x++) { 113 | for (let y = 0; y < this.chessBoardSize; y++) { 114 | const piece: Piece | null = this.chessBoard[x][y]; 115 | if (!piece || piece.color === playerColor) continue; 116 | 117 | for (const { x: dx, y: dy } of piece.directions) { 118 | let newX: number = x + dx; 119 | let newY: number = y + dy; 120 | 121 | if (!this.areCoordsValid(newX, newY)) continue; 122 | 123 | if (piece instanceof Pawn || piece instanceof Knight || piece instanceof King) { 124 | if (piece instanceof Pawn && dy === 0) continue; 125 | 126 | const attackedPiece: Piece | null = this.chessBoard[newX][newY]; 127 | if (attackedPiece instanceof King && attackedPiece.color === playerColor) { 128 | if (checkingCurrentPosition) this._checkState = { isInCheck: true, x: newX, y: newY }; 129 | return true; 130 | } 131 | } 132 | else { 133 | while (this.areCoordsValid(newX, newY)) { 134 | const attackedPiece: Piece | null = this.chessBoard[newX][newY]; 135 | if (attackedPiece instanceof King && attackedPiece.color === playerColor) { 136 | if (checkingCurrentPosition) this._checkState = { isInCheck: true, x: newX, y: newY }; 137 | return true; 138 | } 139 | 140 | if (attackedPiece !== null) break; 141 | 142 | newX += dx; 143 | newY += dy; 144 | } 145 | } 146 | } 147 | } 148 | } 149 | if (checkingCurrentPosition) this._checkState = { isInCheck: false }; 150 | return false; 151 | } 152 | 153 | private isPositionSafeAfterMove(prevX: number, prevY: number, newX: number, newY: number): boolean { 154 | const piece: Piece | null = this.chessBoard[prevX][prevY]; 155 | if (!piece) return false; 156 | 157 | const newPiece: Piece | null = this.chessBoard[newX][newY]; 158 | // we cant put piece on a square that already contains piece of the same square 159 | if (newPiece && newPiece.color === piece.color) return false; 160 | 161 | // simulate position 162 | this.chessBoard[prevX][prevY] = null; 163 | this.chessBoard[newX][newY] = piece; 164 | 165 | const isPositionSafe: boolean = !this.isInCheck(piece.color, false); 166 | 167 | // restore position back 168 | this.chessBoard[prevX][prevY] = piece; 169 | this.chessBoard[newX][newY] = newPiece; 170 | 171 | return isPositionSafe; 172 | } 173 | 174 | private findSafeSqures(): SafeSquares { 175 | const safeSqures: SafeSquares = new Map(); 176 | 177 | for (let x = 0; x < this.chessBoardSize; x++) { 178 | for (let y = 0; y < this.chessBoardSize; y++) { 179 | const piece: Piece | null = this.chessBoard[x][y]; 180 | if (!piece || piece.color !== this._playerColor) continue; 181 | 182 | const pieceSafeSquares: Coords[] = []; 183 | 184 | for (const { x: dx, y: dy } of piece.directions) { 185 | let newX: number = x + dx; 186 | let newY: number = y + dy; 187 | 188 | if (!this.areCoordsValid(newX, newY)) continue; 189 | 190 | let newPiece: Piece | null = this.chessBoard[newX][newY]; 191 | if (newPiece && newPiece.color === piece.color) continue; 192 | 193 | // need to restrict pawn moves in certain directions 194 | if (piece instanceof Pawn) { 195 | // cant move pawn two squares straight if there is piece infront of him 196 | if (dx === 2 || dx === -2) { 197 | if (newPiece) continue; 198 | if (this.chessBoard[newX + (dx === 2 ? -1 : 1)][newY]) continue; 199 | } 200 | 201 | // cant move pawn one square straight if piece is infront of him 202 | if ((dx === 1 || dx === -1) && dy === 0 && newPiece) continue; 203 | 204 | // cant move pawn diagonally if there is no piece, or piece has same color as pawn 205 | if ((dy === 1 || dy === -1) && (!newPiece || piece.color === newPiece.color)) continue; 206 | } 207 | 208 | if (piece instanceof Pawn || piece instanceof Knight || piece instanceof King) { 209 | if (this.isPositionSafeAfterMove(x, y, newX, newY)) 210 | pieceSafeSquares.push({ x: newX, y: newY }); 211 | } 212 | else { 213 | while (this.areCoordsValid(newX, newY)) { 214 | newPiece = this.chessBoard[newX][newY]; 215 | if (newPiece && newPiece.color === piece.color) break; 216 | 217 | if (this.isPositionSafeAfterMove(x, y, newX, newY)) 218 | pieceSafeSquares.push({ x: newX, y: newY }); 219 | 220 | if (newPiece !== null) break; 221 | 222 | newX += dx; 223 | newY += dy; 224 | } 225 | } 226 | } 227 | 228 | if (piece instanceof King) { 229 | if (this.canCastle(piece, true)) 230 | pieceSafeSquares.push({ x, y: 6 }); 231 | 232 | if (this.canCastle(piece, false)) 233 | pieceSafeSquares.push({ x, y: 2 }); 234 | } 235 | else if (piece instanceof Pawn && this.canCaptureEnPassant(piece, x, y)) 236 | pieceSafeSquares.push({ x: x + (piece.color === Color.White ? 1 : -1), y: this._lastMove!.prevY }); 237 | 238 | if (pieceSafeSquares.length) 239 | safeSqures.set(x + "," + y, pieceSafeSquares); 240 | } 241 | } 242 | 243 | return safeSqures; 244 | } 245 | 246 | private canCaptureEnPassant(pawn: Pawn, pawnX: number, pawnY: number): boolean { 247 | if (!this._lastMove) return false; 248 | const { piece, prevX, prevY, currX, currY } = this._lastMove; 249 | 250 | if ( 251 | !(piece instanceof Pawn) || 252 | pawn.color !== this._playerColor || 253 | Math.abs(currX - prevX) !== 2 || 254 | pawnX !== currX || 255 | Math.abs(pawnY - currY) !== 1 256 | ) return false; 257 | 258 | const pawnNewPositionX: number = pawnX + (pawn.color === Color.White ? 1 : -1); 259 | const pawnNewPositionY: number = currY; 260 | 261 | this.chessBoard[currX][currY] = null; 262 | const isPositionSafe: boolean = this.isPositionSafeAfterMove(pawnX, pawnY, pawnNewPositionX, pawnNewPositionY); 263 | this.chessBoard[currX][currY] = piece; 264 | 265 | return isPositionSafe; 266 | } 267 | 268 | private canCastle(king: King, kingSideCastle: boolean): boolean { 269 | if (king.hasMoved) return false; 270 | 271 | const kingPositionX: number = king.color === Color.White ? 0 : 7; 272 | const kingPositionY: number = 4; 273 | const rookPositionX: number = kingPositionX; 274 | const rookPositionY: number = kingSideCastle ? 7 : 0; 275 | const rook: Piece | null = this.chessBoard[rookPositionX][rookPositionY]; 276 | 277 | if (!(rook instanceof Rook) || rook.hasMoved || this._checkState.isInCheck) return false; 278 | 279 | const firstNextKingPositionY: number = kingPositionY + (kingSideCastle ? 1 : -1); 280 | const secondNextKingPositionY: number = kingPositionY + (kingSideCastle ? 2 : -2); 281 | 282 | if (this.chessBoard[kingPositionX][firstNextKingPositionY] || this.chessBoard[kingPositionX][secondNextKingPositionY]) return false; 283 | 284 | if (!kingSideCastle && this.chessBoard[kingPositionX][1]) return false; 285 | 286 | 287 | 288 | return this.isPositionSafeAfterMove(kingPositionX, kingPositionY, kingPositionX, firstNextKingPositionY) && 289 | this.isPositionSafeAfterMove(kingPositionX, kingPositionY, kingPositionX, secondNextKingPositionY); 290 | } 291 | 292 | public move(prevX: number, prevY: number, newX: number, newY: number, promotedPieceType: FENChar | null): void { 293 | if (this._isGameOver) throw new Error("Game is over, you cant play move"); 294 | 295 | if (!this.areCoordsValid(prevX, prevY) || !this.areCoordsValid(newX, newY)) return; 296 | const piece: Piece | null = this.chessBoard[prevX][prevY]; 297 | if (!piece || piece.color !== this._playerColor) return; 298 | 299 | const pieceSafeSquares: Coords[] | undefined = this._safeSquares.get(prevX + "," + prevY); 300 | if (!pieceSafeSquares || !pieceSafeSquares.find(coords => coords.x === newX && coords.y === newY)) 301 | throw new Error("Square is not safe"); 302 | 303 | if ((piece instanceof Pawn || piece instanceof King || piece instanceof Rook) && !piece.hasMoved) 304 | piece.hasMoved = true; 305 | 306 | const moveType = new Set(); 307 | 308 | const isPieceTaken: boolean = this.chessBoard[newX][newY] !== null; 309 | if (isPieceTaken) moveType.add(MoveType.Capture); 310 | 311 | if (piece instanceof Pawn || isPieceTaken) this.fiftyMoveRuleCounter = 0; 312 | else this.fiftyMoveRuleCounter += 0.5; 313 | 314 | this.handlingSpecialMoves(piece, prevX, prevY, newX, newY, moveType); 315 | // update the board 316 | if (promotedPieceType) { 317 | this.chessBoard[newX][newY] = this.promotedPiece(promotedPieceType); 318 | moveType.add(MoveType.Promotion); 319 | } else { 320 | this.chessBoard[newX][newY] = piece; 321 | } 322 | 323 | this.chessBoard[prevX][prevY] = null; 324 | 325 | this._lastMove = { prevX, prevY, currX: newX, currY: newY, piece, moveType }; 326 | this._playerColor = this._playerColor === Color.White ? Color.Black : Color.White; 327 | this.isInCheck(this._playerColor, true); 328 | const safeSquares: SafeSquares = this.findSafeSqures(); 329 | 330 | if (this._checkState.isInCheck) 331 | moveType.add(!safeSquares.size ? MoveType.CheckMate : MoveType.Check); 332 | else if (!moveType.size) 333 | moveType.add(MoveType.BasicMove); 334 | 335 | this.storeMove(promotedPieceType); 336 | this.updateGameHistory(); 337 | 338 | this._safeSquares = safeSquares; 339 | if (this._playerColor === Color.White) this.fullNumberOfMoves++; 340 | this._boardAsFEN = this.FENConverter.convertBoardToFEN(this.chessBoard, this._playerColor, this._lastMove, this.fiftyMoveRuleCounter, this.fullNumberOfMoves); 341 | this.updateThreeFoldRepetitionDictionary(this._boardAsFEN); 342 | 343 | 344 | this._isGameOver = this.isGameFinished(); 345 | } 346 | 347 | private handlingSpecialMoves(piece: Piece, prevX: number, prevY: number, newX: number, newY: number, moveType: Set): void { 348 | if (piece instanceof King && Math.abs(newY - prevY) === 2) { 349 | // newY > prevY === king side castle 350 | 351 | const rookPositionX: number = prevX; 352 | const rookPositionY: number = newY > prevY ? 7 : 0; 353 | const rook = this.chessBoard[rookPositionX][rookPositionY] as Rook; 354 | const rookNewPositionY: number = newY > prevY ? 5 : 3; 355 | this.chessBoard[rookPositionX][rookPositionY] = null; 356 | this.chessBoard[rookPositionX][rookNewPositionY] = rook; 357 | rook.hasMoved = true; 358 | moveType.add(MoveType.Castling); 359 | } 360 | else if ( 361 | piece instanceof Pawn && 362 | this._lastMove && 363 | this._lastMove.piece instanceof Pawn && 364 | Math.abs(this._lastMove.currX - this._lastMove.prevX) === 2 && 365 | prevX === this._lastMove.currX && 366 | newY === this._lastMove.currY 367 | ) { 368 | this.chessBoard[this._lastMove.currX][this._lastMove.currY] = null; 369 | moveType.add(MoveType.Capture); 370 | } 371 | } 372 | 373 | private promotedPiece(promtoedPieceType: FENChar): Knight | Bishop | Rook | Queen { 374 | if (promtoedPieceType === FENChar.WhiteKnight || promtoedPieceType === FENChar.BlackKnight) 375 | return new Knight(this._playerColor); 376 | 377 | if (promtoedPieceType === FENChar.WhiteBishop || promtoedPieceType === FENChar.BlackBishop) 378 | return new Bishop(this._playerColor); 379 | 380 | if (promtoedPieceType === FENChar.WhiteRook || promtoedPieceType === FENChar.BlackRook) 381 | return new Rook(this._playerColor); 382 | 383 | return new Queen(this._playerColor); 384 | } 385 | 386 | private isGameFinished(): boolean { 387 | if (this.insufficientMaterial()) { 388 | this._gameOverMessage = "Draw due insufficient material"; 389 | return true; 390 | } 391 | 392 | if (!this._safeSquares.size) { 393 | if (this._checkState.isInCheck) { 394 | const prevPlayer: string = this._playerColor === Color.White ? "Black" : "White"; 395 | this._gameOverMessage = prevPlayer + " won by checkmate"; 396 | } 397 | else this._gameOverMessage = "Stalemate"; 398 | 399 | return true; 400 | } 401 | 402 | if (this.threeFoldRepetitionFlag) { 403 | this._gameOverMessage = "Draw due three fold repetition rule"; 404 | return true; 405 | } 406 | 407 | if (this.fiftyMoveRuleCounter === 50) { 408 | this._gameOverMessage = "Draw due fifty move rule"; 409 | return true; 410 | } 411 | 412 | return false; 413 | } 414 | 415 | // Insufficient material 416 | 417 | private playerHasOnlyTwoKnightsAndKing(pieces: { piece: Piece, x: number, y: number }[]): boolean { 418 | return pieces.filter(piece => piece.piece instanceof Knight).length === 2; 419 | } 420 | 421 | private playerHasOnlyBishopsWithSameColorAndKing(pieces: { piece: Piece, x: number, y: number }[]): boolean { 422 | const bishops = pieces.filter(piece => piece.piece instanceof Bishop); 423 | const areAllBishopsOfSameColor = new Set(bishops.map(bishop => ChessBoard.isSquareDark(bishop.x, bishop.y))).size === 1; 424 | return bishops.length === pieces.length - 1 && areAllBishopsOfSameColor; 425 | } 426 | 427 | private insufficientMaterial(): boolean { 428 | const whitePieces: { piece: Piece, x: number, y: number }[] = []; 429 | const blackPieces: { piece: Piece, x: number, y: number }[] = []; 430 | 431 | for (let x = 0; x < this.chessBoardSize; x++) { 432 | for (let y = 0; y < this.chessBoardSize; y++) { 433 | const piece: Piece | null = this.chessBoard[x][y]; 434 | if (!piece) continue; 435 | 436 | if (piece.color === Color.White) whitePieces.push({ piece, x, y }); 437 | else blackPieces.push({ piece, x, y }); 438 | } 439 | } 440 | 441 | // King vs King 442 | if (whitePieces.length === 1 && blackPieces.length === 1) 443 | return true; 444 | 445 | // King and Minor Piece vs King 446 | if (whitePieces.length === 1 && blackPieces.length === 2) 447 | return blackPieces.some(piece => piece.piece instanceof Knight || piece.piece instanceof Bishop); 448 | 449 | else if (whitePieces.length === 2 && blackPieces.length === 1) 450 | return whitePieces.some(piece => piece.piece instanceof Knight || piece.piece instanceof Bishop); 451 | 452 | // both sides have bishop of same color 453 | else if (whitePieces.length === 2 && blackPieces.length === 2) { 454 | const whiteBishop = whitePieces.find(piece => piece.piece instanceof Bishop); 455 | const blackBishop = blackPieces.find(piece => piece.piece instanceof Bishop); 456 | 457 | if (whiteBishop && blackBishop) { 458 | 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); 459 | 460 | return areBishopsOfSameColor; 461 | } 462 | } 463 | 464 | if (whitePieces.length === 3 && blackPieces.length === 1 && this.playerHasOnlyTwoKnightsAndKing(whitePieces) || 465 | whitePieces.length === 1 && blackPieces.length === 3 && this.playerHasOnlyTwoKnightsAndKing(blackPieces) 466 | ) return true; 467 | 468 | if (whitePieces.length >= 3 && blackPieces.length === 1 && this.playerHasOnlyBishopsWithSameColorAndKing(whitePieces) || 469 | whitePieces.length === 1 && blackPieces.length >= 3 && this.playerHasOnlyBishopsWithSameColorAndKing(blackPieces) 470 | ) return true; 471 | 472 | return false; 473 | } 474 | 475 | private updateThreeFoldRepetitionDictionary(FEN: string): void { 476 | const threeFoldRepetitionFENKey: string = FEN.split(" ").slice(0, 4).join(""); 477 | const threeFoldRepetionValue: number | undefined = this.threeFoldRepetitionDictionary.get(threeFoldRepetitionFENKey); 478 | 479 | if (threeFoldRepetionValue === undefined) 480 | this.threeFoldRepetitionDictionary.set(threeFoldRepetitionFENKey, 1); 481 | else { 482 | if (threeFoldRepetionValue === 2) { 483 | this.threeFoldRepetitionFlag = true; 484 | return; 485 | } 486 | this.threeFoldRepetitionDictionary.set(threeFoldRepetitionFENKey, 2); 487 | } 488 | } 489 | 490 | private storeMove(promotedPiece: FENChar | null): void { 491 | const { piece, currX, currY, prevX, prevY, moveType } = this._lastMove!; 492 | let pieceName: string = !(piece instanceof Pawn) ? piece.FENChar.toUpperCase() : ""; 493 | let move: string; 494 | 495 | if (moveType.has(MoveType.Castling)) 496 | move = currY - prevY === 2 ? "O-O" : "O-O-O"; 497 | else { 498 | move = pieceName + this.startingPieceCoordsNotation(); 499 | if (moveType.has(MoveType.Capture)) 500 | move += (piece instanceof Pawn) ? columns[prevY] + "x" : "x"; 501 | move += columns[currY] + String(currX + 1); 502 | 503 | if (promotedPiece) 504 | move += "=" + promotedPiece.toUpperCase(); 505 | } 506 | 507 | if (moveType.has(MoveType.Check)) move += "+"; 508 | else if (moveType.has(MoveType.CheckMate)) move += "#"; 509 | 510 | if (!this._moveList[this.fullNumberOfMoves - 1]) 511 | this._moveList[this.fullNumberOfMoves - 1] = [move]; 512 | else 513 | this._moveList[this.fullNumberOfMoves - 1].push(move); 514 | } 515 | 516 | private startingPieceCoordsNotation(): string { 517 | const { piece: currPiece, prevX, prevY, currX, currY } = this._lastMove!; 518 | if (currPiece instanceof Pawn || currPiece instanceof King) return ""; 519 | 520 | const samePiecesCoords: Coords[] = [{ x: prevX, y: prevY }]; 521 | 522 | for (let x = 0; x < this.chessBoardSize; x++) { 523 | for (let y = 0; y < this.chessBoardSize; y++) { 524 | const piece: Piece | null = this.chessBoard[x][y]; 525 | if (!piece || (currX === x && currY === y)) continue; 526 | 527 | if (piece.FENChar === currPiece.FENChar) { 528 | const safeSquares: Coords[] = this._safeSquares.get(x + "," + y) || []; 529 | const pieceHasSameTargetSquare: boolean = safeSquares.some(coords => coords.x === currX && coords.y === currY); 530 | if (pieceHasSameTargetSquare) samePiecesCoords.push({ x, y }); 531 | } 532 | } 533 | } 534 | 535 | if (samePiecesCoords.length === 1) return ""; 536 | 537 | const piecesFile = new Set(samePiecesCoords.map(coords => coords.y)); 538 | const piecesRank = new Set(samePiecesCoords.map(coords => coords.x)); 539 | 540 | // means that all of the pieces are on different files (a, b, c, ...) 541 | if (piecesFile.size === samePiecesCoords.length) 542 | return columns[prevY]; 543 | 544 | // means that all of the pieces are on different rank (1, 2, 3, ...) 545 | if (piecesRank.size === samePiecesCoords.length) 546 | return String(prevX + 1); 547 | 548 | // in case that there are pieces that shares both rank and a file with multiple or one piece 549 | return columns[prevY] + String(prevX + 1); 550 | } 551 | 552 | private updateGameHistory(): void { 553 | this._gameHistory.push({ 554 | board: [...this.chessBoardView.map(row => [...row])], 555 | checkState: { ...this._checkState }, 556 | lastMove: this._lastMove ? { ...this._lastMove } : undefined 557 | }); 558 | } 559 | } -------------------------------------------------------------------------------- /src/app/chess-logic/models.ts: -------------------------------------------------------------------------------- 1 | import { Piece } from "./pieces/piece"; 2 | 3 | export enum Color { 4 | White, 5 | Black 6 | } 7 | 8 | export type Coords = { 9 | x: number; 10 | y: number; 11 | } 12 | 13 | export enum FENChar { 14 | WhitePawn = "P", 15 | WhiteKnight = "N", 16 | WhiteBishop = "B", 17 | WhiteRook = "R", 18 | WhiteQueen = "Q", 19 | WhiteKing = "K", 20 | BlackPawn = "p", 21 | BlackKnight = "n", 22 | BlackBishop = "b", 23 | BlackRook = "r", 24 | BlackQueen = "q", 25 | BlackKing = "k" 26 | } 27 | 28 | export const pieceImagePaths: Readonly> = { 29 | [FENChar.WhitePawn]: "assets/pieces/white pawn.svg", 30 | [FENChar.WhiteKnight]: "assets/pieces/white knight.svg", 31 | [FENChar.WhiteBishop]: "assets/pieces/white bishop.svg", 32 | [FENChar.WhiteRook]: "assets/pieces/white rook.svg", 33 | [FENChar.WhiteQueen]: "assets/pieces/white queen.svg", 34 | [FENChar.WhiteKing]: "assets/pieces/white king.svg", 35 | [FENChar.BlackPawn]: "assets/pieces/black pawn.svg", 36 | [FENChar.BlackKnight]: "assets/pieces/black knight.svg", 37 | [FENChar.BlackBishop]: "assets/pieces/black bishop.svg", 38 | [FENChar.BlackRook]: "assets/pieces/black rook.svg", 39 | [FENChar.BlackQueen]: "assets/pieces/black queen.svg", 40 | [FENChar.BlackKing]: "assets/pieces/black king.svg" 41 | } 42 | 43 | export type SafeSquares = Map; 44 | 45 | export enum MoveType { 46 | Capture, 47 | Castling, 48 | Promotion, 49 | Check, 50 | CheckMate, 51 | BasicMove 52 | } 53 | 54 | export type LastMove = { 55 | piece: Piece; 56 | prevX: number; 57 | prevY: number; 58 | currX: number; 59 | currY: number; 60 | moveType: Set; 61 | } 62 | 63 | type KingChecked = { 64 | isInCheck: true; 65 | x: number; 66 | y: number; 67 | } 68 | 69 | type KingNotChecked = { 70 | isInCheck: false; 71 | } 72 | 73 | export type CheckState = KingChecked | KingNotChecked; 74 | 75 | export type MoveList = ([string, string?])[]; 76 | 77 | export type GameHistory = { 78 | lastMove: LastMove | undefined; 79 | checkState: CheckState; 80 | board: (FENChar | null)[][]; 81 | }[]; -------------------------------------------------------------------------------- /src/app/chess-logic/pieces/bishop.ts: -------------------------------------------------------------------------------- 1 | import { FENChar, Coords, Color } from "../models"; 2 | import { Piece } from "./piece"; 3 | 4 | export class Bishop extends Piece { 5 | protected override _FENChar: FENChar; 6 | protected override _directions: Coords[] = [ 7 | { x: 1, y: 1 }, 8 | { x: 1, y: -1 }, 9 | { x: -1, y: 1 }, 10 | { x: -1, y: -1 } 11 | ]; 12 | 13 | constructor(private pieceColor: Color) { 14 | super(pieceColor); 15 | this._FENChar = pieceColor === Color.White ? FENChar.WhiteBishop : FENChar.BlackBishop; 16 | } 17 | } -------------------------------------------------------------------------------- /src/app/chess-logic/pieces/king.ts: -------------------------------------------------------------------------------- 1 | import { FENChar, Coords, Color } from "../models"; 2 | import { Piece } from "./piece"; 3 | 4 | export class King extends Piece { 5 | private _hasMoved: boolean = false; 6 | protected override _FENChar: FENChar; 7 | protected override _directions: Coords[] = [ 8 | { x: 0, y: 1 }, 9 | { x: 0, y: -1 }, 10 | { x: 1, y: 0 }, 11 | { x: 1, y: -1 }, 12 | { x: 1, y: 1 }, 13 | { x: -1, y: 0 }, 14 | { x: -1, y: 1 }, 15 | { x: -1, y: -1 } 16 | ]; 17 | 18 | constructor(private pieceColor: Color) { 19 | super(pieceColor); 20 | this._FENChar = pieceColor === Color.White ? FENChar.WhiteKing : FENChar.BlackKing; 21 | } 22 | 23 | public get hasMoved(): boolean { 24 | return this._hasMoved; 25 | } 26 | 27 | public set hasMoved(_) { 28 | this._hasMoved = true; 29 | } 30 | } -------------------------------------------------------------------------------- /src/app/chess-logic/pieces/knight.ts: -------------------------------------------------------------------------------- 1 | import { FENChar, Coords, Color } from "../models"; 2 | import { Piece } from "./piece"; 3 | 4 | export class Knight extends Piece { 5 | protected override _FENChar: FENChar; 6 | protected override _directions: Coords[] = [ 7 | { x: 1, y: 2 }, 8 | { x: 1, y: -2 }, 9 | { x: -1, y: 2 }, 10 | { x: -1, y: -2 }, 11 | { x: 2, y: 1 }, 12 | { x: 2, y: -1 }, 13 | { x: -2, y: 1 }, 14 | { x: -2, y: -1 } 15 | ]; 16 | 17 | constructor(private pieceColor: Color) { 18 | super(pieceColor); 19 | this._FENChar = pieceColor === Color.White ? FENChar.WhiteKnight : FENChar.BlackKnight; 20 | } 21 | } -------------------------------------------------------------------------------- /src/app/chess-logic/pieces/pawn.ts: -------------------------------------------------------------------------------- 1 | import { FENChar, Coords, Color } from "../models"; 2 | import { Piece } from "./piece"; 3 | 4 | export class Pawn extends Piece { 5 | private _hasMoved: boolean = false; 6 | protected override _FENChar: FENChar; 7 | protected override _directions: Coords[] = [ 8 | { x: 1, y: 0 }, 9 | { x: 2, y: 0 }, 10 | { x: 1, y: 1 }, 11 | { x: 1, y: -1 } 12 | ]; 13 | 14 | constructor(private pieceColor: Color) { 15 | super(pieceColor); 16 | if (pieceColor === Color.Black) this.setBlackPawnDirections(); 17 | this._FENChar = pieceColor === Color.White ? FENChar.WhitePawn : FENChar.BlackPawn; 18 | } 19 | 20 | private setBlackPawnDirections(): void { 21 | this._directions = this._directions.map(({ x, y }) => ({ x: -1 * x, y })); 22 | } 23 | 24 | public get hasMoved(): boolean { 25 | return this._hasMoved; 26 | } 27 | 28 | public set hasMoved(_) { 29 | this._hasMoved = true; 30 | this._directions = [ 31 | { x: 1, y: 0 }, 32 | { x: 1, y: 1 }, 33 | { x: 1, y: -1 } 34 | ]; 35 | if (this.pieceColor === Color.Black) this.setBlackPawnDirections(); 36 | } 37 | } -------------------------------------------------------------------------------- /src/app/chess-logic/pieces/piece.ts: -------------------------------------------------------------------------------- 1 | import { Color, Coords, FENChar } from "../models"; 2 | 3 | export abstract class Piece { 4 | protected abstract _FENChar: FENChar; 5 | protected abstract _directions: Coords[]; 6 | 7 | constructor(private _color: Color) { } 8 | 9 | public get FENChar(): FENChar { 10 | return this._FENChar; 11 | } 12 | 13 | public get directions(): Coords[] { 14 | return this._directions; 15 | } 16 | 17 | public get color(): Color { 18 | return this._color; 19 | } 20 | } -------------------------------------------------------------------------------- /src/app/chess-logic/pieces/queen.ts: -------------------------------------------------------------------------------- 1 | import { FENChar, Coords, Color } from "../models"; 2 | import { Piece } from "./piece"; 3 | 4 | export class Queen extends Piece { 5 | protected override _FENChar: FENChar; 6 | protected override _directions: Coords[] = [ 7 | { x: 0, y: 1 }, 8 | { x: 0, y: -1 }, 9 | { x: 1, y: 0 }, 10 | { x: 1, y: -1 }, 11 | { x: 1, y: 1 }, 12 | { x: -1, y: 0 }, 13 | { x: -1, y: 1 }, 14 | { x: -1, y: -1 } 15 | ]; 16 | 17 | constructor(private pieceColor: Color) { 18 | super(pieceColor); 19 | this._FENChar = pieceColor === Color.White ? FENChar.WhiteQueen : FENChar.BlackQueen; 20 | } 21 | } -------------------------------------------------------------------------------- /src/app/chess-logic/pieces/rook.ts: -------------------------------------------------------------------------------- 1 | import { FENChar, Coords, Color } from "../models"; 2 | import { Piece } from "./piece"; 3 | 4 | export class Rook extends Piece { 5 | private _hasMoved: boolean = false; 6 | protected override _FENChar: FENChar; 7 | protected override _directions: Coords[] = [ 8 | { x: 1, y: 0 }, 9 | { x: -1, y: 0 }, 10 | { x: 0, y: 1 }, 11 | { x: 0, y: -1 } 12 | ]; 13 | 14 | constructor(private pieceColor: Color) { 15 | super(pieceColor); 16 | this._FENChar = pieceColor === Color.White ? FENChar.WhiteRook : FENChar.BlackRook; 17 | } 18 | 19 | public get hasMoved(): boolean { 20 | return this._hasMoved; 21 | } 22 | 23 | public set hasMoved(_) { 24 | this._hasMoved = true; 25 | } 26 | } -------------------------------------------------------------------------------- /src/app/modules/chess-board/chess-board.component.css: -------------------------------------------------------------------------------- 1 | .chess-board { 2 | display: flex; 3 | justify-content: center; 4 | flex-direction: column-reverse; 5 | width: 480px; 6 | height: 480px; 7 | } 8 | 9 | .rotated { 10 | transform: rotate(180deg); 11 | } 12 | 13 | .row { 14 | display: flex; 15 | flex-direction: row; 16 | } 17 | 18 | .square { 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | height: 60px; 23 | width: 60px; 24 | cursor: pointer; 25 | border: 1px solid white; 26 | background-color: white; 27 | } 28 | 29 | 30 | .game-over-message { 31 | color: white; 32 | } 33 | 34 | .promotion-dialog { 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | width: 300px; 39 | height: 100px; 40 | } 41 | 42 | .promotion-dialog img { 43 | height: 70px; 44 | cursor: pointer; 45 | } 46 | 47 | .close-promotion-dialog { 48 | font-size: 45px; 49 | cursor: pointer; 50 | color: white; 51 | } 52 | 53 | .dark { 54 | background-color: #779AAF 55 | } 56 | 57 | .light { 58 | background-color: #D9E4E8; 59 | } 60 | 61 | .piece { 62 | width: 50px; 63 | } 64 | 65 | .selected-square { 66 | 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; 67 | } 68 | 69 | .safe-square { 70 | position: absolute; 71 | height: 20px; 72 | width: 20px; 73 | background-color: #bbb; 74 | border-radius: 50%; 75 | z-index: 100; 76 | } 77 | 78 | .king-in-check { 79 | 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; 80 | } 81 | 82 | .last-move { 83 | 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; 84 | } 85 | 86 | .promotion-square { 87 | 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; 88 | } -------------------------------------------------------------------------------- /src/app/modules/chess-board/chess-board.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
14 | 15 |
16 | 17 | 19 |
20 |
21 | 22 | 23 |
24 | 25 | 26 | 27 |

28 | {{gameOverMessage}} 29 |

30 | 31 |
32 | 33 | 34 | 35 | × 36 | 37 |
38 | 39 | 41 | 42 | -------------------------------------------------------------------------------- /src/app/modules/chess-board/chess-board.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { ChessBoard } from 'src/app/chess-logic/chess-board'; 3 | import { CheckState, Color, Coords, FENChar, GameHistory, LastMove, MoveList, MoveType, SafeSquares, pieceImagePaths } from 'src/app/chess-logic/models'; 4 | import { SelectedSquare } from './models'; 5 | import { ChessBoardService } from './chess-board.service'; 6 | import { Subscription, filter, fromEvent, tap } from 'rxjs'; 7 | import { FENConverter } from 'src/app/chess-logic/FENConverter'; 8 | 9 | @Component({ 10 | selector: 'app-chess-board', 11 | templateUrl: './chess-board.component.html', 12 | styleUrls: ['./chess-board.component.css'] 13 | }) 14 | export class ChessBoardComponent implements OnInit, OnDestroy { 15 | public pieceImagePaths = pieceImagePaths; 16 | 17 | protected chessBoard = new ChessBoard(); 18 | public chessBoardView: (FENChar | null)[][] = this.chessBoard.chessBoardView; 19 | public get playerColor(): Color { return this.chessBoard.playerColor; }; 20 | public get safeSquares(): SafeSquares { return this.chessBoard.safeSquares; }; 21 | public get gameOverMessage(): string | undefined { return this.chessBoard.gameOverMessage; }; 22 | 23 | private selectedSquare: SelectedSquare = { piece: null }; 24 | private pieceSafeSquares: Coords[] = []; 25 | private lastMove: LastMove | undefined = this.chessBoard.lastMove; 26 | private checkState: CheckState = this.chessBoard.checkState; 27 | 28 | public get moveList(): MoveList { return this.chessBoard.moveList; }; 29 | public get gameHistory(): GameHistory { return this.chessBoard.gameHistory; }; 30 | public gameHistoryPointer: number = 0; 31 | 32 | // promotion properties 33 | public isPromotionActive: boolean = false; 34 | private promotionCoords: Coords | null = null; 35 | private promotedPiece: FENChar | null = null; 36 | public promotionPieces(): FENChar[] { 37 | return this.playerColor === Color.White ? 38 | [FENChar.WhiteKnight, FENChar.WhiteBishop, FENChar.WhiteRook, FENChar.WhiteQueen] : 39 | [FENChar.BlackKnight, FENChar.BlackBishop, FENChar.BlackRook, FENChar.BlackQueen]; 40 | } 41 | 42 | public flipMode: boolean = false; 43 | private subscriptions$ = new Subscription(); 44 | 45 | constructor(protected chessBoardService: ChessBoardService) { } 46 | 47 | public ngOnInit(): void { 48 | const keyEventSubscription$: Subscription = fromEvent(document, "keyup") 49 | .pipe( 50 | filter(event => event.key === "ArrowRight" || event.key === "ArrowLeft"), 51 | tap(event => { 52 | switch (event.key) { 53 | case "ArrowRight": 54 | if (this.gameHistoryPointer === this.gameHistory.length - 1) return; 55 | this.gameHistoryPointer++; 56 | break; 57 | case "ArrowLeft": 58 | if (this.gameHistoryPointer === 0) return; 59 | this.gameHistoryPointer--; 60 | break; 61 | default: 62 | break; 63 | } 64 | 65 | this.showPreviousPosition(this.gameHistoryPointer); 66 | }) 67 | ) 68 | .subscribe(); 69 | 70 | this.subscriptions$.add(keyEventSubscription$); 71 | } 72 | 73 | public ngOnDestroy(): void { 74 | this.subscriptions$.unsubscribe(); 75 | this.chessBoardService.chessBoardState$.next(FENConverter.initalPosition); 76 | } 77 | 78 | public flipBoard(): void { 79 | this.flipMode = !this.flipMode; 80 | } 81 | 82 | public isSquareDark(x: number, y: number): boolean { 83 | return ChessBoard.isSquareDark(x, y); 84 | } 85 | 86 | public isSquareSelected(x: number, y: number): boolean { 87 | if (!this.selectedSquare.piece) return false; 88 | return this.selectedSquare.x === x && this.selectedSquare.y === y; 89 | } 90 | 91 | public isSquareSafeForSelectedPiece(x: number, y: number): boolean { 92 | return this.pieceSafeSquares.some(coords => coords.x === x && coords.y === y); 93 | } 94 | 95 | public isSquareLastMove(x: number, y: number): boolean { 96 | if (!this.lastMove) return false; 97 | const { prevX, prevY, currX, currY } = this.lastMove; 98 | return x === prevX && y === prevY || x === currX && y === currY; 99 | } 100 | 101 | public isSquareChecked(x: number, y: number): boolean { 102 | return this.checkState.isInCheck && this.checkState.x === x && this.checkState.y === y; 103 | } 104 | 105 | public isSquarePromotionSquare(x: number, y: number): boolean { 106 | if (!this.promotionCoords) return false; 107 | return this.promotionCoords.x === x && this.promotionCoords.y === y; 108 | } 109 | 110 | private unmarkingPreviouslySlectedAndSafeSquares(): void { 111 | this.selectedSquare = { piece: null }; 112 | this.pieceSafeSquares = []; 113 | 114 | if (this.isPromotionActive) { 115 | this.isPromotionActive = false; 116 | this.promotedPiece = null; 117 | this.promotionCoords = null; 118 | } 119 | } 120 | 121 | private selectingPiece(x: number, y: number): void { 122 | if (this.gameOverMessage !== undefined) return; 123 | const piece: FENChar | null = this.chessBoardView[x][y]; 124 | if (!piece) return; 125 | if (this.isWrongPieceSelected(piece)) return; 126 | 127 | const isSameSquareClicked: boolean = !!this.selectedSquare.piece && this.selectedSquare.x === x && this.selectedSquare.y === y; 128 | this.unmarkingPreviouslySlectedAndSafeSquares(); 129 | if (isSameSquareClicked) return; 130 | 131 | this.selectedSquare = { piece, x, y }; 132 | this.pieceSafeSquares = this.safeSquares.get(x + "," + y) || []; 133 | } 134 | 135 | private placingPiece(newX: number, newY: number): void { 136 | if (!this.selectedSquare.piece) return; 137 | if (!this.isSquareSafeForSelectedPiece(newX, newY)) return; 138 | 139 | // pawn promotion 140 | const isPawnSelected: boolean = this.selectedSquare.piece === FENChar.WhitePawn || this.selectedSquare.piece === FENChar.BlackPawn; 141 | const isPawnOnlastRank: boolean = isPawnSelected && (newX === 7 || newX === 0); 142 | const shouldOpenPromotionDialog: boolean = !this.isPromotionActive && isPawnOnlastRank; 143 | 144 | if (shouldOpenPromotionDialog) { 145 | this.pieceSafeSquares = []; 146 | this.isPromotionActive = true; 147 | this.promotionCoords = { x: newX, y: newY }; 148 | // because now we wait for player to choose promoted piece 149 | return; 150 | } 151 | 152 | const { x: prevX, y: prevY } = this.selectedSquare; 153 | this.updateBoard(prevX, prevY, newX, newY, this.promotedPiece); 154 | } 155 | 156 | protected updateBoard(prevX: number, prevY: number, newX: number, newY: number, promotedPiece: FENChar | null): void { 157 | this.chessBoard.move(prevX, prevY, newX, newY, promotedPiece); 158 | this.chessBoardView = this.chessBoard.chessBoardView; 159 | this.markLastMoveAndCheckState(this.chessBoard.lastMove, this.chessBoard.checkState); 160 | this.unmarkingPreviouslySlectedAndSafeSquares(); 161 | this.chessBoardService.chessBoardState$.next(this.chessBoard.boardAsFEN); 162 | this.gameHistoryPointer++; 163 | } 164 | 165 | public promotePiece(piece: FENChar): void { 166 | if (!this.promotionCoords || !this.selectedSquare.piece) return; 167 | this.promotedPiece = piece; 168 | const { x: newX, y: newY } = this.promotionCoords; 169 | const { x: prevX, y: prevY } = this.selectedSquare; 170 | this.updateBoard(prevX, prevY, newX, newY, this.promotedPiece); 171 | } 172 | 173 | public closePawnPromotionDialog(): void { 174 | this.unmarkingPreviouslySlectedAndSafeSquares(); 175 | } 176 | 177 | private markLastMoveAndCheckState(lastMove: LastMove | undefined, checkState: CheckState): void { 178 | this.lastMove = lastMove; 179 | this.checkState = checkState; 180 | 181 | if (this.lastMove) 182 | this.moveSound(this.lastMove.moveType); 183 | else 184 | this.moveSound(new Set([MoveType.BasicMove])); 185 | } 186 | public move(x: number, y: number): void { 187 | this.selectingPiece(x, y); 188 | this.placingPiece(x, y); 189 | } 190 | 191 | private isWrongPieceSelected(piece: FENChar): boolean { 192 | const isWhitePieceSelected: boolean = piece === piece.toUpperCase(); 193 | return isWhitePieceSelected && this.playerColor === Color.Black || 194 | !isWhitePieceSelected && this.playerColor === Color.White; 195 | } 196 | 197 | public showPreviousPosition(moveIndex: number): void { 198 | const { board, checkState, lastMove } = this.gameHistory[moveIndex]; 199 | this.chessBoardView = board; 200 | this.markLastMoveAndCheckState(lastMove, checkState); 201 | this.gameHistoryPointer = moveIndex; 202 | } 203 | 204 | private moveSound(moveType: Set): void { 205 | const moveSound = new Audio("assets/sound/move.mp3"); 206 | 207 | if (moveType.has(MoveType.Promotion)) moveSound.src = "assets/sound/promote.mp3"; 208 | else if (moveType.has(MoveType.Capture)) moveSound.src = "assets/sound/capture.mp3"; 209 | else if (moveType.has(MoveType.Castling)) moveSound.src = "assets/sound/castling.mp3"; 210 | 211 | if (moveType.has(MoveType.CheckMate)) moveSound.src = "assets/sound/checkmate.mp3"; 212 | else if (moveType.has(MoveType.Check)) moveSound.src = "assets/sound/check.mp3"; 213 | 214 | moveSound.play(); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/app/modules/chess-board/chess-board.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { FENConverter } from 'src/app/chess-logic/FENConverter'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class ChessBoardService { 9 | public chessBoardState$ = new BehaviorSubject(FENConverter.initalPosition); 10 | } -------------------------------------------------------------------------------- /src/app/modules/chess-board/models.ts: -------------------------------------------------------------------------------- 1 | import { FENChar } from "src/app/chess-logic/models" 2 | 3 | type SquareWithPiece = { 4 | piece: FENChar; 5 | x: number; 6 | y: number; 7 | } 8 | 9 | type SquareWithoutPiece = { 10 | piece: null; 11 | } 12 | 13 | export type SelectedSquare = SquareWithPiece | SquareWithoutPiece; 14 | 15 | export const columns = ["a", "b", "c", "d", "e", "f", "g", "h"] as const; -------------------------------------------------------------------------------- /src/app/modules/computer-mode/computer-mode.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit, inject } from '@angular/core'; 2 | import { ChessBoardComponent } from '../chess-board/chess-board.component'; 3 | import { StockfishService } from './stockfish.service'; 4 | import { ChessBoardService } from '../chess-board/chess-board.service'; 5 | import { Subscription, firstValueFrom } from 'rxjs'; 6 | import { Color } from 'src/app/chess-logic/models'; 7 | 8 | @Component({ 9 | selector: 'app-computer-mode', 10 | templateUrl: '../chess-board/chess-board.component.html', 11 | styleUrls: ['../chess-board/chess-board.component.css'] 12 | }) 13 | export class ComputerModeComponent extends ChessBoardComponent implements OnInit, OnDestroy { 14 | private computerSubscriptions$ = new Subscription(); 15 | 16 | constructor(private stockfishService: StockfishService) { 17 | super(inject(ChessBoardService)); 18 | } 19 | 20 | public override ngOnInit(): void { 21 | super.ngOnInit(); 22 | 23 | const computerConfiSubscription$: Subscription = this.stockfishService.computerConfiguration$.subscribe({ 24 | next: (computerConfiguration) => { 25 | if (computerConfiguration.color === Color.White) this.flipBoard(); 26 | } 27 | }); 28 | 29 | const chessBoardStateSubscription$: Subscription = this.chessBoardService.chessBoardState$.subscribe({ 30 | next: async (FEN: string) => { 31 | if (this.chessBoard.isGameOver) { 32 | chessBoardStateSubscription$.unsubscribe(); 33 | return; 34 | } 35 | 36 | const player: Color = FEN.split(" ")[1] === "w" ? Color.White : Color.Black; 37 | if (player !== this.stockfishService.computerConfiguration$.value.color) return; 38 | 39 | const { prevX, prevY, newX, newY, promotedPiece } = await firstValueFrom(this.stockfishService.getBestMove(FEN)); 40 | this.updateBoard(prevX, prevY, newX, newY, promotedPiece); 41 | } 42 | }); 43 | 44 | this.computerSubscriptions$.add(chessBoardStateSubscription$); 45 | this.computerSubscriptions$.add(computerConfiSubscription$); 46 | } 47 | 48 | public override ngOnDestroy(): void { 49 | super.ngOnDestroy(); 50 | this.computerSubscriptions$.unsubscribe(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/modules/computer-mode/models.ts: -------------------------------------------------------------------------------- 1 | import { Color, FENChar } from "src/app/chess-logic/models"; 2 | 3 | export type StockfishQueryParams = { 4 | fen: string; 5 | depth: number; 6 | } 7 | 8 | export type ChessMove = { 9 | prevX: number; 10 | prevY: number; 11 | newX: number; 12 | newY: number; 13 | promotedPiece: FENChar | null; 14 | } 15 | 16 | export type StockfishResponse = { 17 | success: boolean; 18 | evaulatuion: number | null; 19 | mate: number | null; 20 | bestmove: string; 21 | continuation: string; 22 | } 23 | 24 | export type ComputerConfiguration = { 25 | color: Color; 26 | level: number; 27 | } 28 | 29 | export const stockfishLevels: Readonly> = { 30 | 1: 10, 31 | 2: 11, 32 | 3: 12, 33 | 4: 13, 34 | 5: 15 35 | } -------------------------------------------------------------------------------- /src/app/modules/computer-mode/stockfish.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpParams } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { ChessMove, ComputerConfiguration, StockfishQueryParams, StockfishResponse, stockfishLevels } from './models'; 4 | import { BehaviorSubject, Observable, of, switchMap } from 'rxjs'; 5 | import { Color, FENChar } from 'src/app/chess-logic/models'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class StockfishService { 11 | private readonly api: string = "https://stockfish.online/api/s/v2.php"; 12 | 13 | public computerConfiguration$ = new BehaviorSubject({ color: Color.Black, level: 1 }); 14 | 15 | constructor(private http: HttpClient) { } 16 | 17 | private convertColumnLetterToYCoord(string: string): number { 18 | return string.charCodeAt(0) - "a".charCodeAt(0); 19 | } 20 | 21 | private promotedPiece(piece: string | undefined): FENChar | null { 22 | if (!piece) return null; 23 | const computerColor: Color = this.computerConfiguration$.value.color; 24 | if (piece === "n") return computerColor === Color.White ? FENChar.WhiteKnight : FENChar.BlackKnight; 25 | if (piece === "b") return computerColor === Color.White ? FENChar.WhiteBishop : FENChar.BlackBishop; 26 | if (piece === "r") return computerColor === Color.White ? FENChar.WhiteRook : FENChar.BlackRook; 27 | return computerColor === Color.White ? FENChar.WhiteQueen : FENChar.BlackQueen; 28 | } 29 | 30 | private moveFromStockfishString(move: string): ChessMove { 31 | const prevY: number = this.convertColumnLetterToYCoord(move[0]); 32 | const prevX: number = Number(move[1]) - 1; 33 | const newY: number = this.convertColumnLetterToYCoord(move[2]); 34 | const newX: number = Number(move[3]) - 1; 35 | const promotedPiece = this.promotedPiece(move[4]); 36 | return { prevX, prevY, newX, newY, promotedPiece }; 37 | } 38 | 39 | public getBestMove(fen: string): Observable { 40 | const queryParams: StockfishQueryParams = { 41 | fen, 42 | depth: stockfishLevels[this.computerConfiguration$.value.level], 43 | }; 44 | 45 | let params = new HttpParams().appendAll(queryParams); 46 | 47 | return this.http.get(this.api, { params }) 48 | .pipe( 49 | switchMap(response => { 50 | const bestMove: string = response.bestmove.split(" ")[1]; 51 | return of(this.moveFromStockfishString(bestMove)); 52 | }) 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/modules/move-list/move-list.component.css: -------------------------------------------------------------------------------- 1 | .move-list { 2 | max-height: 100px; 3 | overflow-y: scroll; 4 | display: flex; 5 | flex-direction: column-reverse; 6 | } 7 | 8 | .move-list::-webkit-scrollbar { 9 | display: none; 10 | } 11 | 12 | .move-list { 13 | -ms-overflow-style: none; 14 | scrollbar-width: none; 15 | } 16 | 17 | .row { 18 | width: 250px; 19 | display: flex; 20 | color: white 21 | } 22 | 23 | .move-number { 24 | width: 50px; 25 | } 26 | 27 | .current-move { 28 | background-color: rgb(0, 143, 168); 29 | } 30 | 31 | .move { 32 | cursor: pointer; 33 | width: 100px; 34 | } 35 | 36 | .move:hover { 37 | background-color: rgb(0, 143, 168); 38 | opacity: 0.7 39 | } -------------------------------------------------------------------------------- /src/app/modules/move-list/move-list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 10 | 11 | 15 | 16 | 20 |
21 | 22 |
23 |
24 |
25 |
{{moveNumber + 1}}.
26 | 27 |
29 | {{move[0]}} 30 |
31 | 32 |
34 | {{move[1]}} 35 |
36 |
37 |
38 |
-------------------------------------------------------------------------------- /src/app/modules/move-list/move-list.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatIconModule } from "@angular/material/icon"; 5 | import { MoveList } from 'src/app/chess-logic/models'; 6 | 7 | @Component({ 8 | selector: 'app-move-list', 9 | templateUrl: './move-list.component.html', 10 | styleUrls: ['./move-list.component.css'], 11 | standalone: true, 12 | imports: [CommonModule, MatButtonModule, MatIconModule] 13 | }) 14 | export class MoveListComponent { 15 | @Input({ required: true }) public moveList!: MoveList; 16 | @Input({ required: true }) public gameHistoryPointer: number = 0; 17 | @Input({ required: true }) public gameHistoryLength: number = 1; 18 | @Output() public showPreviousPositionEvent = new EventEmitter(); 19 | 20 | public showPreviousPosition(moveIndex: number): void { 21 | this.showPreviousPositionEvent.emit(moveIndex); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/modules/nav-menu/nav-menu.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awsomeCStutorials/chess-game/87420327bd8eea68a6d462eac60372daee263441/src/app/modules/nav-menu/nav-menu.component.css -------------------------------------------------------------------------------- /src/app/modules/nav-menu/nav-menu.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/modules/nav-menu/nav-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatToolbarModule } from "@angular/material/toolbar"; 3 | import { MatButtonModule } from "@angular/material/button" 4 | import { RouterModule } from '@angular/router'; 5 | import { MatDialog, MatDialogModule } from "@angular/material/dialog"; 6 | import { PlayAgainstComputerDialogComponent } from '../play-against-computer-dialog/play-against-computer-dialog.component'; 7 | 8 | @Component({ 9 | selector: 'app-nav-menu', 10 | templateUrl: './nav-menu.component.html', 11 | styleUrls: ['./nav-menu.component.css'], 12 | standalone: true, 13 | imports: [MatToolbarModule, MatButtonModule, RouterModule, MatDialogModule] 14 | }) 15 | export class NavMenuComponent { 16 | 17 | constructor(private dialog: MatDialog) { } 18 | 19 | public playAgainstComputer(): void { 20 | this.dialog.open(PlayAgainstComputerDialogComponent); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/modules/play-against-computer-dialog/play-against-computer-dialog.component.css: -------------------------------------------------------------------------------- 1 | .stockfish-strength-container { 2 | display: flex; 3 | justify-content: space-evenly; 4 | } 5 | 6 | .strength-div { 7 | border: 1px solid white; 8 | cursor: pointer; 9 | width: 40px; 10 | height: 40px; 11 | text-align: center; 12 | font-size: 20px; 13 | line-height: 40px; 14 | } 15 | 16 | .choose-side { 17 | display: flex; 18 | justify-content: space-evenly; 19 | } 20 | 21 | .king-img { 22 | width: 50px; 23 | cursor: pointer; 24 | } 25 | 26 | .selected { 27 | background-color: green; 28 | } -------------------------------------------------------------------------------- /src/app/modules/play-against-computer-dialog/play-against-computer-dialog.component.html: -------------------------------------------------------------------------------- 1 | 2 |

Stockfish Strength

3 | 4 |
5 |
7 | {{level}} 8 |
9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/app/modules/play-against-computer-dialog/play-against-computer-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatDialog, MatDialogModule } from "@angular/material/dialog"; 5 | import { StockfishService } from '../computer-mode/stockfish.service'; 6 | import { Color } from 'src/app/chess-logic/models'; 7 | import { Router } from '@angular/router'; 8 | 9 | @Component({ 10 | selector: 'app-play-against-computer-dialog', 11 | templateUrl: './play-against-computer-dialog.component.html', 12 | styleUrls: ['./play-against-computer-dialog.component.css'], 13 | standalone: true, 14 | imports: [MatDialogModule, MatButtonModule, CommonModule] 15 | }) 16 | export class PlayAgainstComputerDialogComponent { 17 | public stockfishLevels: readonly number[] = [1, 2, 3, 4, 5]; 18 | public stockfishLevel: number = 1; 19 | 20 | constructor( 21 | private stockfishService: StockfishService, 22 | private dialog: MatDialog, 23 | private router: Router 24 | ) { } 25 | 26 | public selectStockfishLevel(level: number): void { 27 | this.stockfishLevel = level; 28 | } 29 | 30 | public play(color: "w" | "b"): void { 31 | this.dialog.closeAll(); 32 | this.stockfishService.computerConfiguration$.next({ 33 | color: color === "w" ? Color.Black : Color.White, 34 | level: this.stockfishLevel 35 | }); 36 | this.router.navigate(["against-computer"]); 37 | } 38 | 39 | public closeDialog(): void { 40 | this.router.navigate(["against-friend"]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/routes/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { ChessBoardComponent } from "../modules/chess-board/chess-board.component"; 3 | import { ComputerModeComponent } from "../modules/computer-mode/computer-mode.component"; 4 | import { RouterModule, Routes } from "@angular/router"; 5 | 6 | const routes: Routes = [ 7 | { path: "against-friend", component: ChessBoardComponent, title: "Play against friend" }, 8 | { path: "against-computer", component: ComputerModeComponent, title: "Play against computer" } 9 | 10 | ] 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forRoot(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class AppRoutingModule { } -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awsomeCStutorials/chess-game/87420327bd8eea68a6d462eac60372daee263441/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/pieces/black bishop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pieces/black king.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pieces/black knight.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pieces/black pawn.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pieces/black queen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pieces/black rook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pieces/white bishop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pieces/white king.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pieces/white knight.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pieces/white pawn.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pieces/white queen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pieces/white rook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/sound/capture.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awsomeCStutorials/chess-game/87420327bd8eea68a6d462eac60372daee263441/src/assets/sound/capture.mp3 -------------------------------------------------------------------------------- /src/assets/sound/castling.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awsomeCStutorials/chess-game/87420327bd8eea68a6d462eac60372daee263441/src/assets/sound/castling.mp3 -------------------------------------------------------------------------------- /src/assets/sound/check.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awsomeCStutorials/chess-game/87420327bd8eea68a6d462eac60372daee263441/src/assets/sound/check.mp3 -------------------------------------------------------------------------------- /src/assets/sound/checkmate.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awsomeCStutorials/chess-game/87420327bd8eea68a6d462eac60372daee263441/src/assets/sound/checkmate.mp3 -------------------------------------------------------------------------------- /src/assets/sound/incorrect-move.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awsomeCStutorials/chess-game/87420327bd8eea68a6d462eac60372daee263441/src/assets/sound/incorrect-move.mp3 -------------------------------------------------------------------------------- /src/assets/sound/move.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awsomeCStutorials/chess-game/87420327bd8eea68a6d462eac60372daee263441/src/assets/sound/move.mp3 -------------------------------------------------------------------------------- /src/assets/sound/promote.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awsomeCStutorials/chess-game/87420327bd8eea68a6d462eac60372daee263441/src/assets/sound/promote.mp3 -------------------------------------------------------------------------------- /src/custom-theme.scss: -------------------------------------------------------------------------------- 1 | 2 | // Custom Theming for Angular Material 3 | // For more information: https://material.angular.io/guide/theming 4 | @use '@angular/material' as mat; 5 | // Plus imports for other components in your app. 6 | 7 | // Include the common styles for Angular Material. We include this here so that you only 8 | // have to load a single css file for Angular Material in your app. 9 | // Be sure that you only ever include this mixin once! 10 | @include mat.core(); 11 | 12 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 13 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 14 | // hue. Available color palettes: https://material.io/design/color/ 15 | $chess-game-primary: mat.define-palette(mat.$indigo-palette); 16 | $chess-game-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); 17 | 18 | // The warn palette is optional (defaults to red). 19 | $chess-game-warn: mat.define-palette(mat.$red-palette); 20 | 21 | // Create the theme object. A theme consists of configurations for individual 22 | // theming systems such as "color" or "typography". 23 | $chess-game-theme: mat.define-light-theme(( 24 | color: ( 25 | primary: $chess-game-primary, 26 | accent: $chess-game-accent, 27 | warn: $chess-game-warn, 28 | ) 29 | )); 30 | 31 | // Include theme styles for core and each component used in your app. 32 | // Alternatively, you can import and @include the theme mixins for each component 33 | // that you are using. 34 | @include mat.all-component-themes($chess-game-theme); 35 | 36 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awsomeCStutorials/chess-game/87420327bd8eea68a6d462eac60372daee263441/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ChessGame 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | box-sizing: border-box; 6 | } 7 | 8 | html, 9 | body { 10 | height: 100%; 11 | background-color: black; 12 | } 13 | 14 | body { 15 | margin: 0; 16 | font-family: Roboto, "Helvetica Neue", sans-serif; 17 | } 18 | html, body { height: 100%; } 19 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 20 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------