Building a Blazor Chess AI App

TJ GokkenTJ Gokken
9 min read

Introduction

I love playing chess. Although I have not been playing much lately, I love watching videos of famous games and try to learn from them.

With the rise of Machine Learning and Artificial Intelligence, I thought of writing a chess app for a while. It is something I have never done before and at the end, some aspects of it proved to be pretty challenging. Although the final application leaves a lot of room for improvement, it is playable with basic rules implemented and it was a great learning experience.

Tech Stack

For the user interface, I wanted something simple and interactive. I certainly did not want to over-engineer this app. I also needed an opponent and AI seemed like a good fit for that purpose.

With that in mind, I chose Blazor WebAssembly and decided to use Stockfish as it is one of the most powerful open-source chess engines. I wanted a powerful, adjustable AI that could provide a challenge for players of different skill levels without having to implement complex chess algorithms myself.

Overview of the Solution

Blazor WebAssembly runs directly in the browser, allowing us to create a responsive, client-side application. This decision brought its own set of challenges and benefits, which I'll explore throughout this post.

The solution consists of several components:

  • Models: Defines the chess pieces, moves, and board.

  • Game: Game related files.

  • Pages: Contains the user interface for the chessboard and controls.

  • Services: Contains the logic for interacting with the Stockfish engine.

  • wwwroot: Holds static assets like images for the chess pieces.

Here is an overview of the solution structure:

Chess Rules

I wanted to build a simple yet still powerful chess app. To accomplish my goal, I took some shortcuts when implementing the game.

  • The user is always white and starts first.

  • AI movement is very fast, so I introduced a one-second delay before it makes its move.

Chess rules are complicated. Various pieces move in different ways, but particularly the knight is interesting. Castling is also another one. This meant that I needed to implement various checks:

  • Piece move validation

  • Check, checkmate and stalemate detection

  • Special moves like castling and en passant.

Chess App

1. Chess Board UI

The chessboard UI is built using Blazor components. Each square is represented by a component, and chess pieces are dynamically rendered based on the game state. This is achieved by binding the chessboard model to the UI and updating the display when moves are made.

Our only UI page, Index, looks like below:

@page "/"
@using ChessApp.Models
@using ChessAI.Models
@using ChessAI.Services
@inject StockfishService StockfishService

<div class="header">
    <h1>Chess AI</h1>
    <button @onclick="ResetBoard" class="new-game-button">New Game</button>
</div>

<!-- Spinner overlay, positioned absolutely so it doesn't push the board down -->
@if (isThinking)
{
    <div class="spinner-overlay">
        <div class="spinner-border" role="status">
        </div>
    </div>
}

<div class="chess-board-container">
    <table class="chess-board">
        <!-- Column letters (A-H) at the top and bottom -->
        @RenderColumnLabels()

        <!-- Chessboard with row numbers on the sides -->
        @for (var row = 7; row >= 0; row--)
        {
            <tr>
                <td class="row-label">@GetRowLabel(row)</td> <!-- Row number on the left side -->
                @for (var col = 0; col < 8; col++)
                {
                    var currentRow = row;
                    var currentCol = col;
                    <td class="square @GetSquareColorClass(GetSquareColor(currentRow, currentCol))"
                        ondragover="event.preventDefault();"
                        @ondrop="@(() => DropPiece(currentRow, currentCol))"
                        @ondragover:preventDefault>
                        @if (Board.Pieces[currentRow, currentCol] != null)
                        {
                            <div class="piece @GetPieceColor(Board.Pieces[currentRow, currentCol])"
                                 draggable="true"
                                 @ondragstart="@(() => DragPiece(currentRow, currentCol))"
                                 @ondragend="@DragEnd">
                                <img src="@GetPieceImagePath(Board.Pieces[currentRow, currentCol])" class="chess-piece-img" />
                            </div>
                        }
                    </td>
                }
                <td class="row-label">@GetRowLabel(row)</td> <!-- Row number on the right side -->
            </tr>
        }

        @RenderColumnLabels()
    </table>
</div>

@if (IsGameOver)
{
    <div class="game-over-message">
        @GameOverMessage
    </div>
}

This piece of code draws the chess board and puts the pieces on it. Here is the relevant code behind:

private ChessBoard Board { get; set; } = new();
    private GameState State { get; set; }
    private (int Row, int Col)? DraggedPiecePosition { get; set; }
    private bool IsGameOver { get; set; }
    private string GameOverMessage { get; set; } = "";
    private bool isThinking;

    private static readonly string[] ColumnLabels = { "A", "B", "C", "D", "E", "F", "G", "H" };

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        ResetBoard();
        await StockfishService.InitializeEngine();
    }

    private void ResetBoard()
    {
        Board = new ChessBoard();
        State = new GameState();
        Board.InitializeBoard();
        State.ResetGame();
        IsGameOver = false;
        GameOverMessage = "";
        StateHasChanged();
    }

    private RenderFragment RenderColumnLabels() =>@<tr>
            <td></td>
            @foreach (var col in ColumnLabels)
            {
                <td class="column-label">@col</td>
            }
            <td></td>
        </tr>;

   private string GetRowLabel(int rowIndex) => (8 - rowIndex).ToString();

   private enum SquareColor { Light, Dark }

   private SquareColor GetSquareColor(int row, int col) => (row + col) % 2 == 0 ? SquareColor.Light : SquareColor.Dark;

   private string GetSquareColorClass(SquareColor color) => color == SquareColor.Light ? "light" : "dark";

   private string GetPieceColor(ChessPiece piece) => piece.Color == ChessPiece.PieceColor.White ? "white-piece" : "black-piece";

   private string GetPieceImagePath(ChessPiece piece)
   {
       var color = piece.Color == ChessPiece.PieceColor.White ? "w" : "b";
       var pieceType = piece.Type switch
       {
           ChessPiece.PieceType.Pawn => "p",
           ChessPiece.PieceType.Rook => "r",
           ChessPiece.PieceType.Knight => "n",
           ChessPiece.PieceType.Bishop => "b",
           ChessPiece.PieceType.Queen => "q",
           ChessPiece.PieceType.King => "k",
           _ => throw new ArgumentException("Invalid piece type")
       };
       return $"/images/chess-pieces/{color}{pieceType}.svg";
   }

The chess pieces are downloaded from https://github.com/lichess-org/lila/tree/master/public/piece/cburnett.

This is how it looks when the application loads:

If you look at this code;

@if (Board.Pieces[currentRow, currentCol] != null)
                        {
                            <div class="piece @GetPieceColor(Board.Pieces[currentRow, currentCol])"
                                 draggable="true"
                                 @ondragstart="@(() => DragPiece(currentRow, currentCol))"
                                 @ondragend="@DragEnd">
                                <img src="@GetPieceImagePath(Board.Pieces[currentRow, currentCol])" class="chess-piece-img" />
                            </div>
                        }

you will see that dragging is also handled by Blazor. If you look at the source code in INdex.razor, you will see this code:

 private async Task MakeAIMove()
   {
       if (!IsGameOver)
       {
           isThinking = true;
           StateHasChanged();

           var fen = Board.GetFEN();
           var bestMove = await StockfishService.GetBestMove(fen);

           await Task.Delay(1000);

           isThinking = false;
           StateHasChanged();

           var move = ChessMove.FromUci(bestMove);
           Board.ApplyMove(move);

           StateHasChanged();
       }
   }

This is how we are using Stockfish to make its move. One line there;

var fen = Board.GetFEN();

So, what is FEN? Forsyth–Edwards Notation (FEN) is a standard notation for describing a particular board position of a chess game. The purpose of FEN is to provide all the necessary information to restart a game from a particular position. As can be seen from the next line, Stockfish needs this information to determine the next best move.

Speaking of Stockfish…

2. Interacting with the Stockfish Engine

The AI component is handled by the StockfishService, which communicates with the Stockfish chess engine to calculate the best moves. This service runs Stockfish in the background, sending the current board state in FEN notation and receiving the AI's best move.

Basically, you copy Sotckfish.wasm file to wwwroot and we initialize it in a js file and then we reference that file in our index.html.

let stockfish;

function initializeStockfish() {
    return new Promise((resolve) => {
        stockfish = new Worker('stockfish.js');
        stockfish.onmessage = function (event) {
            if (event.data.startsWith("readyok")) {
                resolve();
            }
        };
        stockfish.postMessage('uci');
        stockfish.postMessage('isready');
    });
}

function getBestMove(fen, depth) {
    return new Promise((resolve) => {
        stockfish.onmessage = function (event) {
            if (event.data.startsWith("bestmove")) {
                resolve(event.data.split(" ")[1]);
            }
        };
        stockfish.postMessage(`position fen ${fen}`);
        stockfish.postMessage(`go depth ${depth}`);
    });
}

window.initializeStockfish = initializeStockfish;
window.getBestMove = getBestMove;

3. Game Logic and Validation

The Models folder contains the core game logic. This includes:

  • ChessPiece: Represents the type and color of each chess piece.

  • ChessBoard: Handles the state of the board, move validation, and game rules (like check, stalemate and checkmate detection).

  • ChessMove: Encapsulates each move, keeping track of source and destination squares.

ChessBoard.cs is the bread and butter of the application. It is fairly large, so I added high-level comments for each method describing what the method does.

One of the challenges was not to fall into an infinite recursion when implementing game rules. This is certainly true for GetValidMoves method.

Initially, this method checked if each move would leave the king in check. However, to determine if a move leaves the king in check, we needed to simulate the move and then check if any opponent piece could capture the king. This led to a recursive problem:

  • GetValidMoves calls IsInCheck

  • IsInCheck needs to get valid moves for opponent pieces

  • This calls GetValidMoves again, leading to infinite recursion

To break this recursive loop, I introduced GetValidMovesWithoutCheck. This method generates all possible moves for a piece without considering whether they leave the king in check. Then, in GetValidMoves, we use GetValidMovesWithoutCheck and filter out the moves that would leave the king in check:

 public List<(int, int)> GetValidMoves(ChessPiece piece)
    {
        var moves = GetValidMovesWithoutCheckTest(piece);

        // Filter out moves that would leave the king in check
        return moves.Where(move => !WouldLeaveKingInCheck(piece, move)).ToList();
    }

This is also helpful as we can use this method to generate potential moves for the opponent without causing a recursive loop when checking for check or checkmate.

Challenges and Solutions

1. King Safety

Making sure that the king cannot move into check was one of the major challenges. The solution involved a function that checks if any opponent pieces can attack the square the king intends to move to.

csharpCopy codepublic bool IsSquareUnderAttack(int row, int col, PieceColor attackerColor)
{
    // Check if any opponent piece can move to (row, col)
    return Pieces.Any(piece => piece.CanAttack(row, col));
}

2. User Experience (UX) Improvements

  • AI Thinking Time: Instead of having the AI respond instantly, I added a slight delay to simulate "thinking time," making the game feel more natural.

  • Error Handling: Also implemented error handling to display a user-friendly message if something goes wrong (such as when Stockfish is not properly configured) as Blazor only displayed the ever-friendly message that there was an error.

💡
You may already know this, but debugging Blazor webAssembly is not as straight forward. If nothing else, you’ll see that none of your breakpoints are hit. Fortunately, the solution is easy. Just select Microsoft Edge as your browser in Visual Studio and hit F5. You will see that you can debug your running code.

Running the Application

Once everything is set up, running the application is as simple as starting the Blazor WebAssembly project. You can play against the Stockfish AI directly in your browser, with smooth interactions provided by Blazor and powerful move analysis by Stockfish.

To start a new game or reset the board, simply click the New Game button. The AI will calculate its move after you make yours, offering a challenging opponent every time.

Here is the source code: https://github.com/tjgokken/ChessAI

Conclusion

Building this chess game was a challenging but rewarding experience. When you are playing chess, you kind of execute the moves automatically but you never think about them. This project definitely made me think of all the rules.

Although the application runs fine as is, it can be improved a lot. For example, we can show potential squares that a piece can move to when it is “picked up”, it can load various games from any point, adjusting Stockfish difficulty levels, adding a history of the moves, and selecting which side you want to play as (right now, you are white and that’s it).

We can even enhance the application further by using Azure Speech Service by making the game more accessible. We can use Azure ML to analyze games and suggest improvements to players. Actually these two Azure related improvements might be in the books, so watch this space!

0
Subscribe to my newsletter

Read articles from TJ Gokken directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

TJ Gokken
TJ Gokken