Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
Negamax.H
Go to the documentation of this file.
1/*
2 Aleph_w
3
4 Data structures & Algorithms
5 version 2.0.0b
6 https://github.com/lrleon/Aleph-w
7
8 This file is part of Aleph-w library
9
10 Copyright (c) 2002-2026 Leandro Rabindranath Leon
11
12 Permission is hereby granted, free of charge, to any person obtaining a copy
13 of this software and associated documentation files (the "Software"), to deal
14 in the Software without restriction, including without limitation the rights
15 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16 copies of the Software, and to permit persons to whom the Software is
17 furnished to do so, subject to the following conditions:
18
19 The above copyright notice and this permission notice shall be included in all
20 copies or substantial portions of the Software.
21
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 SOFTWARE.
29*/
30
50# ifndef NEGAMAX_H
51# define NEGAMAX_H
52
53#include <concepts>
54#include <limits>
55#include <type_traits>
56#include <utility>
57
58#include <ah-errors.H>
59#include <state_search_common.H>
60#include <Transposition_Table.H>
61
62namespace Aleph {
63
69template <typename T>
70concept AdversarialScore = std::signed_integral<T> or std::floating_point<T>;
71
79
81template <SearchMove Move, AdversarialScore Score>
92
102template <SearchMove Move, AdversarialScore Score>
104{
106
107 bool operator()(const Entry &candidate, const Entry &current) const noexcept
108 {
109 if (candidate.depth != current.depth)
110 return candidate.depth > current.depth;
111
112 if (candidate.bound != current.bound)
113 return candidate.bound == TranspositionBound::Exact;
114
115 return false;
116 }
117};
118
124template <typename StateKey>
126{
128 size_t depth = 0;
129
130 [[nodiscard]] bool operator==(const AdversarialTranspositionKey &) const noexcept = default;
131};
132
133template <typename StateKey>
135{
136 size_t seed = dft_hash_fct(key.state_key);
137 hash_combine(seed, dft_hash_fct(key.depth));
138 return seed;
139}
140
141template <typename StateKey,
142 SearchMove Move,
143 AdversarialScore Score,
144 template <typename, typename, class> class HashMapTable = MapOLhash,
150 Cmp,
152
154template <typename Table, typename StateKey, typename Move, typename Score>
158
165template <SearchMove Move, AdversarialScore Score>
167{
168 using Move_Type = Move;
169 using Score_Type = Score;
170
175 Score value = Score{};
177
180 {
182 }
183
189
192 {
193 return not principal_variation.is_empty();
194 }
195
199 [[nodiscard]] const Move &first_move() const
200 {
202 << "AdversarialSearchResult::first_move: no principal variation available";
203 return principal_variation[0];
204 }
205};
206
222
224template <SearchMove Move, AdversarialScore Score>
226{
227 using Move_Type = Move;
228 using Score_Type = Score;
229
231 size_t depth = 0;
232 size_t remaining_depth = 0;
233 size_t iteration = 0;
234 size_t horizon = 0;
236 Score value = Score{};
237 Score alpha = Score{};
238 Score beta = Score{};
239 std::optional<Move> move;
241};
242
244template <typename Tracer, typename Move, typename Score>
247and requires(Tracer &tracer, const AdversarialTraceEvent<Move, Score> &event) {
248 { tracer(event) } -> std::same_as<void>;
249 };
250
252template <typename Table, typename Keyer, typename State, typename Move, typename Score>
255and (not AdversarialSearchTracer<Keyer, Move, Score>) and requires(Keyer &keyer, const State &state) {
256 typename std::invoke_result_t<Keyer &, const State &>;
258 };
259
262{
263 template <typename Event>
264 void operator()(const Event &) const noexcept
265 {
266 // empty
267 }
268};
269
271template <SearchMove Move, AdversarialScore Score>
273{
274public:
277
279 void operator()(const Event &event)
280 {
281 events_.append(event);
282 }
283
286 {
287 events_.clear();
288 }
289
292 {
293 return events_.size();
294 }
295
298 {
299 return events_.is_empty();
300 }
301
304 {
305 return events_;
306 }
307
308private:
310};
311
313template <AdversarialScore Score>
315{
316 Score half_window = Score{};
317 Score growth = Score{};
318
321 {
322 return half_window > Score{};
323 }
324};
325
327template <AdversarialScore Score>
334
336template <SearchMove Move, AdversarialScore Score>
347
349template <SearchMove Move, AdversarialScore Score>
372
379template <typename Domain>
380concept GameEvaluator = requires(Domain &domain, const typename Domain::State &state) {
381 typename Domain::Score;
383 { domain.is_terminal(state) } -> std::convertible_to<bool>;
384 { domain.evaluate(state) } -> std::convertible_to<typename Domain::Score>;
385};
386
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>;
414 };
415
416namespace adversarial_search_detail {
417
418template <typename Entry>
420{
421 static constexpr bool enabled = false;
422
423 template <typename Key>
424 [[nodiscard]] Entry *probe(const Key &) noexcept
425 {
426 return nullptr;
427 }
428
429 template <typename Key>
430 [[nodiscard]] TranspositionStoreStatus store(const Key &, const Entry &) noexcept
431 {
433 }
434};
435
437{
438 template <typename State>
439 [[nodiscard]] constexpr size_t operator()(const State &) const noexcept
440 {
441 return 0;
442 }
443};
444
445template <typename Domain, bool Supported = MoveKeyProvider<Domain>>
450
451template <typename Domain>
456
457template <AdversarialScore Score>
458[[nodiscard]] constexpr Score score_floor() noexcept
459{
460 return std::numeric_limits<Score>::lowest() / Score(2);
461}
462
463template <AdversarialScore Score>
464[[nodiscard]] constexpr Score score_ceiling() noexcept
465{
466 return std::numeric_limits<Score>::max() / Score(2);
467}
468
469template <typename Result>
470void mark_limit_reached(Result &result)
471{
472 if (result.status != SearchStatus::LimitReached)
473 {
474 ++result.stats.limit_hits;
475 result.status = SearchStatus::LimitReached;
476 }
477}
478
479template <SearchMove Move>
480[[nodiscard]] SearchPath<Move> prepend_move(const Move &move, const SearchPath<Move> &tail)
481{
482 SearchPath<Move> path;
483 path.reserve(tail.size() + 1);
484 path.append(move);
485 for (const auto &item : tail)
486 path.append(item);
487 return path;
488}
489
490template <SearchMove Move, AdversarialScore Score>
496
497template <typename Domain, typename Result>
498[[nodiscard]] auto evaluate_leaf(Domain &domain, const typename Domain::State &state, Result &result)
499{
500 using Move = typename Domain::Move;
501 using Score = typename Domain::Score;
502
503 ++result.stats.heuristic_evaluations;
504
506 out.value = domain.evaluate(state);
507 return out;
508}
509
510[[nodiscard]] inline constexpr size_t remaining_depth(const SearchLimits &limits,
511 const size_t depth) noexcept
512{
513 return limits.max_depth == Search_Unlimited ? Search_Unlimited : limits.max_depth - depth;
514}
515
517{
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;
523}
524
526 const TranspositionStats &src) noexcept
527{
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;
535}
536
537inline void accumulate_search_stats(SearchStats &dst, const SearchStats &src) noexcept
538{
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;
551 accumulate_move_ordering_stats(dst.move_ordering, src.move_ordering);
552}
553
555 const AdversarialSearchStats &src) noexcept
556{
558 dst.heuristic_evaluations += src.heuristic_evaluations;
559 dst.alpha_beta_cutoffs += src.alpha_beta_cutoffs;
560 accumulate_transposition_stats(dst.transpositions, src.transpositions);
561}
562
563template <SearchMove Move, AdversarialScore Score, typename Tracer>
566 const AdversarialTraceEventKind kind,
567 const size_t depth,
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,
576 const SearchPath<Move> *principal_variation = nullptr)
577{
579 event.kind = kind;
580 event.depth = depth;
581 event.remaining_depth = remaining;
582 event.iteration = iteration;
583 event.horizon = horizon;
584 event.aspiration_retries = aspiration_retries;
585 event.value = value;
586 event.alpha = alpha;
587 event.beta = beta;
588 event.move = move;
589 if (principal_variation != nullptr)
590 event.principal_variation = *principal_variation;
591 tracer(event);
592}
593
594template <SearchMove Move>
595[[nodiscard]] std::optional<Move> first_move_of(const SearchPath<Move> &path)
596{
597 if (path.is_empty())
598 return std::nullopt;
599
600 return std::optional<Move>{path[0]};
601}
602
603template <AdversarialScore Score>
604[[nodiscard]] constexpr Score saturating_subtract(const Score value, const Score delta) noexcept
605{
606 const Score floor = score_floor<Score>();
607 if (delta <= Score{})
608 return value;
609 if (value <= floor + delta)
610 return floor;
611 return value - delta;
612}
613
614template <AdversarialScore Score>
615[[nodiscard]] constexpr Score saturating_add(const Score value, const Score delta) noexcept
616{
617 const Score ceiling = score_ceiling<Score>();
618 if (delta <= Score{})
619 return value;
620 if (value >= ceiling - delta)
621 return ceiling;
622 return value + delta;
623}
624
625template <AdversarialScore Score>
626[[nodiscard]] constexpr Score grow_aspiration_half_window(const Score current,
627 const AspirationWindow<Score> &window) noexcept
628{
629 const Score step
630 = window.growth > Score{} ? window.growth : (current > Score{} ? current : Score{1});
631 return saturating_add(current, step);
632}
633
654template <SearchMove Move, AdversarialScore Score,
655 typename State, typename Table, typename Keyer, typename Result>
657 const size_t remaining,
658 Result &result,
659 Table &table,
660 Keyer &keyer,
661 const NodeEvaluation<Move, Score> &value,
662 const TranspositionBound bound,
663 const bool stop)
664{
665 if constexpr (Table::enabled)
666 {
667 if (stop or result.limit_reached())
668 return;
669
671 entry.value = value.value;
672 entry.depth = remaining;
673 entry.bound = bound;
675
676 const auto status = table.store(typename Table::Key_Type{keyer(state), remaining}, entry);
678 ++result.stats.transpositions.rejected_updates;
679 else
680 ++result.stats.transpositions.stores;
682 ++result.stats.transpositions.replacements;
683 }
684}
685
705template <SearchMove Move, AdversarialScore Score,
706 typename State, typename Table, typename Keyer, typename Result>
709 const size_t remaining,
710 Result &result,
711 Table &table,
712 Keyer &keyer)
713{
714 if constexpr (Table::enabled)
715 {
716 ++result.stats.transpositions.probes;
717
718 if (auto *entry = table.probe(typename Table::Key_Type{keyer(state), remaining});
719 entry != nullptr)
720 {
721 ++result.stats.transpositions.hits;
722 return entry;
723 }
724
725 ++result.stats.transpositions.misses;
726 }
727
728 return nullptr;
729}
730
757template <SearchMove Move, AdversarialScore Score,
758 typename Engine, typename Tracer, typename RunOneIteration>
762 const ExplorationPolicy &policy,
763 const SearchLimits &limits,
765 Tracer &tracer,
767{
769 out.result.policy = policy;
770 out.result.limits = limits;
771 out.iterations.reserve((limits.max_depth - options.initial_depth) / options.depth_step + 1);
772
773 size_t depth = options.initial_depth;
774 size_t iteration_index = 0;
775 for (;;)
776 {
779
782 engine.set_limits(iteration_limits);
783
785 iteration.depth = depth;
786
787 std::forward<RunOneIteration>(run_one_iteration)(out, iteration, depth, iteration_index);
788
789 accumulate_adversarial_stats(out.total_stats, iteration.total_stats);
790
793 0,
794 depth,
796 depth,
797 iteration.aspiration_researches,
798 iteration.result.value,
799 iteration.aspiration_alpha,
800 iteration.aspiration_beta,
801 first_move_of(iteration.result.principal_variation),
802 &iteration.result.principal_variation);
803
804 out.result = iteration.result;
805 out.iterations.append(iteration);
806
807 if (iteration.result.limit_reached())
808 break;
809
810 ++out.completed_iterations;
811
812 if (depth >= limits.max_depth)
813 break;
814
815 const size_t next_depth = depth > limits.max_depth - options.depth_step
816 ? limits.max_depth
817 : depth + options.depth_step;
818 if (next_depth == depth)
819 break;
820
821 depth = next_depth;
823 }
824
825 return out;
826}
827
828} // end namespace adversarial_search_detail
829
838template <AdversarialGameDomain Domain>
840{
841public:
845 using State = typename Domain::State;
847 using Move = typename Domain::Move;
849 using Score = typename Domain::Score;
852
859 static constexpr bool supports_best_first = false;
860
869
872 const SearchLimits &limits = {})
874 {
875 // empty
876 }
877
880 {
881 return domain_;
882 }
883
886 {
887 return domain_;
888 }
889
892 {
893 return policy_;
894 }
895
898 {
899 return limits_;
900 }
901
903 void set_policy(const ExplorationPolicy &policy) noexcept
904 {
905 policy_ = policy;
906 }
907
909 void set_limits(const SearchLimits &limits) noexcept
910 {
911 limits_ = limits;
912 }
913
933
935 template <typename Tracer>
943
950 template <typename Table, typename Keyer>
957
959 template <typename Table, typename Keyer, typename Tracer>
963 {
964 return search_impl(std::move(initial_state), table, keyer, tracer);
965 }
966
968 template <typename Table, typename D_ = Domain>
972 {
974 auto keyer = [this](const State &state)
975 {
976 return domain_.state_key(state);
977 };
978
979 return search_impl(std::move(initial_state), table, keyer, tracer);
980 }
981
983 template <typename Table, typename Tracer, typename D_ = Domain>
988 {
989 auto keyer = [this](const State &state)
990 {
991 return domain_.state_key(state);
992 };
993
994 return search_impl(std::move(initial_state), table, keyer, tracer);
995 }
996
997private:
1001 bool stop_ = false;
1002
1003 template <typename Table, typename Keyer, typename Tracer>
1006 {
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";
1015
1016 Result result;
1017 result.policy = policy_;
1018 result.limits = limits_;
1019 const auto start_time = SearchClock::now();
1020
1021 stop_ = false;
1022 auto root = search_node(initial_state, 0, result, table, keyer, tracer);
1023 result.value = root.value;
1024 result.principal_variation = std::move(root.principal_variation);
1025
1026 if (result.status == SearchStatus::NotStarted)
1028
1029 result.stats.elapsed_ms = search_elapsed_ms(SearchClock::now() - start_time);
1030
1031 return result;
1032 }
1033
1035 {
1037 {
1038 stop_ = true;
1039 return true;
1040 }
1041
1042 return false;
1043 }
1044
1045 template <typename Table, typename Keyer>
1047 const size_t remaining,
1048 Result &result,
1049 Table &table,
1050 Keyer &keyer,
1052 const TranspositionBound bound)
1053 {
1054 adversarial_search_detail::store_adversarial_transposition<Move, Score>(
1055 state, remaining, result, table, keyer, value, bound, stop_);
1056 }
1057
1058 template <typename Table, typename Keyer>
1060 const size_t remaining,
1061 Result &result,
1062 Table &table,
1063 Keyer &keyer,
1065 {
1066 auto *entry = adversarial_search_detail::probe_and_count_transposition<Move, Score>(
1067 state, remaining, result, table, keyer);
1068
1069 if (entry != nullptr and entry->bound == TranspositionBound::Exact)
1070 {
1071 ++result.stats.transpositions.cutoffs;
1072 out.value = entry->value;
1073 out.principal_variation = entry->principal_variation;
1074 return true;
1075 }
1076
1077 return false;
1078 }
1079
1080 template <typename Table, typename Keyer, typename Tracer>
1083 State &state, const size_t depth, Result &result, Table &table, Keyer &keyer, Tracer &tracer)
1084 {
1089
1091
1093
1094 const size_t remaining = remaining_depth(limits_, depth);
1096
1098 if (probe_transposition(state, remaining, result, table, keyer, cached))
1099 {
1102 depth,
1103 remaining,
1104 0,
1105 0,
1106 0,
1107 cached.value,
1108 Score{},
1109 Score{},
1110 first_move_of(cached.principal_variation));
1111 return cached;
1112 }
1113
1114 if (domain_.is_terminal(state))
1115 {
1116 ++result.stats.terminal_states;
1117 auto leaf = evaluate_leaf(domain_, state, result);
1119 tracer, AdversarialTraceEventKind::Terminal_Node, depth, remaining, 0, 0, 0, leaf.value);
1120 store_transposition(state, remaining, result, table, keyer, leaf, TranspositionBound::Exact);
1121 return leaf;
1122 }
1123
1124 if (depth >= limits_.max_depth)
1125 {
1126 ++result.stats.pruned_by_depth;
1127 auto leaf = evaluate_leaf(domain_, state, result);
1129 tracer, AdversarialTraceEventKind::Depth_Cutoff, depth, remaining, 0, 0, 0, leaf.value);
1130 store_transposition(state, remaining, result, table, keyer, leaf, TranspositionBound::Exact);
1131 return leaf;
1132 }
1133
1135 {
1136 ++result.stats.pruned_by_domain;
1137 auto leaf = evaluate_leaf(domain_, state, result);
1139 tracer, AdversarialTraceEventKind::Domain_Prune, depth, remaining, 0, 0, 0, leaf.value);
1140 store_transposition(state, remaining, result, table, keyer, leaf, TranspositionBound::Exact);
1141 return leaf;
1142 }
1143
1144 if (expansion_limit_reached(result))
1145 {
1146 auto leaf = evaluate_leaf(domain_, state, result);
1148 tracer, AdversarialTraceEventKind::Expansion_Limit, depth, remaining, 0, 0, 0, leaf.value);
1149 return leaf;
1150 }
1151
1153 bool has_move = false;
1154 bool has_best = false;
1155 bool counted_expansion = false;
1156
1157 (void) domain_.for_each_successor(state,
1158 [&](const Move &move) -> bool
1159 {
1160 if (stop_)
1161 return false;
1162
1164 {
1165 ++result.stats.expanded_states;
1166 counted_expansion = true;
1167 }
1168
1169 has_move = true;
1170 ++result.stats.generated_successors;
1172 bool applied = false;
1173 try
1174 {
1175 domain_.apply(state, move);
1176 applied = true;
1177 child = search_node(state, depth + 1, result, table, keyer, tracer);
1178 }
1179 catch (...)
1180 {
1181 if (applied)
1182 domain_.undo(state, move);
1183 throw;
1184 }
1185 domain_.undo(state, move);
1186
1187 // Negate child score to switch perspective. lowest() is the engine's
1188 // internal sentinel (never returned by domain evaluators, which are
1189 // bounded by score_floor()/score_ceiling()); negating lowest() would
1190 // overflow signed types, so it maps to max() instead.
1191 const Score candidate = (child.value == std::numeric_limits<Score>::lowest())
1192 ? std::numeric_limits<Score>::max()
1193 : -child.value;
1194 if (not has_best or candidate > best.value)
1195 {
1196 best.value = candidate;
1197 best.principal_variation = prepend_move(move, child.principal_variation);
1198 has_best = true;
1199 }
1200
1201 return not stop_;
1202 });
1203
1204 if (not has_move)
1205 {
1206 ++result.stats.terminal_states;
1207 auto leaf = evaluate_leaf(domain_, state, result);
1209 tracer, AdversarialTraceEventKind::Terminal_Node, depth, remaining, 0, 0, 0, leaf.value);
1210 store_transposition(state, remaining, result, table, keyer, leaf, TranspositionBound::Exact);
1211 return leaf;
1212 }
1213
1216 depth,
1217 remaining,
1218 0,
1219 0,
1220 0,
1221 best.value,
1222 Score{},
1223 Score{},
1224 first_move_of(best.principal_variation));
1225
1226 store_transposition(state, remaining, result, table, keyer, best, TranspositionBound::Exact);
1227 return best;
1228 }
1229};
1230
1232template <AdversarialGameDomain Domain>
1234 typename Domain::State initial_state,
1236 SearchLimits limits = {})
1237{
1238 Negamax<Domain> engine(std::move(domain), policy, limits);
1239 return engine.search(std::move(initial_state));
1240}
1241
1243template <AdversarialGameDomain Domain, typename Tracer>
1246 typename Domain::State initial_state,
1247 Tracer &tracer,
1249 SearchLimits limits = {})
1250{
1251 Negamax<Domain> engine(std::move(domain), policy, limits);
1252 return engine.search(std::move(initial_state), tracer);
1253}
1254
1256template <AdversarialGameDomain Domain, typename Table, typename Keyer>
1259 typename Domain::State initial_state,
1260 Table &table,
1261 Keyer keyer,
1263 SearchLimits limits = {})
1264{
1265 Negamax<Domain> engine(std::move(domain), policy, limits);
1266 return engine.search(std::move(initial_state), table, keyer);
1267}
1268
1270template <AdversarialGameDomain Domain, typename Table, typename Keyer, typename Tracer>
1274 typename Domain::State initial_state,
1275 Table &table,
1276 Keyer keyer,
1277 Tracer &tracer,
1279 SearchLimits limits = {})
1280{
1281 Negamax<Domain> engine(std::move(domain), policy, limits);
1282 return engine.search(std::move(initial_state), table, keyer, tracer);
1283}
1284
1286template <AdversarialGameDomain Domain, typename Table>
1290 typename Domain::State initial_state,
1291 Table &table,
1293 SearchLimits limits = {})
1294{
1295 Negamax<Domain> engine(std::move(domain), policy, limits);
1296 return engine.search(std::move(initial_state), table);
1297}
1298
1300template <AdversarialGameDomain Domain, typename Table, typename Tracer>
1305 typename Domain::State initial_state,
1306 Table &table,
1307 Tracer &tracer,
1309 SearchLimits limits = {})
1310{
1311 Negamax<Domain> engine(std::move(domain), policy, limits);
1312 return engine.search(std::move(initial_state), table, tracer);
1313}
1314
1316template <AdversarialGameDomain Domain>
1318 Domain domain,
1319 typename Domain::State initial_state,
1321 SearchLimits limits = {},
1323{
1326 std::move(domain), std::move(initial_state), tracer, policy, limits, options);
1327}
1328
1330template <AdversarialGameDomain Domain, typename Tracer>
1333 Domain domain,
1334 typename Domain::State initial_state,
1335 Tracer &tracer,
1337 SearchLimits limits = {},
1339{
1340 using Move = typename Domain::Move;
1341 using Score = typename Domain::Score;
1342
1343 ah_invalid_argument_if(limits.max_depth == Search_Unlimited)
1344 << "iterative_deepening_negamax_search requires a finite SearchLimits::max_depth";
1345 ah_invalid_argument_if(options.depth_step == 0)
1346 << "AdversarialIterativeDeepeningOptions::depth_step must be positive";
1347 ah_invalid_argument_if(options.initial_depth > limits.max_depth)
1348 << "AdversarialIterativeDeepeningOptions::initial_depth exceeds SearchLimits::max_depth";
1349 ah_invalid_argument_if(options.aspiration.half_window < Score{})
1350 << "AspirationWindow::half_window must be non-negative";
1351 ah_invalid_argument_if(options.aspiration.enabled())
1352 << "Aspiration windows are only available through iterative_deepening_alpha_beta_search";
1353
1354 Negamax<Domain> engine(std::move(domain), policy, limits);
1355 return adversarial_search_detail::run_iterative_deepening<Move, Score>(
1356 engine, policy, limits, options, tracer,
1359 const size_t, const size_t)
1360 {
1361 auto result = engine.search(initial_state, tracer);
1362 iteration.result = result;
1363 iteration.total_stats = result.stats;
1364 });
1365}
1366
1368template <AdversarialGameDomain Domain, typename Table, typename Keyer>
1371 Domain domain,
1372 typename Domain::State initial_state,
1373 Table &table,
1374 Keyer keyer,
1376 SearchLimits limits = {},
1378{
1381 std::move(domain), std::move(initial_state), table, keyer, tracer, policy, limits, options);
1382}
1383
1385template <AdversarialGameDomain Domain, typename Table, typename Keyer, typename Tracer>
1389 Domain domain,
1390 typename Domain::State initial_state,
1391 Table &table,
1392 Keyer keyer,
1393 Tracer &tracer,
1395 SearchLimits limits = {},
1397{
1398 using Move = typename Domain::Move;
1399 using Score = typename Domain::Score;
1400
1401 ah_invalid_argument_if(limits.max_depth == Search_Unlimited)
1402 << "iterative_deepening_negamax_search requires a finite SearchLimits::max_depth";
1403 ah_invalid_argument_if(options.depth_step == 0)
1404 << "AdversarialIterativeDeepeningOptions::depth_step must be positive";
1405 ah_invalid_argument_if(options.initial_depth > limits.max_depth)
1406 << "AdversarialIterativeDeepeningOptions::initial_depth exceeds SearchLimits::max_depth";
1407 ah_invalid_argument_if(options.aspiration.half_window < Score{})
1408 << "AspirationWindow::half_window must be non-negative";
1409 ah_invalid_argument_if(options.aspiration.enabled())
1410 << "Aspiration windows are only available through iterative_deepening_alpha_beta_search";
1411
1412 Negamax<Domain> engine(std::move(domain), policy, limits);
1413 return adversarial_search_detail::run_iterative_deepening<Move, Score>(
1414 engine, policy, limits, options, tracer,
1417 const size_t, const size_t)
1418 {
1419 auto result = engine.search(initial_state, table, keyer, tracer);
1420 iteration.result = result;
1421 iteration.total_stats = result.stats;
1422 });
1423}
1424
1426template <AdversarialGameDomain Domain, typename Table>
1430 Domain domain,
1431 typename Domain::State initial_state,
1432 Table &table,
1434 SearchLimits limits = {},
1436{
1439 std::move(domain), std::move(initial_state), table, tracer, policy, limits, options);
1440}
1441
1443template <AdversarialGameDomain Domain, typename Table, typename Tracer>
1448 Domain domain,
1449 typename Domain::State initial_state,
1450 Table &table,
1451 Tracer &tracer,
1453 SearchLimits limits = {},
1455{
1456 auto key_domain = domain;
1457 auto keyer = [key_domain = std::move(key_domain)](const typename Domain::State &state) mutable
1458 {
1459 return key_domain.state_key(state);
1460 };
1461
1463 std::move(domain), std::move(initial_state), table, keyer, tracer, policy, limits, options);
1464}
1465
1466} // end namespace Aleph
1467
1468# endif // NEGAMAX_H
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.
Definition ah-errors.H:250
#define ah_invalid_argument_if(C)
Throws std::invalid_argument if condition holds.
Definition ah-errors.H:639
Collector that stores trace events in an Aleph list.
Definition Negamax.H:273
const Container_Type & events() const noexcept
Read-only access to the recorded event list.
Definition Negamax.H:303
bool is_empty() const noexcept
True when no events have been recorded.
Definition Negamax.H:297
void operator()(const Event &event)
Record one trace event.
Definition Negamax.H:279
void clear() noexcept
Discard all recorded events.
Definition Negamax.H:285
size_t size() const noexcept
Number of recorded events.
Definition Negamax.H:291
constexpr size_t size() const noexcept
Return the number of elements stored in the stack.
Definition tpl_array.H:351
constexpr bool is_empty() const noexcept
Checks if the container is empty.
Definition tpl_array.H:348
T & append(const T &data)
Append a copy of data
Definition tpl_array.H:245
void reserve(size_t cap)
Reserves cap cells into the array.
Definition tpl_array.H:315
T & append(const T &item)
Definition htlist.H:1271
void clear() noexcept
Empties the container.
Definition htlist.H:1411
Generic linear hash table.
constexpr bool is_empty() const noexcept
Definition htlist.H:419
size_t size() const noexcept
Count the number of elements of the list.
Definition htlist.H:1065
Sparse history heuristic table over Aleph hash maps.
Pure Negamax search over an implicit game tree.
Definition Negamax.H:840
static ExplorationPolicy default_policy() noexcept
Return the default exploration policy for adversarial search.
Definition Negamax.H:862
Result search_impl(State initial_state, Table &table, Keyer &keyer, Tracer &tracer)
Definition Negamax.H:1005
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)
Definition Negamax.H:1046
const ExplorationPolicy & policy() const noexcept
Current exploration policy.
Definition Negamax.H:891
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().
Definition Negamax.H:971
Domain Domain_Type
Type of the problem domain.
Definition Negamax.H:843
typename Domain::Score Score
Score type used for board evaluation.
Definition Negamax.H:849
Domain & domain() noexcept
Mutable access to the bound game domain.
Definition Negamax.H:885
Negamax(Domain domain, ExplorationPolicy policy=default_policy(), const SearchLimits &limits={})
Build a Negamax engine bound to one game domain.
Definition Negamax.H:871
const SearchLimits & limits() const noexcept
Current hard limits.
Definition Negamax.H:897
void set_policy(const ExplorationPolicy &policy) noexcept
Replace the exploration policy for future runs.
Definition Negamax.H:903
void set_limits(const SearchLimits &limits) noexcept
Replace the hard limits for future runs.
Definition Negamax.H:909
Result search(State initial_state, Table &table, Keyer keyer)
Execute Negamax with a transposition table and explicit keyer.
Definition Negamax.H:952
SearchLimits limits_
Definition Negamax.H:1000
typename Domain::Move Move
Move type.
Definition Negamax.H:847
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.
Definition Negamax.H:962
ExplorationPolicy policy_
Definition Negamax.H:999
Result search(State initial_state)
Execute Negamax from initial_state.
Definition Negamax.H:928
Result search(State initial_state, Tracer &tracer)
Execute Negamax from initial_state while emitting trace events.
Definition Negamax.H:937
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.
Definition Negamax.H:987
typename Domain::State State
Concrete search state type.
Definition Negamax.H:845
const Domain & domain() const noexcept
Read-only access to the bound game domain.
Definition Negamax.H:879
Domain domain_
Definition Negamax.H:998
adversarial_search_detail::NodeEvaluation< Move, Score > search_node(State &state, const size_t depth, Result &result, Table &table, Keyer &keyer, Tracer &tracer)
Definition Negamax.H:1082
bool probe_transposition(State &state, const size_t remaining, Result &result, Table &table, Keyer &keyer, adversarial_search_detail::NodeEvaluation< Move, Score > &out)
Definition Negamax.H:1059
bool expansion_limit_reached(Result &result)
Definition Negamax.H:1034
static constexpr bool supports_best_first
Compile-time marker: Negamax only supports Depth_First strategy.
Definition Negamax.H:859
Transposition-table container built on top of Aleph hash maps.
Stats stats() const
Computes statistics about chain length distribution.
Definition hashDry.H:1047
Minimal contract for alternating-turn zero-sum games.
Definition Negamax.H:408
Score type accepted by adversarial search engines.
Definition Negamax.H:70
Concept for explicit state-key providers used by adversarial TT overloads.
Definition Negamax.H:254
Concept for adversarial-search trace sinks.
Definition Negamax.H:246
Concept for memo tables storing adversarial-search entries.
Definition Negamax.H:156
Required evaluator contract for zero-sum games.
Definition Negamax.H:380
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)
Definition gmpfrxx.h:4055
__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)
Definition gmpfrxx.h:4060
LinearHashTable< int > Table
Definition lin-hash.cc:53
constexpr Score score_ceiling() noexcept
Definition Negamax.H:464
std::optional< Move > first_move_of(const SearchPath< Move > &path)
Definition Negamax.H:595
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.
Definition Negamax.H:656
constexpr size_t remaining_depth(const SearchLimits &limits, const size_t depth) noexcept
Definition Negamax.H:510
void accumulate_search_stats(SearchStats &dst, const SearchStats &src) noexcept
Definition Negamax.H:537
constexpr Score saturating_add(const Score value, const Score delta) noexcept
Definition Negamax.H:615
constexpr Score grow_aspiration_half_window(const Score current, const AspirationWindow< Score > &window) noexcept
Definition Negamax.H:626
constexpr Score saturating_subtract(const Score value, const Score delta) noexcept
Definition Negamax.H:604
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)
Definition Negamax.H:565
constexpr Score score_floor() noexcept
Definition Negamax.H:458
void accumulate_adversarial_stats(AdversarialSearchStats &dst, const AdversarialSearchStats &src) noexcept
Definition Negamax.H:554
void mark_limit_reached(Result &result)
Definition Negamax.H:470
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.
Definition Negamax.H:708
void accumulate_move_ordering_stats(MoveOrderingStats &dst, const MoveOrderingStats &src) noexcept
Definition Negamax.H:516
void accumulate_transposition_stats(TranspositionStats &dst, const TranspositionStats &src) noexcept
Definition Negamax.H:525
SearchPath< Move > prepend_move(const Move &move, const SearchPath< Move > &tail)
Definition Negamax.H:480
auto evaluate_leaf(Domain &domain, const typename Domain::State &state, Result &result)
Definition Negamax.H:498
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.
Definition Negamax.H:761
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.
Definition ah-arena.H:89
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.
Definition Negamax.H:1317
size_t aleph_hash_value(const AdversarialTranspositionKey< StateKey > &key) noexcept
Definition Negamax.H:134
size_t dft_hash_fct(const Key &key) noexcept
Primary default hash: best speed/quality trade-off.
Definition hash-fct.H:1003
void hash_combine(size_t &seed, size_t h) noexcept
Non-commutative hash combiner (Boost-style golden-ratio mix).
Definition hash-fct.H:850
AdversarialTraceEventKind
Trace event kinds emitted by adversarial search engines.
Definition Negamax.H:209
@ 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.
Definition Negamax.H:1233
STL namespace.
static struct argp_option options[]
Definition ntreepic.C:1886
Common infrastructure for implicit state-space search.
Result of one iterative-deepening iteration.
Definition Negamax.H:338
size_t aspiration_researches
Number of retries triggered by aspiration failure.
Definition Negamax.H:341
bool used_aspiration_window
Whether the iteration started from a bounded window.
Definition Negamax.H:340
Score aspiration_beta
Final upper bound used in the last search attempt.
Definition Negamax.H:343
size_t depth
Horizon reached by this iteration.
Definition Negamax.H:339
AdversarialSearchResult< Move, Score > result
Final result returned for this depth.
Definition Negamax.H:344
AdversarialSearchStats total_stats
Aggregate cost of all attempts at this depth.
Definition Negamax.H:345
Score aspiration_alpha
Final lower bound used in the last search attempt.
Definition Negamax.H:342
Controls for adversarial iterative deepening.
Definition Negamax.H:329
size_t initial_depth
First horizon to search.
Definition Negamax.H:330
AspirationWindow< Score > aspiration
Optional aspiration-window policy.
Definition Negamax.H:332
size_t depth_step
Increment applied after each completed iteration.
Definition Negamax.H:331
Aggregate result of adversarial iterative deepening.
Definition Negamax.H:351
Array< AdversarialIterativeDeepeningIteration< Move, Score > > iterations
Per-depth results.
Definition Negamax.H:353
size_t aspiration_researches
Total number of aspiration retries.
Definition Negamax.H:356
AdversarialSearchResult< Move, Score > result
Result of the deepest iteration (may be partial if a limit was hit).
Definition Negamax.H:352
AdversarialSearchStats total_stats
Aggregate cost across all iterations and retries.
Definition Negamax.H:354
size_t completed_iterations
Number of fully completed iterations (excludes the last one if it hit a limit).
Definition Negamax.H:355
bool has_iterations() const noexcept
True when at least one iteration result has been recorded.
Definition Negamax.H:367
Result of one adversarial search execution.
Definition Negamax.H:167
SearchLimits limits
Limits used for the run.
Definition Negamax.H:173
bool has_principal_variation() const noexcept
True when at least one move is available in the principal variation.
Definition Negamax.H:191
const Move & first_move() const
First move of the principal variation.
Definition Negamax.H:199
bool exhausted() const noexcept
True when the search space was fully explored.
Definition Negamax.H:179
bool limit_reached() const noexcept
True when a resource limit was hit before exhaustion.
Definition Negamax.H:185
AdversarialSearchStats stats
Collected adversarial-search statistics.
Definition Negamax.H:174
Score value
Root score from the current player's perspective.
Definition Negamax.H:175
ExplorationPolicy policy
Exploration policy used during the run.
Definition Negamax.H:172
SearchStatus status
Final execution state.
Definition Negamax.H:171
SearchPath< Move > principal_variation
Best line found from the root.
Definition Negamax.H:176
Statistics collected by adversarial search engines.
Definition Negamax.H:74
size_t alpha_beta_cutoffs
Number of cutoffs performed by Alpha-Beta.
Definition Negamax.H:76
size_t heuristic_evaluations
Number of calls to evaluate(state).
Definition Negamax.H:75
TranspositionStats transpositions
Transposition-table usage during this run.
Definition Negamax.H:77
One trace event produced by an adversarial search.
Definition Negamax.H:226
Score beta
Current Alpha-Beta upper window bound, if any.
Definition Negamax.H:238
size_t remaining_depth
Remaining horizon from this node.
Definition Negamax.H:232
Score value
Value known at the event point, if any.
Definition Negamax.H:236
std::optional< Move > move
Best or cutoff move when meaningful.
Definition Negamax.H:239
SearchPath< Move > principal_variation
PV snapshot for iteration-end events.
Definition Negamax.H:240
size_t iteration
Iteration index in iterative deepening.
Definition Negamax.H:233
size_t depth
Current node depth from the root.
Definition Negamax.H:231
size_t aspiration_retries
Number of aspiration retries so far.
Definition Negamax.H:235
size_t horizon
Horizon of the current iterative iteration.
Definition Negamax.H:234
Score alpha
Current Alpha-Beta lower window bound, if any.
Definition Negamax.H:237
AdversarialTraceEventKind kind
Definition Negamax.H:230
Cached adversarial-search entry stored in transposition tables.
Definition Negamax.H:83
Score value
Cached search value.
Definition Negamax.H:87
size_t depth
Remaining depth searched for this entry.
Definition Negamax.H:88
SearchPath< Move > principal_variation
Best line known for this entry.
Definition Negamax.H:90
TranspositionBound bound
Bound classification.
Definition Negamax.H:89
Convenience alias for adversarial transposition tables.
Definition Negamax.H:126
bool operator==(const AdversarialTranspositionKey &) const noexcept=default
Aspiration-window configuration for iterative deepening.
Definition Negamax.H:315
bool enabled() const noexcept
True when the aspiration window is active (half_window > 0).
Definition Negamax.H:320
Score half_window
Initial half-width around the previous root value.
Definition Negamax.H:316
Score growth
Extra widening applied after each failed attempt.
Definition Negamax.H:317
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.
Definition Negamax.H:262
void operator()(const Event &) const noexcept
Definition Negamax.H:264
Null history heuristic hook used when a domain has no move key.
Replacement policy for adversarial transposition entries.
Definition Negamax.H:104
bool operator()(const Entry &candidate, const Entry &current) const noexcept
Definition Negamax.H:107
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
Definition Negamax.H:439
TranspositionStoreStatus store(const Key &, const Entry &) noexcept
Definition Negamax.H:430
ValueArg< size_t > seed
Definition testHash.C:48
static mt19937 engine