#include "Uci.hpp" #include "Engine.hpp" #include "Fen.hpp" #include #include #include #include #include #include class UciOptionBase { public: virtual ~UciOptionBase() = default; virtual std::string name() const = 0; virtual std::string type() const = 0; virtual void streamOptionCommand(std::ostream& stream) const = 0; virtual bool setValue(Engine& engine, std::istream& stream) const = 0; }; template class UciOption : public UciOptionBase { public: using Value = T; using OptionalValue = std::optional; virtual OptionalValue default_() const { return std::nullopt; } virtual OptionalValue min() const { return std::nullopt; } virtual OptionalValue max() const { return std::nullopt; } virtual std::vector vars() const { return {}; } void streamOptionCommand(std::ostream& stream) const override { stream << "option name " << name() << " type " << type(); streamIfHasValue(stream, "default", default_()); streamIfHasValue(stream, "min", min()); streamIfHasValue(stream, "max", max()); for (auto var : vars()) { stream << ' ' << "var " << var; } } bool setValue(Engine& engine, std::istream& stream) const override { if (Value value; stream >> value) { return setValue(engine, value); } else { return false; } } virtual bool setValue(Engine& engine, Value value) const = 0; private: void streamIfHasValue(std::ostream& stream, const char* name, OptionalValue info) const { if (info.has_value()) { stream << ' ' << name << ' ' << info.value(); } } }; template class UciSpinOption : public UciOption { public: std::string type() const override { return "spin"; } }; class UciHashOption : public UciSpinOption { public: UciHashOption(const HashInfo& hashInfo) : hashInfo_(hashInfo) {} std::string name() const override { return "Hash"; } OptionalValue default_() const override { return hashInfo_.defaultSize; } OptionalValue min() const override { return hashInfo_.minSize; } OptionalValue max() const override { return hashInfo_.maxSize; } bool setValue(Engine& engine, Value value) const override { if (value >= hashInfo_.minSize && value <= hashInfo_.maxSize) { engine.setHashSize(value); return true; } else { return false; } } private: HashInfo hashInfo_; }; Uci::Uci(std::unique_ptr engine, std::istream& cmdIn, std::ostream& cmdOut, std::ostream& log ) : engine_(std::move(engine)), cmdIn_(cmdIn), cmdOut_(cmdOut), log_(log) { if (auto hashInfo = engine_->hashInfo(); hashInfo) { auto hashOption = std::make_unique(*hashInfo); options_[hashOption->name()] = std::move(hashOption); } } // Needed here because Engine is only forward-declared in Uci.hpp causing an // error when compiling the destructor of std::unique_ptr. Uci::~Uci() = default; void Uci::run() { log_ << "UCI engine started" << std::endl; while (!cmdIn_.eof()) { std::string line; std::getline(cmdIn_, line); runCommand(line); } } void Uci::runCommand(const std::string& line) { log_ << "> " << line << std::endl; auto stream = std::stringstream(line); auto command = std::string(); stream >> command; if (command == "uci") { uciCommand(stream); } else if (command == "setoption") { setoptionCommand(stream); } else if (command == "isready") { isreadyCommand(stream); } else if (command == "ucinewgame") { ucinewgameCommand(stream); } else if (command == "position") { positionCommand(stream); } else if (command == "go") { goCommand(stream); } else if (command == "quit") { quitCommand(stream); } } void Uci::uciCommand(std::istream&) { std::stringstream nameCommand; nameCommand << "id name " << engine_->name() << " " << engine_->version(); sendCommand(nameCommand.str()); std::stringstream authorCommand; authorCommand << "id author " << engine_->author(); sendCommand(authorCommand.str()); sendOptions(); sendCommand("uciok"); } void Uci::isreadyCommand(std::istream&) { sendCommand("readyok"); } void Uci::ucinewgameCommand(std::istream&) { engine_->newGame(); } void Uci::positionCommand(std::istream& stream) { auto type = std::string(); stream >> type; auto newBoard = Board::Optional(); if (type == "startpos") { newBoard = Fen::createBoard(Fen::StartingPos); } else if (type == "fen") { newBoard = Fen::createBoard(stream); } else { error("Illegal position type " + type); return; } if (!newBoard.has_value()) { error("Illegal FEN"); return; } board_ = newBoard.value(); auto moves = std::string(); stream >> moves; if (moves == "moves") { while (!stream.eof()) { auto uciMove = std::string(); stream >> uciMove; if (uciMove.empty()) { continue; } auto optMove = Move::fromUci(uciMove); if (!optMove.has_value()) { error("Illegal move " + uciMove); return; } board_.makeMove(optMove.value()); } } log_ << board_ << std::endl; } template static std::optional readValue(std::istream& stream) { if (T value; stream >> value) { return value; } else { return std::nullopt; } } TimeInfo::Optional Uci::readTimeInfo(std::istream& stream) { std::optional wtime, winc, btime, binc, movestogo; for (std::string command; stream >> command;) { auto value = readValue(stream); if (command == "wtime") { wtime = value; } else if (command == "winc") { winc = value; } else if (command == "btime") { btime = value; } else if (command == "binc") { binc = value; } else if (command == "movestogo") { movestogo = value; } else if (command == "infinite") { error("go infinite not supported"); return std::nullopt; } } if (wtime.has_value() && btime.has_value()) { PlayerTimeInfo whiteTime, blackTime; whiteTime.timeLeft = std::chrono::milliseconds(wtime.value()); whiteTime.increment = std::chrono::milliseconds(winc.value_or(0)); blackTime.timeLeft = std::chrono::milliseconds(btime.value()); blackTime.increment = std::chrono::milliseconds(binc.value_or(0)); TimeInfo timeInfo; timeInfo.white = whiteTime; timeInfo.black = blackTime; timeInfo.movesToGo = movestogo; return timeInfo; } else { return std::nullopt; } } void Uci::goCommand(std::istream& stream) { auto timeInfo = readTimeInfo(stream); auto pv = engine_->pv(board_, timeInfo); if (pv.length() == 0) { error("Engine returned no PV"); return; } log_ << "PV: " << pv << std::endl; sendPvInfo(pv); auto bestMove = *pv.begin(); board_.makeMove(bestMove); log_ << board_ << std::endl; auto bestMoveCmd = std::stringstream(); bestMoveCmd << "bestmove " << bestMove; sendCommand(bestMoveCmd.str()); } void Uci::quitCommand(std::istream&) { std::exit(EXIT_SUCCESS); } void Uci::setoptionCommand(std::istream& stream) { std::string nameCommand; stream >> nameCommand; if (nameCommand != "name") { error("Illegal setoption: did not start with 'name'"); return; } std::string name; stream >> name; for (std::string nextPart; stream >> nextPart;) { if (nextPart == "value") { break; } name += ' ' + nextPart; } // We could get here without a "value" command. This is by design because // "button" types don't have one. auto optionIt = options_.find(name); if (optionIt == options_.end()) { error("Illegal option: " + name); return; } if (!optionIt->second->setValue(*engine_, stream)) { error("Illegal option value"); return; } } void Uci::sendPvInfo(const PrincipalVariation& pv) { auto stream = std::stringstream(); stream << "info score "; auto score = pv.score(); if (pv.isMate()) { auto numMoves = score < 0 ? std::floor(score / 2.0) : std::ceil(score / 2.0); stream << "mate " << numMoves; } else { stream << "cp " << score; } stream << " pv"; for (auto move : pv) { stream << ' ' << move; } sendCommand(stream.str()); } void Uci::sendOptions() { for (const auto& [name, option] : options_) { std::stringstream cmd; option->streamOptionCommand(cmd); sendCommand(cmd.str()); } } void Uci::sendCommand(const std::string& command) { log_ << "< " << command << std::endl; cmdOut_ << command << std::endl; } void Uci::error(const std::string& msg) { log_ << "UCI error: " << msg << std::endl; std::exit(EXIT_FAILURE); }