stellar

Stellar - UCI Chess engine written in C++20
git clone git://git.dimitrijedobrota.com/stellar.git
Log | Files | Refs | README | LICENSE

commit d519ce0daa0a384b31511d5a730900697f6c3fe6
parent 0dc53eb006ea1ea7da949a6f096ee81f70ae22e7
Author: Dimitrije Dobrota <mail@dimitrijedobrota.com>
Date:   Tue, 19 Sep 2023 12:06:59 +0000

Version 1.1

- General Improvements
    * Fix UCI time bugs
    * Improve formatting
    * Report score from engine
    * Terminate when mate found
    * Play last best move when search has been terminated
    * MoveList generate only legal moves -- expensive!
    * Extract repetition logic
    * Extract timer logic

- Add Arena for testing purposes
    * Start the engine and make communication
    * Play multiple games between engines
    * Custom starting position
    * Game PGN info with SAN notation
    * Arena detects threefold, illegal moves, and timeouts
    * Multilevel Logging

Diffstat:
M.clang-format | 6+++---
MCMakeLists.txt | 2+-
Asrc/arena/CMakeLists.txt | 20++++++++++++++++++++
Asrc/arena/arena.cpp | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arena/engine.cpp | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arena/engine.hpp | 39+++++++++++++++++++++++++++++++++++++++
Asrc/arena/game.cpp | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arena/game.hpp | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arena/logger.hpp | 31+++++++++++++++++++++++++++++++
Asrc/arena/match.cpp | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arena/match.hpp | 29+++++++++++++++++++++++++++++
Msrc/board/board.hpp | 8++++++++
Msrc/engine/engine.cpp | 78++++++++++++++++++++++++++++++++++--------------------------------------------
Msrc/engine/uci.cpp | 45++++++++++++---------------------------------
Msrc/engine/uci.hpp | 9+--------
Msrc/move/move.cpp | 11++++++-----
Msrc/move/move.hpp | 3++-
Msrc/move/movelist.cpp | 4++--
Msrc/move/movelist.hpp | 10+++++++++-
Msrc/perft/perft.cpp | 14++++----------
Msrc/utils/CMakeLists.txt | 4++++
Msrc/utils/color.hpp | 5+++++
Asrc/utils/repetition.hpp | 37+++++++++++++++++++++++++++++++++++++
Msrc/utils/square.hpp | 2+-
Asrc/utils/timer.hpp | 16++++++++++++++++
25 files changed, 770 insertions(+), 109 deletions(-)

diff --git a/.clang-format b/.clang-format @@ -34,11 +34,11 @@ AlignTrailingComments: true AllowAllArgumentsOnNextLine: true AllowAllParametersOfDeclarationOnNextLine: true AllowShortEnumsOnASingleLine: false -AllowShortBlocksOnASingleLine: Never -AllowShortCaseLabelsOnASingleLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: true AllowShortFunctionsOnASingleLine: true AllowShortLambdasOnASingleLine: All -AllowShortIfStatementsOnASingleLine: true +AllowShortIfStatementsOnASingleLine: Always AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -3,7 +3,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) project( Stellar - VERSION 1.0.0 + VERSION 1.1.0 DESCRIPTION "Chess engine written in C++" HOMEPAGE_URL https://git.dimitrijedobrota.com/stellar.git LANGUAGES CXX diff --git a/src/arena/CMakeLists.txt b/src/arena/CMakeLists.txt @@ -0,0 +1,20 @@ +add_executable(arena + arena.cpp + match.cpp + engine.cpp + game.cpp +) + +target_link_libraries(arena + PRIVATE Stellar_version + PRIVATE board + PRIVATE moves + PRIVATE piece + PRIVATE utils +) + +set_target_properties(arena PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) diff --git a/src/arena/arena.cpp b/src/arena/arena.cpp @@ -0,0 +1,102 @@ +#include <format> +#include <functional> +#include <iostream> +#include <mutex> +#include <thread> + +#include <bits/getopt_core.h> +#include <stdexcept> + +#include "logger.hpp" +#include "match.hpp" + +class Arena { + public: + Arena(const Arena &) = delete; + Arena &operator==(const Arena &) = delete; + Arena(const char *name1, const char *name2) : engine1(new Engine(name1)), engine2(new Engine(name2)) { + logger::log(std::format("Arena {}: created", id), logger::Debug); + } + + ~Arena() { + delete (engine1), delete (engine2); + logger::log(std::format("Arena {}: destroyed", id), logger::Debug); + } + + void operator()(const std::vector<std::string> positions, Match::Settings swhite, + Match::Settings sblack) { + Match match(*engine1, *engine2); + for (const std::string &fen : positions) { + logger::log( + std::format("Arena {}: match from {}", id, fen == Game::startPosition ? "startpos" : fen), + logger::Debug); + Arena::print(match.play(swhite, sblack, fen)); + } + } + + private: + static void print(const Game game) { + mutex.lock(); + std::cout << game << std::endl; + mutex.unlock(); + } + + static uint16_t id_t; + uint16_t id = id_t++; + + Engine *engine1; + Engine *engine2; + + static std::mutex mutex; +}; + +uint16_t Arena::id_t = 0; + +std::mutex Arena::mutex; +void usage(const char *program) { + std::cerr << program << ": "; + std::cerr << "[-f fen]"; + std::cerr << "\n[-G wmovestogo -g bmovestogo]"; + std::cerr << "\n[-D wdepth -d bdepth]"; + std::cerr << "\n[-T wtime -t btime]"; + std::cerr << "\n[-I winc -i binc]"; + std::cerr << "\n-E engine1 -e engine2"; + std::cerr << "\n[- startpos] ... fen ..."; +} + +int main(int argc, char *argv[]) { + char *engine1 = NULL, *engine2 = NULL; + Match::Settings settings1, settings2; + + char c; + while ((c = getopt(argc, argv, "hE:e:D:d:T:t:I:i:G:g:N:")) != -1) { + switch (c) { + case 'E': engine1 = optarg; break; + case 'e': engine2 = optarg; break; + case 'D': settings1.depth = atoi(optarg); break; + case 'd': settings2.depth = atoi(optarg); break; + case 'T': settings1.time = atoll(optarg); break; + case 't': settings2.time = atoll(optarg); break; + case 'I': settings1.inc = atoll(optarg); break; + case 'i': settings2.inc = atoll(optarg); break; + case 'G': settings1.togo = atoi(optarg); break; + case 'g': settings2.togo = atoi(optarg); break; + case 'h': usage(argv[0]); return 1; + default: usage(argv[0]), abort(); + } + } + + if (!engine1 || !engine2) { + usage(argv[0]); + abort(); + } + + std::vector<std::string> positions; + for (int i = optind; i < argc; i++) + positions.push_back(!strcmp(argv[i], "-") ? start_position : argv[i]); + + Arena arena(engine1, engine2); + arena(positions, settings1, settings2); + + return 0; +} diff --git a/src/arena/engine.cpp b/src/arena/engine.cpp @@ -0,0 +1,121 @@ +#include "engine.hpp" +#include "logger.hpp" + +#include <sstream> + +#include <sys/wait.h> +#include <unistd.h> + + +uint16_t Engine::id_t = 0; +Engine::Engine(const char *file) : file(file) { + if (pipe(fd_to) < 0 || pipe(fd_from) < 0) { + logger::error("pipe"); + throw std::runtime_error("pipe failed"); + } + + if ((engine_pid = fork()) > 0) { + start_engine(); + } else if (engine_pid < 0) { + logger::error("fork"); + throw std::runtime_error("fork failed"); + } + + if (close(fd_to[0]) < 0 || close(fd_from[1])) { + logger::error("close"); + throw std::runtime_error("close failed"); + } + + send("uci"); + + logger::log(std::format("Engine {}: waiting for uciok from {}", id, file)); + while (true) { + std::string tmp, response = receive(); + if (response == "uciok") break; + std::stringstream ss(response); + ss >> tmp >> tmp; + ss.ignore(1); + if (tmp == "name") getline(ss, name); + if (tmp == "author") getline(ss, author); + } + logger::log(std::format("Engine {}: {} is {} by {}", id, file, name, author)); + logger::log(std::format("Engine {}: created", id), logger::Debug); +} + +Engine::~Engine() { + send("quit"); + waitpid(engine_pid, NULL, 0); + // kill(engine_pid, SIGKILL); + + if (close(fd_to[1]) < 0) logger::error("close"); + if (close(fd_from[0]) < 0) logger::error("close"); + logger::log("Engine: destroyed", logger::Debug); +} + +void Engine::send(std::string &&command) { + command.push_back('\n'); + const char *buffer = command.data(); + size_t to_write = command.size(); + while (to_write) { + ssize_t size = write(fd_to[1], buffer, to_write); + if (size == -1) { + logger::error("write"); + throw std::runtime_error("write failed"); + } + buffer += size, to_write -= size; + } + command.pop_back(); + logger::log(std::format("Engine {}: TO {}: {}", id, name.size() ? name : file, command), logger::Info); +} + +std::string Engine::receive(void) { + int size = 0; + + while (true) { + if (!q.empty()) { + std::string response = q.front(); + logger::log(std::format("Engine {}: FROM {}: {}", id, name.size() ? name : file, response), + logger::Info); + q.pop(); + return response; + } + + if ((size = read(fd_from[0], rb + rb_size, sizeof(rb) - rb_size)) == -1) { + logger::error("read"); + throw std::runtime_error("read failed"); + } + + int last = 0; + for (int i = rb_size; i < rb_size + size; i++) { + if (rb[i] == '\n') { + q.push(q_buffer); + q_buffer.clear(); + last = i; + continue; + } + q_buffer += rb[i]; + } + + rb_size += size; + if (last) { + rb_size -= last + 1; + memcpy(rb, rb + last + 1, rb_size); + } + } +} + +[[noreturn]] void Engine::start_engine(void) { + if (close(fd_to[1]) < 0 || close(fd_from[0])) { + logger::error("close"); + throw std::runtime_error("close failed"); + } + + if (dup2(fd_to[0], 0) < 0 || dup2(fd_from[1], 1) < 0) { + logger::error("dup2"); + throw std::runtime_error("dup2 failed"); + } + + execl(file, file, (char *)NULL); + logger::error("execl"); + throw std::runtime_error("execl failed"); +} diff --git a/src/arena/engine.hpp b/src/arena/engine.hpp @@ -0,0 +1,39 @@ +#ifndef STELLAR_ARENA_ENGINE_H +#define STELLAR_ARENA_ENGINE_H + +#include <queue> + +class Engine { + public: + Engine(const Engine &) = delete; + Engine &operator=(const Engine &) = delete; + Engine(const char *file); + + ~Engine(); + + const std::string &get_name(void) const { return name; } + const std::string &get_author(void) const { return author; } + + void send(std::string &&command); + std::string receive(void); + + private: + [[noreturn]] void start_engine(void); + + const char *file = nullptr; + int fd_to[2], fd_from[2]; + pid_t engine_pid; + + std::string name, author; + + char rb[1000]; + int rb_size = 0; + + std::queue<std::string> q; + std::string q_buffer; + + static uint16_t id_t; + uint16_t id = id_t++; +}; + +#endif diff --git a/src/arena/game.cpp b/src/arena/game.cpp @@ -0,0 +1,106 @@ +#include "game.hpp" + +#include "board.hpp" +#include "logger.hpp" +#include "timer.hpp" + +#include <format> + +bool Game::san = true; +const std::string Game::startPosition = start_position; + +uint16_t Game::id_t = 0; +Game::~Game() { logger::log(std::format("Game {}: destroyed", id), logger::Debug); } +Game::Game(const uint16_t match_id, const std::string white, const std::string black, const std::string fen) + : match_id(match_id), white(white), black(black), fen(fen) { + logger::log(std::format("Game {}: started", id), logger::Debug); +} + +const std::string Game::get_moves(void) const { + std::string res; + if (list.size()) res += (std::string)list[0]; + for (int i = 1; i < list.size(); i++) + res += " " + (std::string)list[i]; + return res; +} + +const std::string Game::to_san(const Board &board, const Move move) { + Board copy = board; + if (!move.make(copy)) { + logger::log("illegal move", logger::Critical); + throw std::runtime_error("illegal move"); + } + + if (move.is_castle_king()) return "O-O"; + if (move.is_castle_queen()) return "O-O-O"; + + const piece::Type piece = board.get_square_piece_type(move.source()); + const piece::Type target = board.get_square_piece_type(move.target()); + + std::string res; + if (piece != piece::PAWN) { + U64 potential = board.get_bitboard_square_land(move.target(), piece, board.get_side()); + + if (bit::count(potential) > 1) { + int file[9] = {0}, rank[9] = {0}; + uint8_t square_i; + bitboard_for_each_bit(square_i, potential) { + const std::string crd = square::to_coordinates(static_cast<square::Square>(square_i)); + file[crd[0] & 0x3]++; + rank[crd[1] & 0x3]++; + } + + const std::string crd = square::to_coordinates(move.source()); + if (file[crd[0] & 0x3] == 1) res += crd[0]; + else if (rank[crd[1] & 0x3] == 1) + res += crd[1]; + else + res += crd; + } + + res += piece::get_code(piece, color::WHITE); + if (target != piece::NONE) res += "x"; + res += square::to_coordinates(move.target()); + } else { + if (target != piece::NONE) res += std::format("{}x", square::to_coordinates(move.source())[0]); + res += square::to_coordinates(move.target()); + if (move.is_promote()) res += piece::get_code(move.promoted(), color::WHITE); + if (move.is_enpassant()) res += " e.p."; + } + + if (!MoveList(copy, false, true).size()) res += "#"; + else if (copy.is_check()) + res += "+"; + + return res; +} + +std::ostream &operator<<(std::ostream &os, const Game &game) { + static const std::string name[] = {"death", "time forfeit", "rules infraction", "repetition"}; + os << std::format("[Event \"Match {}\"]", game.match_id); + os << std::format("\n[Site \"{}\"]", "Stellar Arena"); + os << std::format("\n[Date \"{}\"]", timer::get_ms()); + os << std::format("\n[Round \"{}\"]", game.id); + os << std::format("\n[White \"{}\"]", game.get_white()); + os << std::format("\n[Black \"{}\"]", game.get_black()); + os << std::format("\n[Result \"{}-{}\"]", (int)game.is_win_white(), (int)game.is_win_black()); + os << std::format("\n[Termination \"{}\"]", name[to_underlying(game.get_terminate())]); + if (game.fen != Game::startPosition) { + os << std::format("\n[SetUp \"1\"]"); + os << std::format("\n[FEN \"{}\"]", game.fen); + } + os << '\n'; + + if (!game.list.size()) return os; + Board board(game.fen); + + const color::Color side = board.get_side(); + if (side == color::BLACK) os << std::format("1. ... "); + for (int i = 0; i < game.list.size(); i++) { + if (i % 2 == to_underlying(side)) os << std::format("{}. ", i / 2 + 1); + os << std::format("{} ", Game::san ? Game::to_san(board, game.list[i]) : (std::string)game.list[i]); + game.list[i].make(board); + } + + return os; +} diff --git a/src/arena/game.hpp b/src/arena/game.hpp @@ -0,0 +1,56 @@ +#ifndef STELLAR_ARENA_GAME_H +#define STELLAR_ARENA_GAME_H + +#include "movelist.hpp" + +class Game { + public: + static const std::string startPosition; + enum Terminate { + Deatch, + Timeout, + Illegal, + Repetition, + }; + + Game(const uint16_t match_id, const std::string white, const std::string black, const std::string fen); + ~Game(); + + void play(const Move move) { list.push(move); } + + const std::string get_moves(void) const; + const std::string &get_white(void) const { return white; } + const std::string &get_black(void) const { return black; } + const Terminate get_terminate(void) const { return terminate; } + + const bool is_win_white(void) const { return !draw && winner == color::WHITE; } + const bool is_win_black(void) const { return !draw && winner == color::BLACK; } + const bool is_draw(void) const { return draw; } + + void set_terminate(const Terminate terminate) { this->terminate = terminate; } + void set_winner(const color::Color winner) { this->winner = winner; } + void set_draw(const bool draw) { this->draw = draw; } + + static void set_san(bool san) { Game::san = san; } + + friend std::ostream &operator<<(std::ostream &os, const Game &game); + + private: + static const std::string to_san(const Board &board, const Move move); + + static uint16_t id_t; + uint16_t id = id_t++; + uint16_t match_id; + + const std::string white, black; + const std::string fen; + MoveList list; + + bool draw = false; + color::Color winner; + Terminate terminate = Terminate::Deatch; + + static bool san; +}; + +#endif diff --git a/src/arena/logger.hpp b/src/arena/logger.hpp @@ -0,0 +1,31 @@ +#ifndef STELLAR_ARENA_LOGGER_H +#define STELLAR_ARENA_LOGGER_H + +#include "utils.hpp" +#include <cerrno> +#include <cstring> +#include <format> + +namespace logger { + +enum Level { + Critical, + Arena, + Debug, + Info, +}; + +static Level active = Debug; +inline void set_level(const Level lvl) { active = lvl; } + +inline void log(const std::string &message, const Level lvl = Arena) { + static const std::string name[] = {"crit", "arena", "debug", "info"}; + if (lvl > active) return; + std::cerr << std::format("[ {:>5} ] {}\n", name[to_underlying(lvl)], message); +} + +inline void error(const char *call) { log(std::format("{}, {}", call, std::strerror(errno)), Critical); } + +} // namespace logger + +#endif diff --git a/src/arena/match.cpp b/src/arena/match.cpp @@ -0,0 +1,121 @@ +#include "match.hpp" +#include "logger.hpp" +#include "repetition.hpp" +#include "timer.hpp" + +uint16_t Match::id_t = 0; +Match::~Match() { logger::log(std::format("Match {}: destroyed", id), logger::Debug); } +Match::Match(Engine &white, Engine &black) : engines({&white, &black}) { + logger::log(std::format("Match {}: created", id), logger::Debug); +} + +Game Match::play(Settings swhite, Settings sblack, const std::string fen = Game::startPosition) { + const std::string position = "position " + (fen == Game::startPosition ? "startpos" : "fen " + fen); + + repetition::Table rtable; + Board board(fen); + Move move; + + logger::log(std::format("Match {}: Play a game between {}(white) and {}(black)", id, + engines[0]->get_name(), engines[1]->get_name())); + Game game(id, engines[0]->get_name(), engines[1]->get_name(), fen); + + engines[0]->send("ucinewgame"); + engines[1]->send("ucinewgame"); + + color::Color turn = board.get_side(); + while (true) { + const MoveList list = MoveList(board, false, true); + if (!list.size()) { + game.set_winner(color::other(turn)); + break; + } + + Engine *engine = engines[to_underlying(turn)]; + engine->send(std::format("{} moves {}", position, game.get_moves())); + engine->send(get_go(swhite, sblack, turn)); + + uint64_t time_start = timer::get_ms(); + + std::string response; + while (true) { + response = engine->receive(); + if (response.starts_with("bestmove")) break; + } + + std::string move_str = response.substr(9); + if ((move = parse_move(list, move_str)) == Move() || !move.make(board)) { + logger::log( + std::format("Match {}: {} illegal {}", id, color::to_string(turn), (std::string)move)); + game.set_terminate(Game::Illegal); + game.set_winner(color::other(turn)); + break; + } + + if (rtable.is_repetition(board.get_hash())) { + logger::log(std::format("Match {}: {} repetition", id, color::to_string(turn))); + game.set_terminate(Game::Repetition); + game.set_draw(true); + break; + } + + rtable.push_hash(board.get_hash()); + if (!move.is_repeatable()) rtable.push_null(); + game.play(move); + + uint64_t time_passed = timer::get_ms() - time_start; + if (turn == color::WHITE ? swhite.time <= time_passed : sblack.time <= time_passed) { + logger::log(std::format("Match {}: {} timeout", id, color::to_string(turn))); + game.set_terminate(Game::Timeout); + game.set_winner(color::other(turn)); + break; + } + + if (turn == color::WHITE && !swhite.depth) swhite.time -= time_passed; + if (turn == color::BLACK && !sblack.depth) sblack.time -= time_passed; + + turn = color::other(turn); + } + + if (!game.is_draw()) { + logger::log(std::format("Match {}: winner is {}", id, color::to_string(turn))); + } else { + logger::log(std::format("Match {}: ended in a draw", id)); + } + + std::swap(engines[0], engines[1]); + return game; +} + +std::string Match::get_go(Settings &swhite, Settings &sblack, color::Color side) { + std::string go = "go"; + if (side == color::WHITE && swhite.depth) go += " depth " + std::to_string(swhite.depth); + else { + if (side == color::WHITE && swhite.togo) go += " movestogo " + std::to_string(swhite.togo); + if (!sblack.depth && swhite.time) go += " wtime " + std::to_string(swhite.time); + if (swhite.inc) go += " winc " + std::to_string(swhite.inc); + if (swhite.movetime) go += " movetime " + std::to_string(swhite.movetime); + } + + if (side == color::BLACK && sblack.depth) go += " depth " + std::to_string(sblack.depth); + else { + if (side == color::BLACK && sblack.togo) go += " movestogo " + std::to_string(sblack.togo); + if (!swhite.depth && sblack.time) go += " btime " + std::to_string(sblack.time); + if (sblack.inc) go += " binc " + std::to_string(sblack.inc); + if (swhite.movetime) go += " movetime " + std::to_string(sblack.movetime); + } + return go; +} + +Move Match::parse_move(const MoveList list, const std::string &move_string) { + const square::Square source = square::from_coordinates(move_string.substr(0, 2)); + const square::Square target = square::from_coordinates(move_string.substr(2, 2)); + + for (int i = 0; i < list.size(); i++) { + const Move crnt = list[i]; + if (crnt.source() != source || crnt.target() != target) continue; + if (move_string[4] && tolower(piece::get_code(crnt.promoted())) != move_string[4]) continue; + return crnt; + } + return {}; +} diff --git a/src/arena/match.hpp b/src/arena/match.hpp @@ -0,0 +1,29 @@ +#ifndef STELLAR_ARENA_MATCH_H +#define STELLAR_ARENA_MATCH_H + +#include "engine.hpp" +#include "game.hpp" + +class Match { + public: + struct Settings { + uint64_t time = 30000, inc = 0, movetime = 0; + uint8_t depth = 0, togo = 0; + }; + + Match(Engine &white, Engine &black); + ~Match(); + + Game play(Settings swhite, Settings sblack, const std::string fen); + + private: + static std::string get_go(Settings &swhite, Settings &sblack, color::Color side); + static Move parse_move(const MoveList list, const std::string &move_string); + + std::array<Engine *, 2> engines; + + static uint16_t id_t; + uint16_t id = id_t++; +}; + +#endif diff --git a/src/board/board.hpp b/src/board/board.hpp @@ -44,6 +44,8 @@ class Board { square::Square from) const; inline constexpr U64 get_bitboard_piece_moves(piece::Type piece, color::Color color, square::Square from) const; + inline constexpr U64 get_bitboard_square_land(square::Square land, piece::Type piece, + color::Color side) const; inline constexpr color::Color get_square_piece_color(square::Square square) const; inline constexpr piece::Type get_square_piece_type(square::Square square) const; @@ -109,6 +111,12 @@ constexpr U64 Board::get_bitboard_piece_moves(piece::Type type, color::Color col return get_bitboard_piece_attacks(type, color, square) & ~get_bitboard_color(color); } +constexpr U64 Board::get_bitboard_square_land(square::Square land, piece::Type piece, + color::Color side) const { + + return get_bitboard_piece_attacks(piece, color::other(side), land) & get_bitboard_piece(piece, side); +} + constexpr color::Color Board::get_square_piece_color(square::Square square) const { if (bit::get(colors[to_underlying(color::WHITE)], to_underlying(square))) return color::WHITE; if (bit::get(colors[to_underlying(color::BLACK)], to_underlying(square))) return color::BLACK; diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp @@ -10,7 +10,9 @@ #include "move.hpp" #include "movelist.hpp" #include "piece.hpp" +#include "repetition.hpp" #include "score.hpp" +#include "timer.hpp" #include "uci.hpp" #include "utils.hpp" @@ -76,35 +78,6 @@ class TTable { std::vector<Hashe> table; }; -class RTable { - public: - RTable() = default; - - bool is_repetition(const U64 hash) const { - for (int i = repetitions.size() - 1; i >= 0; i--) { - if (repetitions[i] == hash) return true; - if (repetitions[i] == hashNull) return false; - } - return false; - } - - void pop(void) { repetitions.pop_back(); } - void clear(void) { repetitions.clear(); } - void push_null(void) { repetitions.push_back(hashNull); } - void push_hash(U64 hash) { repetitions.push_back(hash); } - - friend std::ostream &operator<<(std::ostream &os, const RTable &rtable) { - for (const U64 hash : rtable.repetitions) - os << hash << " "; - return os; - } - - private: - std::vector<U64> repetitions; - - const static int hashNull = 0; -}; - class PVTable { public: Move best(uint8_t ply = 0) { return table[0][ply]; } @@ -133,7 +106,7 @@ std::ostream &operator<<(std::ostream &os, const PVTable &pvtable) { static const uci::Settings *settings = nullptr; static Board board; static TTable ttable; -static RTable rtable; +static repetition::Table rtable; static PVTable pvtable; @@ -191,7 +164,7 @@ int stats_move_make(Board &copy, const Move move) { } ply++; rtable.push_hash(copy.get_hash()); - if (!move.is_repetable()) rtable.push_null(); + if (!move.is_repeatable()) rtable.push_null(); return 1; } @@ -209,7 +182,7 @@ void stats_move_unmake_pruning(Board &copy) { void stats_move_unmake(Board &copy, const Move move) { board = copy; - if (!move.is_repetable()) rtable.pop(); + if (!move.is_repeatable()) rtable.pop(); rtable.pop(); ply--; } @@ -391,8 +364,7 @@ int16_t negamax(int16_t alpha, int16_t beta, uint8_t depth, bool null) { } if (legal_moves == 0) { - if (isCheck) - return -MATE_VALUE + ply; + if (isCheck) return -MATE_VALUE + ply; else return 0; } @@ -414,7 +386,7 @@ Move search_position(const uci::Settings &settingsr) { for (int i = 0; i < settings->madeMoves.size(); i++) { rtable.push_hash(board.get_hash()); settings->madeMoves[i].make(board); - if (!settings->madeMoves[i].is_repetable()) rtable.clear(); + if (!settings->madeMoves[i].is_repeatable()) rtable.clear(); } ply = 0; @@ -422,31 +394,49 @@ Move search_position(const uci::Settings &settingsr) { settings->stopped = false; memset(killer, 0x00, sizeof(killer)); memset(history, 0x00, sizeof(history)); - rtable = RTable(); + rtable = repetition::Table(); + + Move lastBest; + uint8_t max_depth = settings->depth ? settings->depth : MAX_PLY; + for (uint8_t depth = 1; depth <= max_depth; depth++) { + lastBest = pvtable.best(); + follow_pv = 1; + int16_t score = negamax(alpha, beta, depth, true); - for (uint8_t depth = 1; depth <= settings->depth; depth++) { uci::communicate(settings); if (settings->stopped) break; - follow_pv = 1; - - int16_t score = negamax(alpha, beta, depth, true); if ((score <= alpha) || (score >= beta)) { alpha = -SCORE_INFINITY; beta = SCORE_INFINITY; depth--; continue; } + alpha = score - WINDOW; beta = score + WINDOW; - uci::pv_print(score, depth, nodes, pvtable); - if (settings->depth == 64 && uci::get_time_ms() >= (settings->stoptime + settings->starttime) / 2) - break; + uint8_t mate_ply = 0xFF; + if (score > -MATE_VALUE && score < -MATE_SCORE) { + mate_ply = (score + MATE_VALUE) / 2 + 1; + std::cout << "info score mate -" << (int)mate_ply; + } else if (score > MATE_SCORE && score < MATE_VALUE) { + mate_ply = (MATE_VALUE - score) / 2 + 1; + std::cout << "info score mate " << (int)mate_ply; + } else { + std::cout << "info score cp " << score; + } + + std::cout << " depth " << (unsigned)depth; + std::cout << " nodes " << nodes; + std::cout << " time " << timer::get_ms() - settings->starttime; + std::cout << " pv " << pvtable << std::endl; + + if (depth >= mate_ply) break; } settings->board = board; - return pvtable.best(); + return !settings->stopped ? pvtable.best() : lastBest; } } // namespace engine diff --git a/src/engine/uci.cpp b/src/engine/uci.cpp @@ -2,36 +2,16 @@ #include <limits> #include <sstream> #include <string> -#include <sys/time.h> #include "engine.hpp" #include "stellar_version.hpp" +#include "timer.hpp" #include "uci.hpp" namespace uci { -uint32_t get_time_ms(void) { - struct timeval time; - gettimeofday(&time, NULL); - return time.tv_sec * 1000 + time.tv_usec / 1000; -} - -void pv_print(int16_t score, uint8_t depth, uint64_t nodes, const engine::PVTable &pvtable) { - if (score > -MATE_VALUE && score < -MATE_SCORE) { - std::cout << "info score mate " << -(score + MATE_VALUE) / 2 - 1; - } else if (score > MATE_SCORE && score < MATE_VALUE) { - std::cout << "info score mate " << (MATE_VALUE - score) / 2 + 1; - } else { - std::cout << "info score cp " << score; - } - - std::cout << " depth " << (unsigned)depth; - std::cout << " nodes " << nodes; - std::cout << " pv " << pvtable << "\n"; -} - void communicate(const uci::Settings *settings) { - if (!settings->infinite && uci::get_time_ms() > settings->stoptime) { + if (!settings->infinite && timer::get_ms() > settings->stoptime) { settings->stopped = true; return; } @@ -60,6 +40,8 @@ void loop(void) { static std::string line, command; static Move move; + Board board; + while (true) { std::getline(std::cin, line); std::istringstream iss(line); @@ -91,7 +73,7 @@ void loop(void) { while (iss >> command) if (command == "moves") break; - Board board = settings.board; + board = settings.board; while (iss >> command) { if (!parse_move(board, move, command)) break; settings.madeMoves.push(move); @@ -127,29 +109,26 @@ void loop(void) { settings.infinite = true; else if (command == "searchmoves") { while (iss >> command) { - if (!parse_move(settings.board, move, command)) break; + if (!parse_move(board, move, command)) break; settings.searchMoves.push(move); } } } - settings.starttime = get_time_ms(); - uint32_t time = (settings.board.get_side() == color::WHITE) ? wtime : btime; + settings.starttime = timer::get_ms(); + uint64_t time = (board.get_side() == color::WHITE) ? wtime : btime; if (movetime != 0) { time = movetime; movestogo = 1; - } - - if (time != 0) { - uint16_t inc = (settings.board.get_side() == color::WHITE) ? winc : binc; + } else if (time != 0) { + uint16_t inc = (board.get_side() == color::WHITE) ? winc : binc; time /= movestogo; time -= 50; settings.stoptime = settings.starttime + time + inc; settings.infinite = false; - } - - if (!time) settings.infinite = true; + } else + settings.infinite = true; const Move best = engine::search_position(settings); std::cout << "bestmove " << best << '\n'; diff --git a/src/engine/uci.hpp b/src/engine/uci.hpp @@ -7,12 +7,6 @@ #include "score.hpp" #include "utils.hpp" -namespace engine { - -class PVTable; - -} // namespace engine - namespace uci { struct Settings { @@ -23,7 +17,7 @@ struct Settings { uint64_t starttime; uint64_t stoptime; - uint16_t depth = 64; + uint16_t depth = 0; uint32_t nodes = 0; bool ponder = false; @@ -38,7 +32,6 @@ struct Settings { void loop(void); uint32_t get_time_ms(void); void communicate(const uci::Settings *settings); -void pv_print(int16_t score, uint8_t depth, uint64_t nodes, const engine::PVTable &pvtable); } // namespace uci #endif diff --git a/src/move/move.cpp b/src/move/move.cpp @@ -102,9 +102,10 @@ void Move::print(void) const { std::cout << is_castle(); } -std::ostream &operator<<(std::ostream &os, Move move) { - std::cout << square::to_coordinates(move.source()) << square::to_coordinates(move.target()); - if (move.is_promote()) std::cout << piece::get_code(move.promoted()); - - return os; +Move::operator std::string() const { + std::string res = square::to_coordinates(source()) + square::to_coordinates(target()); + if (is_promote()) res += piece::get_code(promoted()); + return res; } + +std::ostream &operator<<(std::ostream &os, Move move) { return os << (std::string)move; } diff --git a/src/move/move.hpp b/src/move/move.hpp @@ -42,7 +42,7 @@ struct Move { bool is_promote(void) const { return flags_i & 0x8; } bool is_double(void) const { return flags_i == DOUBLE; } - bool is_repetable(void) const { return flags_i == QUIET; } + bool is_repeatable(void) const { return flags_i == QUIET; } bool is_quiet(void) const { return flags_i == QUIET || flags_i == PQUIET; } bool is_castle(void) const { return flags_i == CASTLEK || flags_i == CASTLEQ; } @@ -55,6 +55,7 @@ struct Move { bool make(Board &board) const; + operator std::string() const; friend std::ostream &operator<<(std::ostream &os, Move move); void print(void) const; diff --git a/src/move/movelist.cpp b/src/move/movelist.cpp @@ -1,4 +1,5 @@ #include "movelist.hpp" +#include "color.hpp" #include "piece.hpp" #include <iomanip> @@ -15,8 +16,7 @@ using piece::Type::PAWN; void MoveList::generate(const Board &board, bool attacks_only) { uint8_t src_i, tgt_i; - const color::Color color = board.get_side(); - const color::Color colorOther = color == color::BLACK ? color::WHITE : color::BLACK; + const color::Color color = board.get_side(), colorOther = color::other(color); // pawn moves const int add = (color == color::WHITE) ? +8 : -8; diff --git a/src/move/movelist.hpp b/src/move/movelist.hpp @@ -15,9 +15,17 @@ class MoveList { public: MoveList() : list(){}; - MoveList(const Board &board, bool attacks_only = false) : list() { + MoveList(const Board &board, bool attacks_only = false, bool legal = false) : list() { list.reserve(256); generate(board, attacks_only); + if (!legal) return; + + int size = 0; + for (int i = 0; i < list.size(); i++) { + Board copy = board; + if (list[i].make(copy)) list[size++] = list[i]; + } + list.resize(size); } void clear() { list.clear(); } diff --git a/src/perft/perft.cpp b/src/perft/perft.cpp @@ -126,7 +126,7 @@ void usage(const char *program) { std::cout << " [-h]"; std::cout << " [-t thread number]"; std::cout << " [-d depth]"; - std::cout << " [-f fen]" <<std::endl; + std::cout << " [-f fen]" << std::endl; } int main(int argc, char *argv[]) { @@ -139,19 +139,13 @@ int main(int argc, char *argv[]) { thread_num = atoi(optarg); if (thread_num <= 0 && thread_num > THREAD_MAX) abort(); break; - case 'f': - fen = optarg; - break; + case 'f': fen = optarg; break; case 'd': depth = atoi(optarg); if (depth <= 0) abort(); break; - case 'h': - usage(argv[0]); - return 1; - default: - usage(argv[0]); - abort(); + case 'h': usage(argv[0]); return 1; + default: usage(argv[0]); abort(); } } diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt @@ -14,6 +14,10 @@ add_library(square INTERFACE) target_include_directories(square INTERFACE .) stellar_target_precompile_headers(square INTERFACE "square.hpp") +add_library(timer INTERFACE) +target_include_directories(timer INTERFACE .) +stellar_target_precompile_headers(timer INTERFACE "timer.hpp") + add_library(utils INTERFACE) target_include_directories(utils INTERFACE .) stellar_target_precompile_headers(utils INTERFACE "utils.hpp") diff --git a/src/utils/color.hpp b/src/utils/color.hpp @@ -1,6 +1,8 @@ #ifndef STELLAR_COLOR_H #define STELLAR_COLOR_H +#include <string> + namespace color { enum Color { @@ -9,6 +11,9 @@ enum Color { }; inline constexpr const Color other(const Color color) { return color == WHITE ? BLACK : WHITE; } +inline constexpr const std::string to_string(const Color color) { + return std::string(color == WHITE ? "white" : "black"); +} } // namespace color diff --git a/src/utils/repetition.hpp b/src/utils/repetition.hpp @@ -0,0 +1,37 @@ +#ifndef STELLAR_REPETITION_H +#define STELLAR_REPETITION_H + +namespace repetition { + +class Table { + public: + Table() = default; + + bool is_repetition(const U64 hash) const { + for (int i = repetitions.size() - 1; i >= 0; i--) { + if (repetitions[i] == hash) return true; + if (repetitions[i] == hashNull) return false; + } + return false; + } + + void pop(void) { repetitions.pop_back(); } + void clear(void) { repetitions.clear(); } + void push_null(void) { repetitions.push_back(hashNull); } + void push_hash(U64 hash) { repetitions.push_back(hash); } + + friend std::ostream &operator<<(std::ostream &os, const Table &rtable) { + for (const U64 hash : rtable.repetitions) + os << hash << " "; + return os; + } + + private: + std::vector<U64> repetitions; + + const static int hashNull = 0; +}; + +} // namespace repetition + +#endif diff --git a/src/utils/square.hpp b/src/utils/square.hpp @@ -51,7 +51,7 @@ inline constexpr const uint8_t file(const Square square) { return to_underlying( inline constexpr const uint8_t rank(const Square square) { return to_underlying(square) >> 3; } inline constexpr const Square mirror(const Square square) { return mirror_array[square]; } -inline constexpr const char *to_coordinates(const Square square) { +inline constexpr const std::string to_coordinates(const Square square) { return coordinates_array[to_underlying(square)]; } diff --git a/src/utils/timer.hpp b/src/utils/timer.hpp @@ -0,0 +1,16 @@ +#ifndef STELLAR_TIME_H +#define STELLAR_TIME_H + +#include <sys/time.h> + +namespace timer { + +inline uint32_t get_ms(void) { + struct timeval time; + gettimeofday(&time, NULL); + return time.tv_sec * 1000 + time.tv_usec / 1000; +} + +} // namespace timer + +#endif