#include <SFML/Graphics.hpp>
#include <array>
#include <vector>
#include <algorithm>
#include <cmath>
#include <iostream>
#include <random>
#include <set>
#include <ranges>
constexpr int boardSize = 8;
constexpr float tileSize = 75.f;
constexpr int blackOffset = 0;
constexpr int whiteOffset = 6;
const sf::Color lightColor(245, 222, 179);
const sf::Color darkColor(139, 69, 19);
// Maximum recursion depth for checkmate check
constexpr int MAX_DEPTH = 3;
enum class PieceType { Pawn, Knight, Bishop, Rook, Queen, King };
enum class PieceColor { Black, White };
struct Piece {
PieceType type;
PieceColor color;
sf::Sprite sprite;
Piece(PieceType t, PieceColor c) : type(t), color(c) {}
};
struct GameState {
std::vector<Piece> pieces;
PieceColor currentTurn = PieceColor::White;
int fiftyMoveCounter = 0;
int moveCount = 0;
std::vector<GameState> history;
};
bool loadTextures(std::array<sf::Texture, 12>& textures)
{
const std::array<std::string, 12> fileNames = {
"images/bpawn.png", "images/bknight.png", "images/bbishop.png",
"images/brook.png", "images/bqueen.png", "images/bking.png",
"images/wpawn.png", "images/wknight.png", "images/wbishop.png",
"images/wrook.png", "images/wqueen.png", "images/wking.png"
};
for (size_t i = 0; i < textures.size(); ++i)
{
if (!textures[i].loadFromFile(fileNames[i]))
{
std::cerr << "Failed to load image: " << fileNames[i] << std::endl;
return false;
}
}
return true;
}
void drawBoard(sf::RenderWindow& window)
{
for (int row = 0; row < boardSize; ++row)
{
for (int col = 0; col < boardSize; ++col)
{
sf::RectangleShape tile(sf::Vector2f(tileSize, tileSize));
tile.setPosition(static_cast<float>(col) * tileSize, static_cast<float>(row) * tileSize);
tile.setFillColor((row + col) % 2 == 0 ? lightColor : darkColor);
window.draw(tile);
}
}
}
void placePieces(GameState& gameState, const std::array<sf::Texture, 12>& textures)
{
gameState.pieces.clear();
auto addPiece = [&](PieceType type, PieceColor color, int row, int col) {
Piece piece(type, color);
int textureIndex = static_cast<int>(type) + (color == PieceColor::White ? whiteOffset : blackOffset);
piece.sprite.setTexture(textures[textureIndex]);
piece.sprite.setPosition(static_cast<float>(col) * tileSize, static_cast<float>(row) * tileSize);
piece.sprite.setScale(tileSize / static_cast<float>(textures[textureIndex].getSize().x),
tileSize / static_cast<float>(textures[textureIndex].getSize().y));
gameState.pieces.push_back(piece);
};
for (int i = 0; i < 8; ++i)
{
addPiece(PieceType::Pawn, PieceColor::Black, 1, i);
addPiece(PieceType::Pawn, PieceColor::White, 6, i);
}
const std::array<PieceType, 8> backRowOrder = {
PieceType::Rook, PieceType::Knight, PieceType::Bishop, PieceType::Queen,
PieceType::King, PieceType::Bishop, PieceType::Knight, PieceType::Rook
};
for (int i = 0; i < 8; ++i)
{
addPiece(backRowOrder[i], PieceColor::Black, 0, i);
addPiece(backRowOrder[i], PieceColor::White, 7, i);
}
}
bool isCellEmpty(int row, int col, const GameState& gameState)
{
return std::none_of(gameState.pieces.begin(), gameState.pieces.end(),
[row, col](const Piece& piece) {
return static_cast<int>(piece.sprite.getPosition().x / tileSize) == col &&
static_cast<int>(piece.sprite.getPosition().y / tileSize) == row;
});
}
bool isPathBlocked(int startRow, int startCol, int endRow, int endCol, const GameState& gameState)
{
int dRow = endRow - startRow;
int dCol = endCol - startCol;
int steps = std::max(std::abs(dRow), std::abs(dCol));
int rowStep = (dRow != 0) ? dRow / std::abs(dRow) : 0;
int colStep = (dCol != 0) ? dCol / std::abs(dCol) : 0;
for (int i = 1; i < steps; ++i)
{
int checkRow = startRow + i * rowStep;
int checkCol = startCol + i * colStep;
if (!isCellEmpty(checkRow, checkCol, gameState))
return true;
}
return false;
}
bool isLegalMove(const Piece& piece, int startRow, int startCol, int endRow, int endCol, const GameState& gameState)
{
// Check if the end position is within the board
if (endRow < 0 || endRow >= boardSize || endCol < 0 || endCol >= boardSize)
return false;
int dRow = endRow - startRow;
int dCol = endCol - startCol;
// Check if the destination is occupied by a piece of the same color
for (const auto& otherPiece : gameState.pieces)
{
if (static_cast<int>(otherPiece.sprite.getPosition().x / tileSize) == endCol &&
static_cast<int>(otherPiece.sprite.getPosition().y / tileSize) == endRow &&
otherPiece.color == piece.color)
return false;
}
switch (piece.type)
{
case PieceType::Pawn:
{
int direction = (piece.color == PieceColor::White) ? -1 : 1;
bool isFirstMove = (piece.color == PieceColor::White && startRow == 6) || (piece.color == PieceColor::Black && startRow == 1);
// Move forward
if (dCol == 0 && dRow == direction && isCellEmpty(endRow, endCol, gameState))
return true;
// First move can be two squares
if (dCol == 0 && dRow == 2 * direction && isFirstMove &&
isCellEmpty(startRow + direction, startCol, gameState) &&
isCellEmpty(endRow, endCol, gameState))
return true;
// Capture diagonally
if (std::abs(dCol) == 1 && dRow == direction)
{
for (const auto& otherPiece : gameState.pieces)
{
if (static_cast<int>(otherPiece.sprite.getPosition().x / tileSize) == endCol &&
static_cast<int>(otherPiece.sprite.getPosition().y / tileSize) == endRow &&
otherPiece.color != piece.color)
return true;
}
}
return false;
}
case PieceType::Knight:
return (std::abs(dRow) == 2 && std::abs(dCol) == 1) || (std::abs(dRow) == 1 && std::abs(dCol) == 2);
case PieceType::Bishop:
if (std::abs(dRow) == std::abs(dCol))
return !isPathBlocked(startRow, startCol, endRow, endCol, gameState);
return false;
case PieceType::Rook:
if (dRow == 0 || dCol == 0)
return !isPathBlocked(startRow, startCol, endRow, endCol, gameState);
return false;
case PieceType::Queen:
if (dRow == 0 || dCol == 0 || std::abs(dRow) == std::abs(dCol))
return !isPathBlocked(startRow, startCol, endRow, endCol, gameState);
return false;
case PieceType::King:
// Normal move
if (std::abs(dRow) <= 1 && std::abs(dCol) <= 1)
return true;
// Castling
if (std::abs(dCol) == 2 && dRow == 0)
{
// Check if it's the king's first move
if ((piece.color == PieceColor::White && startRow != 7) || (piece.color == PieceColor::Black && startRow != 0))
return false;
// Check if the path is clear
int rookCol = (dCol > 0) ? 7 : 0;
return !isPathBlocked(startRow, startCol, startRow, rookCol, gameState);
}
return false;
default:
return false;
}
}
bool isCheck(const GameState& gameState, PieceColor kingColor)
{
int kingRow = -1, kingCol = -1;
for (const auto& piece : gameState.pieces)
{
if (piece.type == PieceType::King && piece.color == kingColor)
{
kingRow = static_cast<int>(piece.sprite.getPosition().y / tileSize);
kingCol = static_cast<int>(piece.sprite.getPosition().x / tileSize);
break;
}
}
if (kingRow == -1 || kingCol == -1)
return false;
return std::ranges::any_of(gameState.pieces, [kingRow, kingCol, kingColor, &gameState](const Piece& piece) {
if (piece.color != kingColor)
{
int startRow = static_cast<int>(piece.sprite.getPosition().y / tileSize);
int startCol = static_cast<int>(piece.sprite.getPosition().x / tileSize);
return isLegalMove(piece, startRow, startCol, kingRow, kingCol, gameState);
}
return false;
});
}
bool isCheckmate(GameState& gameState, PieceColor kingColor, int depth = 0)
{
if (depth > MAX_DEPTH) {
return false;
}
if (!isCheck(gameState, kingColor))
return false;
for (auto& piece : gameState.pieces)
{
if (piece.color == kingColor)
{
int startRow = static_cast<int>(piece.sprite.getPosition().y / tileSize);
int startCol = static_cast<int>(piece.sprite.getPosition().x / tileSize);
for (int row = 0; row < boardSize; ++row)
{
for (int col = 0; col < boardSize; ++col)
{
GameState tempState = gameState;
if (isLegalMove(piece, startRow, startCol, row, col, tempState))
{
Piece* tempPiece = nullptr;
for (auto& p : tempState.pieces)
{
if (p.sprite.getPosition() == piece.sprite.getPosition())
{
tempPiece = &p;
break;
}
}
if (tempPiece) {
tempPiece->sprite.setPosition(static_cast<float>(col) * tileSize, static_cast<float>(row) * tileSize);
if (!isCheck(tempState, kingColor))
return false;
}
}
}
}
}
}
return true;
}
bool isStalemate(GameState& gameState, PieceColor playerColor) {
// Check if the player cannot make any moves
for (auto& piece : gameState.pieces) {
if (piece.color == playerColor) {
int startRow = static_cast<int>(piece.sprite.getPosition().y / tileSize);
int startCol = static_cast<int>(piece.sprite.getPosition().x / tileSize);
for (int endRow = 0; endRow < boardSize; ++endRow) {
for (int endCol = 0; endCol < boardSize; ++endCol) {
if (isLegalMove(piece, startRow, startCol, endRow, endCol, gameState)) {
return false; // Found at least one legal move
}
}
}
}
}
// If no legal moves, but the king is not in check, it's a stalemate
return !isCheck(gameState, playerColor);
}
void validateBoardState(GameState& gameState) {
for (const auto& piece : gameState.pieces) {
int row = static_cast<int>(piece.sprite.getPosition().y / tileSize);
int col = static_cast<int>(piece.sprite.getPosition().x / tileSize);
if (row < 0 || row >= boardSize || col < 0 || col >= boardSize) {
std::cout << "Error: Piece out of bound" << std::endl;
// Maybe revert the game to the previous state or fix the position
}
}
// Additional check for duplicate positions
std::set<std::pair<int, int>> positions;
for (const auto& piece : gameState.pieces) {
int row = static_cast<int>(piece.sprite.getPosition().y / tileSize);
int col = static_cast<int>(piece.sprite.getPosition().x / tileSize);
auto [it, inserted] = positions.insert({row, col});
if (!inserted) {
std::cout << "ErrOr: Multiple pieces at the same position" << std::endl;
// Maybe fix this situation
}
}
}
std::string coordsToNotation(int col, int row) {
char letter = 'a' + col;
char number = '1' + row;
return std::string(1, letter) + std::string(1, number);
}
void makeBotMove(GameState& gameState) {
if (gameState.currentTurn != PieceColor::Black) {
std::cout << "It's not Black's turn" << std::endl;
return;
}
std::vector<std::tuple<Piece*, sf::Vector2i, sf::Vector2i>> legalMoves;
// Find all legal moves for the bot
for (auto& piece : gameState.pieces) {
if (piece.color == PieceColor::Black) {
int startRow = static_cast<int>(piece.sprite.getPosition().y / tileSize);
int startCol = static_cast<int>(piece.sprite.getPosition().x / tileSize);
for (int endRow = 0; endRow < boardSize; ++endRow) {
for (int endCol = 0; endCol < boardSize; ++endCol) {
if (isLegalMove(piece, startRow, startCol, endRow, endCol, gameState)) {
legalMoves.push_back(std::make_tuple(&piece, sf::Vector2i(startCol, startRow), sf::Vector2i(endCol, endRow)));
}
}
}
}
}
if (!legalMoves.empty()) {
// Randomly select a legal move
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> distrib(0, legalMoves.size() - 1);
auto [selectedPiece, startPos, endPos] = legalMoves[distrib(gen)];
// Save the game state before the move
GameState previousState = gameState;
// Remove the captured piece (if any)
auto it = std::remove_if(gameState.pieces.begin(), gameState.pieces.end(),
[endPos](const Piece& piece) {
return static_cast<int>(piece.sprite.getPosition().x / tileSize) == endPos.x &&
static_cast<int>(piece.sprite.getPosition().y / tileSize) == endPos.y &&
piece.color != PieceColor::Black;
});
gameState.pieces.erase(it, gameState.pieces.end());
// Move the selected piece
selectedPiece->sprite.setPosition(static_cast<float>(endPos.x) * tileSize, static_cast<float>(endPos.y) * tileSize);
// Print the move
std::cout << "Black: " << coordsToNotation(startPos.x, startPos.y)
<< " " << coordsToNotation(endPos.x, endPos.y) << std::endl;
// Check if the move puts the bot's king in check
if (isCheck(gameState, PieceColor::Black)) {
// Revert the move
gameState = previousState;
std::cout << "Bot made an illegal move. Reverting." << std::endl;
makeBotMove(gameState); // Try again
} else {
// Change turn and check for game end conditions
gameState.currentTurn = PieceColor::White;
if (isCheckmate(gameState, PieceColor::White)) {
std::cout << "Checkmate. Black wins." << std::endl;
} else if (isStalemate(gameState, PieceColor::White)) {
std::cout << "Stalemate." << std::endl;
}
}
} else {
std::cout << "No legal moves for Black." << std::endl;
}
validateBoardState(gameState);
}
void handleClick(float mouseX, float mouseY, GameState& gameState, bool& selecting,
sf::Vector2i& startPos, Piece*& selectedPiece) {
int col = static_cast<int>(mouseX / tileSize);
int row = static_cast<int>(mouseY / tileSize);
if (gameState.currentTurn == PieceColor::White) {
if (selecting && selectedPiece) {
int startRow = static_cast<int>(selectedPiece->sprite.getPosition().y / tileSize);
int startCol = static_cast<int>(selectedPiece->sprite.getPosition().x / tileSize);
if (isLegalMove(*selectedPiece, startRow, startCol, row, col, gameState)) {
// Save the game state before the move
GameState previousState = gameState;
// Remove the captured piece (if any)
auto it = std::remove_if(gameState.pieces.begin(), gameState.pieces.end(),
[row, col](const Piece& piece) {
return static_cast<int>(piece.sprite.getPosition().x / tileSize) == col &&
static_cast<int>(piece.sprite.getPosition().y / tileSize) == row &&
piece.color != PieceColor::White;
});
gameState.pieces.erase(it, gameState.pieces.end());
// Move the selected piece
selectedPiece->sprite.setPosition(static_cast<float>(col) * tileSize, static_cast<float>(row) * tileSize);
// Print the move
std::cout << "White: " << coordsToNotation(startCol, startRow)
<< " " << coordsToNotation(col, row) << std::endl;
// Check if the move puts the player's king in check
if (isCheck(gameState, PieceColor::White)) {
// Revert the move
gameState = previousState;
std::cout << "Illegal move: King would be in check." << std::endl;
} else {
// Change turn and check for game end conditions
gameState.currentTurn = PieceColor::Black;
if (isCheckmate(gameState, PieceColor::Black)) {
std::cout << "Checkmate. White wins." << std::endl;
} else if (isStalemate(gameState, PieceColor::Black)) {
std::cout << "Stalemate." << std::endl;
} else {
// Bot's move
makeBotMove(gameState);
}
}
}
selecting = false;
selectedPiece = nullptr;
} else {
// Select a piece
for (auto& piece : gameState.pieces) {
sf::FloatRect bounds = piece.sprite.getGlobalBounds();
if (bounds.contains(mouseX, mouseY) && piece.color == PieceColor::White) {
selectedPiece = &piece;
startPos = {col, row};
selecting = true;
break;
}
}
}
}
validateBoardState(gameState);
}
int main()
{
sf::RenderWindow window(sf::VideoMode(boardSize * tileSize, boardSize * tileSize), "Chess");
std::array<sf::Texture, 12> textures;
if (!loadTextures(textures))
{
return 1;
}
GameState gameState;
placePieces(gameState, textures);
bool selecting = false;
sf::Vector2i startPos;
Piece* selectedPiece = nullptr;
while (window.isOpen())
{
sf::Event event; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init)
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed)
window.close();
else if (event.type == sf::Event::MouseButtonPressed)
{
if (event.mouseButton.button == sf::Mouse::Left)
{
handleClick(static_cast<float>(event.mouseButton.x),
static_cast<float>(event.mouseButton.y),
gameState, selecting, startPos, selectedPiece);
}
}
}
window.clear();
drawBoard(window);
for (const auto& piece : gameState.pieces)
{
window.draw(piece.sprite);
}
window.display();
}
return 0;
}
I’m experiencing two main issues with my chess game, which is developed using C++ and SFML:
Bot Making Illegal Moves:
Description: After the player captures one of the bot’s pieces, I observe that the bot occasionally makes moves that are not legal according to the rules of chess. This issue appears to happen specifically after a piece is removed from the board. It seems like the bot sometimes makes moves that would normally be disallowed, such as moving to squares that are occupied or making moves that do not comply with the piece’s movement rules.
Player Piece Not Moving to Captured Bot’s Position:
Description: When a bot’s piece is captured by the player, the player’s piece is not positioned correctly on the square where the bot’s piece was. Instead of the player’s piece moving to the captured piece’s position, the piece either stays in its original location or moves incorrectly. This issue occurs only after a bot’s piece is captured, leading to discrepancies in the game state and potentially affecting subsequent moves.
These problems suggest that there might be issues with how the game state is updated and validated, particularly after a capture. The bot’s move generation and validation logic may also be flawed, allowing illegal moves to be executed.
-
I added debug statements to track the positions of pieces before and after moves, especially after a capture
-
I reviewed the logic within the makeBotMove function to ensure it does not allow the bot to make moves that would place its own king in check.
-
I tested the bot’s behavior with different scenarios to ensure it adheres to the rules, but this doesn’t help.
-
I expected that after capturing a bot’s piece, the bot should only make moves that are legal and conform to the rules of chess. The bot’s moves should not place its own king in check, and it should not move to occupied squares or violate movement rules.
-
I expected that after capturing a bot’s piece, the player’s piece should move to the exact position where the captured piece was located.
Назар Клецький is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.