Introduction
So, I recently completed a chess game with a GUI in Python, using Pygame. Upon research, I learned that Pygame is built on SDL, and since I wanted to practice C++ more, I decided to code chess again in C++.
- I have 5 source files and 6 header files. Since this is a large project, feel free only to review the code design/style or just one file.
Questions
- Is my use of
namespaces
correct? - Is my header/source splitting (organization) correct? I come from Python so don't have too much experience
- Is SDL2 used properly? (eg. is there a better way to handle events, draw the board...)
- Is my game logic correct? (eg. a better way to check bishop moves, pawn moves...)
- Is my piece/board representation good?
- Are there missed optimizations/shortcuts?
- Generally, is this idiomatic C++?
Project Info
This project was compiled with g++
on a Late 2012 MacBookPro.
Compiler flags
- -I $(INCLUDE_DIR)
- -Wall
- -Wextra
- -std=c++11
Linker flags
- -D_THREAD_SAFE
- -lSDL2
- -lSDL2_image
- -lSDL2_ttf (not used but planning to use)
Code
board.cpp
#include "board.hpp"
#include <cctype>
#include <iostream>
#include <string>
#include "defines.hpp"
#include "game.hpp"
#include "pieces.hpp"
// TODO: add other FEN fields
void Board::readFen(std::string fen) {
int row = 0;
int col = 0;
for (char c : fen) {
if (c == '/') {
row++;
col = 0;
continue;
} else {
if (isdigit(c)) {
for (int _ = 0; _ < static_cast<int>(c - '0'); _++) {
this->board[row][col] = 0x00;
col++;
}
} else {
PieceColor color = isupper(c) ? WHITE : BLACK;
Piece p;
c = tolower(c);
switch (c) {
case 'p':
p = Pieces::makePiece(PAWN, color);
break;
case 'r':
p = Pieces::makePiece(ROOK, color);
break;
case 'n':
p = Pieces::makePiece(KNIGHT, color);
break;
case 'b':
p = Pieces::makePiece(BISHOP, color);
break;
case 'q':
p = Pieces::makePiece(QUEEN, color);
break;
case 'k':
p = Pieces::makePiece(KING, color);
break;
default:
p = Pieces::makePiece(PAWN, color);
}
this->board[row][col] = p;
col++;
}
}
}
}
// TODO: add other info to board printing
void Board::printBoard() {
std::cout << "CHESSBOARD INFO" << std::endl;
std::cout << "Turn: " << (this->getActiveColor() == WHITE ? "white" : "black") << std::endl;
std::cout << "Move: " << this->getMoveNumber() << std::endl;
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
std::cout << this->board[i][j] << std::string((board[i][j] >= 10 ? 1 : 2), ' ');
}
std::cout << std::endl;
}
std::cout << std::endl;
}
PieceColor Board::getActiveColor() {
return this->active_color;
}
float Board::getMoveNumber() {
return this->move_number;
}
void Board::incrementMoveNumber() {
this->move_number += 0.5f;
}
void Board::toggleActiveColor() {
this->active_color = this->getActiveColor() == WHITE ? BLACK : WHITE;
}
void Board::reverse() {
int board[8][8];
for (int i = 7; i >= 0; i--) {
for (int j = 0; j < 8; j++) {
board[7 - i][j] = this->board[i][j];
}
}
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
this->board[i][j] = board[i][j];
}
}
this->is_reversed = !this->is_reversed;
}
bool Board::isReversed() {
return this->is_reversed;
}
void Board::tryMove(Location starting, Location ending) {
if (this->getActiveColor() != Pieces::getPieceColor(this->getPieceAt(starting))) { // out of turn
return;
}
if (Game::isValidMove(this, starting, ending, true)) {
this->makeMove(starting, ending);
this->clearSelectedPiece();
this->incrementMoveNumber();
this->toggleActiveColor();
}
}
void Board::makeMove(Location starting, Location ending) {
int r1, c1, r2, c2;
r1 = starting.first;
c1 = starting.second;
r2 = ending.first;
c2 = ending.second;
this->board[r2][c2] = this->board[r1][c1];
this->board[r1][c1] = 0x00;
}
int Board::getPieceAt(Location location) {
return this->board[location.first][location.second];
}
Location Board::getSelectedPiece() {
return this->selected_piece;
}
void Board::setSelectedPiece(int i, int j) {
this->selected_piece = makeLocation(i, j);
}
void Board::clearSelectedPiece() {
this->selected_piece = makeLocation(-1, -1);
}
bool Board::hasSelectedPiece() {
return (this->selected_piece.first != -1 && this->selected_piece.second != -1);
}
game.cpp
#include "game.hpp"
#include <iostream>
#include "board.hpp"
#include "defines.hpp"
#include "pieces.hpp"
bool Game::isValidMove(Board* board, Location starting, Location ending, bool check_king) {
Piece starting_piece = board->getPieceAt(starting);
Piece ending_piece = board->getPieceAt(ending);
PieceColor starting_color = Pieces::getPieceColor(starting_piece);
if (ending_piece != 0x00 && starting_color == Pieces::getPieceColor(ending_piece)) {
return false; // can't capture your own piece
}
bool result = true;
switch (Pieces::getPieceClass(starting_piece)) {
case PAWN:
result = isValidPawnMove(board, starting, ending, starting_color);
break;
case KNIGHT:
result = isValidKnightMove(starting, ending);
break;
case BISHOP:
result = isValidBishopMove(board, starting, ending, starting_color);
break;
case ROOK:
result = isValidRookMove(board, starting, ending, starting_color);
break;
case QUEEN:
result = isValidQueenMove(board, starting, ending, starting_color);
break;
case KING:
result = isValidKingMove(starting, ending);
break;
}
if (!check_king) {
return result;
} else if (result == false) {
return result;
}
board->makeMove(starting, ending);
if (isInCheck(board, starting_color)) {
board->makeMove(ending, starting);
board->board[ending.first][ending.second] = ending_piece;
return false;
}
board->makeMove(ending, starting);
board->board[ending.first][ending.second] = ending_piece;
return true;
}
bool Game::isValidPawnMove(Board* board, Location starting, Location ending, PieceColor starting_color) {
Piece ending_piece = board->getPieceAt(ending);
PieceColor opposite_color = starting_color == WHITE ? BLACK : WHITE;
if (!board->isReversed()) {
opposite_color = starting_color;
}
if (ending_piece == 0) { // open piece
if (opposite_color == WHITE) {
if (
starting.first == 6 && starting.first - ending.first == 2 && starting.second == ending.second) {
return true;
}
if (
starting.first - ending.first == 1 && starting.second == ending.second) {
return true;
}
} else if (opposite_color == BLACK) {
if (
starting.first == 1 && ending.first - starting.first == 2 && starting.second == ending.second) {
return true;
}
if (
ending.first - starting.first == 1 && starting.second == ending.second) {
return true;
}
}
} else { // piece is occupied
if (starting_color == WHITE) {
if (ending.first == starting.first - 1) {
if (abs(ending.second - starting.second) == 1) {
return true;
}
}
} else if (starting_color == BLACK) {
if (ending.first == starting.first + 1) {
if (abs(ending.second - starting.second) == 1) {
return true;
}
}
}
return false;
}
return false;
}
bool Game::isValidKnightMove(Location starting, Location ending) {
int x_diff = abs(ending.first - starting.first);
int y_diff = abs(ending.second - starting.second);
if (x_diff == 1 && y_diff == 2) {
return true;
}
if (x_diff == 2 && y_diff == 1) {
return true;
}
return false;
}
bool Game::isValidBishopMove(Board* board, Location starting, Location ending, PieceColor starting_color) {
if (starting.first + starting.second != ending.first + ending.second && starting.first - starting.second != ending.first - ending.second) {
return false;
}
int x_change = ending.second < starting.second ? -1 : 1;
int y_change = ending.first < starting.first ? -1 : 1;
Location indices = starting;
while (0 <= indices.first <= 7 && 0 <= indices.second <= 7 && indices != ending) {
indices.second += x_change;
indices.first += y_change;
if (board->getPieceAt(indices)) {
if (Pieces::getPieceColor(board->getPieceAt(indices)) == starting_color) {
indices.second -= x_change;
indices.first -= y_change;
}
break;
}
}
if (indices == ending) {
return true;
}
return false;
}
bool Game::isValidRookMove(Board* board, Location starting, Location ending, PieceColor starting_color) {
if (
starting.first != ending.first && starting.second != ending.second) {
return false;
}
int x_change, y_change;
x_change = ending.second < starting.second ? -1 : 1;
y_change = ending.first < starting.first ? -1 : 1;
x_change = ending.second == starting.second ? 0 : x_change;
y_change = ending.first == starting.first ? 0 : y_change;
Location indices = (starting);
while (0 <= indices.first <= 7 && 0 <= indices.second <= 7 && indices != ending) {
indices.second += x_change;
indices.first += y_change;
if (board->getPieceAt(indices)) {
if (Pieces::getPieceColor(board->getPieceAt(indices)) == starting_color) {
indices.second -= x_change;
indices.first -= y_change;
}
break;
}
}
if (indices == ending) {
return true;
}
return false;
}
bool Game::isValidQueenMove(Board* board, Location starting, Location ending, PieceColor starting_color) {
return isValidBishopMove(
board, starting, ending, starting_color) ||
isValidRookMove(board, starting, ending, starting_color);
}
bool Game::isValidKingMove(Location starting, Location ending) {
int x_diff = abs(starting.first - ending.first);
int y_diff = abs(starting.second - ending.second);
if (x_diff == 1 and y_diff == 1) {
return true;
}
if (x_diff == 1 and y_diff == 0) {
return true;
}
if (x_diff == 0 and y_diff == 1) {
return true;
}
return false;
}
bool Game::isInCheck(Board* board, PieceColor color) {
Location king_location;
Piece piece;
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
piece = board->getPieceAt(makeLocation(i, j));
if (Pieces::getPieceClass(piece) == KING && Pieces::getPieceColor(piece) == color) {
king_location = makeLocation(i, j);
break;
}
}
}
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
piece = board->getPieceAt(makeLocation(i, j));
if (piece == 0x00) {
continue;
}
if (Pieces::getPieceColor(piece) != color) {
if (Game::isValidMove(board, makeLocation(i, j), king_location, false)) {
return true;
}
}
}
}
return false;
}
bool Game::isInCheckMate(Board* board, PieceColor color) {
Location king_location;
Piece piece;
if (!isInCheck(board, color)) {
return false; // can't be in mate if ur not checked
}
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
piece = board->getPieceAt(makeLocation(i, j));
if (Pieces::getPieceClass(piece) == KING && Pieces::getPieceColor(piece) == color) {
king_location = makeLocation(i, j);
break;
}
}
}
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
piece = board->getPieceAt(makeLocation(i, j));
if (piece == 0x00) {
continue;
}
if (Pieces::getPieceColor(piece) == color) {
Location starting_location = makeLocation(i, j);
Location ending_location;
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 8; y++) {
ending_location = makeLocation(x, y);
if (Game::isValidMove(board, starting_location, ending_location, true)) {
return false;
}
}
}
}
}
}
return true;
}
gui.cpp
#include "gui.hpp"
#include <SDL2/SDL.h>
#include <fstream>
#include <iostream>
#include <string>
#include "board.hpp"
#include "defines.hpp"
#include "game.hpp"
#include "pieces.hpp"
// TODO: add special moves like castle, en passant
// TODO: add showing checkmate
void GUI::initSDL() {
SDL_Init(SDL_INIT_EVERYTHING);
}
void GUI::cleanupSDL(SDL_Renderer* renderer, SDL_Window* window) {
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
SDL_Window* GUI::createSDLWindow() {
return SDL_CreateWindow("C++ Chess", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN);
}
SDL_Renderer* GUI::createSDLRenderer(SDL_Window* window) {
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2");
return SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC);
}
void GUI::drawChessboard(SDL_Renderer* renderer, Board* board) {
Piece p;
Location selected_piece = board->getSelectedPiece();
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
SDL_Rect tile;
tile.x = j * TILE_SIZE;
tile.y = i * TILE_SIZE;
tile.w = TILE_SIZE;
tile.h = TILE_SIZE;
if ((i + j) % 2 == (board->isReversed() ? 1 : 0)) {
SDL_SetRenderDrawColor(renderer, 0xEE, 0xEE, 0xEE, 0xFF);
} else {
SDL_SetRenderDrawColor(renderer, 0x99, 0x99, 0x99, 0xFF);
}
if (selected_piece.first == i && selected_piece.second == j) {
SDL_SetRenderDrawColor(renderer, 0xFF, 0x00, 0x00, 0xFF);
SDL_Rect highlight;
highlight.x = tile.x + 5;
highlight.y = tile.y + 3;
highlight.w = tile.w - 10;
highlight.h = tile.h - 10;
SDL_SetRenderDrawColor(renderer, 0xFF, 0x00, 0x00, 0xFF);
SDL_RenderFillRect(renderer, &tile);
if ((i + j) % 2 == (board->isReversed() ? 1 : 0)) {
SDL_SetRenderDrawColor(renderer, 0xEE, 0xEE, 0xEE, 0xFF);
} else {
SDL_SetRenderDrawColor(renderer, 0x99, 0x99, 0x99, 0xFF);
}
SDL_RenderFillRect(renderer, &highlight);
} else {
SDL_RenderFillRect(renderer, &tile);
}
p = board->board[i][j];
if (p != 0) {
std::string filepath = "../data/bmp/" + Pieces::getPieceFilename(p);
SDL_Surface* image = SDL_LoadBMP(filepath.c_str());
if (image == NULL) {
std::cerr << "image=" << image << " Reason: " << SDL_GetError() << " " << SDL_GetBasePath() << std::endl;
}
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, image);
SDL_RenderCopy(renderer, texture, NULL, &tile);
SDL_FreeSurface(image);
SDL_DestroyTexture(texture);
}
if (board->hasSelectedPiece() && Game::isValidMove(board, board->getSelectedPiece(), makeLocation(i, j), true)) {
SDL_SetRenderDrawColor(renderer, 0xFF, 0x00, 0x00, 0xFF);
int centerX = tile.x + tile.w / 2;
int centerY = tile.y + tile.h / 2;
int radius = 15;
SDL_RenderFillCircle(renderer, centerX, centerY, radius);
}
}
}
}
int GUI::SDL_RenderFillCircle(SDL_Renderer* renderer, int x, int y, int radius) {
int offsetx, offsety, d;
int status;
offsetx = 0;
offsety = radius;
d = radius - 1;
status = 0;
while (offsety >= offsetx) {
status += SDL_RenderDrawLine(renderer, x - offsety, y + offsetx,
x + offsety, y + offsetx);
status += SDL_RenderDrawLine(renderer, x - offsetx, y + offsety,
x + offsetx, y + offsety);
status += SDL_RenderDrawLine(renderer, x - offsetx, y - offsety,
x + offsetx, y - offsety);
status += SDL_RenderDrawLine(renderer, x - offsety, y - offsetx,
x + offsety, y - offsetx);
if (status < 0) {
status = -1;
break;
}
if (d >= 2 * offsetx) {
d -= 2 * offsetx + 1;
offsetx += 1;
} else if (d < 2 * (radius - offsety)) {
d += 2 * offsety - 1;
offsety -= 1;
} else {
d += 2 * (offsety - offsetx - 1);
offsety -= 1;
offsetx += 1;
}
}
return status;
}
void GUI::handleMouseClicked(SDL_MouseButtonEvent event, Board* board) {
if (event.button == SDL_BUTTON_RIGHT) {
return;
}
Location board_indices = getBoardIndices(event.x, event.y);
if (board->getSelectedPiece() == board_indices) { // de-select a piece
board->clearSelectedPiece();
} else if (!board->hasSelectedPiece()) { // select a piece
if (board->getPieceAt(board_indices) != 0 && Pieces::getPieceColor(board->getPieceAt(board_indices)) == board->getActiveColor()) {
board->setSelectedPiece(board_indices.first, board_indices.second);
}
} else if (board->hasSelectedPiece()) { // move a piece
if (Pieces::getPieceColor(board->getPieceAt(board_indices)) == Pieces::getPieceColor(board->getPieceAt(board->getSelectedPiece()))) {
board->setSelectedPiece(board_indices.first, board_indices.second);
} else {
board->tryMove(board->getSelectedPiece(), board_indices);
}
}
}
void GUI::handleKeyPressed(SDL_KeyboardEvent event, Board* board) {
if (event.keysym.sym == SDL_GetKeyFromName("r") || event.keysym.sym == SDL_GetKeyFromName("f")) {
std::cout << "r/f pressed, reversing board" << std::endl;
board->reverse();
}
}
Location GUI::getBoardIndices(int x, int y) {
/**
* Gets board indices from a x, y coordinate of a mouse click.
*
* @param x: integer value of x coordiate of mouse click
* @param y: integer value of y coordiate of mouse click
*
* @returns std::pair with integer values from 0-7 in first and second
*/
Location out;
out.second = x / TILE_SIZE;
out.first = y / TILE_SIZE;
return out;
}
main.cpp
#include "main.hpp"
#include <SDL2/SDL.h>
#include <iostream>
#include "board.hpp"
#include "game.hpp"
#include "gui.hpp"
int main() {
SDL_Window* window = GUI::createSDLWindow();
SDL_Renderer* renderer = GUI::createSDLRenderer(window);
SDL_Event event;
Board board;
board.readFen(STARTING_FEN);
board.printBoard();
bool running = true;
bool gameover = false;
while (running) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = false;
} else if (event.type == SDL_MOUSEBUTTONUP && !gameover) {
GUI::handleMouseClicked(event.button, &board);
} else if (event.type == SDL_KEYUP && !gameover) {
GUI::handleKeyPressed(event.key, &board);
}
}
if (gameover) {
continue;
}
if (Game::isInCheckMate(&board, WHITE)) {
std::cout << "WHITE IS MATED" << std::endl;
gameover = true;
}
if (Game::isInCheckMate(&board, BLACK)) {
std::cout << "BLACK IS MATED" << std::endl;
gameover = true;
}
SDL_RenderClear(renderer);
GUI::drawChessboard(renderer, &board);
SDL_RenderPresent(renderer);
}
GUI::cleanupSDL(renderer, window);
return 0;
}
pieces.cpp
#include "pieces.hpp"
#include <iostream>
#include <string>
PieceColor Pieces::getPieceColor(Piece p) {
int color = p >> 3;
if (color == 1) {
return BLACK;
} else if (color == 2) {
return WHITE;
} else {
return NOCOLOR;
}
}
PieceClass Pieces::getPieceClass(Piece p) {
int pc = p & 7;
return static_cast<PieceEnum>(pc);
}
std::string Pieces::getPieceFilename(Piece p) {
PieceColor color = getPieceColor(p);
PieceClass pc = getPieceClass(p);
std::string filename;
switch (color) {
case WHITE:
filename += "white_";
break;
case BLACK:
filename += "black_";
break;
}
switch (pc) {
case PAWN:
filename += "pawn";
break;
case KNIGHT:
filename += "knight";
break;
case BISHOP:
filename += "bishop";
break;
case ROOK:
filename += "rook";
break;
case QUEEN:
filename += "queen";
break;
case KING:
filename += "king";
break;
}
filename += ".bmp";
return filename;
}
Piece Pieces::makePiece(PieceClass c, PieceColor pc) {
return c | pc;
}
Headers
board.hpp
#ifndef BOARD_H_INCLUDED
#define BOARD_H_INCLUDED
#include <string>
#include "defines.hpp"
#include "pieces.hpp"
const std::string STARTING_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR";
class Board {
private:
PieceColor active_color = WHITE;
float move_number = 1.0f;
bool is_reversed = false;
Location selected_piece = {-1, -1};
public:
int board[8][8];
float getMoveNumber();
void incrementMoveNumber();
PieceColor getActiveColor();
void toggleActiveColor();
Location getSelectedPiece();
int getPieceAt(Location location);
void setSelectedPiece(int i, int j);
bool hasSelectedPiece();
void clearSelectedPiece();
void tryMove(Location starting, Location ending);
void makeMove(Location starting, Location ending);
void readFen(std::string fen);
void printBoard();
void reverse();
bool isReversed();
};
#endif // !BOARD_H_INCLUDED
defines.hpp
#ifndef DEFINES_HPP_INCLUDED
#define DEFINES_HPP_INCLUDED
typedef std::pair<int, int> Location;
inline std::pair<int, int> makeLocation(int i, int j) {
return std::make_pair(i, j);
}
#endif // !DEFINES_HPP_INCLUDED
game.hpp
#ifndef GAME_HPP_INCLUDED
#define GAME_HPP_INCLUDED
#include "board.hpp"
#include "pieces.hpp"
namespace Game {
bool isValidMove(Board* board, Location starting, Location ending, bool check_king);
bool isValidPawnMove(Board* board, Location starting, Location ending, PieceColor starting_color);
bool isValidKnightMove(Location starting, Location ending);
bool isValidBishopMove(Board* board, Location starting, Location ending, PieceColor starting_color);
bool isValidRookMove(Board* board, Location starting, Location ending, PieceColor starting_color);
bool isValidQueenMove(Board* board, Location starting, Location ending, PieceColor starting_color);
bool isValidKingMove(Location starting, Location ending);
bool isInCheck(Board* board, PieceColor color);
bool isInCheckMate(Board* board, PieceColor color);
} // namespace Game
#endif // !GAME_HPP_INCLUDED
gui.hpp
#ifndef GUI_H_INCLUDED
#define GUI_H_INCLUDED
#include <SDL2/SDL.h>
#include "board.hpp"
const int WINDOW_WIDTH = 1200;
const int WINDOW_HEIGHT = 1200;
const int TILE_SIZE = 150;
namespace GUI {
void initSDL();
void cleanupSDL(SDL_Renderer* renderer, SDL_Window* window);
SDL_Window* createSDLWindow();
SDL_Renderer* createSDLRenderer(SDL_Window* window);
void drawChessboard(SDL_Renderer* renderer, Board* board);
int SDL_RenderFillCircle(SDL_Renderer* renderer, int x, int y, int radius);
void handleMouseClicked(SDL_MouseButtonEvent event, Board* board);
void handleKeyPressed(SDL_KeyboardEvent event, Board* board);
std::pair<int, int> getBoardIndices(int x, int y);
} // namespace GUI
#endif // !GUI_H_INCLUDED
main.hpp
#ifndef MAIN_HPP_INCLUDED
#define MAIN_HPP_INCLUDED
// TODO: add stuff or remove if unnessecary
#endif // !MAIN_HPP_INCLUDED
pieces.hpp
#ifndef PIECES_H_INCLUDED
#define PIECES_H_INCLUDED
#include <string>
typedef unsigned int Piece;
typedef unsigned int PieceColor;
typedef unsigned int PieceClass;
enum PieceColors {
WHITE = 16,
BLACK = 8,
NOCOLOR = 0
};
enum PieceEnum {
PAWN,
KNIGHT,
BISHOP,
ROOK,
QUEEN,
KING
};
namespace Pieces {
PieceColor getPieceColor(Piece p);
PieceClass getPieceClass(Piece p);
Piece makePiece(PieceClass c, PieceColor pc);
std::string getPieceFilename(Piece p);
} // namespace Pieces
#endif // !PIECES_H_INCLUDED