← blog

Building a Chess Engine, Part 6

The final part covers UCI - the standard protocol for connecting the engine to a GUI - and what comes next for the engine.

The UCI Protocol

UCI (Universal Chess Interface) is the standard protocol that lets a chess engine communicate with a GUI like Cutechess, Arena, or Lichess's bot infrastructure. The protocol is text-based over stdin/stdout: the GUI sends commands, the engine replies. The engine runs in its own process; the GUI manages the clock and display.

The handshake

When the GUI connects, it sends uci. The engine replies with its name, author, and uciok. The GUI then sends isready, and the engine replies readyok once it's done initialising. The main loop just reads lines and dispatches based on the command token.

while(std::getline(std::cin, line)){
    std::istringstream args(line);
    std::string cmd;
    args >> cmd;
    if      (cmd == "uci")      { std::cout << "id name Chess Engine
"
                                             << "id author r4hulrr
"
                                             << "uciok
"; }
    else if (cmd == "isready") { std::cout << "readyok
"; }
    else if (cmd == "position") { handlePosition(board, args); }
    else if (cmd == "go")       { handleGo(board, args); }
    else if (cmd == "quit")     { break; }
}

The position command tells the engine what position to search. It comes in two forms: position startpos moves e2e4 e7e5 ... which sets up the starting position and then applies a sequence of moves, and position fen <fen_string> moves ... which sets up from a FEN string. This engine currently only handles startpos; FEN parsing is straightforward to add later.

Applying the move list works by generating all legal moves and finding the one that matches the from/to/promotion in the token.

bool applyMove(Board& board, const std::string& token){
    int from  = parseSquare(token.substr(0, 2));
    int to    = parseSquare(token.substr(2, 2));
    Piece promo = (token.size() >= 5)
                  ? parsePromo(token[4]) : QUEEN;
    MoveGen gen(board);
    for (const Move& m : gen.generateMoves()){
        if (m.from != from || m.to != to) continue;
        if ((m.flags == PROMOTION || m.flags == PROMOTION_CAPTURE)
            && m.promotionPiece != promo) continue;
        board.makeMove(m);
        return true;
    }
    return false;
}

The go command

The go command tells the engine to start searching. The most common form for a real game is go wtime 60000 btime 60000 winc 500 binc 500, giving remaining clock times and increments in milliseconds. The engine parses these and allocates a time budget before calling the search.

The engine currently only accepts go depth N for a fixed-depth search, which is how it was first tested - it's the simplest form and useful for perft and capable of playing matches. The output is a required info line (which the GUI uses to show depth, score, and principal variation) followed by bestmove.

void handleGo(Board& board, std::istringstream& args){
    std::string token;
    int depth{8};
    while(args >> token)
        if (token == "depth") args >> depth;
    Search s;
    SearchResult result = s.getBestMove(board, depth);
    std::cout << "info depth " << depth
              << " score cp "  << result.bestScore
              << " nodes "     << result.nodes
              << " pv "        << moveName(result.bestMove) << "
";
    std::cout << "bestmove " << moveName(result.bestMove) << "
";
}

One important detail - the UCI spec requires std::cout << std::unitbuf at startup. Without it, stdout may be buffered and the GUI will hang waiting for output that has been written but not flushed.

What comes next

The engine as described is complete and playable. It correctly generates all legal moves, evaluates positions with material and piece-square bonuses, searches with negamax and alpha-beta pruning, and communicates via UCI. We can hook it up to a GUI like cutechess and play against it.

The natural next improvements, roughly in order of impact, are:

  • quiescence search
  • iterative deepening
  • adding time management to UCI (allocate a per-move budget from the remaining clock)
  • a transposition table
  • and eventually magic bitboards for faster move generation at deeper depths.

Each of these is a substantial improvement on its own. The engine described here is a solid foundation for all of them - the architecture is clean enough that each feature can be added in isolation, and will be added in v2.

← previousMove Ordering ← back to blog