36#include <gtest/gtest.h>
46struct ArtificialGameState
53struct ArtificialGameMove
59class ArtificialGameDomain
62 using State = ArtificialGameState;
63 using Move = ArtificialGameMove;
65 using State_Key = std::uint64_t;
69 return state.depth == 2;
72 Score
evaluate(
const State &state)
const
77 [[
nodiscard]] State_Key state_key(
const State &state)
const noexcept
79 return (
static_cast<State_Key
>(state.code) << 1) |
80 static_cast<State_Key
>(state.player > 0 ? 1 : 0);
83 void apply(State &state,
const Move &move)
const
85 state.code = move.next_code;
86 state.player = -state.player;
90 void undo(State &state,
const Move&)
const
93 state.player = -state.player;
97 template <
typename Visitor>
98 bool for_each_successor(
const State &state,
Visitor visit)
const
100 if (state.depth >= 2)
108 return visit(Move{3,
'B'});
113 return visit(Move{5,
'b'});
118 return visit(Move{7,
'b'});
145 size_t moves_played = 0;
147 TicTacToeState() : board(size_t(9), 0)
163 using State = TicTacToeState;
165 using State_Key = std::uint64_t;
166 using Move_Key = size_t;
170 return winner(state) != 0
or state.moves_played == 9;
173 Score
evaluate(
const State &state)
const
177 return win == state.player ? 100 : -100;
179 if (state.moves_played == 9)
182 return heuristic(state, state.player) - heuristic(state, -state.player);
185 [[
nodiscard]] State_Key state_key(
const State &state)
const noexcept
187 State_Key key =
static_cast<State_Key
>(state.player > 0 ? 1 : 2);
188 for (
size_t i = 0; i < state.board.size(); ++i)
191 if (state.board[i] == 1)
193 else if (state.board[i] == -1)
205 void apply(State &state,
const Move &move)
const
207 state.board[move.cell] = state.player;
208 state.player = -state.player;
209 ++state.moves_played;
212 void undo(State &state,
const Move &move)
const
214 --state.moves_played;
215 state.player = -state.player;
216 state.board[move.cell] = 0;
219 template <
typename Visitor>
220 bool for_each_successor(
const State &state,
Visitor visit)
const
222 static constexpr size_t order[] = {1, 3, 5, 7, 0, 2, 6, 8, 4};
224 for (
const size_t cell : order)
234 static constexpr size_t lines[8][3] = {
235 {0, 1, 2}, {3, 4, 5}, {6, 7, 8},
236 {0, 3, 6}, {1, 4, 7}, {2, 5, 8},
240 for (
const auto &line :
lines)
242 const int a = state.board[line[0]];
243 if (a != 0
and a == state.board[line[1]]
and a == state.board[line[2]])
250 [[
nodiscard]]
static Score heuristic(
const State &state,
const int player)
noexcept
252 static constexpr size_t lines[8][3] = {
253 {0, 1, 2}, {3, 4, 5}, {6, 7, 8},
254 {0, 3, 6}, {1, 4, 7}, {2, 5, 8},
259 for (
const auto &line :
lines)
263 bool blocked =
false;
265 for (
const size_t cell : line)
266 if (state.board[cell] == player)
268 else if (state.board[cell] == 0)
281 else if (
mine == 1
and empty == 2)
295 for (
const auto &move : path)
300template <
typename Move,
typename Score>
308 if (event.
kind == kind)
316 TicTacToeState state;
317 state.player = player;
319 for (
size_t i = 0; i < 9; ++i)
324 ++state.moves_played;
329 ++state.moves_played;
349template <
typename Domain>
352 typename Domain::State state,
355 for (
const auto &move :
pv)
356 domain.apply(state, move);
358 const auto leaf_value = domain.evaluate(state);
366 auto result =
negamax_search(ArtificialGameDomain{}, ArtificialGameState{});
371 EXPECT_EQ(result.stats.visited_states, 7u);
372 EXPECT_EQ(result.stats.expanded_states, 3u);
391 auto result =
negamax_search(ArtificialGameDomain{}, ArtificialGameState{}, {}, limits);
396 EXPECT_EQ(result.stats.pruned_by_depth, 2u);
415 TicTacToeState state;
432 policy.
strategy = ExplorationPolicy::Strategy::Best_First;
443 TicTacToeState state;
469 TicTacToeState state;
475 TicTacToeDomain::Move,
476 TicTacToeDomain::Score>;
492 TicTacToeState state;
498 TicTacToeDomain::Move,
499 TicTacToeDomain::Score>;
518 auto fixed =
negamax_search(ArtificialGameDomain{}, ArtificialGameState{}, {}, limits);
520 ArtificialGameState{},
549 options.aspiration.half_window = 1;
552 auto fixed =
alpha_beta_search(ArtificialGameDomain{}, ArtificialGameState{}, {}, limits);
554 ArtificialGameState{},
575 options.aspiration.half_window = 1;
580 ArtificialGameState{},
596 TicTacToeState state;
609 options.aspiration.half_window = 4;
619 TicTacToeDomain::Move,
620 TicTacToeDomain::Score>;
644 auto windowed =
engine.search_with_window(ArtificialGameState{}, -1000, 1000);
657 full.value - 1, full.value + 1);
669 full.value + 5, full.value + 10);
680 full.value - 10, full.value - 5);
706 TicTacToeDomain::Move,
707 TicTacToeDomain::Score>;
709 auto windowed =
engine.search_with_window(state, table, -200, 200);
719 TicTacToeState state;
734 ArtificialGameDomain domain;
739 result.principal_variation);
745 ArtificialGameDomain domain;
750 result.principal_variation);
756 ArtificialGameDomain domain;
760 auto result =
negamax_search(domain, ArtificialGameState{}, {}, limits);
764 result.principal_variation);
770 TicTacToeDomain domain;
771 TicTacToeState state;
782 TicTacToeDomain domain;
783 TicTacToeState state;
794 TicTacToeDomain domain;
808 TicTacToeDomain domain;
809 TicTacToeState state;
812 TicTacToeDomain::Move,
813 TicTacToeDomain::Score>;
816 auto result =
engine.search(state, table);
825 ArtificialGameDomain domain;
830 ArtificialGameState{},
835 for (
size_t i = 0; i <
iterative.iterations.size(); ++i)
839 <<
"Iteration " << i <<
" has no PV";
841 iter.result.principal_variation);
843 <<
"PV replay mismatch at iteration " << i
844 <<
" (depth " <<
iter.depth <<
")";
855 ArtificialGameDomain domain;
862 options.aspiration.half_window = 1;
866 ArtificialGameState{},
876 for (
size_t i = 0; i <
iterative.iterations.size(); ++i)
880 <<
"Iteration " << i <<
" has no PV";
881 const auto r =
replay_pv(domain, ArtificialGameState{},
882 iter.result.principal_variation);
884 <<
"PV replay mismatch at iteration " << i;
890 TicTacToeDomain domain;
894 auto result =
engine.search_with_window(state, -200, 200);
908struct AdversarialThrowState
910 std::shared_ptr<bool> undo_called;
914struct AdversarialThrowMove
916 bool throw_on_apply =
false;
920struct ThrowingApplyGameDomain
922 using State = AdversarialThrowState;
923 using Move = AdversarialThrowMove;
928 return state.node >= 1;
936 void apply(State &state,
const Move &move)
const
938 if (move.throw_on_apply)
943 void undo(State &state,
const Move &)
const
945 *state.undo_called =
true;
949 template <
typename Visitor>
950 bool for_each_successor(
const State &state,
Visitor visit)
const
954 return visit(Move{
true});
960struct ThrowingPostApplyGameDomain
962 using State = AdversarialThrowState;
963 using Move = AdversarialThrowMove;
978 void apply(State &state,
const Move &)
const
983 void undo(State &state,
const Move &)
const
985 *state.undo_called =
true;
989 template <
typename Visitor>
990 bool for_each_successor(
const State &state,
Visitor visit)
const
994 return visit(Move{
false});
999struct ForcedDrawGameDomain
1001 using State = ArtificialGameState;
1002 using Move = ArtificialGameMove;
1007 return state.depth == 2;
1010 Score
evaluate(
const State &)
const
1015 void apply(State &state,
const Move &move)
const
1017 state.code = move.next_code;
1018 state.player = -state.player;
1022 void undo(State &state,
const Move &)
const
1025 state.player = -state.player;
1029 template <
typename Visitor>
1030 bool for_each_successor(
const State &state,
Visitor visit)
const
1032 if (state.depth >= 2)
1034 if (
not visit(ArtificialGameMove{2,
'L'}))
1036 return visit(ArtificialGameMove{3,
'R'});
1043struct EmptySuccessorGameDomain
1045 using State = ArtificialGameState;
1046 using Move = ArtificialGameMove;
1054 Score
evaluate(
const State &)
const
1059 void apply(State &,
const Move &)
const
1064 void undo(State &,
const Move &)
const
1069 template <
typename Visitor>
1070 bool for_each_successor(
const State &,
Visitor)
const
1078struct ExtremeScoreGameDomain
1080 using State = ArtificialGameState;
1081 using Move = ArtificialGameMove;
1086 return state.depth >= 1;
1089 Score
evaluate(
const State &state)
const
1092 return adversarial_search_detail::score_ceiling<Score>() * state.player;
1095 void apply(State &state,
const Move &move)
const
1097 state.code = move.next_code;
1098 state.player = -state.player;
1102 void undo(State &state,
const Move &)
const
1105 state.player = -state.player;
1109 template <
typename Visitor>
1110 bool for_each_successor(
const State &state,
Visitor visit)
const
1112 if (state.depth >= 1)
1114 return visit(ArtificialGameMove{2,
'A'});
1123class IncrementalEvalGameDomain
1126 using State = ArtificialGameState;
1127 using Move = ArtificialGameMove;
1130 mutable size_t evaluate_after_calls = 0;
1134 return state.depth == 2;
1137 Score
evaluate(
const State &state)
const
1139 return root_score(state.code) * state.player;
1144 ++evaluate_after_calls;
1150 void apply(State &state,
const Move &move)
const
1152 state.code = move.next_code;
1153 state.player = -state.player;
1157 void undo(State &state,
const Move &)
const
1160 state.player = -state.player;
1164 template <
typename Visitor>
1165 bool for_each_successor(
const State &state,
Visitor visit)
const
1167 if (state.depth >= 2)
1175 return visit(Move{3,
'B'});
1180 return visit(Move{5,
'b'});
1185 return visit(Move{7,
'b'});
1216 auto undo_called = std::make_shared<bool>(
false);
1217 ThrowingApplyGameDomain domain;
1223 std::runtime_error);
1229 auto undo_called = std::make_shared<bool>(
false);
1230 ThrowingApplyGameDomain domain;
1236 std::runtime_error);
1246 auto undo_called = std::make_shared<bool>(
false);
1247 ThrowingPostApplyGameDomain domain;
1253 std::runtime_error);
1259 auto undo_called = std::make_shared<bool>(
false);
1260 ThrowingPostApplyGameDomain domain;
1266 std::runtime_error);
1276 ForcedDrawGameDomain domain;
1295 EmptySuccessorGameDomain domain;
1308 EXPECT_GT(ab.stats.terminal_states, 0u);
1317 ExtremeScoreGameDomain domain;
1326 const int ceiling = adversarial_search_detail::score_ceiling<int>();
1339 IncrementalEvalGameDomain domain;
1346 auto result =
engine.search(ArtificialGameState{});
1357 EXPECT_GT(result.stats.move_ordering.priority_estimates, 0u);
Umbrella header for the implicit state-space search framework.
Exception handling system with formatted messages for Aleph-w.
#define ah_runtime_error()
Throws std::runtime_error unconditionally.
Collector that stores trace events in an Aleph list.
Adversarial search engine with Alpha-Beta pruning.
Result search_with_window(State initial_state, const Score alpha, const Score beta)
Execute Alpha-Beta with an explicit root window.
static ExplorationPolicy default_policy() noexcept
Return the default exploration policy for Alpha-Beta.
Result search(State initial_state)
Execute Alpha-Beta from initial_state.
Simple dynamic array with automatic resizing and functional operations.
Pure Negamax search over an implicit game tree.
static ExplorationPolicy default_policy() noexcept
Return the default exploration policy for adversarial search.
Result search(State initial_state)
Execute Negamax from initial_state.
Transposition-table container built on top of Aleph hash maps.
Main namespace for Aleph-w library functions.
and
Check uniqueness with explicit hash + equality functors.
bool operator==(const DynList< T > &l1, const DynList< T > &l2)
Equality operator for DynList.
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_alpha_beta_search(Domain domain, typename Domain::State initial_state, Tracer &tracer, ExplorationPolicy policy=Alpha_Beta< Domain >::default_policy(), SearchLimits limits={}, AdversarialIterativeDeepeningOptions< typename Domain::Score > options={})
Iterative deepening over Alpha-Beta with optional aspiration windows.
std::string code(Node *root)
Compute a string with the Lukasiewicz`s word of a tree.
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.
auto alpha_beta_search(Domain domain, typename Domain::State initial_state, ExplorationPolicy policy=Alpha_Beta< Domain >::default_policy(), SearchLimits limits={})
Convenience wrapper for one-shot Alpha-Beta search.
AdversarialTraceEventKind
Trace event kinds emitted by adversarial search engines.
@ 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.
Itor::difference_type count(const Itor &beg, const Itor &end, const T &value)
Count elements equal to a value.
static struct argp_option options[]
Controls for adversarial iterative deepening.
size_t initial_depth
First horizon to search.
One trace event produced by an adversarial search.
AdversarialTraceEventKind kind
Exploration controls shared across engines.
Strategy strategy
Traversal strategy.
MoveOrderingMode move_ordering
Successor-ordering mode.
Hard bounds applied by the search engine.
size_t max_depth
Maximum expansion depth.