81template <SearchMove Move, AdversarialScore Score>
102template <SearchMove Move, AdversarialScore Score>
124template <
typename StateKey>
133template <
typename StateKey>
143 AdversarialScore Score,
154template <
typename Table,
typename StateKey,
typename Move,
typename Score>
165template <SearchMove Move, AdversarialScore Score>
202 <<
"AdversarialSearchResult::first_move: no principal variation available";
224template <SearchMove Move, AdversarialScore Score>
244template <
typename Tracer,
typename Move,
typename Score>
248 {
tracer(event) } -> std::same_as<void>;
252template <
typename Table,
typename Keyer,
typename State,
typename Move,
typename Score>
256 typename std::invoke_result_t<Keyer &, const State &>;
263 template <
typename Event>
271template <SearchMove Move, AdversarialScore Score>
313template <AdversarialScore Score>
327template <AdversarialScore Score>
336template <SearchMove Move, AdversarialScore Score>
349template <SearchMove Move, AdversarialScore Score>
379template <
typename Domain>
381 typename Domain::Score;
383 { domain.is_terminal(state) } -> std::convertible_to<bool>;
384 { domain.evaluate(state) } -> std::convertible_to<typename Domain::Score>;
406template <
typename Domain>
409and requires(
Domain &domain,
typename Domain::State &state,
const typename Domain::Move &move) {
412 { domain.apply(state, move) } -> std::same_as<void>;
413 { domain.undo(state, move) } -> std::same_as<void>;
416namespace adversarial_search_detail {
418template <
typename Entry>
423 template <
typename Key>
429 template <
typename Key>
438 template <
typename State>
445template <
typename Domain,
bool Supported = MoveKeyProv
ider<Domain>>
451template <
typename Domain>
457template <AdversarialScore Score>
460 return std::numeric_limits<Score>::lowest() / Score(2);
463template <AdversarialScore Score>
466 return std::numeric_limits<Score>::max() / Score(2);
469template <
typename Result>
474 ++result.stats.limit_hits;
479template <SearchMove Move>
485 for (
const auto &item : tail)
490template <SearchMove Move, AdversarialScore Score>
497template <
typename Domain,
typename Result>
500 using Move =
typename Domain::Move;
501 using Score =
typename Domain::Score;
503 ++result.stats.heuristic_evaluations;
511 const size_t depth)
noexcept
518 dst.ordered_batches += src.ordered_batches;
519 dst.ordered_moves += src.ordered_moves;
520 dst.priority_estimates += src.priority_estimates;
521 dst.killer_hits += src.killer_hits;
522 dst.history_hits += src.history_hits;
528 dst.probes += src.probes;
529 dst.hits += src.hits;
530 dst.misses += src.misses;
531 dst.cutoffs += src.cutoffs;
532 dst.stores += src.stores;
533 dst.replacements += src.replacements;
534 dst.rejected_updates += src.rejected_updates;
539 dst.visited_states += src.visited_states;
540 dst.expanded_states += src.expanded_states;
541 dst.generated_successors += src.generated_successors;
542 dst.solutions_found += src.solutions_found;
543 dst.terminal_states += src.terminal_states;
544 dst.pruned_by_depth += src.pruned_by_depth;
545 dst.pruned_by_domain += src.pruned_by_domain;
546 dst.pruned_by_visited += src.pruned_by_visited;
547 dst.limit_hits += src.limit_hits;
548 if (src.max_depth_reached >
dst.max_depth_reached)
549 dst.max_depth_reached = src.max_depth_reached;
550 dst.elapsed_ms += src.elapsed_ms;
558 dst.heuristic_evaluations += src.heuristic_evaluations;
559 dst.alpha_beta_cutoffs += src.alpha_beta_cutoffs;
563template <SearchMove Move, AdversarialScore Score,
typename Tracer>
568 const size_t remaining,
569 const size_t iteration = 0,
570 const size_t horizon = 0,
571 const size_t aspiration_retries = 0,
572 const Score value = Score{},
573 const Score alpha = Score{},
574 const Score beta = Score{},
575 const std::optional<Move> &move = std::nullopt,
581 event.remaining_depth = remaining;
582 event.iteration = iteration;
583 event.horizon = horizon;
584 event.aspiration_retries = aspiration_retries;
589 if (principal_variation !=
nullptr)
590 event.principal_variation = *principal_variation;
594template <SearchMove Move>
600 return std::optional<Move>{path[0]};
603template <AdversarialScore Score>
607 if (delta <= Score{})
609 if (value <=
floor + delta)
611 return value - delta;
614template <AdversarialScore Score>
618 if (delta <= Score{})
622 return value + delta;
625template <AdversarialScore Score>
630 = window.growth > Score{} ? window.growth : (current > Score{} ? current : Score{1});
655 typename State,
typename Table,
typename Keyer,
typename Result>
657 const size_t remaining,
665 if constexpr (Table::enabled)
667 if (stop
or result.limit_reached())
672 entry.
depth = remaining;
678 ++result.
stats.transpositions.rejected_updates;
680 ++result.stats.transpositions.stores;
682 ++result.stats.transpositions.replacements;
706 typename State,
typename Table,
typename Keyer,
typename Result>
709 const size_t remaining,
714 if constexpr (Table::enabled)
716 ++result.stats.transpositions.probes;
718 if (
auto *entry = table.probe(
typename Table::Key_Type{keyer(state), remaining});
721 ++result.stats.transpositions.hits;
725 ++result.stats.transpositions.misses;
770 out.result.limits = limits;
773 size_t depth =
options.initial_depth;
785 iteration.
depth = depth;
802 &iteration.
result.principal_variation);
805 out.iterations.append(iteration);
807 if (iteration.
result.limit_reached())
810 ++
out.completed_iterations;
838template <AdversarialGameDomain Domain>
845 using State =
typename Domain::State;
847 using Move =
typename Domain::Move;
849 using Score =
typename Domain::Score;
935 template <
typename Tracer>
950 template <
typename Table,
typename Keyer>
959 template <
typename Table,
typename Keyer,
typename Tracer>
968 template <
typename Table,
typename D_ = Domain>
976 return domain_.state_key(state);
983 template <
typename Table,
typename Tracer,
typename D_ = Domain>
991 return domain_.state_key(state);
1003 template <
typename Table,
typename Keyer,
typename Tracer>
1008 <<
"Negamax only supports depth-first exploration";
1010 <<
"SearchLimits::max_solutions must be positive or Search_Unlimited";
1013 <<
"Negamax currently preserves domain successor order; move ordering is "
1014 "implemented in Alpha_Beta";
1019 const auto start_time = SearchClock::now();
1045 template <
typename Table,
typename Keyer>
1047 const size_t remaining,
1054 adversarial_search_detail::store_adversarial_transposition<Move, Score>(
1055 state, remaining, result, table,
keyer, value, bound,
stop_);
1058 template <
typename Table,
typename Keyer>
1060 const size_t remaining,
1066 auto *entry = adversarial_search_detail::probe_and_count_transposition<Move, Score>(
1067 state, remaining, result, table,
keyer);
1072 out.value = entry->value;
1073 out.principal_variation = entry->principal_variation;
1080 template <
typename Table,
typename Keyer,
typename Tracer>
1094 const size_t remaining = remaining_depth(
limits_, depth);
1110 first_move_of(
cached.principal_variation));
1114 if (
domain_.is_terminal(state))
1117 auto leaf = evaluate_leaf(
domain_, state, result);
1127 auto leaf = evaluate_leaf(
domain_, state, result);
1137 auto leaf = evaluate_leaf(
domain_, state, result);
1146 auto leaf = evaluate_leaf(
domain_, state, result);
1158 [&](
const Move &move) ->
bool
1192 ? std::numeric_limits<Score>::max()
1207 auto leaf = evaluate_leaf(
domain_, state, result);
1224 first_move_of(
best.principal_variation));
1232template <AdversarialGameDomain Domain>
1243template <AdversarialGameDomain Domain,
typename Tracer>
1256template <AdversarialGameDomain Domain,
typename Table,
typename Keyer>
1270template <AdversarialGameDomain Domain,
typename Table,
typename Keyer,
typename Tracer>
1286template <AdversarialGameDomain Domain,
typename Table>
1300template <AdversarialGameDomain Domain,
typename Table,
typename Tracer>
1316template <AdversarialGameDomain Domain>
1330template <AdversarialGameDomain Domain,
typename Tracer>
1340 using Move =
typename Domain::Move;
1341 using Score =
typename Domain::Score;
1344 <<
"iterative_deepening_negamax_search requires a finite SearchLimits::max_depth";
1346 <<
"AdversarialIterativeDeepeningOptions::depth_step must be positive";
1348 <<
"AdversarialIterativeDeepeningOptions::initial_depth exceeds SearchLimits::max_depth";
1350 <<
"AspirationWindow::half_window must be non-negative";
1352 <<
"Aspiration windows are only available through iterative_deepening_alpha_beta_search";
1355 return adversarial_search_detail::run_iterative_deepening<Move, Score>(
1359 const size_t,
const size_t)
1362 iteration.
result = result;
1368template <AdversarialGameDomain Domain,
typename Table,
typename Keyer>
1385template <AdversarialGameDomain Domain,
typename Table,
typename Keyer,
typename Tracer>
1398 using Move =
typename Domain::Move;
1399 using Score =
typename Domain::Score;
1402 <<
"iterative_deepening_negamax_search requires a finite SearchLimits::max_depth";
1404 <<
"AdversarialIterativeDeepeningOptions::depth_step must be positive";
1406 <<
"AdversarialIterativeDeepeningOptions::initial_depth exceeds SearchLimits::max_depth";
1408 <<
"AspirationWindow::half_window must be non-negative";
1410 <<
"Aspiration windows are only available through iterative_deepening_alpha_beta_search";
1413 return adversarial_search_detail::run_iterative_deepening<Move, Score>(
1417 const size_t,
const size_t)
1420 iteration.
result = result;
1426template <AdversarialGameDomain Domain,
typename Table>
1443template <AdversarialGameDomain Domain,
typename Table,
typename Tracer>
Generic memoization / transposition-table support for state search.
Exception handling system with formatted messages for Aleph-w.
#define ah_runtime_error_unless(C)
Throws std::runtime_error if condition does NOT hold.
#define ah_invalid_argument_if(C)
Throws std::invalid_argument if condition holds.
Collector that stores trace events in an Aleph list.
const Container_Type & events() const noexcept
Read-only access to the recorded event list.
bool is_empty() const noexcept
True when no events have been recorded.
void operator()(const Event &event)
Record one trace event.
void clear() noexcept
Discard all recorded events.
size_t size() const noexcept
Number of recorded events.
constexpr size_t size() const noexcept
Return the number of elements stored in the stack.
constexpr bool is_empty() const noexcept
Checks if the container is empty.
T & append(const T &data)
Append a copy of data
void reserve(size_t cap)
Reserves cap cells into the array.
T & append(const T &item)
void clear() noexcept
Empties the container.
Generic linear hash table.
constexpr bool is_empty() const noexcept
size_t size() const noexcept
Count the number of elements of the list.
Sparse history heuristic table over Aleph hash maps.
Pure Negamax search over an implicit game tree.
static ExplorationPolicy default_policy() noexcept
Return the default exploration policy for adversarial search.
Result search_impl(State initial_state, Table &table, Keyer &keyer, Tracer &tracer)
void store_transposition(State &state, const size_t remaining, Result &result, Table &table, Keyer &keyer, const adversarial_search_detail::NodeEvaluation< Move, Score > &value, const TranspositionBound bound)
const ExplorationPolicy & policy() const noexcept
Current exploration policy.
and AdversarialTranspositionMemo< Table, typename D_::State_Key, Move, Score > Result search(State initial_state, Table &table)
Execute Negamax with a transposition table using domain().state_key().
Domain Domain_Type
Type of the problem domain.
typename Domain::Score Score
Score type used for board evaluation.
Domain & domain() noexcept
Mutable access to the bound game domain.
Negamax(Domain domain, ExplorationPolicy policy=default_policy(), const SearchLimits &limits={})
Build a Negamax engine bound to one game domain.
const SearchLimits & limits() const noexcept
Current hard limits.
void set_policy(const ExplorationPolicy &policy) noexcept
Replace the exploration policy for future runs.
void set_limits(const SearchLimits &limits) noexcept
Replace the hard limits for future runs.
Result search(State initial_state, Table &table, Keyer keyer)
Execute Negamax with a transposition table and explicit keyer.
typename Domain::Move Move
Move type.
and AdversarialSearchTracer< Tracer, Move, Score > Result search(State initial_state, Table &table, Keyer keyer, Tracer &tracer)
Execute Negamax with TT/keyer and a trace sink.
ExplorationPolicy policy_
Result search(State initial_state)
Execute Negamax from initial_state.
Result search(State initial_state, Tracer &tracer)
Execute Negamax from initial_state while emitting trace events.
and AdversarialTranspositionMemo< Table, typename D_::State_Key, Move, Score > and AdversarialSearchTracer< Tracer, Move, Score > Result search(State initial_state, Table &table, Tracer &tracer)
Execute Negamax with domain-provided key and a trace sink.
typename Domain::State State
Concrete search state type.
const Domain & domain() const noexcept
Read-only access to the bound game domain.
adversarial_search_detail::NodeEvaluation< Move, Score > search_node(State &state, const size_t depth, Result &result, Table &table, Keyer &keyer, Tracer &tracer)
bool probe_transposition(State &state, const size_t remaining, Result &result, Table &table, Keyer &keyer, adversarial_search_detail::NodeEvaluation< Move, Score > &out)
bool expansion_limit_reached(Result &result)
static constexpr bool supports_best_first
Compile-time marker: Negamax only supports Depth_First strategy.
Transposition-table container built on top of Aleph hash maps.
Stats stats() const
Computes statistics about chain length distribution.
Minimal contract for alternating-turn zero-sum games.
Score type accepted by adversarial search engines.
Concept for explicit state-key providers used by adversarial TT overloads.
Concept for adversarial-search trace sinks.
Concept for memo tables storing adversarial-search entries.
Required evaluator contract for zero-sum games.
Minimal protocol for memo tables used by search engines.
Minimal requirement for search moves.
Generic concept for domains that can expose a stable state key.
Minimal requirement for mutable search states.
Concept for lazy successor generation.
__gmp_expr< T, __gmp_unary_expr< __gmp_expr< T, U >, __gmp_floor_function > > floor(const __gmp_expr< T, U > &expr)
__gmp_expr< T, __gmp_binary_expr< __gmp_expr< T, U >, unsigned long int, __gmp_root_function > > root(const __gmp_expr< T, U > &expr, unsigned long int l)
LinearHashTable< int > Table
constexpr Score score_ceiling() noexcept
std::optional< Move > first_move_of(const SearchPath< Move > &path)
void store_adversarial_transposition(State &state, const size_t remaining, Result &result, Table &table, Keyer &keyer, const NodeEvaluation< Move, Score > &value, const TranspositionBound bound, const bool stop)
Build an adversarial transposition-table entry and store it.
constexpr size_t remaining_depth(const SearchLimits &limits, const size_t depth) noexcept
void accumulate_search_stats(SearchStats &dst, const SearchStats &src) noexcept
constexpr Score saturating_add(const Score value, const Score delta) noexcept
constexpr Score grow_aspiration_half_window(const Score current, const AspirationWindow< Score > &window) noexcept
constexpr Score saturating_subtract(const Score value, const Score delta) noexcept
void emit_trace(Tracer &tracer, const AdversarialTraceEventKind kind, const size_t depth, const size_t remaining, const size_t iteration=0, const size_t horizon=0, const size_t aspiration_retries=0, const Score value=Score{}, const Score alpha=Score{}, const Score beta=Score{}, const std::optional< Move > &move=std::nullopt, const SearchPath< Move > *principal_variation=nullptr)
constexpr Score score_floor() noexcept
void accumulate_adversarial_stats(AdversarialSearchStats &dst, const AdversarialSearchStats &src) noexcept
void mark_limit_reached(Result &result)
AdversarialTranspositionEntry< Move, Score > * probe_and_count_transposition(State &state, const size_t remaining, Result &result, Table &table, Keyer &keyer)
Probe a transposition table and update probe/hit/miss statistics.
void accumulate_move_ordering_stats(MoveOrderingStats &dst, const MoveOrderingStats &src) noexcept
void accumulate_transposition_stats(TranspositionStats &dst, const TranspositionStats &src) noexcept
SearchPath< Move > prepend_move(const Move &move, const SearchPath< Move > &tail)
auto evaluate_leaf(Domain &domain, const typename Domain::State &state, Result &result)
AdversarialIterativeDeepeningResult< Move, Score > run_iterative_deepening(Engine &engine, const ExplorationPolicy &policy, const SearchLimits &limits, const AdversarialIterativeDeepeningOptions< Score > &options, Tracer &tracer, RunOneIteration &&run_one_iteration)
Core iterative-deepening loop shared by Negamax and Alpha-Beta.
bool should_prune_state(Domain &domain, const typename Domain::State &state, const size_t depth)
Dispatch helper for the optional should_prune hook.
void register_visit(const size_t depth, Result &result)
Update visit counters and max-depth statistic.
bool expansion_limit_reached(Result &result, const SearchLimits &limits)
Check whether the expansion limit has been reached.
Main namespace for Aleph-w library functions.
and
Check uniqueness with explicit hash + equality functors.
TranspositionBound
Stored bound classification for memoized search entries.
@ Exact
Exact value for the stored search configuration.
constexpr size_t Search_Unlimited
Sentinel used by SearchLimits to mean "no bound".
SearchStatus
Final state of a search execution.
@ LimitReached
Search stopped because an external hard limit was hit.
@ Exhausted
Search space within the configured bounds was exhausted.
@ NotStarted
Search object exists but no traversal has run yet.
Divide_Conquer_DP_Result< Cost > divide_and_conquer_partition_dp(const size_t groups, const size_t n, Transition_Cost_Fn transition_cost, const Cost inf=dp_optimization_detail::default_inf< Cost >())
Optimize partition DP using divide-and-conquer optimization.
auto iterative_deepening_negamax_search(Domain domain, typename Domain::State initial_state, ExplorationPolicy policy=Negamax< Domain >::default_policy(), SearchLimits limits={}, AdversarialIterativeDeepeningOptions< typename Domain::Score > options={})
Iterative deepening over Negamax without transposition table.
size_t aleph_hash_value(const AdversarialTranspositionKey< StateKey > &key) noexcept
size_t dft_hash_fct(const Key &key) noexcept
Primary default hash: best speed/quality trade-off.
void hash_combine(size_t &seed, size_t h) noexcept
Non-commutative hash combiner (Boost-style golden-ratio mix).
AdversarialTraceEventKind
Trace event kinds emitted by adversarial search engines.
@ Alpha_Beta_Cutoff
One Alpha-Beta fail-high cutoff occurred.
@ Expansion_Limit
Search stopped because the expansion limit was hit.
@ Aspiration_Retry
An aspiration window failed and was widened.
@ Depth_Cutoff
Search stopped at the configured horizon.
@ Transposition_Hit
One cached transposition entry was reused.
@ Enter_Node
One search node has just been entered.
@ Exit_Node
One search node finished with an exact local result.
@ Iteration_End
One iterative-deepening iteration completed.
@ Iteration_Begin
One iterative-deepening iteration started.
@ Domain_Prune
Domain-side pruning hook discarded the state.
@ Terminal_Node
A terminal state was evaluated.
TranspositionStoreStatus
Outcome of one store/update attempt in a transposition table.
@ Replaced
An existing key was updated according to replacement policy.
@ Rejected
Existing entry was kept and candidate was discarded.
double search_elapsed_ms(const SearchClock::duration &duration) noexcept
@ Domain
Preserve the order emitted by for_each_successor().
auto negamax_search(Domain domain, typename Domain::State initial_state, ExplorationPolicy policy=Negamax< Domain >::default_policy(), SearchLimits limits={})
Convenience wrapper for one-shot Negamax search.
static struct argp_option options[]
Common infrastructure for implicit state-space search.
Result of one iterative-deepening iteration.
size_t aspiration_researches
Number of retries triggered by aspiration failure.
bool used_aspiration_window
Whether the iteration started from a bounded window.
Score aspiration_beta
Final upper bound used in the last search attempt.
size_t depth
Horizon reached by this iteration.
AdversarialSearchResult< Move, Score > result
Final result returned for this depth.
AdversarialSearchStats total_stats
Aggregate cost of all attempts at this depth.
Score aspiration_alpha
Final lower bound used in the last search attempt.
Controls for adversarial iterative deepening.
size_t initial_depth
First horizon to search.
AspirationWindow< Score > aspiration
Optional aspiration-window policy.
size_t depth_step
Increment applied after each completed iteration.
Aggregate result of adversarial iterative deepening.
Array< AdversarialIterativeDeepeningIteration< Move, Score > > iterations
Per-depth results.
size_t aspiration_researches
Total number of aspiration retries.
AdversarialSearchResult< Move, Score > result
Result of the deepest iteration (may be partial if a limit was hit).
AdversarialSearchStats total_stats
Aggregate cost across all iterations and retries.
size_t completed_iterations
Number of fully completed iterations (excludes the last one if it hit a limit).
bool has_iterations() const noexcept
True when at least one iteration result has been recorded.
Result of one adversarial search execution.
SearchLimits limits
Limits used for the run.
bool has_principal_variation() const noexcept
True when at least one move is available in the principal variation.
const Move & first_move() const
First move of the principal variation.
bool exhausted() const noexcept
True when the search space was fully explored.
bool limit_reached() const noexcept
True when a resource limit was hit before exhaustion.
AdversarialSearchStats stats
Collected adversarial-search statistics.
Score value
Root score from the current player's perspective.
ExplorationPolicy policy
Exploration policy used during the run.
SearchStatus status
Final execution state.
SearchPath< Move > principal_variation
Best line found from the root.
Statistics collected by adversarial search engines.
size_t alpha_beta_cutoffs
Number of cutoffs performed by Alpha-Beta.
size_t heuristic_evaluations
Number of calls to evaluate(state).
TranspositionStats transpositions
Transposition-table usage during this run.
One trace event produced by an adversarial search.
Score beta
Current Alpha-Beta upper window bound, if any.
size_t remaining_depth
Remaining horizon from this node.
Score value
Value known at the event point, if any.
std::optional< Move > move
Best or cutoff move when meaningful.
SearchPath< Move > principal_variation
PV snapshot for iteration-end events.
size_t iteration
Iteration index in iterative deepening.
size_t depth
Current node depth from the root.
size_t aspiration_retries
Number of aspiration retries so far.
size_t horizon
Horizon of the current iterative iteration.
Score alpha
Current Alpha-Beta lower window bound, if any.
AdversarialTraceEventKind kind
Cached adversarial-search entry stored in transposition tables.
Score value
Cached search value.
size_t depth
Remaining depth searched for this entry.
SearchPath< Move > principal_variation
Best line known for this entry.
TranspositionBound bound
Bound classification.
Convenience alias for adversarial transposition tables.
bool operator==(const AdversarialTranspositionKey &) const noexcept=default
Aspiration-window configuration for iterative deepening.
bool enabled() const noexcept
True when the aspiration window is active (half_window > 0).
Score half_window
Initial half-width around the previous root value.
Score growth
Extra widening applied after each failed attempt.
Exploration controls shared across engines.
bool stop_at_first_solution
Stop when the first goal is found.
Strategy strategy
Traversal strategy.
MoveOrderingMode move_ordering
Successor-ordering mode.
@ Depth_First
Recursive depth-first traversal (all engines).
bool use_history_heuristic
Enable experimental history heuristic where supported.
bool use_killer_moves
Enable experimental killer heuristic where supported.
Open addressing hash map using linear probing.
Statistics collected by engines that reorder successor batches.
Default no-op tracer used when the caller does not request tracing.
void operator()(const Event &) const noexcept
Null history heuristic hook used when a domain has no move key.
Replacement policy for adversarial transposition entries.
bool operator()(const Entry &candidate, const Entry ¤t) const noexcept
Hard bounds applied by the search engine.
size_t max_solutions
Maximum accepted solutions.
size_t max_depth
Maximum expansion depth.
Counters collected during a search run.
size_t pruned_by_domain
States discarded by domain-side pruning.
size_t terminal_states
Non-solution terminal states cut by the domain.
double elapsed_ms
Wall-clock time spent inside the search call.
size_t pruned_by_depth
States not expanded due to max depth.
size_t generated_successors
Number of successor moves emitted.
size_t expanded_states
Number of non-terminal states expanded.
Statistics collected by memo/transposition storage.
size_t cutoffs
Number of search cutoffs enabled by cached data.
constexpr size_t operator()(const State &) const noexcept
SearchPath< Move > principal_variation
TranspositionStoreStatus store(const Key &, const Entry &) noexcept
static constexpr bool enabled
Entry * probe(const Key &) noexcept