Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ WarningsAsErrors: "*"

CheckOptions:
- key: readability-identifier-length.IgnoredVariableNames
value: "^(sq|to|from|bb|us|A[1-8]|B[1-8]|C[1-8]|D[1-8]|E[1-8]|F[1-8]|G[1-8]|H[1-8])$"
value: "^(sq|to|from|bb|us|it|A[1-8]|B[1-8]|C[1-8]|D[1-8]|E[1-8]|F[1-8]|G[1-8]|H[1-8])$"
- key: readability-identifier-length.IgnoredParameterNames
value: "^(sq|to|from|bb|us)$"
value: "^(sq|to|from|bb|us|it)$"
- key: readability-identifier-length.IgnoredLoopCounterNames
value: "^(r|f|i)$"
24 changes: 24 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,30 @@ help

Response: multiline informational text.

### `bench`

Runs a benchmark search on the current position (non-UCI extension).

Syntax:

```text
bench [depth <n>] [movetime <ms>] [wtime <ms>] [btime <ms>] [winc <ms>] [binc <ms>] [infinite]
```

Implemented behavior (current state):

- Uses the same limit parser as `go`.
- `depth <n>` runs a fixed-depth benchmark.
- If `infinite` is provided, benchmark mode internally converts to `depth 10`.
- A bare `bench` command (no limits) is parsed as `infinite`, then converted internally to `depth 10`.
- `stop` can still interrupt a running benchmark early.

Response when benchmark ends:

```text
bench nodes <total> negamax_nodes <negamax> quiescence_nodes <quiescence> time(s) <seconds>s nps <nps>
```

## Options Support Status

### UCI `setoption`
Expand Down
15 changes: 13 additions & 2 deletions include/bitbishop/engine/search.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ class Position;

namespace Search {

/**
* @brief Contains statistics about a best move search.
*/
struct SearchStats {
uint64_t negamax_nodes = 0; ///< Number of explored negamax nodes
uint64_t quiescence_nodes = 0; ///< Number of explored quiescence nodes
};

// We implement negamax with alpha-beta by flipping the window at each ply:
// score = -negamax(child, depth-1, -beta, -alpha, ...)
// That requires negating `alpha`/`beta`. Negating `INT_MIN` is undefined behaviour in C++,
Expand Down Expand Up @@ -40,6 +48,7 @@ struct BestMove {
* @param position Current position (board + history)
* @param alpha Best score the current side can guarantee
* @param beta Best score the opponent side can guarantee
* @param stats Statistics about the search process
*
* @return Score from the perspective of the side to move
*
Expand All @@ -60,7 +69,8 @@ struct BestMove {
* positions.
* """
*/
[[nodiscard]] int quiesce(Position& position, int alpha, int beta, std::atomic<bool>* stop_flag = nullptr);
[[nodiscard]] int quiesce(Position& position, int alpha, int beta, SearchStats& stats,
std::atomic<bool>* stop_flag = nullptr);

/**
* @brief Finds the best achievable move for the side to move assuming an optimal play on both sides.
Expand All @@ -70,6 +80,7 @@ struct BestMove {
* @param alpha Lower bound, aka. minimum score we already guaranteed to get.
* @param beta Upper bound, aka. maximum score the opponent is willing to let us have.
* @param ply Number of half-moves from root used for mate distance
* @param stats Statistics about the search process
*
* @return Move and score in a BestMove object
*
Expand All @@ -81,7 +92,7 @@ struct BestMove {
* @see https://www.chessprogramming.org/Alpha-Beta
* @see https://www.dogeystamp.com/chess2/
*/
[[nodiscard]] BestMove negamax(Position& position, std::size_t depth, int alpha, int beta, int ply,
[[nodiscard]] BestMove negamax(Position& position, std::size_t depth, int alpha, int beta, int ply, SearchStats& stats,
std::atomic<bool>* stop_flag = nullptr);

} // namespace Search
112 changes: 112 additions & 0 deletions include/bitbishop/interface/search_reporter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#pragma once

#include <bitbishop/engine/search.hpp>
#include <chrono>
#include <functional>

/**
* @brief Interface for reporting progress and results of a search.
*
* SearchReporter provides a callback-based mechanism for observing the
* progress of a search as well as its final result. Implementations can
* forward this information to different outputs (e.g., UCI protocol,
* benchmarking logs, GUI, etc.).
*/
struct SearchReporter {
virtual ~SearchReporter() = default;

/**
* @brief Called after each completed search iteration.
*
* @param best Current best move found so far.
* @param depth Depth reached in the current iteration.
* @param stats Accumulated search statistics.
*/
virtual void on_iteration(const Search::BestMove& best, int depth, const Search::SearchStats& stats) {}

/**
* @brief Called once when the search finishes.
*
* Must be implemented by derived classes to handle final reporting.
*
* @param best Final best move found by the search.
* @param stats Final search statistics.
*/
virtual void on_finish(const Search::BestMove& best, const Search::SearchStats& stats) = 0;
};

/**
* @brief Reporter that outputs results in UCI (Universal Chess Interface) format.
*
* UciReporter writes the final best move to the provided output stream
* following the UCI protocol specification. Intended for communication
* with chess GUIs or other UCI-compatible tools.
*/
struct UciReporter : SearchReporter {
private:
/** Output stream used for writing UCI messages. */
std::ostream& out_stream;

public:
/**
* @brief Constructs a UciReporter.
*
* @param out Output stream where UCI messages will be written.
*/
UciReporter(std::ostream& out);

/**
* @brief Outputs the final best move in UCI format.
*
* Prints a line of the form:
* "bestmove <move>"
* If no move is available, "0000" is used.
*
* @param best Final best move found by the search.
* @param stats Final search statistics (unused).
*/
void on_finish(const Search::BestMove& best, const Search::SearchStats& stats) override;
};

/**
* @brief Reporter that outputs benchmarking information.
*
* BenchReporter measures elapsed time from construction to completion
* and reports total nodes searched along with nodes-per-second (NPS).
*/
struct BenchReporter : SearchReporter {
using Clock = std::chrono::steady_clock;

public:
/** Injectable time source. Mainly for testing.
* Must be declared *before* start.
*/
std::function<Clock::time_point()> now;

private:
/** Output stream used for writing benchmark results. */
std::ostream& out_stream;

/** Start time of the benchmark measurement. */
Clock::time_point start;

public:
/**
* @brief Constructs a BenchReporter and records the start time.
*
* @param out Output stream where benchmark results will be written.
*/
BenchReporter(std::ostream& out);
BenchReporter(std::ostream& out, std::function<Clock::time_point()> now_fn);

/**
* @brief Outputs benchmark statistics upon search completion.
*
* Computes total nodes searched (negamax + quiescence), elapsed time,
* and nodes per second (NPS), then prints a summary line (see implementation for details).
*
* @param best Final best move found by the search (unused).
* @param stats Final search statistics.
*/
void on_finish(const Search::BestMove& best, const Search::SearchStats& stats) override;
};
69 changes: 69 additions & 0 deletions include/bitbishop/interface/search_session.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#pragma once

#include <bitbishop/interface/search_reporter.hpp>
#include <bitbishop/interface/search_worker.hpp>
#include <memory>
#include <ostream>

namespace Uci {

/**
* @brief Owns the lifecycle of one active search and its reporter.
*
* Worker threads only publish events. This class consumes those events on the
* control thread and forwards them to the configured reporter.
*/
class SearchSession {
std::ostream& out_stream;
std::unique_ptr<SearchWorker> worker;
std::unique_ptr<SearchReporter> reporter;

/**
* @brief Emits pending reports from worker to reporter.
*/
void emit_reports();

/**
* @brief Finalizes and clears resources if search already finished.
*/
void finalize_if_done();

public:
explicit SearchSession(std::ostream& out_stream);
~SearchSession();

SearchSession(const SearchSession&) = delete;
SearchSession& operator=(const SearchSession&) = delete;

/**
* @brief Starts a regular UCI search.
*/
void start_go(Board board, SearchLimits limits);

/**
* @brief Starts a benchmark search.
*/
void start_bench(Board board, SearchLimits limits);

/**
* @brief Requests current search to stop (non-blocking).
*/
void request_stop();

/**
* @brief Pumps reports and finalization (non-blocking).
*/
void poll();

/**
* @brief Stops and joins current search if any (blocking).
*/
void stop_and_join();

/**
* @brief Returns true when no search is active.
*/
[[nodiscard]] bool is_idle() const { return worker == nullptr; }
};

} // namespace Uci
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#pragma once

#include <bitbishop/engine/search.hpp>
#include <atomic>
#include <bitbishop/moves/position.hpp>
#include <mutex>
#include <optional>
#include <ostream>
#include <stop_token>
#include <thread>
#include <vector>

namespace Uci {

Expand All @@ -20,29 +21,65 @@ struct SearchLimits {
std::optional<int> wtime, btime; ///< White/black time limits (in milliseconds)
std::optional<int> winc, binc; ///< White/black increment limits (in milliseconds)
bool infinite = false; ///< Flag for infinite search mode

/**
* @brief Parses arguments from a search uci command into a SearchLimits object.
*
* UCI command is like: `go depth 2` or `go movetime 5000`, etc...
*
* @return The built SearchLimits object.
*/
static SearchLimits from_uci_cmd(const std::vector<std::string>& line);
};

/**
* @brief Represents an event generated by a search worker.
*
* Search workers only publish data. Reporting/printing is done by the UCI control thread.
*/
enum class SearchReportKind : std::uint8_t {
Iteration,
Finish,
};

/**
* @brief Structured search report event consumed by the UCI controller.
*/
struct SearchReport {
SearchReportKind kind = SearchReportKind::Finish;
Search::BestMove best;
int depth = 0;
Search::SearchStats stats{};
};

/**
* @brief Manages the search process for UCI commands.
*
* This class handles the execution of search operations based on UCI parameters.
* It uses a background thread to perform the search and communicates results through a callback.
* This class handles the execution of search operations based on UCI parameters. It uses a background thread to
* perform the search and publishes structured reports for the control thread to consume.
*/
class SearchWorker {
std::thread worker; ///< Worker thread for search operations
std::atomic<bool> stop_flag{false}; ///< Flag used to forward the stop order to the worker(s)
std::atomic<bool> finished{true}; ///< Indicates whether worker thread has completed
Board board; ///< Current chess board
Position position; ///< Game position associated to the current chess board
SearchLimits limits; ///< Current search parameters
std::ostream* out; ///< Output stream for UCI communication
std::mutex reports_mutex; ///< Synchronizes report queue access
std::vector<SearchReport> reports; ///< FIFO queue of generated search reports

/**
* @brief Executes the search algorithm in a background thread.
*/
void run();

/**
* @brief Pushes one report event into the queue.
*/
void push_report(SearchReport report);

public:
SearchWorker(Board board, SearchLimits limits, std::ostream& ostream = std::cout);
SearchWorker(Board board, SearchLimits limits);
~SearchWorker();

/**
Expand All @@ -66,7 +103,26 @@ class SearchWorker {
* Thread interruption finishes early the best move search and may not be able to return a best move.
* An intermediate state may be returned instead.
*/
void request_stop();

/**
* @brief Returns whether the current worker run has finished.
*/
[[nodiscard]] bool is_finished() const { return finished.load(); }

/**
* @brief Requests a stop then waits for completion.
*
* This is a blocking helper.
*/
void stop();

/**
* @brief Moves all pending search reports out of the worker queue.
*
* This is thread-safe and intended to be called by the control thread.
*/
[[nodiscard]] std::vector<SearchReport> drain_reports();
};

} // namespace Uci
Loading
Loading