![](https://image.lichess1.org/display?fmt=webp&h=550&op=thumbnail&path=hollowleaf:ublog:32JsEsrs:msURmib2.webp&w=880&sig=a55bff91f20d3ade6f3e0199c09f18b29281c6b2)
Chess Web Programming: Part Five: Game Review
Creating a Game Review ApplicationBuilding chess web applications has been a fun and challenging journey, combining the strategies of chess with the world of web programming. Since starting this project, I’ve watched Chessboard Magic grow from a simple app into a platform with 34 unique features that make learning and playing chess more interactive and enjoyable.
In my previous blogs, I’ve broken down the steps for anyone interested in building their own chess web app:
- Part One: Getting Started – We created a basic chessboard, allowing users to move pieces and setting up the foundation for managing game states and interactions.
- Part Two: Adding Stockfish – We integrated the Stockfish chess engine to analyze moves and provide insights, making the app engaging and interactive for users looking to deepen their understanding of chess.
- Part Three: Deploying Your Application – We covered how to deploy the app using GitHub Pages, allowing anyone to share their chessboard online. This part included setup instructions for Git, GitHub, and deployment configurations to make the app live.
- Part Four: Customization – We added customizations for the chessboard, allowing users to switch between different piece sets and board themes. These changes made the chessboard visually unique and gave users more control over their experience.
Each part offers straightforward steps and practical tips to help readers build and enhance their chess applications.
In this fifth part, Game Review, we’ll dive into building a game review feature by adding engine lines and a move categorization tool. This feature will assess the quality of each move, highlighting whether it was strong or weak, and providing players with insights to help them learn from each game.
Setting Up the React Application
Note: This guide assumes you’ve installed the necessary software and tools covered in the previous blogs, including Node.js, npm, and any preferred code editor like VSCode. If not, please refer to earlier parts of this series for guidance on installing these tools.
In this section, we’ll start by setting up our React application, which we’ll name gamereview. This will serve as the foundation for building our game review application, where we’ll add a move review feature to analyze chess games. Let’s get started!
1. Create the React Application
First, we need to create our React project. Open your terminal and run the following command:
npx create-react-app gamereview
This will set up a basic React app with everything we need to get started.
2. Navigate to the Project Folder
Once the setup is complete, navigate to the new project folder:
cd gamereview
3. Install Required Dependencies
We’ll be using two main libraries to help us build the game review feature:
- react-chessboard: A React component for displaying an interactive chessboard.
- chess.js: A library that allows us to manage chess game logic, including move validation, piece movement, and move history.
To install these libraries, run the following command in the terminal:
npm install react-chessboard chess.js
4. Add the Folder to Your Workspace in VS Code
- Open Visual Studio Code.
- Go to File > Add Folder to Workspace....
- Select the
gamereview
folder you just created. - This will add the project to your workspace, allowing you to view and edit all files within VS Code easily.
5. Verify the Installation
After installing the dependencies, verify that they’re listed in your package.json
file under dependencies. Your package.json
should include the following:
{
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-scripts": "5.0.1",
"react-chessboard": "^1.2.0",
"chess.js": "^1.0.0"
}
}
6. Create a Basic Chessboard Component in App.js
Now, let’s set up a basic chessboard using react-chessboard
and add an onDrop
function to validate moves.
- Open
App.js
: In your project folder, open theApp.js
file. By default, it will contain some boilerplate code from the React setup. We’ll replace this with a simple chessboard setup. - Import Dependencies: First, import the necessary components from
react-chessboard
andchess.js
at the top of the file.
import React, { useState } from "react";
import { Chessboard } from "react-chessboard";
import Chess from "chess.js";
- Set Up the Chessboard Component with
onDrop
: Next, we’ll initialize a newChess
instance and set up anonDrop
function to validate moves using a try-catch block. This function will try to make a move and catch any errors if the move is invalid.
// Main App component
const App = () => {
// Initialize the game state using useState with a new Chess instance
const [game, setGame] = useState(new Chess());
// Function to handle piece movement on the chessboard
const onDrop = (sourceSquare, targetSquare) => {
// Create a copy of the current game state using FEN notation
const gameCopy = new Chess(game.fen());
try {
// Attempt to make the move on the game copy
const move = gameCopy.move({
from: sourceSquare, // Starting square of the move
to: targetSquare, // Target square of the move
promotion: "q", // Always promote to a queen for simplicity
});
// If the move is invalid, move will be null, so we return false to ignore the move
if (move === null) {
return false;
}
// If the move is valid, update the game state with the new position
setGame(gameCopy);
return true; // Return true to indicate a valid move
} catch (error) {
// Catch and log any errors that occur during the move attempt
console.error(error.message);
return false; // Return false to ignore the invalid move
}
};
return (
<div>
<h1>Game Review</h1>
<Chessboard
position={game.fen()} // Set the chessboard position to the current game state
onPieceDrop={onDrop} // Trigger the onDrop function when a piece is moved
boardWidth={500} // Set the width of the chessboard to 500px
/>
</div>
);
};
export default App; // Export the App component as the default export
7. Run the Application
Now, go back to your terminal and start the React development server:
npm start
This command will open your application in the browser at http://localhost:3000
. You should see a chessboard centered on the screen with a width of 500px. You can now drag and drop pieces, and the onDrop
function will validate moves using chess.js
. If a move is invalid, it will be prevented, and an error will be logged in the console.
Note: The basics of setting up a React application with a chessboard component and validating moves were covered in depth in Part One of this series.
Integrating Stockfish
In this section, we’ll integrate Stockfish, a powerful chess engine, into our React chessboard. Stockfish can evaluate each move and provide feedback on the top 3 recommended moves. This feature will allow users to see the best moves in each position, helping them improve their gameplay.
Let’s walk through each step to set up Stockfish.
1. Download Stockfish Lite
First, download stockfish-16.1-lite-single.js
and stockfish-16.1-lite-single.wasm
from the Stockfish.js GitHub repository. These files contain a lightweight version of the Stockfish engine designed to work in web applications.
2. Add Stockfish Files to public/js
Once downloaded, create a js
folder within the public
directory of your React project. Copy stockfish-16.1-lite-single.js
and stockfish-16.1-lite-single.wasm
into public/js
. Your project structure should now look like this:
gamereview
public
js
stockfish-16.1-lite-single.js
stockfish-16.1-lite-single.wasm
index.html
src
This setup will allow us to reference Stockfish directly from the /js
folder within our React app.
3. Load Stockfish in App.js
To load Stockfish in our application, we’ll create a Web Worker for stockfish-16.1-lite-single.js
. This allows Stockfish to run in a separate thread so it doesn’t slow down our main application.
Here’s the code to set up Stockfish in App.js
:
const App = () => {
const [stockfish, setStockfish] = useState(null);
// Load Stockfish using useEffect when the component mounts
useEffect(() => {
// Create a new Web Worker for Stockfish
const stockfishInstance = new Worker(`${process.env.PUBLIC_URL}/js/stockfish-16.1-lite-single.js`);
setStockfish(stockfishInstance); // Store the Stockfish instance in state
// Clean up by terminating the Stockfish instance when the component unmounts
return () => {
stockfishInstance.terminate();
};
}, []);
};
Explanation:
- Creating the Worker:
const stockfishInstance = new Worker("/js/stockfish-16.1-lite-single.js");
loads Stockfish as a Web Worker from the/js
directory. - Setting State: We store this Stockfish instance in a
useState
variable, making it accessible across the component. - Cleanup: The
return
function inuseEffect
ensures that Stockfish is terminated when the component unmounts, freeing up resources.
4. Write the Evaluation Function
The getEvaluation
function will set up Stockfish to evaluate each board position:
const getEvaluation = (fen) => {
return new Promise((resolve) => {
const lines = []; // Array to store the top 3 lines of evaluations
stockfish.postMessage("setoption name MultiPV value 3"); // Set Stockfish to calculate top 3 PVs
stockfish.postMessage(`position fen ${fen}`); // Set the position to the current FEN
stockfish.postMessage("go depth 12"); // Instruct Stockfish to calculate up to a depth of 12
const isBlackTurn = fen.split(" ")[1] === "b"; // Check if it's Black's turn from the FEN string
// Save the current lines as previous lines before starting a new evaluation
setPreviousLines(currentLines);
// Handle messages from Stockfish
stockfish.onmessage = (event) => {
const message = event.data;
// Only process messages that contain evaluations at depth 12
if (message.startsWith("info depth 12")) {
// Extract the evaluation score and principal variation (move sequence)
const match = message.match(/score cp (-?\d+).* pv (.+)/);
if (match) {
let evalScore = parseInt(match[1], 10) / 100; // Convert centipawn score to pawn units
const moves = match[2].split(" "); // Split moves into an array
// Flip the evaluation score if it's Black's turn
if (isBlackTurn) {
evalScore = -evalScore;
}
// Add the evaluation and moves to the lines array
lines.push({ eval: evalScore, moves });
// Stop and resolve once we have the top 3 lines at depth 12
if (lines.length === 3) {
stockfish.postMessage("stop"); // Stop Stockfish once we have 3 evaluations
// Sort lines based on whose turn it is
lines.sort((a, b) => (isBlackTurn ? a.eval - b.eval : b.eval - a.eval));
// Update currentLines with the new sorted evaluations
setCurrentLines(lines);
resolve(lines); // Resolve the promise with the top 3 lines
}
}
}
};
});
};
Explanation:
- Setting MultiPV:
MultiPV
is set to 3 to request the top 3 moves from Stockfish. - Setting Position and Depth: We set the FEN and depth of 12, instructing Stockfish to analyze the position deeply.
- Handling Stockfish Output:
- We listen for
info depth 12
messages from Stockfish and parse these for evaluation scores and moves. - If it’s Black’s turn, we adjust the evaluation score by multiplying it by
-1
.
- We listen for
- Updating State: Once 3 evaluations are collected, we stop Stockfish and update
currentLines
with the sorted lines.
Full Code
Here’s the complete App.js
file with full comments, which concludes the Integrating Stockfish section:
import React, { useState, useEffect } from "react";
import { Chessboard } from "react-chessboard";
import { Chess } from "chess.js";
const App = () => {
// Initialize chess game state and Stockfish instance
const [game, setGame] = useState(new Chess());
const [stockfish, setStockfish] = useState(null);
const [currentLines, setCurrentLines] = useState([]); // Holds evaluations for the current move
const [previousLines, setPreviousLines] = useState([]); // Holds evaluations for the previous move
// Load Stockfish Web Worker using useEffect when the component mounts
useEffect(() => {
// Create a new Web Worker for Stockfish
const stockfishInstance = new Worker(`${process.env.PUBLIC_URL}/js/stockfish-16.1-lite-single.js`);
setStockfish(stockfishInstance); // Store the Stockfish instance in state
// Clean up by terminating the Stockfish instance when the component unmounts
return () => {
stockfishInstance.terminate();
};
}, []);
// Function to get top 3 evaluations and moves from Stockfish at a depth of 12
const getEvaluation = (fen) => {
return new Promise((resolve) => {
const lines = []; // Array to store the top 3 lines of evaluations
stockfish.postMessage("setoption name MultiPV value 3"); // Set Stockfish to calculate top 3 PVs
stockfish.postMessage(`position fen ${fen}`); // Set the position to the current FEN
stockfish.postMessage("go depth 12"); // Instruct Stockfish to calculate up to a depth of 12
const isBlackTurn = fen.split(" ")[1] === "b"; // Check if it's Black's turn from the FEN string
// Save the current lines as previous lines before starting a new evaluation
setPreviousLines(currentLines);
// Handle messages from Stockfish
stockfish.onmessage = (event) => {
const message = event.data;
// Only process messages that contain evaluations at depth 12
if (message.startsWith("info depth 12")) {
// Extract the evaluation score and principal variation (move sequence)
const match = message.match(/score cp (-?\d+).* pv (.+)/);
if (match) {
let evalScore = parseInt(match[1], 10) / 100; // Convert centipawn score to pawn units
const moves = match[2].split(" "); // Split moves into an array
// Flip the evaluation score if it's Black's turn
if (isBlackTurn) {
evalScore = -evalScore;
}
// Add the evaluation and moves to the lines array
lines.push({ eval: evalScore, moves });
// Stop and resolve once we have the top 3 lines at depth 12
if (lines.length === 3) {
stockfish.postMessage("stop"); // Stop Stockfish once we have 3 evaluations
// Sort lines based on whose turn it is
lines.sort((a, b) => (isBlackTurn ? a.eval - b.eval : b.eval - a.eval));
// Update currentLines with the new sorted evaluations
setCurrentLines(lines);
resolve(lines); // Resolve the promise with the top 3 lines
}
}
}
};
});
};
// onDrop function to handle piece movement and trigger Stockfish evaluation
const onDrop = async (sourceSquare, targetSquare) => {
const gameCopy = new Chess(game.fen()); // Create a copy of the current game
try {
// Attempt to make the move on the game copy
const move = gameCopy.move({
from: sourceSquare,
to: targetSquare,
promotion: "q", // Automatically promote to a queen for simplicity
});
if (move === null) return false; // If move is invalid, return false
setGame(gameCopy); // Update the game state with the new move
// Get top 3 moves and evaluations from Stockfish at depth 12
await getEvaluation(gameCopy.fen());
return true;
} catch (error) {
console.error(error.message); // Log any errors during move attempt
return false;
}
};
return (
<div>
<h1>Game Review with Stockfish</h1>
{/* Chessboard component to display the game board */}
<Chessboard
position={game.fen()} // Set the chessboard position to the current game state
onPieceDrop={onDrop} // Trigger onDrop function when a piece is moved
boardWidth={500} // Set the width of the chessboard to 500px
/>
{/* Display the top 3 evaluation lines */}
<div>
<h2>Top 3 Lines at Depth 12</h2>
<ul style={{ listStyleType: "none", paddingLeft: 0 }}>
{currentLines.map((line, index) => (
<li key={index} style={{ marginBottom: "10px" }}>
<strong>Line {index + 1}:</strong> {line.eval} <br />
<strong>Moves:</strong> {line.moves.join(" ")}
</li>
))}
</ul>
</div>
</div>
);
};
export default App;
With this setup, Stockfish will evaluate each move, displaying the top 3 recommendations with evaluations below the chessboard. This enables users to explore the best moves and improve their understanding of the game.
You should now see the following:
Showing Move Category
In this section, we’ll extend our Chessboard application to categorize each move based on how well it aligns with the best moves according to Stockfish. We'll break down each step, making it easy to understand how we’re adding the “move category” feature to show insights into each move.
Step 1: Set Up State for Move Analysis
To support move categorization, we’ll introduce three new useState
hooks:
bestEvaluation
- Stores the evaluation of the top move from Stockfish.lastMove
- Saves the most recent move played by the user.moveCategory
- Holds the category label for the player’s move.
Add the following code at the beginning of your component:
const [bestEvaluation, setBestEvaluation] = useState(null); // Holds the eval of Stockfish's best move
const [lastMove, setLastMove] = useState(null); // Stores the player's last move
const [moveCategory, setMoveCategory] = useState(""); // Stores the category label of the move
These new states will help us keep track of essential information for categorizing the player’s move.
Step 2: Modify getEvaluation
to Save the Best Move Evaluation
The getEvaluation
function gathers evaluations and moves from Stockfish. We’ll modify it to store only the evaluation score of the top recommended move in bestEvaluation
. This way, we can later use bestEvaluation
to determine if the player’s move was optimal or not.
Add the following inside getEvaluation
after updating currentLines
:
setBestEvaluation(lines[0].eval); // Set bestEvaluation to the eval value of the top move
This stores only the numerical evaluation of Stockfish’s best move for easy comparison in the next steps.
Step 3: Save lastMove
in onDrop
To keep track of the player’s most recent move, update the onDrop
function to save lastMove
every time a new move is made.
Update onDrop
with the following line:
setLastMove(`${sourceSquare}${targetSquare}`); // Save the last move
This code converts the move coordinates (like e2
to e4
) into a single string and stores it in lastMove
.
Step 4: Update PreviousLines
To keep track of the previousLines, we will update this useState before updating the currentLines in the getEvaluation function. By doing this way we have a way to look back one move.
setPreviousLines(currentLines); // Added
setCurrentLines(lines);
Step 5: Create getMoveCategory
to Classify the Player’s Move
We’ll add a new function called getMoveCategory
to categorize the player’s move. The function will classify the move based on whether it matches Stockfish’s top recommendations or how it deviates from the best move’s evaluation score.
Why Use useCallback
?
By using useCallback
, we can ensure that getMoveCategory
is only recreated when its dependencies (bestEvaluation
, lastMove
, and previousLines
) change. This helps improve performance by avoiding unnecessary recalculations and re-renders whenever the component updates.
Importing useCallback
To get started, make sure useCallback
is imported from React at the top of your file. You can import it along with other hooks like this:
import React, { useState, useEffect, useCallback } from "react";
Writing the getMoveCategory
Function
Now, define getMoveCategory
using useCallback
to optimize its performance. Here’s how the function will look:
const getMoveCategory = useCallback(() => {
const previousTopLine = previousLines[0];
const previousSecondLine = previousLines[1];
const previousThirdLine = previousLines[2];
// If any required data is missing, reset move category
if (!bestEvaluation || !lastMove || !previousTopLine) {
setMoveCategory("");
return;
}
// Get the best moves from Stockfish's previous evaluations
const previousTopMove = previousTopLine?.moves[0];
const previousSecondMove = previousSecondLine?.moves[0];
const previousThirdMove = previousThirdLine?.moves[0];
// Categorize move based on whether it matches the best moves or evaluation difference
if (lastMove === previousTopMove) {
setMoveCategory("Top");
} else if (lastMove === previousSecondMove || lastMove === previousThirdMove) {
setMoveCategory("Good Move");
} else {
const evaluationDifference = Math.abs(bestEvaluation - previousTopLine.eval);
// Set move category based on evaluation difference thresholds
if (evaluationDifference <= 1) {
setMoveCategory("Ok");
} else if (evaluationDifference <= 2) {
setMoveCategory("Inaccuracy");
} else if (evaluationDifference >= 3) {
setMoveCategory("Blunder");
}
}
}, [bestEvaluation, lastMove, previousLines]);
The getMoveCategory
function is designed to analyze the player’s last move and categorize it based on how well it aligns with Stockfish’s top recommended moves. Here’s a step-by-step breakdown of the logic:
- Extract Top Recommended Moves:
- The function begins by retrieving the top three moves (or “lines”) from Stockfish’s previous evaluations, stored in
previousLines
. previousTopLine
represents Stockfish’s best recommendation, whilepreviousSecondLine
andpreviousThirdLine
represent the second and third best moves, respectively.
- The function begins by retrieving the top three moves (or “lines”) from Stockfish’s previous evaluations, stored in
- Check for Required Data:
- If any of the key data (best evaluation score, the last move made, or the top recommendation) is missing, the function exits and resets the
moveCategory
to an empty string. - This ensures the function only runs if all necessary information is available.
- If any of the key data (best evaluation score, the last move made, or the top recommendation) is missing, the function exits and resets the
- Direct Match Categorization:
- The function checks if the player’s last move (
lastMove
) matches one of Stockfish’s top three recommendations. - If
lastMove
exactly matchespreviousTopMove
, it is categorized as “Top,” indicating the player made the optimal move. - If
lastMove
matches eitherpreviousSecondMove
orpreviousThirdMove
, it is categorized as a “Good Move,” meaning the player’s move was strong but not the absolute best.
- The function checks if the player’s last move (
- Evaluation Difference for Non-Matching Moves:
- If
lastMove
doesn’t match any of Stockfish’s top three moves, the function calculates an evaluation difference. This difference is the absolute value betweenbestEvaluation
(the score of Stockfish’s best move) andpreviousTopLine.eval
(the evaluation of the player’s move). - By measuring this difference, the function can quantify how much the player’s move deviates from the optimal choice.
- If
- Threshold-Based Categorization:
- Based on the evaluation difference, the function assigns a category to the move:
- Ok: If the difference is within 1 point, the move is close enough to the optimal line to be considered “Ok.”
- Inaccuracy: If the difference is within 2 points, the move is still reasonable but somewhat off from the best choice.
- Blunder: If the difference exceeds 3 points, the move is significantly worse than the best option.
- Based on the evaluation difference, the function assigns a category to the move:
In summary, getMoveCategory
uses both direct comparisons and evaluation-based thresholds to classify moves, providing a clear assessment of the move quality based on Stockfish’s analysis. This gives users immediate feedback on how their move stacks up against the optimal choices.
Note: This is an example of how this function can be written, it does not currently take into account mating evaluations, but that is something you can look into.
Step 6: Trigger getMoveCategory
in useEffect
We need getMoveCategory
to run whenever previousLines
, bestEvaluation
, or lastMove
changes. We’ll use a useEffect
hook to automatically call getMoveCategory
when any of these dependencies update.
Add the following code:
useEffect(() => {
if (previousLines.length > 0 && bestEvaluation !== null && lastMove) {
getMoveCategory();
}
}, [previousLines, bestEvaluation, lastMove, getMoveCategory]);
This ensures that each time a new move is made, getMoveCategory
will determine its category based on Stockfish’s evaluations.
Step 7: Display the Move Category
Finally, let’s display the move category label on the page below the board.
Add the following JSX code within the return block:
<div>
<h3>Move Category: {moveCategory}</h3>
</div>
This displays the category (e.g., "Top," "Good Move," or "Blunder") each time a move is made and evaluated.
Full Code
After completing these steps, here’s the full code:
import React, { useState, useEffect, useCallback } from "react";
import { Chessboard } from "react-chessboard";
import { Chess } from "chess.js";
const App = () => {
// State to manage the chess game and Stockfish instance
const [game, setGame] = useState(new Chess());
const [stockfish, setStockfish] = useState(null);
const [currentLines, setCurrentLines] = useState([]); // Holds evaluations for the current move
const [previousLines, setPreviousLines] = useState([]); // Holds evaluations for the previous move
const [bestEvaluation, setBestEvaluation] = useState(null); // Stores the eval value of the best move
const [lastMove, setLastMove] = useState(null); // Stores the last move played by the user
const [moveCategory, setMoveCategory] = useState(""); // Stores the category of the user's move
// Load Stockfish Web Worker when the component mounts
useEffect(() => {
const stockfishInstance = new Worker(`${process.env.PUBLIC_URL}/js/stockfish-16.1-lite-single.js`);
setStockfish(stockfishInstance);
// Clean up by terminating the Stockfish instance when the component unmounts
return () => {
stockfishInstance.terminate();
};
}, []);
// Function to get top 3 evaluations and moves from Stockfish at a depth of 12
const getEvaluation = (fen) => {
if (!stockfish) return;
return new Promise((resolve) => {
const lines = []; // Array to store the top 3 lines of evaluations
stockfish.postMessage("setoption name MultiPV value 3"); // Set Stockfish to calculate top 3 PVs
stockfish.postMessage(`position fen ${fen}`); // Set the position to the current FEN
stockfish.postMessage("go depth 12"); // Instruct Stockfish to calculate up to a depth of 12
const isBlackTurn = fen.split(" ")[1] === "b"; // Check if it's Black's turn
// Handle messages from Stockfish
stockfish.onmessage = (event) => {
const message = event.data;
// Only process messages that contain evaluations at depth 12
if (message.startsWith("info depth 12")) {
// Extract the evaluation score and principal variation (move sequence)
const match = message.match(/score cp (-?\d+).* pv (.+)/);
if (match) {
let evalScore = parseInt(match[1], 10) / 100; // Convert centipawn score to pawn units
const moves = match[2].split(" "); // Split moves into an array
// Flip the evaluation score if it's Black's turn
if (isBlackTurn) {
evalScore = -evalScore;
}
// Add the evaluation and moves to the lines array
lines.push({ eval: evalScore, moves });
// Stop and resolve once we have the top 3 lines at depth 12
if (lines.length === 3) {
stockfish.postMessage("stop"); // Stop Stockfish once we have 3 evaluations
// Sort lines based on whose turn it is
lines.sort((a, b) => (isBlackTurn ? a.eval - b.eval : b.eval - a.eval));
// Update previousLines with the current currentLines before refreshing currentLines
setPreviousLines(currentLines);
setCurrentLines(lines);
// Set bestEvaluation to the eval value of the top line for comparison
setBestEvaluation(lines[0].eval);
resolve(lines); // Resolve the promise with the top 3 lines
}
}
}
};
});
};
// Function to determine and set the category of the last move based on evaluation
const getMoveCategory = useCallback(() => {
const previousTopLine = previousLines[0];
const previousSecondLine = previousLines[1];
const previousThirdLine = previousLines[2];
// If any required data is missing, reset move category
if (!bestEvaluation || !lastMove || !previousTopLine) {
setMoveCategory("");
return;
}
// Get the best moves from Stockfish's previous evaluations
const previousTopMove = previousTopLine?.moves[0];
const previousSecondMove = previousSecondLine?.moves[0];
const previousThirdMove = previousThirdLine?.moves[0];
// Categorize move based on whether it matches the best moves or evaluation difference
if (lastMove === previousTopMove) {
setMoveCategory("Top");
} else if (lastMove === previousSecondMove || lastMove === previousThirdMove) {
setMoveCategory("Good Move");
} else {
const evaluationDifference = Math.abs(bestEvaluation - previousTopLine.eval);
// Set move category based on evaluation difference thresholds
if (evaluationDifference <= 1) {
setMoveCategory("Ok");
} else if (evaluationDifference <= 2) {
setMoveCategory("Inaccuracy");
} else if (evaluationDifference >= 3) {
setMoveCategory("Blunder");
}
}
}, [bestEvaluation, lastMove, previousLines]);
// Trigger getMoveCategory whenever evaluations or the last move change
useEffect(() => {
if (previousLines.length > 0 && bestEvaluation !== null && lastMove) {
getMoveCategory();
}
}, [previousLines, bestEvaluation, lastMove, getMoveCategory]);
// Handle piece movement on the chessboard and trigger Stockfish evaluation
const onDrop = async (sourceSquare, targetSquare) => {
const gameCopy = new Chess(game.fen()); // Create a copy of the current game
try {
// Attempt to make the move on the game copy
const move = gameCopy.move({
from: sourceSquare,
to: targetSquare,
promotion: "q", // Automatically promote to a queen for simplicity
});
// If the move is invalid, exit the function
if (move === null) return false;
setGame(gameCopy); // Update the game state with the new move
setLastMove(`${sourceSquare}${targetSquare}`); // Save the last move played
// Get top 3 moves and evaluations from Stockfish at depth 12
await getEvaluation(gameCopy.fen());
return true;
} catch (error) {
console.error(error.message); // Log any errors during move attempt
return false;
}
};
return (
<div>
<h1>Game Review with Stockfish</h1>
{/* Chessboard component to display the game board */}
<Chessboard position={game.fen()} onPieceDrop={onDrop} boardWidth={500} />
{/* Display the top 3 evaluation lines */}
<div>
<h2>Top 3 Lines at Depth 12</h2>
<ul style={{ listStyleType: "none", paddingLeft: 0 }}>
{currentLines.map((line, index) => (
<li key={index} style={{ marginBottom: "10px" }}>
<strong>Line {index + 1}:</strong> {line.eval} <br />
<strong>Moves:</strong> {line.moves.join(" ")}
</li>
))}
</ul>
</div>
{/* Display the move category */}
<div>
<h3>Move Category: {moveCategory}</h3>
</div>
</div>
);
};
export default App;
You should now see the following:
Summary
In this project, we’ve developed an interactive chessboard application that not only allows users to play moves but also evaluates each move in real-time using Stockfish, a powerful chess engine. Our application provides insights into each move by categorizing it based on how closely it aligns with the engine’s top recommendations, labeling moves as “Top,” “Good Move,” “Ok,” “Inaccuracy,” or “Blunder.” This categorization allows users to understand the quality of each move, helping them improve their play and learn from Stockfish's analysis.
This project is a basic example of a game review tool, designed to showcase what can be achieved with a chess engine like Stockfish integrated into a web application. There’s plenty of potential for further development: we could fine-tune the move categorization, implement logic to identify theoretical or "book" moves, display visual representations of suggested moves on the board, or add customization options. With these enhancements, the application could offer a more comprehensive and engaging experience for users looking to analyze and learn from their games.
I hope you have enjoyed this five-part series on building a Chess Web Application. Feel free to reach out with any questions or leave a comment—I’d love to hear your thoughts!
Learn more
- React – A JavaScript library for building user interfaces.
- react-chessboard – A React component for rendering a chessboard.
- chess.js – A library for handling chess game rules and move validation.
- Stockfish – A powerful open-source chess engine.
- stockfish.js – A JavaScript and WebAssembly version of Stockfish for web applications.
- CSS – A styling language used to design and customize HTML elements.
More blog posts by HollowLeaf
![](https://image.lichess1.org/display?fmt=webp&h=250&op=thumbnail&path=hollowleaf:ublog:2RcZvKl8:1YUIL7bd.webp&w=400&sig=09e3f04070a55b6cccc93b0aaefe4aa320846350)
Chess Web Programming: Part Nine: Chessground
Exploring Lichess Chessground![](https://image.lichess1.org/display?fmt=webp&h=250&op=thumbnail&path=hollowleaf:ublog:mGHcVFEJ:e1pkhJis.webp&w=400&sig=72101f61f94769c6f9e3662a7af4bbe75c392ef2)
Chess Web Programming: Part Eight: Chess.com API
Working with the Chess.com API![](https://image.lichess1.org/display?fmt=webp&h=250&op=thumbnail&path=hollowleaf:ublog:6MgfKoeh:3Swlww3A.webp&w=400&sig=9999180e0cdc0d3634cb127fbfb79cdeae6bf04b)
Chess Web Programming: Part Seven: Lichess API
Working with the Lichess API![](https://image.lichess1.org/display?fmt=webp&h=250&op=thumbnail&path=hollowleaf:ublog:I9wcO6Mf:U7sSJuJE.jpg&w=400&sig=3a646f6321aea041d076f5e900745324ead27bb7)