Build a Chess Engine in C++

Build a terminal chess engine with minimax search and AI opponent.

Introduction

30 Second Summary

Chess is the ultimate test of strategic thinking. For decades, programmers have tried to teach computers to play it well. The algorithms behind chess engines power everything from game AI to decision-making systems in robotics and finance.

In this project, you will build a complete chess engine in C++ that runs in your terminal. You'll implement the rules of chess, teach the engine to evaluate positions, and give it the ability to search ahead and find strong moves.

What You'll Build

A multi-file C++ chess engine with full move generation, positional evaluation using PeSTO piece-square tables, and a minimax search with alpha-beta pruning that plays against you in the terminal.

By the end of this project, you'll have:

  • A board representation with piece encoding and full game state tracking (castling, en passant, promotion).
  • A complete move generator that handles every rule in chess and produces only legal moves.
  • A PeSTO evaluation function with piece-square tables that gives the engine positional awareness in both the middlegame and endgame.
  • A minimax search with alpha-beta pruning that lets the engine look 4-5 moves ahead and play a legitimate game of chess.
  • Secret Mission: add MVV-LVA move ordering to dramatically speed up the engine's search.

Are there any prerequisites?

You should have basic C++ knowledge covering variables, functions, arrays, loops, and classes or structs. No prior game programming experience is required.

Set Up Your C++ Environment

A chess engine has a lot of moving parts. Board logic, move generation, evaluation, search, and a game loop all live in separate files.

Before writing any chess code, you need an organized project skeleton and a build system that compiles everything together. In this step, you'll create the project directory, set up all source files, and write a Makefile that builds your engine with a single command.

In this step, get ready to:
  • Create the project directory and all source files.
  • Write a Makefile with C++17 compilation flags.
  • Verify the build toolchain works with a hello-world test.
Confirm your C++ compiler is ready

Your Apple Clang compiler is already installed via Xcode Command Line Tools. The clang++ binary is what compiles your C++ source files into an executable.

With the compiler confirmed, you're ready to create the project structure.

Create the project directory and source files

A chess engine this size benefits from separating concerns into distinct files. Each file handles one responsibility: types, board logic, move generation, evaluation, search, and the main game loop.

  • Switch back to your Terminal and move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir chess-engine
cd chess-engine
  • Create all the header and source files by running this command:
touch types.h board.h board.cpp movegen.h movegen.cpp eval.h eval.cpp search.h search.cpp main.cpp Makefile
  • Confirm the files exist by running:
ls

You should see all 11 files listed: types.h, board.h, board.cpp, movegen.h, movegen.cpp, eval.h, eval.cpp, search.h, search.cpp, main.cpp, and Makefile.

Don't see all the files?

Make sure you ran the touch command from inside the chess-engine directory. If you see fewer files, re-run the touch command from the previous step.

Help me figure out why my files aren't showing up.

Why so many files?

Each file handles one job. types.h defines shared data types. board.cpp handles the board state. movegen.cpp generates legal moves. eval.cpp scores positions. search.cpp finds the best move. main.cpp runs the game loop.

This separation makes the engine easier to debug and extend. You can work on evaluation without touching the move generator.

Write the Makefile and test the build

A Makefile tells the make tool how to compile your project. Instead of typing long clang++ commands every time, you just run make and it handles everything.

  • Open Makefile in your editor and paste the following code:
CXX = clang++
CXXFLAGS = -std=c++17 -O2 -Wall
TARGET = chess
SRCS = main.cpp board.cpp movegen.cpp eval.cpp search.cpp
OBJS = $(SRCS:.cpp=.o)

$(TARGET): $(OBJS)
	$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS)

%.o: %.cpp
	$(CXX) $(CXXFLAGS) -c $< -o $@

clean:
	rm -f $(OBJS) $(TARGET)

.PHONY: clean

What does this Makefile do?

  • CXX = clang++ sets Apple Clang as the compiler.
  • -std=c++17 enables C++17 features. Apple Clang defaults to C++98 without this flag.
  • -O2 turns on optimization so the engine searches faster.
  • -Wall enables all common warnings to catch mistakes early.
  • The %.o: %.cpp pattern rule compiles each .cpp file into a .o object file, then the target rule links them all into one chess binary.

Tabs matter in Makefiles

The indented lines under each rule MUST use a real tab character, not spaces. If you see an error like missing separator, your editor replaced tabs with spaces. Check your editor settings or re-type the indentation with the Tab key.

  • Save Makefile.

Now you'll write a quick hello-world program to confirm everything compiles and links correctly.

  • Open main.cpp in your editor and paste this temporary test code:
#include <iostream>

int main() {
    std::cout << "Build successful!" << std::endl;
    return 0;
}
  • Save main.cpp.
  • In your Terminal, compile the project by running:
make
  • Run the compiled binary by running:
./chess

You should see Build successful! printed to the terminal. This confirms your compiler, Makefile, and linker are all working together.

Seeing a "missing separator" error?

This means your Makefile has spaces instead of tabs on the indented lines. Open Makefile and delete the whitespace before each $(CXX) and rm line. Re-type it using the Tab key on your keyboard.

Help me fix my Makefile build error.

Seeing "no input files" or other clang++ errors?

Make sure you saved main.cpp with the hello-world code before running make. Also confirm you're inside the chess-engine directory by running pwd.

Help me debug my chess engine build failure.

✔️ Awesome, I've got everything!

Great. Make sure Makefile is saved. You'll replace the hello-world code in main.cpp with real chess code in a later step.

ⓧ I'd like to double check the full code

Here's the complete Makefile that should be in your chess-engine folder:

CXX = clang++
CXXFLAGS = -std=c++17 -O2 -Wall
TARGET = chess
SRCS = main.cpp board.cpp movegen.cpp eval.cpp search.cpp
OBJS = $(SRCS:.cpp=.o)

$(TARGET): $(OBJS)
	$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS)

%.o: %.cpp
	$(CXX) $(CXXFLAGS) -c $< -o $@

clean:
	rm -f $(OBJS) $(TARGET)

.PHONY: clean

Your build toolchain is locked in. Next up, you'll define the chess piece types and build the board representation that everything else sits on.

Define Types and Board Representation

Before the engine can think about moves, it needs a way to describe the chessboard. Every chess program starts with a board representation: a data structure that answers "what piece is on which square?" at any moment in the game.

In this step, you will define the type system (pieces, colors, board state) and build the functions that initialize, display, and update the board. By the end, your terminal will print the starting position using Unicode chess symbols.

In this step, get ready to:
  • Define piece enums, board struct, and move struct in a header file.
  • Implement board initialization, display, attack detection, and move execution.
  • Compile and test by printing the starting chess position.
Define the type system in types.h

A chess engine's type system answers three questions: what pieces exist, what does the board look like right now, and how is a move described? You will define all three in a single header file.

  • Open types.h in your editor and paste the following type definitions:
#ifndef TYPES_H
#define TYPES_H

#include <cstdint>
#include <vector>
#include <string>

// Piece codes: EMPTY=0, white pieces 1-6, black pieces 7-12
enum Piece : int {
    EMPTY = 0,
    wP = 1, wN = 2, wB = 3, wR = 4, wQ = 5, wK = 6,
    bP = 7, bN = 8, bB = 9, bR = 10, bQ = 11, bK = 12
};

// Side to move
enum Color : int { WHITE = 0, BLACK = 1 };

// Castling rights bitmask constants
enum { CASTLE_WK = 1, CASTLE_WQ = 2, CASTLE_BK = 4, CASTLE_BQ = 8 };

// Full board state at any point in the game
struct Board {
    int squares[64];      // piece on each square (index 0=a1, 63=h8)
    int sideToMove;       // WHITE or BLACK
    int castleRights;     // bitmask of remaining castling rights
    int epSquare;         // en passant target square (-1 if none)
    int halfMoveClock;    // moves since last pawn push or capture
    int fullMoveNumber;   // increments after Black moves
};

// A single move with all info needed to make/unmake it
struct Move {
    int from;        // source square
    int to;          // destination square
    int captured;    // piece captured (EMPTY if none)
    int promoted;    // piece promoted to (EMPTY if none)
    int flags;       // 0=normal, 1=castling, 2=en passant, 3=promotion
};

// Coordinate helpers: squares are stored rank*8 + file
inline int rankOf(int sq) { return sq / 8; }
inline int fileOf(int sq) { return sq % 8; }
inline int sqOf(int rank, int file) { return rank * 8 + file; }

// Piece color checks
inline bool isWhitePiece(int p) { return p >= wP && p <= wK; }
inline bool isBlackPiece(int p) { return p >= bP && p <= bK; }

inline int pieceColor(int p) {
    if (p >= wP && p <= wK) return WHITE;
    if (p >= bP && p <= bK) return BLACK;
    return -1;
}

// Returns piece type (1=Pawn..6=King) regardless of color
inline int pieceType(int p) {
    if (p >= wP && p <= wK) return p;
    if (p >= bP && p <= bK) return p - 6;
    return 0;
}

#endif

What does this code do?

The Piece enum assigns integer codes to each piece. White pieces are 1-6, black pieces are 7-12, and empty squares are 0. This lets you store an entire board as a simple 64-element integer array.

The Board struct holds everything about the current game state: which piece sits on each square, whose turn it is, what castling is still possible, and whether en passant is available.

The Move struct describes a single move with enough detail to execute it and undo it later. The flags field distinguishes normal moves from special chess rules (castling, en passant, promotion).

  • Save types.h.

Seeing a compile error later?

Make sure the #ifndef TYPES_H guard is at the very top of the file. Missing include guards cause "redefinition" errors when multiple files include the same header.

help me debug a redefinition error in my types.h header

Now declare the functions that will operate on the board. These declarations go in a separate header so other files can call them.

  • Open board.h and paste these function declarations:
#ifndef BOARD_H
#define BOARD_H

#include "types.h"

// Board lifecycle functions
void initBoard(Board& b);
void printBoard(const Board& b);
std::string sqToAlgebraic(int sq);
int algebraicToSq(const std::string& s);
void makeMove(Board& b, const Move& m);
bool isSquareAttacked(const Board& b, int sq, int byColor);
bool isInCheck(const Board& b, int color);
int findKing(const Board& b, int color);

#endif

Why separate header and implementation files?

In C++, header files (.h) declare what functions exist. Implementation files (.cpp) define what those functions actually do. This separation lets multiple .cpp files use the same functions without duplicating code.

  • Save board.h.
Implement board initialization and display

With the types defined, you can now write the functions that set up the starting position and display it in the terminal using Unicode chess symbols.

  • Open board.cpp and paste the board initialization and display functions:
#include "board.h"
#include <iostream>

void initBoard(Board& b) {
    // Standard chess starting position (rank 1 at index 0-7, rank 8 at 56-63)
    int startPos[64] = {
        wR, wN, wB, wQ, wK, wB, wN, wR,
        wP, wP, wP, wP, wP, wP, wP, wP,
        EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
        EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
        EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
        EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
        bP, bP, bP, bP, bP, bP, bP, bP,
        bR, bN, bB, bQ, bK, bB, bN, bR
    };
    for (int i = 0; i < 64; i++) b.squares[i] = startPos[i];
    b.sideToMove = WHITE;
    b.castleRights = CASTLE_WK | CASTLE_WQ | CASTLE_BK | CASTLE_BQ;
    b.epSquare = -1;
    b.halfMoveClock = 0;
    b.fullMoveNumber = 1;
}

void printBoard(const Board& b) {
    // Unicode symbols indexed by piece code (0=empty, 1-6=white, 7-12=black)
    const std::string symbols[] = {
        ".", "\u2659", "\u2658", "\u2657", "\u2656", "\u2655", "\u2654",
             "\u265F", "\u265E", "\u265D", "\u265C", "\u265B", "\u265A"
    };
    std::cout << "\n";
    // Print from rank 8 (top) down to rank 1 (bottom)
    for (int rank = 7; rank >= 0; rank--) {
        std::cout << (rank + 1) << "  ";
        for (int file = 0; file < 8; file++) {
            int piece = b.squares[sqOf(rank, file)];
            std::cout << symbols[piece] << " ";
        }
        std::cout << "\n";
    }
    std::cout << "   a b c d e f g h\n\n";
}

What does this code do?

initBoard fills the 64-square array with the standard starting position. Index 0 is square a1 (white rook) and index 63 is h8 (black rook). It also sets White to move first and enables all four castling rights.

printBoard loops from rank 8 down to rank 1 (so the board displays with White at the bottom) and uses Unicode escape sequences to render real chess piece symbols (\u2654 through \u265F) in your terminal.

Next, add the coordinate conversion utilities and king-finder function below the code you just pasted.

  • In board.cpp, paste the following utility functions below printBoard:
std::string sqToAlgebraic(int sq) {
    // Convert square index to algebraic notation (e.g., 0 -> "a1")
    std::string s;
    s += (char)('a' + fileOf(sq));
    s += (char)('1' + rankOf(sq));
    return s;
}

int algebraicToSq(const std::string& s) {
    // Convert algebraic notation (e.g., "e2") to square index
    if (s.length() < 2) return -1;
    int file = s[0] - 'a';
    int rank = s[1] - '1';
    if (file < 0 || file > 7 || rank < 0 || rank > 7) return -1;
    return sqOf(rank, file);
}

int findKing(const Board& b, int color) {
    // Scan all squares to locate the king of the given color
    int king = (color == WHITE) ? wK : bK;
    for (int i = 0; i < 64; i++) {
        if (b.squares[i] == king) return i;
    }
    return -1;
}

What do these helpers do?

sqToAlgebraic converts an internal square index (0-63) to human-readable notation like "e4". algebraicToSq does the reverse, converting player input like "e2" back to an index the engine understands.

findKing scans the board to locate a king. The engine calls this constantly during move generation to verify whether the king is in check.

Now add the attack detection function. This is the engine's core safety check. It determines whether a given square is under attack by any enemy piece.

  • In board.cpp, paste the isSquareAttacked function below findKing:
bool isSquareAttacked(const Board& b, int sq, int byColor) {
    int rank = rankOf(sq), file = fileOf(sq);

    // Check knight attacks from all 8 possible knight positions
    int knightOffsets[8][2] = {{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}};
    int enemyKnight = (byColor == WHITE) ? wN : bN;
    for (auto& off : knightOffsets) {
        int r = rank + off[0], f = file + off[1];
        if (r >= 0 && r < 8 && f >= 0 && f < 8) {
            if (b.squares[sqOf(r, f)] == enemyKnight) return true;
        }
    }

    // Check pawn attacks (pawns attack diagonally)
    if (byColor == WHITE) {
        if (rank > 0) {
            if (file > 0 && b.squares[sqOf(rank - 1, file - 1)] == wP) return true;
            if (file < 7 && b.squares[sqOf(rank - 1, file + 1)] == wP) return true;
        }
    } else {
        if (rank < 7) {
            if (file > 0 && b.squares[sqOf(rank + 1, file - 1)] == bP) return true;
            if (file < 7 && b.squares[sqOf(rank + 1, file + 1)] == bP) return true;
        }
    }

    // Check king attacks (adjacent squares)
    int enemyKing = (byColor == WHITE) ? wK : bK;
    for (int dr = -1; dr <= 1; dr++) {
        for (int df = -1; df <= 1; df++) {
            if (dr == 0 && df == 0) continue;
            int r = rank + dr, f = file + df;
            if (r >= 0 && r < 8 && f >= 0 && f < 8) {
                if (b.squares[sqOf(r, f)] == enemyKing) return true;
            }
        }
    }

    // Check sliding piece attacks (bishop/queen on diagonals)
    int enemyBishop = (byColor == WHITE) ? wB : bB;
    int enemyRook = (byColor == WHITE) ? wR : bR;
    int enemyQueen = (byColor == WHITE) ? wQ : bQ;

    int diagDirs[4][2] = {{1,1},{1,-1},{-1,1},{-1,-1}};
    for (auto& dir : diagDirs) {
        int r = rank + dir[0], f = file + dir[1];
        while (r >= 0 && r < 8 && f >= 0 && f < 8) {
            int p = b.squares[sqOf(r, f)];
            if (p != EMPTY) {
                if (p == enemyBishop || p == enemyQueen) return true;
                break;
            }
            r += dir[0]; f += dir[1];
        }
    }

    // Check sliding piece attacks (rook/queen on straights)
    int straightDirs[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
    for (auto& dir : straightDirs) {
        int r = rank + dir[0], f = file + dir[1];
        while (r >= 0 && r < 8 && f >= 0 && f < 8) {
            int p = b.squares[sqOf(r, f)];
            if (p != EMPTY) {
                if (p == enemyRook || p == enemyQueen) return true;
                break;
            }
            r += dir[0]; f += dir[1];
        }
    }

    return false;
}

What does isSquareAttacked do?

This function checks whether a given square is attacked by any piece of a specified color. It works by asking: "Is there an enemy knight at any of the 8 knight-hop positions? Is there an enemy pawn diagonally adjacent? Is there a sliding piece (bishop, rook, queen) along any ray?"

For sliding pieces, it scans outward in each direction and stops when it hits any piece. If the first piece it hits is an enemy bishop/queen (for diagonals) or rook/queen (for straight lines), the square is attacked.

Finally, add the check detection and move execution functions. These complete the board logic.

  • In board.cpp, paste isInCheck and makeMove below isSquareAttacked:
bool isInCheck(const Board& b, int color) {
    int kingSq = findKing(b, color);
    return isSquareAttacked(b, kingSq, 1 - color);
}

void makeMove(Board& b, const Move& m) {
    int piece = b.squares[m.from];

    // Handle en passant capture (remove pawn from its actual square)
    if (m.flags == 2) {
        int capturedPawnSq = sqOf(rankOf(m.from), fileOf(m.to));
        b.squares[capturedPawnSq] = EMPTY;
    }

    // Move the piece
    b.squares[m.to] = piece;
    b.squares[m.from] = EMPTY;

    // Handle promotion (replace pawn with promoted piece)
    if (m.flags == 3) {
        b.squares[m.to] = m.promoted;
    }

    // Handle castling (move the rook to its new position)
    if (m.flags == 1) {
        if (m.to == sqOf(0, 6)) { b.squares[sqOf(0, 5)] = wR; b.squares[sqOf(0, 7)] = EMPTY; }
        else if (m.to == sqOf(0, 2)) { b.squares[sqOf(0, 3)] = wR; b.squares[sqOf(0, 0)] = EMPTY; }
        else if (m.to == sqOf(7, 6)) { b.squares[sqOf(7, 5)] = bR; b.squares[sqOf(7, 7)] = EMPTY; }
        else if (m.to == sqOf(7, 2)) { b.squares[sqOf(7, 3)] = bR; b.squares[sqOf(7, 0)] = EMPTY; }
    }

    // Update en passant square (only after double pawn push)
    b.epSquare = -1;
    if ((piece == wP || piece == bP) && std::abs(rankOf(m.to) - rankOf(m.from)) == 2) {
        b.epSquare = sqOf((rankOf(m.from) + rankOf(m.to)) / 2, fileOf(m.from));
    }

    // Update castling rights when king or rook moves
    if (piece == wK) b.castleRights &= ~(CASTLE_WK | CASTLE_WQ);
    if (piece == bK) b.castleRights &= ~(CASTLE_BK | CASTLE_BQ);
    if (m.from == sqOf(0, 0) || m.to == sqOf(0, 0)) b.castleRights &= ~CASTLE_WQ;
    if (m.from == sqOf(0, 7) || m.to == sqOf(0, 7)) b.castleRights &= ~CASTLE_WK;
    if (m.from == sqOf(7, 0) || m.to == sqOf(7, 0)) b.castleRights &= ~CASTLE_BQ;
    if (m.from == sqOf(7, 7) || m.to == sqOf(7, 7)) b.castleRights &= ~CASTLE_BK;

    // Update half-move clock (resets on pawn move or capture)
    if (piece == wP || piece == bP || m.captured != EMPTY) {
        b.halfMoveClock = 0;
    } else {
        b.halfMoveClock++;
    }

    // Switch side to move and increment full move number after Black
    if (b.sideToMove == BLACK) b.fullMoveNumber++;
    b.sideToMove = 1 - b.sideToMove;
}

What does makeMove handle?

makeMove applies a move to the board by handling all chess special cases: en passant (capturing a pawn that moved beside yours), promotion (replacing a pawn with a queen or other piece), and castling (moving the king and rook together).

It also updates the game state metadata. Castling rights are revoked when a king or rook moves or gets captured. The en passant square is set only after a double pawn push. The half-move clock tracks draws under the 50-move rule.

  • Save board.cpp.

Getting undeclared identifier errors?

Make sure the very first line of board.cpp is #include "board.h". This pulls in types.h automatically (since board.h includes it), giving you access to Board, Move, and all piece constants.

help me fix undeclared identifier errors in board.cpp

✔️ Awesome, I've got everything!

Great. Make sure you have saved board.cpp with all functions in order.

ⓧ I'd like to double check the full code

Here is the complete types.h:

#ifndef TYPES_H
#define TYPES_H

#include <cstdint>
#include <vector>
#include <string>

enum Piece : int {
    EMPTY = 0,
    wP = 1, wN = 2, wB = 3, wR = 4, wQ = 5, wK = 6,
    bP = 7, bN = 8, bB = 9, bR = 10, bQ = 11, bK = 12
};

enum Color : int { WHITE = 0, BLACK = 1 };

enum { CASTLE_WK = 1, CASTLE_WQ = 2, CASTLE_BK = 4, CASTLE_BQ = 8 };

struct Board {
    int squares[64];
    int sideToMove;
    int castleRights;
    int epSquare;
    int halfMoveClock;
    int fullMoveNumber;
};

struct Move {
    int from;
    int to;
    int captured;
    int promoted;
    int flags;
};

inline int rankOf(int sq) { return sq / 8; }
inline int fileOf(int sq) { return sq % 8; }
inline int sqOf(int rank, int file) { return rank * 8 + file; }

inline bool isWhitePiece(int p) { return p >= wP && p <= wK; }
inline bool isBlackPiece(int p) { return p >= bP && p <= bK; }

inline int pieceColor(int p) {
    if (p >= wP && p <= wK) return WHITE;
    if (p >= bP && p <= bK) return BLACK;
    return -1;
}

inline int pieceType(int p) {
    if (p >= wP && p <= wK) return p;
    if (p >= bP && p <= bK) return p - 6;
    return 0;
}

#endif

Here is the complete board.h:

#ifndef BOARD_H
#define BOARD_H

#include "types.h"

void initBoard(Board& b);
void printBoard(const Board& b);
std::string sqToAlgebraic(int sq);
int algebraicToSq(const std::string& s);
void makeMove(Board& b, const Move& m);
bool isSquareAttacked(const Board& b, int sq, int byColor);
bool isInCheck(const Board& b, int color);
int findKing(const Board& b, int color);

#endif

Here is the complete board.cpp:

#include "board.h"
#include <iostream>

void initBoard(Board& b) {
    int startPos[64] = {
        wR, wN, wB, wQ, wK, wB, wN, wR,
        wP, wP, wP, wP, wP, wP, wP, wP,
        EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
        EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
        EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
        EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
        bP, bP, bP, bP, bP, bP, bP, bP,
        bR, bN, bB, bQ, bK, bB, bN, bR
    };
    for (int i = 0; i < 64; i++) b.squares[i] = startPos[i];
    b.sideToMove = WHITE;
    b.castleRights = CASTLE_WK | CASTLE_WQ | CASTLE_BK | CASTLE_BQ;
    b.epSquare = -1;
    b.halfMoveClock = 0;
    b.fullMoveNumber = 1;
}

void printBoard(const Board& b) {
    const std::string symbols[] = {
        ".", "\u2659", "\u2658", "\u2657", "\u2656", "\u2655", "\u2654",
             "\u265F", "\u265E", "\u265D", "\u265C", "\u265B", "\u265A"
    };
    std::cout << "\n";
    for (int rank = 7; rank >= 0; rank--) {
        std::cout << (rank + 1) << "  ";
        for (int file = 0; file < 8; file++) {
            int piece = b.squares[sqOf(rank, file)];
            std::cout << symbols[piece] << " ";
        }
        std::cout << "\n";
    }
    std::cout << "   a b c d e f g h\n\n";
}

std::string sqToAlgebraic(int sq) {
    std::string s;
    s += (char)('a' + fileOf(sq));
    s += (char)('1' + rankOf(sq));
    return s;
}

int algebraicToSq(const std::string& s) {
    if (s.length() < 2) return -1;
    int file = s[0] - 'a';
    int rank = s[1] - '1';
    if (file < 0 || file > 7 || rank < 0 || rank > 7) return -1;
    return sqOf(rank, file);
}

int findKing(const Board& b, int color) {
    int king = (color == WHITE) ? wK : bK;
    for (int i = 0; i < 64; i++) {
        if (b.squares[i] == king) return i;
    }
    return -1;
}

bool isSquareAttacked(const Board& b, int sq, int byColor) {
    int rank = rankOf(sq), file = fileOf(sq);

    int knightOffsets[8][2] = {{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}};
    int enemyKnight = (byColor == WHITE) ? wN : bN;
    for (auto& off : knightOffsets) {
        int r = rank + off[0], f = file + off[1];
        if (r >= 0 && r < 8 && f >= 0 && f < 8) {
            if (b.squares[sqOf(r, f)] == enemyKnight) return true;
        }
    }

    if (byColor == WHITE) {
        if (rank > 0) {
            if (file > 0 && b.squares[sqOf(rank - 1, file - 1)] == wP) return true;
            if (file < 7 && b.squares[sqOf(rank - 1, file + 1)] == wP) return true;
        }
    } else {
        if (rank < 7) {
            if (file > 0 && b.squares[sqOf(rank + 1, file - 1)] == bP) return true;
            if (file < 7 && b.squares[sqOf(rank + 1, file + 1)] == bP) return true;
        }
    }

    int enemyKing = (byColor == WHITE) ? wK : bK;
    for (int dr = -1; dr <= 1; dr++) {
        for (int df = -1; df <= 1; df++) {
            if (dr == 0 && df == 0) continue;
            int r = rank + dr, f = file + df;
            if (r >= 0 && r < 8 && f >= 0 && f < 8) {
                if (b.squares[sqOf(r, f)] == enemyKing) return true;
            }
        }
    }

    int enemyBishop = (byColor == WHITE) ? wB : bB;
    int enemyRook = (byColor == WHITE) ? wR : bR;
    int enemyQueen = (byColor == WHITE) ? wQ : bQ;

    int diagDirs[4][2] = {{1,1},{1,-1},{-1,1},{-1,-1}};
    for (auto& dir : diagDirs) {
        int r = rank + dir[0], f = file + dir[1];
        while (r >= 0 && r < 8 && f >= 0 && f < 8) {
            int p = b.squares[sqOf(r, f)];
            if (p != EMPTY) {
                if (p == enemyBishop || p == enemyQueen) return true;
                break;
            }
            r += dir[0]; f += dir[1];
        }
    }

    int straightDirs[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
    for (auto& dir : straightDirs) {
        int r = rank + dir[0], f = file + dir[1];
        while (r >= 0 && r < 8 && f >= 0 && f < 8) {
            int p = b.squares[sqOf(r, f)];
            if (p != EMPTY) {
                if (p == enemyRook || p == enemyQueen) return true;
                break;
            }
            r += dir[0]; f += dir[1];
        }
    }

    return false;
}

bool isInCheck(const Board& b, int color) {
    int kingSq = findKing(b, color);
    return isSquareAttacked(b, kingSq, 1 - color);
}

void makeMove(Board& b, const Move& m) {
    int piece = b.squares[m.from];

    if (m.flags == 2) {
        int capturedPawnSq = sqOf(rankOf(m.from), fileOf(m.to));
        b.squares[capturedPawnSq] = EMPTY;
    }

    b.squares[m.to] = piece;
    b.squares[m.from] = EMPTY;

    if (m.flags == 3) {
        b.squares[m.to] = m.promoted;
    }

    if (m.flags == 1) {
        if (m.to == sqOf(0, 6)) { b.squares[sqOf(0, 5)] = wR; b.squares[sqOf(0, 7)] = EMPTY; }
        else if (m.to == sqOf(0, 2)) { b.squares[sqOf(0, 3)] = wR; b.squares[sqOf(0, 0)] = EMPTY; }
        else if (m.to == sqOf(7, 6)) { b.squares[sqOf(7, 5)] = bR; b.squares[sqOf(7, 7)] = EMPTY; }
        else if (m.to == sqOf(7, 2)) { b.squares[sqOf(7, 3)] = bR; b.squares[sqOf(7, 0)] = EMPTY; }
    }

    b.epSquare = -1;
    if ((piece == wP || piece == bP) && std::abs(rankOf(m.to) - rankOf(m.from)) == 2) {
        b.epSquare = sqOf((rankOf(m.from) + rankOf(m.to)) / 2, fileOf(m.from));
    }

    if (piece == wK) b.castleRights &= ~(CASTLE_WK | CASTLE_WQ);
    if (piece == bK) b.castleRights &= ~(CASTLE_BK | CASTLE_BQ);
    if (m.from == sqOf(0, 0) || m.to == sqOf(0, 0)) b.castleRights &= ~CASTLE_WQ;
    if (m.from == sqOf(0, 7) || m.to == sqOf(0, 7)) b.castleRights &= ~CASTLE_WK;
    if (m.from == sqOf(7, 0) || m.to == sqOf(7, 0)) b.castleRights &= ~CASTLE_BQ;
    if (m.from == sqOf(7, 7) || m.to == sqOf(7, 7)) b.castleRights &= ~CASTLE_BK;

    if (piece == wP || piece == bP || m.captured != EMPTY) {
        b.halfMoveClock = 0;
    } else {
        b.halfMoveClock++;
    }

    if (b.sideToMove == BLACK) b.fullMoveNumber++;
    b.sideToMove = 1 - b.sideToMove;
}
Compile and test the board display

Time to verify everything compiles and the starting position displays correctly. You will write a quick test in main.cpp that initializes the board and prints it.

  • Open main.cpp and paste this temporary test program:
#include "board.h"
#include <iostream>

int main() {
    Board board;
    initBoard(board);
    printBoard(board);
    std::cout << "Board initialized successfully.\n";
    return 0;
}

Why a temporary main?

This is a throwaway test. In a later step you will replace main.cpp with the full game loop. For now, it just confirms your board code compiles and displays correctly.

  • Save main.cpp.
  • The other source files (movegen.cpp, eval.cpp, search.cpp) are still empty, which will cause linker errors. Create stub files so the build succeeds. Add an empty function body to each by running:
echo '#include "movegen.h"' > movegen.cpp
echo '#include "eval.h"' > eval.cpp
echo '#include "search.h"' > search.cpp
  • You also need minimal headers for those stubs. Create them by running:
echo -e '#ifndef MOVEGEN_H\n#define MOVEGEN_H\n#include "types.h"\n#endif' > movegen.h
echo -e '#ifndef EVAL_H\n#define EVAL_H\n#include "types.h"\n#endif' > eval.h
echo -e '#ifndef SEARCH_H\n#define SEARCH_H\n#include "types.h"\n#endif' > search.h
  • Build the project by running:
make
  • Run the compiled program by running:
./chess
  • You should see the starting chess position printed with Unicode symbols, rank numbers on the left, and file letters along the bottom.

What should I see?

Your terminal should display an 8x8 grid with white pieces (♖♘♗♕♔♗♘♖) on ranks 1-2 and black pieces (♜♞♝♛♚♝♞♜) on ranks 7-8. Empty squares show as dots. File labels a b c d e f g h appear at the bottom.

Seeing squares or question marks instead of chess pieces?

Your terminal may not support Unicode rendering with your current font. The default macOS Terminal.app supports these symbols. If you see garbled output, try switching to Terminal.app or ensure your terminal's font includes the Unicode Miscellaneous Symbols block (U+2654 through U+265F).

help me fix Unicode chess symbols not displaying in my terminal

Your board representation is complete. The engine can now set up a game, display positions, detect attacks, and execute moves. Next up, you will teach it to generate all legal moves from any position.

Implement Move Generation

Your board can display a position, but it has no idea what moves are legal. A chess engine needs to generate every possible move for the side to move, then filter out any that leave the king in check.

Move generation is the engine's workhorse. Every time the engine evaluates a position, it needs the full list of legal moves. In this step, you'll build generators for every piece type including special moves like castling, en passant, and promotion.

In this step, get ready to:
  • Implement pawn and knight move generation with offset tables.
  • Implement sliding piece moves (bishop, rook, queen) and king moves with castling.
  • Add legality filtering and verify 20 legal moves from the starting position.
Implement pawn and knight move generation

Pawns are the trickiest piece to generate moves for. They push forward, capture diagonally, can double-push from their starting rank, promote on the back rank, and capture en passant. Knights are simpler since they always jump to the same 8 offsets regardless of other pieces.

  • Create the move generation header by opening movegen.h and adding these declarations:
#ifndef MOVEGEN_H
#define MOVEGEN_H

#include "types.h"

// Public interface: generate all legal moves for the current side
std::vector<Move> generateLegalMoves(const Board& b);
std::vector<Move> generatePseudoLegalMoves(const Board& b);

#endif

Why two functions?

Pseudo-legal moves ignore whether the king ends up in check. Generating them is fast. The legal move function then filters out any move that leaves your own king attacked. This two-pass approach keeps the code modular.

  • Save movegen.h.

Now for the implementation. Pawn moves have five cases: single push, double push, diagonal capture, en passant, and promotion. You'll handle all of them in one helper function.

  • Open movegen.cpp and add the includes plus the pawn move generator:
#include "movegen.h"
#include "board.h"
#include <cstdlib>

static void addPawnMoves(const Board& b, std::vector<Move>& moves) {
    int color = b.sideToMove;
    int pawn = (color == WHITE) ? wP : bP;
    int dir = (color == WHITE) ? 1 : -1;
    int startRank = (color == WHITE) ? 1 : 6;
    int promoRank = (color == WHITE) ? 7 : 0;

    for (int sq = 0; sq < 64; sq++) {
        if (b.squares[sq] != pawn) continue;
        int rank = rankOf(sq), file = fileOf(sq);
        int toSq = sqOf(rank + dir, file);
        if (b.squares[toSq] == EMPTY) {
            if (rankOf(toSq) == promoRank) {
                // Promote to all four piece types
                int base = (color == WHITE) ? wN : bN;
                for (int promo = base; promo <= base + 3; promo++) {
                    moves.push_back({sq, toSq, EMPTY, promo, 3});
                }
            } else {
                moves.push_back({sq, toSq, EMPTY, EMPTY, 0});
                // Double push from starting rank
                if (rank == startRank) {
                    int toSq2 = sqOf(rank + 2 * dir, file);
                    if (b.squares[toSq2] == EMPTY) {
                        moves.push_back({sq, toSq2, EMPTY, EMPTY, 0});
                    }
                }
            }
        }
        // Diagonal captures (including en passant)
        for (int df = -1; df <= 1; df += 2) {
            int f2 = file + df;
            if (f2 < 0 || f2 > 7) continue;
            int capSq = sqOf(rank + dir, f2);
            int target = b.squares[capSq];
            if (target != EMPTY && pieceColor(target) != color) {
                if (rankOf(capSq) == promoRank) {
                    int base = (color == WHITE) ? wN : bN;
                    for (int promo = base; promo <= base + 3; promo++) {
                        moves.push_back({sq, capSq, target, promo, 3});
                    }
                } else {
                    moves.push_back({sq, capSq, target, EMPTY, 0});
                }
            }
            // En passant capture
            if (capSq == b.epSquare) {
                int epCaptured = (color == WHITE) ? bP : wP;
                moves.push_back({sq, capSq, epCaptured, EMPTY, 2});
            }
        }
    }
}

What does this code do?

The function scans all 64 squares looking for pawns of the current color. For each pawn found, it generates:

  • A single forward push (if the target square is empty).
  • A double push (only from the starting rank, and only if both squares ahead are empty).
  • Diagonal captures (if an enemy piece occupies the diagonal square).
  • Promotions (when reaching the back rank, all four promotion types are generated).
  • En passant (when the diagonal square matches the board's epSquare).

Next, add the knight move generator directly below the pawn function.

  • Add the addKnightMoves function below addPawnMoves in movegen.cpp:
static void addKnightMoves(const Board& b, std::vector<Move>& moves) {
    int color = b.sideToMove;
    int knight = (color == WHITE) ? wN : bN;
    // All 8 possible L-shaped jumps
    int offsets[8][2] = {{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}};
    for (int sq = 0; sq < 64; sq++) {
        if (b.squares[sq] != knight) continue;
        int rank = rankOf(sq), file = fileOf(sq);
        for (auto& off : offsets) {
            int r = rank + off[0], f = file + off[1];
            if (r < 0 || r > 7 || f < 0 || f > 7) continue;
            int toSq = sqOf(r, f);
            int target = b.squares[toSq];
            // Can move to empty squares or squares with enemy pieces
            if (target == EMPTY || pieceColor(target) != color) {
                moves.push_back({sq, toSq, target, EMPTY, 0});
            }
        }
    }
}

What does this code do?

Knights use a precomputed offset table of 8 L-shaped jumps. For each knight on the board, the function tries all 8 destinations. If the destination is on the board and not occupied by a friendly piece, the move is added.

  • Save movegen.cpp.

Seeing compiler errors?

Make sure you included all three headers at the top: movegen.h, board.h, and <cstdlib>. The static keyword on each helper function keeps them private to this file.

Implement sliding piece and king moves

Sliding pieces (bishop, rook, queen) share the same movement pattern: cast a ray in each direction until you hit the edge of the board or another piece. The only difference is which directions each piece uses.

  • Add the addSlidingMoves function below addKnightMoves in movegen.cpp:
static void addSlidingMoves(const Board& b, std::vector<Move>& moves,
                            int piece, const int dirs[][2], int numDirs) {
    int color = b.sideToMove;
    for (int sq = 0; sq < 64; sq++) {
        if (b.squares[sq] != piece) continue;
        int rank = rankOf(sq), file = fileOf(sq);
        // Cast a ray in each direction
        for (int d = 0; d < numDirs; d++) {
            int r = rank + dirs[d][0], f = file + dirs[d][1];
            while (r >= 0 && r < 8 && f >= 0 && f < 8) {
                int toSq = sqOf(r, f);
                int target = b.squares[toSq];
                if (target == EMPTY) {
                    moves.push_back({sq, toSq, EMPTY, EMPTY, 0});
                } else {
                    // Can capture enemy piece, then ray stops
                    if (pieceColor(target) != color) {
                        moves.push_back({sq, toSq, target, EMPTY, 0});
                    }
                    break;
                }
                r += dirs[d][0]; f += dirs[d][1];
            }
        }
    }
}

What does this code do?

This single function handles bishops, rooks, and queens. It takes a piece code and a direction table as parameters. For each direction, it walks outward square by square. Empty squares generate a move and continue. Occupied squares generate a capture (if enemy) and stop the ray.

Bishops pass 4 diagonal directions. Rooks pass 4 straight directions. Queens pass all 8.

The king moves like a queen but only one square. It also has castling, which requires checking that the path is clear and not attacked.

  • Add the addKingMoves function below addSlidingMoves in movegen.cpp:
static void addKingMoves(const Board& b, std::vector<Move>& moves) {
    int color = b.sideToMove;
    int kingSq = findKing(b, color);
    if (kingSq == -1) return;
    int rank = rankOf(kingSq), file = fileOf(kingSq);
    // Normal king moves (one square in any direction)
    for (int dr = -1; dr <= 1; dr++) {
        for (int df = -1; df <= 1; df++) {
            if (dr == 0 && df == 0) continue;
            int r = rank + dr, f = file + df;
            if (r < 0 || r > 7 || f < 0 || f > 7) continue;
            int toSq = sqOf(r, f);
            int target = b.squares[toSq];
            if (target == EMPTY || pieceColor(target) != color) {
                moves.push_back({kingSq, toSq, target, EMPTY, 0});
            }
        }
    }
    // Castling: king must not be in check, path must be clear and unattacked
    int baseRank = (color == WHITE) ? 0 : 7;
    int opponent = 1 - color;
    if (!isSquareAttacked(b, kingSq, opponent)) {
        // Kingside castling
        int ksFlag = (color == WHITE) ? CASTLE_WK : CASTLE_BK;
        if (b.castleRights & ksFlag) {
            int f5 = sqOf(baseRank, 5), g1 = sqOf(baseRank, 6);
            if (b.squares[f5] == EMPTY && b.squares[g1] == EMPTY &&
                !isSquareAttacked(b, f5, opponent) &&
                !isSquareAttacked(b, g1, opponent)) {
                moves.push_back({kingSq, g1, EMPTY, EMPTY, 1});
            }
        }
        // Queenside castling
        int qsFlag = (color == WHITE) ? CASTLE_WQ : CASTLE_BQ;
        if (b.castleRights & qsFlag) {
            int d1 = sqOf(baseRank, 3), c1 = sqOf(baseRank, 2), b1 = sqOf(baseRank, 1);
            if (b.squares[d1] == EMPTY && b.squares[c1] == EMPTY && b.squares[b1] == EMPTY &&
                !isSquareAttacked(b, d1, opponent) &&
                !isSquareAttacked(b, c1, opponent)) {
                moves.push_back({kingSq, c1, EMPTY, EMPTY, 1});
            }
        }
    }
}

What does this code do?

The first half generates the king's 8 normal one-square moves. The second half handles castling. Castling requires three conditions:

  • The king is not currently in check.
  • The squares between king and rook are empty.
  • The king does not pass through or land on an attacked square.

The flag value 1 marks the move as castling so makeMove knows to also move the rook.

  • Save movegen.cpp.

Undeclared identifier errors?

The addKingMoves function calls findKing and isSquareAttacked from board.h. Make sure #include "board.h" is at the top of your file.

Add legality filtering and test

The helper functions generate pseudo-legal moves. Some of those moves might leave the king in check, which is illegal. The final step is to filter: try each move on a copy of the board, and keep only the moves where the king is safe.

  • Add the two public functions at the bottom of movegen.cpp:
std::vector<Move> generatePseudoLegalMoves(const Board& b) {
    std::vector<Move> moves;
    moves.reserve(256);
    int color = b.sideToMove;
    // Generate moves for each piece type
    addPawnMoves(b, moves);
    addKnightMoves(b, moves);
    // Direction tables for sliding pieces
    int bishopDirs[4][2] = {{1,1},{1,-1},{-1,1},{-1,-1}};
    int rookDirs[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
    int allDirs[8][2] = {{1,1},{1,-1},{-1,1},{-1,-1},{1,0},{-1,0},{0,1},{0,-1}};
    int bishop = (color == WHITE) ? wB : bB;
    int rook = (color == WHITE) ? wR : bR;
    int queen = (color == WHITE) ? wQ : bQ;
    addSlidingMoves(b, moves, bishop, bishopDirs, 4);
    addSlidingMoves(b, moves, rook, rookDirs, 4);
    addSlidingMoves(b, moves, queen, allDirs, 8);
    addKingMoves(b, moves);
    return moves;
}

std::vector<Move> generateLegalMoves(const Board& b) {
    auto pseudoMoves = generatePseudoLegalMoves(b);
    std::vector<Move> legal;
    legal.reserve(pseudoMoves.size());
    // Filter: only keep moves where the king is not left in check
    for (const auto& m : pseudoMoves) {
        Board copy = b;
        makeMove(copy, m);
        if (!isInCheck(copy, b.sideToMove)) {
            legal.push_back(m);
        }
    }
    return legal;
}

What does this code do?

The generatePseudoLegalMoves function calls all four helpers and collects every possible move. It reserves 256 slots upfront to avoid repeated memory allocations.

The generateLegalMoves function takes each pseudo-legal move, copies the board, applies the move, and checks if the moving side's king is in check. If it is, that move is discarded. This is the copy-make approach. It is simpler than make/unmake and fast enough for depth 4-5 search.

  • Save movegen.cpp.

Now test that the move generator produces the correct number of moves from the starting position. The standard chess starting position has exactly 20 legal moves for White (16 pawn moves and 4 knight moves).

  • Open main.cpp and add this temporary test code:
#include "types.h"
#include "board.h"
#include "movegen.h"
#include <iostream>

int main() {
    Board board;
    initBoard(board);
    auto moves = generateLegalMoves(board);
    std::cout << "Legal moves from starting position: " << moves.size() << "\n";
    for (const auto& m : moves) {
        std::cout << sqToAlgebraic(m.from) << sqToAlgebraic(m.to) << " ";
    }
    std::cout << "\n";
    return 0;
}

What does this code do?

This test initializes the starting position, generates all legal moves, and prints the count plus each move in algebraic notation. You should see exactly 20 moves listed.

  • Save main.cpp.
  • Build and run the test by running these commands:
make clean && make && ./chess
  • You should see Legal moves from starting position: 20 followed by all 20 moves printed in algebraic notation.

Not seeing 20 moves?

If you see fewer than 20, check that addPawnMoves generates double pushes from rank 1 (for White). The double push is nested inside the single push check, so it only fires when the single push square is also empty.

If you see more than 20, make sure generateLegalMoves is filtering with isInCheck(copy, b.sideToMove). Note that b.sideToMove refers to the side that just moved (before the copy's side flipped).

Why 20 moves exactly?

From the starting position, each of White's 8 pawns can push one or two squares (16 pawn moves total). Each of White's 2 knights has exactly 2 legal squares to jump to (4 knight moves). No other pieces can move because they are blocked by pawns. That gives 16 + 4 = 20.

✔️ Awesome, I've got everything!

Great. Double check you have saved both movegen.h and movegen.cpp.

ⓧ I'd like to double check the full code

Here is the complete movegen.h:

#ifndef MOVEGEN_H
#define MOVEGEN_H

#include "types.h"

std::vector<Move> generateLegalMoves(const Board& b);
std::vector<Move> generatePseudoLegalMoves(const Board& b);

#endif

Here is the complete movegen.cpp:

#include "movegen.h"
#include "board.h"
#include <cstdlib>

static void addPawnMoves(const Board& b, std::vector<Move>& moves) {
    int color = b.sideToMove;
    int pawn = (color == WHITE) ? wP : bP;
    int dir = (color == WHITE) ? 1 : -1;
    int startRank = (color == WHITE) ? 1 : 6;
    int promoRank = (color == WHITE) ? 7 : 0;

    for (int sq = 0; sq < 64; sq++) {
        if (b.squares[sq] != pawn) continue;
        int rank = rankOf(sq), file = fileOf(sq);
        int toSq = sqOf(rank + dir, file);
        if (b.squares[toSq] == EMPTY) {
            if (rankOf(toSq) == promoRank) {
                int base = (color == WHITE) ? wN : bN;
                for (int promo = base; promo <= base + 3; promo++) {
                    moves.push_back({sq, toSq, EMPTY, promo, 3});
                }
            } else {
                moves.push_back({sq, toSq, EMPTY, EMPTY, 0});
                if (rank == startRank) {
                    int toSq2 = sqOf(rank + 2 * dir, file);
                    if (b.squares[toSq2] == EMPTY) {
                        moves.push_back({sq, toSq2, EMPTY, EMPTY, 0});
                    }
                }
            }
        }
        for (int df = -1; df <= 1; df += 2) {
            int f2 = file + df;
            if (f2 < 0 || f2 > 7) continue;
            int capSq = sqOf(rank + dir, f2);
            int target = b.squares[capSq];
            if (target != EMPTY && pieceColor(target) != color) {
                if (rankOf(capSq) == promoRank) {
                    int base = (color == WHITE) ? wN : bN;
                    for (int promo = base; promo <= base + 3; promo++) {
                        moves.push_back({sq, capSq, target, promo, 3});
                    }
                } else {
                    moves.push_back({sq, capSq, target, EMPTY, 0});
                }
            }
            if (capSq == b.epSquare) {
                int epCaptured = (color == WHITE) ? bP : wP;
                moves.push_back({sq, capSq, epCaptured, EMPTY, 2});
            }
        }
    }
}

static void addKnightMoves(const Board& b, std::vector<Move>& moves) {
    int color = b.sideToMove;
    int knight = (color == WHITE) ? wN : bN;
    int offsets[8][2] = {{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}};
    for (int sq = 0; sq < 64; sq++) {
        if (b.squares[sq] != knight) continue;
        int rank = rankOf(sq), file = fileOf(sq);
        for (auto& off : offsets) {
            int r = rank + off[0], f = file + off[1];
            if (r < 0 || r > 7 || f < 0 || f > 7) continue;
            int toSq = sqOf(r, f);
            int target = b.squares[toSq];
            if (target == EMPTY || pieceColor(target) != color) {
                moves.push_back({sq, toSq, target, EMPTY, 0});
            }
        }
    }
}

static void addSlidingMoves(const Board& b, std::vector<Move>& moves,
                            int piece, const int dirs[][2], int numDirs) {
    int color = b.sideToMove;
    for (int sq = 0; sq < 64; sq++) {
        if (b.squares[sq] != piece) continue;
        int rank = rankOf(sq), file = fileOf(sq);
        for (int d = 0; d < numDirs; d++) {
            int r = rank + dirs[d][0], f = file + dirs[d][1];
            while (r >= 0 && r < 8 && f >= 0 && f < 8) {
                int toSq = sqOf(r, f);
                int target = b.squares[toSq];
                if (target == EMPTY) {
                    moves.push_back({sq, toSq, EMPTY, EMPTY, 0});
                } else {
                    if (pieceColor(target) != color) {
                        moves.push_back({sq, toSq, target, EMPTY, 0});
                    }
                    break;
                }
                r += dirs[d][0]; f += dirs[d][1];
            }
        }
    }
}

static void addKingMoves(const Board& b, std::vector<Move>& moves) {
    int color = b.sideToMove;
    int kingSq = findKing(b, color);
    if (kingSq == -1) return;
    int rank = rankOf(kingSq), file = fileOf(kingSq);
    for (int dr = -1; dr <= 1; dr++) {
        for (int df = -1; df <= 1; df++) {
            if (dr == 0 && df == 0) continue;
            int r = rank + dr, f = file + df;
            if (r < 0 || r > 7 || f < 0 || f > 7) continue;
            int toSq = sqOf(r, f);
            int target = b.squares[toSq];
            if (target == EMPTY || pieceColor(target) != color) {
                moves.push_back({kingSq, toSq, target, EMPTY, 0});
            }
        }
    }
    int baseRank = (color == WHITE) ? 0 : 7;
    int opponent = 1 - color;
    if (!isSquareAttacked(b, kingSq, opponent)) {
        int ksFlag = (color == WHITE) ? CASTLE_WK : CASTLE_BK;
        if (b.castleRights & ksFlag) {
            int f5 = sqOf(baseRank, 5), g1 = sqOf(baseRank, 6);
            if (b.squares[f5] == EMPTY && b.squares[g1] == EMPTY &&
                !isSquareAttacked(b, f5, opponent) &&
                !isSquareAttacked(b, g1, opponent)) {
                moves.push_back({kingSq, g1, EMPTY, EMPTY, 1});
            }
        }
        int qsFlag = (color == WHITE) ? CASTLE_WQ : CASTLE_BQ;
        if (b.castleRights & qsFlag) {
            int d1 = sqOf(baseRank, 3), c1 = sqOf(baseRank, 2), b1 = sqOf(baseRank, 1);
            if (b.squares[d1] == EMPTY && b.squares[c1] == EMPTY && b.squares[b1] == EMPTY &&
                !isSquareAttacked(b, d1, opponent) &&
                !isSquareAttacked(b, c1, opponent)) {
                moves.push_back({kingSq, c1, EMPTY, EMPTY, 1});
            }
        }
    }
}

std::vector<Move> generatePseudoLegalMoves(const Board& b) {
    std::vector<Move> moves;
    moves.reserve(256);
    int color = b.sideToMove;
    addPawnMoves(b, moves);
    addKnightMoves(b, moves);
    int bishopDirs[4][2] = {{1,1},{1,-1},{-1,1},{-1,-1}};
    int rookDirs[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
    int allDirs[8][2] = {{1,1},{1,-1},{-1,1},{-1,-1},{1,0},{-1,0},{0,1},{0,-1}};
    int bishop = (color == WHITE) ? wB : bB;
    int rook = (color == WHITE) ? wR : bR;
    int queen = (color == WHITE) ? wQ : bQ;
    addSlidingMoves(b, moves, bishop, bishopDirs, 4);
    addSlidingMoves(b, moves, rook, rookDirs, 4);
    addSlidingMoves(b, moves, queen, allDirs, 8);
    addKingMoves(b, moves);
    return moves;
}

std::vector<Move> generateLegalMoves(const Board& b) {
    auto pseudoMoves = generatePseudoLegalMoves(b);
    std::vector<Move> legal;
    legal.reserve(pseudoMoves.size());
    for (const auto& m : pseudoMoves) {
        Board copy = b;
        makeMove(copy, m);
        if (!isInCheck(copy, b.sideToMove)) {
            legal.push_back(m);
        }
    }
    return legal;
}

Your engine can now generate every legal move in any position. Next up, you'll teach it to evaluate which positions are good and which are bad using piece-square tables.

Build the Evaluation Function

Your engine can generate legal moves, but it has no idea which moves are good. A human player intuitively knows that controlling the center is strong and leaving a queen hanging is bad.

In this step, you'll give your engine positional awareness using piece-square tables. These tables assign a bonus or penalty to every piece on every square, so the engine learns that knights belong in the center and kings belong tucked away in the middlegame.

In this step, get ready to:
  • Define PeSTO piece values and middlegame piece-square tables.
  • Add endgame tables and tapered evaluation blending.
  • Wire up the evaluate function and verify it returns 0 for the starting position.
Define piece values and middlegame tables

The evaluation function needs two ingredients: how much each piece is worth (material), and where each piece prefers to be (positional bonuses). You'll use the PeSTO evaluation system, which provides tuned values for both.

  • Create the eval.h header file by adding the following code:
#ifndef EVAL_H
#define EVAL_H

#include "types.h"

int evaluate(const Board& b);

#endif

What does this header declare?

This declares a single function evaluate() that takes a board position and returns an integer score in centipawns. Positive means the current side to move is winning.

  • Save eval.h.

✔️ Awesome, I've got everything!

Great. Save the file and move on to the implementation.

ⓧ I'd like to double check the full code

#ifndef EVAL_H
#define EVAL_H

#include "types.h"

int evaluate(const Board& b);

#endif

Now for the big one. The eval.cpp file will hold all the piece-square table data. These tables come from the PeSTO evaluation function, a well-known set of tuned values from the chess programming community.

What is PeSTO?

PeSTO (Piece-Square Tables Only) is an evaluation function developed by Ronald Friederich, based on Tomasz Michniewski's simplified evaluation. It uses separate middlegame and endgame piece-square tables interpolated by game phase.

Traditional piece values (Pawn=100, Knight=320, Bishop=330, Rook=500, Queen=900) are a starting point. PeSTO tunes these values using machine learning against millions of games, landing on slightly different numbers (Pawn=82, Knight=337, etc.).

  • In eval.cpp, add the include, piece values, and all six middlegame piece-square tables:
#include "eval.h"

static const int mgPieceValue[7] = {0, 82, 337, 365, 477, 1025, 20000};
static const int egPieceValue[7] = {0, 94, 281, 297, 512, 936, 20000};

static const int mgPawnTable[64] = {
      0,   0,   0,   0,   0,   0,  0,   0,
     98, 134,  61,  95,  68, 126, 34, -11,
     -6,   7,  26,  31,  65,  56, 25, -20,
    -14,  13,   6,  21,  23,  12, 17, -23,
    -27,  -2,  -5,  12,  17,   6, 10, -25,
    -26,  -4,  -4, -10,   3,   3, 33, -12,
    -35,  -1, -20, -23, -15,  24, 38, -22,
      0,   0,   0,   0,   0,   0,  0,   0
};

static const int mgKnightTable[64] = {
    -167, -89, -34, -49,  61, -97, -15, -107,
     -73, -41,  72,  36,  23,  62,   7,  -17,
     -47,  60,  37,  65,  84, 129,  73,   44,
      -9,  17,  19,  53,  37,  69,  18,   22,
     -13,   4,  16,  13,  28,  19,  21,   -8,
     -23,  -9,  12,  10,  19,  17,  25,  -16,
     -29, -53, -12,  -3,  -1,  18, -14,  -19,
    -105, -21, -58, -33, -17, -28, -19,  -23
};

static const int mgBishopTable[64] = {
    -29,   4, -82, -37, -25, -42,   7,  -8,
    -26,  16, -18, -13,  30,  59,  18, -47,
    -16,  37,  43,  40,  35,  50,  37,  -2,
     -4,   5,  19,  50,  37,  37,   7,  -2,
     -6,  13,  13,  26,  34,  12,  10,   4,
      0,  15,  15,  15,  14,  27,  18,  10,
      4,  15,  16,   0,   7,  21,  33,   1,
    -33,  -3, -14, -21, -13, -12, -39, -21
};

static const int mgRookTable[64] = {
     32,  42,  32,  51, 63,  9,  31,  43,
     27,  32,  58,  62, 80, 67,  26,  44,
     -5,  19,  26,  36, 17, 45,  61,  16,
    -24, -11,   7,  26, 24, 35,  -8, -20,
    -36, -26, -12,  -1,  9, -7,   6, -23,
    -45, -25, -16, -17,  3,  0,  -5, -33,
    -44, -16, -20,  -9, -1, 11,  -6, -71,
    -19, -13,   1,  17, 16,  7, -37, -26
};

static const int mgQueenTable[64] = {
    -28,   0,  29,  12,  59,  44,  43,  45,
    -24, -39,  -5,   1, -16,  57,  28,  54,
    -13, -17,   7,   8,  29,  56,  47,  57,
    -27, -27, -16, -16,  -1,  17,  -2,   1,
     -9, -26,  -9, -10,  -2,  -4,   3,  -3,
    -14,   2, -11,  -2,  -5,   2,  14,   5,
    -35,  -8,  11,   2,   8,  15,  -3,   1,
     -1, -18,  -9,  10, -15, -25, -31, -50
};

static const int mgKingTable[64] = {
    -65,  23,  16, -15, -56, -34,   2,  13,
     29,  -1, -20,  -7,  -8,  -4, -38, -29,
     -9,  24,   2, -16, -20,   6,  22, -22,
    -17, -20, -12, -27, -30, -25, -14, -36,
    -49,  -1, -27, -39, -46, -44, -33, -51,
    -14, -14, -22, -46, -44, -30, -15, -27,
      1,   7,  -8, -64, -43, -16,   9,   8,
    -15,  36,  12, -54,   8, -28,  24,  14
};

What do these tables mean?

Each table has 64 entries corresponding to the 64 squares. The values are positional bonuses (or penalties) in centipawns. The tables are laid out with rank 8 at the top (index 0) and rank 1 at the bottom (index 56-63).

For example, the knight table gives high bonuses to central squares (up to +129 for a knight on f6) and big penalties to corner squares (-167 for a8). This teaches the engine that centralized knights are strong.

The king table heavily penalizes central positions in the middlegame (-56 for d8) but gives bonuses for staying castled on the wings. This keeps the king safe during active play.

  • Save eval.cpp.

Seeing typos in large tables?

The piece-square tables are the most error-prone part to type manually. If you get compile errors or strange evaluation scores later, double-check the table values against the code above.

Pay special attention to comma placement and negative signs. A missing negative on a knight corner penalty changes -167 to 167, which would make the engine love putting knights in corners.

help me check my piece-square table values

Add endgame tables and phase calculation

Piece placement that works in the middlegame can be wrong in the endgame. Kings should centralize when few pieces remain, and passed pawns become much more valuable. The endgame tables capture these different priorities.

  • In eval.cpp, add the six endgame tables, the pointer arrays, and the phase values below the middlegame tables:
static const int egPawnTable[64] = {
      0,   0,   0,   0,   0,   0,   0,   0,
    178, 173, 158, 134, 147, 132, 165, 187,
     94, 100,  85,  67,  56,  53,  82,  84,
     32,  24,  13,   5,  -2,   4,  17,  17,
     13,   9,  -3,  -7,  -7,  -8,   3,  -1,
      4,   7,  -6,   1,   0,  -5,  -1,  -8,
     13,   8,   8,  10,  13,   0,   2,  -7,
      0,   0,   0,   0,   0,   0,   0,   0
};

static const int egKnightTable[64] = {
    -58, -38, -13, -28, -31, -27, -63, -99,
    -25,  -8, -25,  -2,  -9, -25, -24, -52,
    -24, -20,  10,   9,  -1,  -9, -19, -41,
    -17,   3,  22,  22,  22,  11,   8, -18,
    -18,  -6,  16,  25,  16,  17,   4, -18,
    -23,  -3,  -1,  15,  10,  -3, -20, -22,
    -42, -20, -10,  -5,  -2, -20, -23, -44,
    -29, -51, -23, -15, -22, -18, -50, -64
};

static const int egBishopTable[64] = {
    -14, -21, -11,  -8, -7,  -9, -17, -24,
     -8,  -4,   7, -12, -3, -13,  -4, -14,
      2,  -8,   0,  -1, -2,   6,   0,   4,
     -3,   9,  12,   9, 14,  10,   3,   2,
     -6,   3,  13,  19,  7,  10,  -3,  -9,
    -12,  -3,   8,  10, 13,   3,  -7, -15,
    -14, -18,  -7,  -1,  4,  -9, -15, -27,
    -23,  -9, -23,  -5, -9, -16,  -5, -17
};

static const int egRookTable[64] = {
     13, 10, 18, 15, 12,  12,   8,   5,
     11, 13, 13, 11, -3,   3,   8,   3,
      7,  7,  7,  5,  4,  -3,  -5,  -3,
      4,  3, 13,  1,  2,   1,  -1,   2,
      3,  5,  8,  4, -5,  -6,  -8, -11,
     -4,  0, -5, -1, -7, -12,  -8, -16,
     -6, -6,  0,  2, -9,  -9, -11,  -3,
     -9,  2,  3, -1, -5, -13,   4, -20
};

static const int egQueenTable[64] = {
    -9,  22,  22,  27,  27,  19,  10,  20,
    -17,  20,  32,  41,  58,  25,  30,   0,
    -20,   6,   9,  49,  47,  35,  19,   9,
      3,  22,  24,  45,  57,  40,  57,  36,
    -18,  28,  19,  47,  31,  34,  39,  23,
    -16, -27,  15,   6,   9,  17,  10,   5,
    -22, -23, -30, -16, -16, -23, -36, -32,
    -33, -28, -22, -43,  -5, -32, -20, -41
};

static const int egKingTable[64] = {
    -74, -35, -18, -18, -11,  15,   4, -17,
    -12,  17,  14,  17,  17,  38,  23,  11,
     10,  17,  23,  15,  20,  45,  44,  13,
     -8,  22,  24,  27,  26,  33,  26,   3,
    -18,  -4,  21,  24,  27,  23,   9, -11,
    -19,  -3,  11,  21,  23,  16,   7,  -9,
    -27, -11,   4,  13,  14,   4,  -5, -17,
    -53, -34, -21, -11, -28, -14, -24, -43
};

static const int* mgTables[7] = { nullptr, mgPawnTable, mgKnightTable, mgBishopTable, mgRookTable, mgQueenTable, mgKingTable };
static const int* egTables[7] = { nullptr, egPawnTable, egKnightTable, egBishopTable, egRookTable, egQueenTable, egKingTable };
static const int phaseVal[7] = {0, 0, 1, 1, 2, 4, 0};

What is tapered evaluation?

The endgame king table is nearly opposite to the middlegame one. In the endgame, a centralized king is strong (bonuses for d4, e4, d5, e5), while in the middlegame it should hide on the wings.

The phaseVal array assigns a phase weight to each piece type: knights and bishops count as 1, rooks as 2, queens as 4. Pawns and kings contribute 0. The total phase starts at 24 (all pieces present) and decreases as pieces are captured.

The mgTables and egTables pointer arrays let the evaluate function look up the correct table using the piece type as an index. Index 0 is nullptr because EMPTY has type 0.

  • Save eval.cpp.

File getting long?

Your eval.cpp should be around 150 lines at this point. That is expected. Piece-square tables are data-heavy by nature. The actual logic comes next and is only about 25 lines.

help me organize my eval.cpp file

Implement the evaluate function and test

With all the table data in place, the evaluate function itself is straightforward. It loops over all 64 squares, looks up each piece's value and positional bonus, then blends the middlegame and endgame scores based on how much material is left on the board.

  • In eval.cpp, add the evaluate() function below the phaseVal array:
int evaluate(const Board& b) {
    int mgScore[2] = {0, 0};
    int egScore[2] = {0, 0};
    int phase = 0;
    for (int sq = 0; sq < 64; sq++) {
        int piece = b.squares[sq];
        if (piece == EMPTY) continue;
        int color = pieceColor(piece);
        int pt = pieceType(piece);
        int tableSq;
        if (color == WHITE) { tableSq = ((7 - rankOf(sq)) * 8) + fileOf(sq); }
        else { tableSq = (rankOf(sq) * 8) + fileOf(sq); }
        mgScore[color] += mgPieceValue[pt] + mgTables[pt][tableSq];
        egScore[color] += egPieceValue[pt] + egTables[pt][tableSq];
        phase += phaseVal[pt];
    }
    if (phase > 24) phase = 24;
    int mgTotal = mgScore[WHITE] - mgScore[BLACK];
    int egTotal = egScore[WHITE] - egScore[BLACK];
    int score = (mgTotal * phase + egTotal * (24 - phase)) / 24;
    return (b.sideToMove == WHITE) ? score : -score;
}

What does this function do?

The function accumulates separate middlegame and endgame scores for each color. For each piece on the board, it adds the piece's material value plus its positional bonus from the appropriate table.

The tableSq calculation flips the index for white pieces. The tables are stored with rank 8 at index 0, but white's rank 1 is at the bottom of our board array. For black, no flip is needed because the table is already from the opponent's perspective.

The tapered evaluation formula (mgTotal * phase + egTotal * (24 - phase)) / 24 smoothly blends between middlegame and endgame scores. When all pieces are present (phase=24), the score is pure middlegame. As pieces disappear, the endgame score gains more weight.

The final line returns the score relative to the side to move. This convention (positive = good for the current player) simplifies the search code in the next step.

  • Save eval.cpp.

✔️ Awesome, I've got everything!

Great. Double check you have saved both eval.h and eval.cpp.

ⓧ I'd like to double check the full code

#include "eval.h"

static const int mgPieceValue[7] = {0, 82, 337, 365, 477, 1025, 20000};
static const int egPieceValue[7] = {0, 94, 281, 297, 512, 936, 20000};

static const int mgPawnTable[64] = {
      0,   0,   0,   0,   0,   0,  0,   0,
     98, 134,  61,  95,  68, 126, 34, -11,
     -6,   7,  26,  31,  65,  56, 25, -20,
    -14,  13,   6,  21,  23,  12, 17, -23,
    -27,  -2,  -5,  12,  17,   6, 10, -25,
    -26,  -4,  -4, -10,   3,   3, 33, -12,
    -35,  -1, -20, -23, -15,  24, 38, -22,
      0,   0,   0,   0,   0,   0,  0,   0
};

static const int mgKnightTable[64] = {
    -167, -89, -34, -49,  61, -97, -15, -107,
     -73, -41,  72,  36,  23,  62,   7,  -17,
     -47,  60,  37,  65,  84, 129,  73,   44,
      -9,  17,  19,  53,  37,  69,  18,   22,
     -13,   4,  16,  13,  28,  19,  21,   -8,
     -23,  -9,  12,  10,  19,  17,  25,  -16,
     -29, -53, -12,  -3,  -1,  18, -14,  -19,
    -105, -21, -58, -33, -17, -28, -19,  -23
};

static const int mgBishopTable[64] = {
    -29,   4, -82, -37, -25, -42,   7,  -8,
    -26,  16, -18, -13,  30,  59,  18, -47,
    -16,  37,  43,  40,  35,  50,  37,  -2,
     -4,   5,  19,  50,  37,  37,   7,  -2,
     -6,  13,  13,  26,  34,  12,  10,   4,
      0,  15,  15,  15,  14,  27,  18,  10,
      4,  15,  16,   0,   7,  21,  33,   1,
    -33,  -3, -14, -21, -13, -12, -39, -21
};

static const int mgRookTable[64] = {
     32,  42,  32,  51, 63,  9,  31,  43,
     27,  32,  58,  62, 80, 67,  26,  44,
     -5,  19,  26,  36, 17, 45,  61,  16,
    -24, -11,   7,  26, 24, 35,  -8, -20,
    -36, -26, -12,  -1,  9, -7,   6, -23,
    -45, -25, -16, -17,  3,  0,  -5, -33,
    -44, -16, -20,  -9, -1, 11,  -6, -71,
    -19, -13,   1,  17, 16,  7, -37, -26
};

static const int mgQueenTable[64] = {
    -28,   0,  29,  12,  59,  44,  43,  45,
    -24, -39,  -5,   1, -16,  57,  28,  54,
    -13, -17,   7,   8,  29,  56,  47,  57,
    -27, -27, -16, -16,  -1,  17,  -2,   1,
     -9, -26,  -9, -10,  -2,  -4,   3,  -3,
    -14,   2, -11,  -2,  -5,   2,  14,   5,
    -35,  -8,  11,   2,   8,  15,  -3,   1,
     -1, -18,  -9,  10, -15, -25, -31, -50
};

static const int mgKingTable[64] = {
    -65,  23,  16, -15, -56, -34,   2,  13,
     29,  -1, -20,  -7,  -8,  -4, -38, -29,
     -9,  24,   2, -16, -20,   6,  22, -22,
    -17, -20, -12, -27, -30, -25, -14, -36,
    -49,  -1, -27, -39, -46, -44, -33, -51,
    -14, -14, -22, -46, -44, -30, -15, -27,
      1,   7,  -8, -64, -43, -16,   9,   8,
    -15,  36,  12, -54,   8, -28,  24,  14
};

static const int egPawnTable[64] = {
      0,   0,   0,   0,   0,   0,   0,   0,
    178, 173, 158, 134, 147, 132, 165, 187,
     94, 100,  85,  67,  56,  53,  82,  84,
     32,  24,  13,   5,  -2,   4,  17,  17,
     13,   9,  -3,  -7,  -7,  -8,   3,  -1,
      4,   7,  -6,   1,   0,  -5,  -1,  -8,
     13,   8,   8,  10,  13,   0,   2,  -7,
      0,   0,   0,   0,   0,   0,   0,   0
};

static const int egKnightTable[64] = {
    -58, -38, -13, -28, -31, -27, -63, -99,
    -25,  -8, -25,  -2,  -9, -25, -24, -52,
    -24, -20,  10,   9,  -1,  -9, -19, -41,
    -17,   3,  22,  22,  22,  11,   8, -18,
    -18,  -6,  16,  25,  16,  17,   4, -18,
    -23,  -3,  -1,  15,  10,  -3, -20, -22,
    -42, -20, -10,  -5,  -2, -20, -23, -44,
    -29, -51, -23, -15, -22, -18, -50, -64
};

static const int egBishopTable[64] = {
    -14, -21, -11,  -8, -7,  -9, -17, -24,
     -8,  -4,   7, -12, -3, -13,  -4, -14,
      2,  -8,   0,  -1, -2,   6,   0,   4,
     -3,   9,  12,   9, 14,  10,   3,   2,
     -6,   3,  13,  19,  7,  10,  -3,  -9,
    -12,  -3,   8,  10, 13,   3,  -7, -15,
    -14, -18,  -7,  -1,  4,  -9, -15, -27,
    -23,  -9, -23,  -5, -9, -16,  -5, -17
};

static const int egRookTable[64] = {
     13, 10, 18, 15, 12,  12,   8,   5,
     11, 13, 13, 11, -3,   3,   8,   3,
      7,  7,  7,  5,  4,  -3,  -5,  -3,
      4,  3, 13,  1,  2,   1,  -1,   2,
      3,  5,  8,  4, -5,  -6,  -8, -11,
     -4,  0, -5, -1, -7, -12,  -8, -16,
     -6, -6,  0,  2, -9,  -9, -11,  -3,
     -9,  2,  3, -1, -5, -13,   4, -20
};

static const int egQueenTable[64] = {
    -9,  22,  22,  27,  27,  19,  10,  20,
    -17,  20,  32,  41,  58,  25,  30,   0,
    -20,   6,   9,  49,  47,  35,  19,   9,
      3,  22,  24,  45,  57,  40,  57,  36,
    -18,  28,  19,  47,  31,  34,  39,  23,
    -16, -27,  15,   6,   9,  17,  10,   5,
    -22, -23, -30, -16, -16, -23, -36, -32,
    -33, -28, -22, -43,  -5, -32, -20, -41
};

static const int egKingTable[64] = {
    -74, -35, -18, -18, -11,  15,   4, -17,
    -12,  17,  14,  17,  17,  38,  23,  11,
     10,  17,  23,  15,  20,  45,  44,  13,
     -8,  22,  24,  27,  26,  33,  26,   3,
    -18,  -4,  21,  24,  27,  23,   9, -11,
    -19,  -3,  11,  21,  23,  16,   7,  -9,
    -27, -11,   4,  13,  14,   4,  -5, -17,
    -53, -34, -21, -11, -28, -14, -24, -43
};

static const int* mgTables[7] = { nullptr, mgPawnTable, mgKnightTable, mgBishopTable, mgRookTable, mgQueenTable, mgKingTable };
static const int* egTables[7] = { nullptr, egPawnTable, egKnightTable, egBishopTable, egRookTable, egQueenTable, egKingTable };
static const int phaseVal[7] = {0, 0, 1, 1, 2, 4, 0};

int evaluate(const Board& b) {
    int mgScore[2] = {0, 0};
    int egScore[2] = {0, 0};
    int phase = 0;
    for (int sq = 0; sq < 64; sq++) {
        int piece = b.squares[sq];
        if (piece == EMPTY) continue;
        int color = pieceColor(piece);
        int pt = pieceType(piece);
        int tableSq;
        if (color == WHITE) { tableSq = ((7 - rankOf(sq)) * 8) + fileOf(sq); }
        else { tableSq = (rankOf(sq) * 8) + fileOf(sq); }
        mgScore[color] += mgPieceValue[pt] + mgTables[pt][tableSq];
        egScore[color] += egPieceValue[pt] + egTables[pt][tableSq];
        phase += phaseVal[pt];
    }
    if (phase > 24) phase = 24;
    int mgTotal = mgScore[WHITE] - mgScore[BLACK];
    int egTotal = egScore[WHITE] - egScore[BLACK];
    int score = (mgTotal * phase + egTotal * (24 - phase)) / 24;
    return (b.sideToMove == WHITE) ? score : -score;
}

Now verify the evaluation compiles and produces a sensible result for the starting position.

  • Update main.cpp with a quick test by adding the following code:
#include "types.h"
#include "board.h"
#include "movegen.h"
#include "eval.h"
#include <iostream>

int main() {
    Board board;
    initBoard(board);
    printBoard(board);

    auto moves = generateLegalMoves(board);
    std::cout << "Legal moves: " << moves.size() << std::endl;

    int score = evaluate(board);
    std::cout << "Starting position eval: " << score << std::endl;

    return 0;
}

Why should the starting position be close to 0?

The starting position is perfectly symmetrical. Both sides have the same material and mirror-image piece placements. A well-tuned evaluation should return 0 (or very close to it) because neither side has an advantage.

A small non-zero value is acceptable due to rounding in the tapered evaluation formula. If you see a value like 10 or -5, that is fine. A value in the hundreds means a table has a typo.

  • Save main.cpp and compile the project by running:
make clean && make
  • Run the test by executing:
./chess
  • You should see the board display, "Legal moves: 20", and a "Starting position eval" value close to 0.

Seeing a large eval score?

If the starting position evaluates to hundreds or thousands of centipawns, you likely have a typo in one of the piece-square tables. The most common issue is a flipped sign on a table value.

If you get compile errors about undefined references to evaluate, make sure eval.cpp is saved and that your Makefile includes eval.cpp in the SRCS list.

help me debug my evaluation function

Your engine can now look at any board position and produce a score that reflects both material balance and piece placement quality. Next up, you'll build the search algorithm that uses this evaluation to look ahead and find the best move.

Implement Minimax with Alpha-Beta Pruning

Your engine can now evaluate any static position. But strong chess requires looking ahead.

In this step, you'll implement the search algorithm that lets the engine think multiple moves into the future. The engine will use negamax (a simplified form of minimax) with alpha-beta pruning to skip branches that can't possibly affect the final decision.

In this step, get ready to:
  • Implement a negamax search function with alpha-beta pruning.
  • Add a root search function that finds the best move and tracks nodes searched.
  • Test the engine from the starting position at depth 4.
Implement the negamax search function

Negamax is algebraically equivalent to minimax but only needs one recursive function instead of separate max and min branches. The trick is that the score is always from the current player's perspective, so you negate it when passing back to the parent.

Alpha-beta pruning adds two bounds. Alpha is the best score the current player can guarantee. Beta is the best score the opponent can guarantee. If any move scores at or above beta, the opponent would never allow this position, so you stop searching.

  • The search needs a header file first. Open search.h and add the following code:
#ifndef SEARCH_H
#define SEARCH_H

#include "types.h"

// Stores the result of a search: best move, its score, and how many positions were evaluated
struct SearchResult {
    Move bestMove;
    int score;
    int nodesSearched;
};

SearchResult findBestMove(const Board& b, int depth);

#endif

What does this code do?

The SearchResult struct bundles everything the caller needs after a search: which move to play, how good it is (in centipawns), and how many nodes were evaluated (useful for performance tuning).

  • Save search.h.
  • Now open search.cpp and add the includes, node counter, and negamax function:
#include "search.h"
#include "movegen.h"
#include "eval.h"
#include "board.h"
#include <algorithm>
#include <climits>

// Global counter for nodes searched (reset before each search)
static int nodesCount = 0;

// Negamax with alpha-beta pruning
// Score is always from the perspective of the side to move
static int negamax(Board& b, int depth, int alpha, int beta) {
    nodesCount++;

    // Base case: evaluate the position statically
    if (depth == 0) return evaluate(b);

    auto moves = generateLegalMoves(b);

    // No legal moves means checkmate or stalemate
    if (moves.empty()) {
        if (isInCheck(b, b.sideToMove)) return -100000 + (100 - depth);
        return 0;
    }

    for (const auto& m : moves) {
        Board copy = b;
        makeMove(copy, m);
        // Negate the score (opponent's best is our worst)
        int score = -negamax(copy, depth - 1, -beta, -alpha);

        // Beta cutoff: opponent would never allow this position
        if (score >= beta) return beta;
        // Improve alpha if this move is better than our current best
        if (score > alpha) alpha = score;
    }

    return alpha;
}

What does this code do?

The negamax function recursively searches the game tree. At depth 0, it returns the static evaluation. Otherwise, it tries every legal move and picks the best score.

The checkmate score -100000 + (100 - depth) ensures the engine prefers shorter checkmates (more remaining depth means earlier checkmate, so a higher magnitude score). Stalemate returns 0 (draw).

The alpha-beta logic is the key optimization. When score >= beta, we know the opponent has a better option elsewhere, so searching further is pointless. This prunes entire subtrees without affecting the final result.

  • Save search.cpp.

Why negamax instead of plain minimax?

Plain minimax needs two functions: one that maximizes for the current player and one that minimizes for the opponent. Negamax exploits the fact that max(a, b) = -min(-a, -b). By negating the score and swapping alpha/beta at each recursive call, you get the same result with half the code.

Add the root search function

The negamax function returns a score, but the caller needs to know which move produced that score. The root search function iterates through all legal moves at the top level and tracks which one scored highest.

  • In search.cpp, add the findBestMove function below the negamax function:
SearchResult findBestMove(const Board& b, int depth) {
    nodesCount = 0;
    auto moves = generateLegalMoves(b);

    SearchResult result;
    result.score = -INT_MAX;
    result.bestMove = moves.empty() ? Move{0, 0, 0, 0, 0} : moves[0];

    int alpha = -INT_MAX;
    int beta = INT_MAX;

    // Try each legal move and keep the one with the highest score
    for (const auto& m : moves) {
        Board copy = b;
        makeMove(copy, m);
        int score = -negamax(copy, depth - 1, -beta, -alpha);

        if (score > result.score) { result.score = score; result.bestMove = m; }
        if (score > alpha) alpha = score;
    }

    result.nodesSearched = nodesCount;
    return result;
}

What does this code do?

This function resets the node counter, generates all legal moves, then calls negamax at depth - 1 for each one. The move with the highest returned score becomes the best move.

The initial result.bestMove is set to the first legal move as a fallback. This guarantees the engine always returns a valid move even if all scores are very negative.

  • Save search.cpp.

Seeing a compile error about INT_MAX?

Make sure you included <climits> at the top of search.cpp. That header defines INT_MAX.

help me fix a compile error in search.cpp

✔️ Awesome, I've got everything!

Great. Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete search.h:

#ifndef SEARCH_H
#define SEARCH_H

#include "types.h"

struct SearchResult {
    Move bestMove;
    int score;
    int nodesSearched;
};

SearchResult findBestMove(const Board& b, int depth);

#endif

And here is the complete search.cpp:

#include "search.h"
#include "movegen.h"
#include "eval.h"
#include "board.h"
#include <algorithm>
#include <climits>

static int nodesCount = 0;

static int negamax(Board& b, int depth, int alpha, int beta) {
    nodesCount++;
    if (depth == 0) return evaluate(b);
    auto moves = generateLegalMoves(b);
    if (moves.empty()) {
        if (isInCheck(b, b.sideToMove)) return -100000 + (100 - depth);
        return 0;
    }
    for (const auto& m : moves) {
        Board copy = b;
        makeMove(copy, m);
        int score = -negamax(copy, depth - 1, -beta, -alpha);
        if (score >= beta) return beta;
        if (score > alpha) alpha = score;
    }
    return alpha;
}

SearchResult findBestMove(const Board& b, int depth) {
    nodesCount = 0;
    auto moves = generateLegalMoves(b);
    SearchResult result;
    result.score = -INT_MAX;
    result.bestMove = moves.empty() ? Move{0, 0, 0, 0, 0} : moves[0];
    int alpha = -INT_MAX;
    int beta = INT_MAX;
    for (const auto& m : moves) {
        Board copy = b;
        makeMove(copy, m);
        int score = -negamax(copy, depth - 1, -beta, -alpha);
        if (score > result.score) { result.score = score; result.bestMove = m; }
        if (score > alpha) alpha = score;
    }
    result.nodesSearched = nodesCount;
    return result;
}
Test the engine from the starting position

You need a small test program to verify the search works. You'll replace this with the full game loop in the next step.

  • Open main.cpp and add this temporary test program:
#include "types.h"
#include "board.h"
#include "movegen.h"
#include "search.h"
#include <iostream>
#include <chrono>

int main() {
    Board board;
    initBoard(board);
    printBoard(board);

    std::cout << "Searching at depth 4...\n";

    // Measure how long the search takes
    auto start = std::chrono::high_resolution_clock::now();
    auto result = findBestMove(board, 4);
    auto end = std::chrono::high_resolution_clock::now();
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

    std::cout << "Best move: " << sqToAlgebraic(result.bestMove.from)
              << sqToAlgebraic(result.bestMove.to) << "\n";
    std::cout << "Score: " << result.score << "\n";
    std::cout << "Nodes searched: " << result.nodesSearched << "\n";
    std::cout << "Time: " << ms << "ms\n";

    return 0;
}

What does this code do?

This initializes the starting position, runs findBestMove at depth 4, and prints the result along with performance stats. The chrono library measures wall-clock time so you can see how fast the search runs.

  • Save main.cpp.
  • Compile the full project by running:
make clean && make
  • Run the engine by executing:
./chess

You should see the starting board printed, followed by the engine's best move. A sensible result is a common opening move like e2e4, d2d4, or a knight move like g1f3. The search should complete within a few seconds and report roughly 50,000 to 200,000 nodes searched.

Search takes more than 10 seconds?

Check that the alpha-beta pruning logic in negamax is correct. If you accidentally wrote score > beta instead of score >= beta, pruning is less effective. Also verify you're negating correctly: -negamax(copy, depth - 1, -beta, -alpha) (note the negated and swapped alpha/beta).

If it's still slow, try reducing the depth to 3 temporarily to confirm the algorithm works at all.

help me debug my slow negamax search

How much does alpha-beta pruning help?

Without alpha-beta pruning, searching to depth 4 from the starting position would evaluate roughly 3 million nodes. With pruning, the engine typically evaluates 50,000 to 200,000 nodes for the same depth. That's a 15-60x reduction in work, all without changing the final result.

Your engine can now think ahead and find strong moves. Next up, you'll build the interactive game loop so you can actually play a full game against it.

Create the Game Loop and Play

You have a board that displays, a move generator that produces legal moves, an evaluation function that scores positions, and a search that finds the best move. The only missing piece is the glue that connects everything into a playable game.

In this step, you will write the game loop that ties all your modules together. The loop displays the board, accepts your moves as White, runs the engine as Black, and detects when the game ends.

In this step, get ready to:
  • Build the interactive game loop with player input parsing and move validation.
  • Wire up the engine to respond after each player move with timed search output.
  • Compile the final build and play a full game against your engine.
Build the interactive game loop with player input

The game loop is the main interaction cycle. Each iteration displays the board, checks if the game has ended, then handles whoever's turn it is. For your turn (White), the program reads algebraic notation like e2e4 and validates it against the legal move list.

  • Open main.cpp in your editor and add the following code:
#include "types.h"
#include "board.h"
#include "movegen.h"
#include "search.h"
#include <iostream>
#include <string>
#include <chrono>

int main() {
    Board board;
    initBoard(board);

    std::cout << "=== Chess Engine ===\n";
    std::cout << "Enter moves as: e2e4 (from-square to-square)\n";
    std::cout << "Promotions: e7e8q (append piece: q/r/b/n)\n";
    std::cout << "Type 'quit' to exit.\n\n";

    const int ENGINE_DEPTH = 4;

    while (true) {
        printBoard(board);
        auto legalMoves = generateLegalMoves(board);

        if (legalMoves.empty()) {
            if (isInCheck(board, board.sideToMove)) {
                std::cout << (board.sideToMove == WHITE ? "Black" : "White") << " wins by checkmate!\n";
            } else {
                std::cout << "Stalemate! It's a draw.\n";
            }
            break;
        }

        if (board.sideToMove == WHITE) {
            std::cout << "Your move: ";
            std::string input;
            std::getline(std::cin, input);

            if (input == "quit") break;

            if (input.length() < 4) {
                std::cout << "Invalid input. Use format: e2e4\n";
                continue;
            }

            int from = algebraicToSq(input.substr(0, 2));
            int to = algebraicToSq(input.substr(2, 2));

            if (from == -1 || to == -1) {
                std::cout << "Invalid square.\n";
                continue;
            }

            // Find matching legal move
            Move playerMove{0, 0, 0, 0, 0};
            bool found = false;
            for (const auto& m : legalMoves) {
                if (m.from == from && m.to == to) {
                    if (m.flags == 3) { // Promotion
                        char promoChar = (input.length() > 4) ? input[4] : 'q';
                        int promoBase = wN; // white pieces
                        int promoPiece = wQ;
                        switch (promoChar) {
                            case 'n': promoPiece = promoBase; break;
                            case 'b': promoPiece = promoBase + 1; break;
                            case 'r': promoPiece = promoBase + 2; break;
                            case 'q': promoPiece = promoBase + 3; break;
                        }
                        if (m.promoted == promoPiece) {
                            playerMove = m;
                            found = true;
                            break;
                        }
                    } else {
                        playerMove = m;
                        found = true;
                        break;
                    }
                }
            }

            if (!found) {
                std::cout << "Illegal move. Try again.\n";
                continue;
            }

            makeMove(board, playerMove);

What does this code do?

This first section sets up the game infrastructure and handles your turn as White.

  • initBoard(board) places all pieces in their starting positions and sets White to move first.
  • Each loop iteration calls generateLegalMoves(). If the list is empty, the game is over. The code checks isInCheck() to distinguish checkmate from stalemate.
  • Player input is parsed by splitting the string into from/to squares using algebraicToSq(). The code then searches the legal move list for a match.
  • For promotions (flag 3), the player appends a piece letter like e7e8q. The switch maps that letter to the matching promotion piece.

Do not save the file yet. You still need to add the engine's response in the next section.

Add the engine's turn with search timing

When it is Black's turn, the engine calls findBestMove() at depth 4. You will also use chrono to measure how long the search took, giving you insight into engine performance.

  • Directly after the closing brace of the if (board.sideToMove == WHITE) block, add the following else branch along with the closing braces for the while loop and main function:
        } else {
            std::cout << "Engine thinking (depth " << ENGINE_DEPTH << ")...\n";

            auto start = std::chrono::high_resolution_clock::now();
            auto result = findBestMove(board, ENGINE_DEPTH);
            auto end = std::chrono::high_resolution_clock::now();
            auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

            std::cout << "Engine plays: " << sqToAlgebraic(result.bestMove.from)
                      << sqToAlgebraic(result.bestMove.to);
            if (result.bestMove.flags == 3) {
                int pt = pieceType(result.bestMove.promoted);
                const char promoChars[] = " pnbrqk";
                std::cout << promoChars[pt];
            }
            std::cout << " (score: " << result.score
                      << ", nodes: " << result.nodesSearched
                      << ", time: " << ms << "ms)\n\n";

            makeMove(board, result.bestMove);
        }
    }

    return 0;
}

What does the engine turn do?

  • chrono::high_resolution_clock wraps the search call to measure elapsed milliseconds. This tells you how hard the engine had to work.
  • findBestMove() returns a SearchResult containing the best move, its evaluation score, and how many nodes were searched.
  • The output line shows the move in algebraic notation, the score in centipawns (positive means Black is winning), the total nodes searched, and the time taken.
  • If the engine promotes a pawn, it appends the promotion character using the promoChars lookup array.
  • Save main.cpp.

Seeing a syntax error when you save?

  • Make sure the } else { line connects directly to the closing brace of the White player block. There should be no stray braces between them.
  • Count your closing braces at the bottom of the file. You need three: one for the else block, one for the while loop, and one for main().
  • Check that the backslash-n sequences in the std::cout lines use \n and not a literal newline inside the string.

Help me fix a brace-mismatch error in my main.cpp game loop

✔️ Awesome, I've got everything!

Great. Double check you have saved main.cpp before moving on to compile.

ⓧ I'd like to double check the full code

Here is the complete main.cpp file for comparison:

#include "types.h"
#include "board.h"
#include "movegen.h"
#include "search.h"
#include <iostream>
#include <string>
#include <chrono>

int main() {
    Board board;
    initBoard(board);

    std::cout << "=== Chess Engine ===\n";
    std::cout << "Enter moves as: e2e4 (from-square to-square)\n";
    std::cout << "Promotions: e7e8q (append piece: q/r/b/n)\n";
    std::cout << "Type 'quit' to exit.\n\n";

    const int ENGINE_DEPTH = 4;

    while (true) {
        printBoard(board);
        auto legalMoves = generateLegalMoves(board);

        if (legalMoves.empty()) {
            if (isInCheck(board, board.sideToMove)) {
                std::cout << (board.sideToMove == WHITE ? "Black" : "White") << " wins by checkmate!\n";
            } else {
                std::cout << "Stalemate! It's a draw.\n";
            }
            break;
        }

        if (board.sideToMove == WHITE) {
            std::cout << "Your move: ";
            std::string input;
            std::getline(std::cin, input);

            if (input == "quit") break;

            if (input.length() < 4) {
                std::cout << "Invalid input. Use format: e2e4\n";
                continue;
            }

            int from = algebraicToSq(input.substr(0, 2));
            int to = algebraicToSq(input.substr(2, 2));

            if (from == -1 || to == -1) {
                std::cout << "Invalid square.\n";
                continue;
            }

            // Find matching legal move
            Move playerMove{0, 0, 0, 0, 0};
            bool found = false;
            for (const auto& m : legalMoves) {
                if (m.from == from && m.to == to) {
                    if (m.flags == 3) { // Promotion
                        char promoChar = (input.length() > 4) ? input[4] : 'q';
                        int promoBase = wN; // white pieces
                        int promoPiece = wQ;
                        switch (promoChar) {
                            case 'n': promoPiece = promoBase; break;
                            case 'b': promoPiece = promoBase + 1; break;
                            case 'r': promoPiece = promoBase + 2; break;
                            case 'q': promoPiece = promoBase + 3; break;
                        }
                        if (m.promoted == promoPiece) {
                            playerMove = m;
                            found = true;
                            break;
                        }
                    } else {
                        playerMove = m;
                        found = true;
                        break;
                    }
                }
            }

            if (!found) {
                std::cout << "Illegal move. Try again.\n";
                continue;
            }

            makeMove(board, playerMove);

        } else {
            std::cout << "Engine thinking (depth " << ENGINE_DEPTH << ")...\n";

            auto start = std::chrono::high_resolution_clock::now();
            auto result = findBestMove(board, ENGINE_DEPTH);
            auto end = std::chrono::high_resolution_clock::now();
            auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

            std::cout << "Engine plays: " << sqToAlgebraic(result.bestMove.from)
                      << sqToAlgebraic(result.bestMove.to);
            if (result.bestMove.flags == 3) {
                int pt = pieceType(result.bestMove.promoted);
                const char promoChars[] = " pnbrqk";
                std::cout << promoChars[pt];
            }
            std::cout << " (score: " << result.score
                      << ", nodes: " << result.nodesSearched
                      << ", time: " << ms << "ms)\n\n";

            makeMove(board, result.bestMove);
        }
    }

    return 0;
}
Compile the final build and play a game

All source files are now complete. Time to compile everything and play your first game against the engine.

  • Compile the full project by running:
make
  • You should see the compiler process each .cpp file with no errors, then link them into the chess binary.

Seeing linker errors or undefined references?

  • An "undefined reference" error means a function declared in a header has no matching implementation. Verify that all .cpp files are saved and listed in the SRCS variable of your Makefile.
  • If you see errors about chrono or auto not being recognized, confirm your Makefile has -std=c++17 in CXXFLAGS. Apple Clang defaults to C++98 without it.

Help me fix a compilation error in my chess engine build

  • Launch your chess engine by running:
./chess
  • You should see the starting board displayed with Unicode chess pieces, followed by a Your move: prompt.
  • Type e2e4 and press Enter to make your first move.
  • The engine will think for a moment, then print its response with the move, score, node count, and time.
  • Play several moves to confirm the game flows correctly. Try typing an invalid move like e2e5 to verify the illegality check works.
  • Type quit to exit when you are ready to move on.

What should I see?

After you play e2e4, the engine will respond with something like Engine plays: e7e5 (score: 10, nodes: 84302, time: 1200ms). The exact numbers depend on your hardware, but the engine should respond within a few seconds at depth 4.

Test special moves

To verify castling works, try a game where you develop your kingside pieces and castle with e1g1. For en passant, advance a pawn to the 5th rank and capture when the opponent double-pushes beside it. These special rules are what make a chess engine truly complete.

Engine takes too long to respond?

  • At depth 4, most positions should complete in under 5 seconds. If the engine takes much longer, verify you compiled with -O2 optimization in your Makefile.
  • If you want faster responses during testing, change ENGINE_DEPTH from 4 to 3 in main.cpp. The engine will be slightly weaker but respond much faster.

My chess engine is too slow or not responding

You now have a fully working chess engine that you built from scratch. It displays the board, accepts your moves, validates them against the rules, searches for the best response, and detects when the game ends. Next up, the Secret Mission will make it significantly faster with move ordering.

Secret mission

Add MVV-LVA Move Ordering

Your engine searches every move in whatever order the generator produces them. That means alpha-beta pruning is leaving performance on the table. By searching the best captures first, you can make the engine 2-5x faster at the same depth.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The chess-engine/ project directory on your Desktop (source files, compiled binary, object files).

Keep everything running

No action needed. Choose this if you're still actively building or want to keep playing against your engine.

  • Your project files remain in ~/Desktop/chess-engine/.
  • The compiled chess binary is ready to run anytime with ./chess.
  • Xcode Command Line Tools remain installed on your system. These are useful for any future C++ or macOS development.

Pause - I'll come back to this later

Free up a small amount of disk space by removing compiled artifacts while keeping your source code intact.

  • Navigate to your project directory and run the clean target to remove object files and the binary:
cd ~/Desktop/chess-engine
make clean
  • Your source files (.h and .cpp) and Makefile remain untouched.
  • When you come back, rebuild with make from inside the project folder.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes everything you built in this project.

  • Delete the entire project directory by running:
cd ~/Desktop
rm -rf chess-engine
  • Confirm the folder is gone by running:
ls ~/Desktop | grep chess
  • You should see no output, confirming the directory has been removed.

Nice Work!

Nice Work!

You just built a chess engine from scratch in C++. Not a toy demo, but a real engine that searches thousands of positions per second and plays a legitimate game of chess.

Here's what you built and learned:

  • A complete legal move generator with an 8x8 board representation handling all chess rules including castling, en passant, and promotion.
  • A PeSTO evaluation function using piece-square tables with middlegame/endgame blending to give the engine positional awareness.
  • Minimax search with alpha-beta pruning powering an interactive terminal chess game where you play against your own AI.
  • Secret Mission: Added MVV-LVA move ordering for dramatically faster search, reducing nodes evaluated by 2-5x.

Ready to quiz yourself?