alphabeta_search: replace recursion with explicit stack

This commit is contained in:
2025-12-21 00:12:52 +01:00
parent 97b6934024
commit ef33b23f70

347
engine.h
View File

@@ -1859,6 +1859,67 @@ double quiesce(struct pos const* pos,
return highscore; return highscore;
} }
struct ab_frame {
enum search_stage {
ST_INIT = 0,
ST_LOOP = 1,
ST_WAIT_CHILD = 2,
} stage;
struct pos pos;
enum piece mailbox[SQ_INDEX_COUNT];
enum player us;
int8_t depth;
uint64_t mattr_filter;
double alpha;
double beta;
double alpha_orig;
struct search_option tte;
struct move moves[MOVE_MAX];
size_t move_count;
double best_score;
struct move best_move;
size_t old_hist_length;
struct move pending_move;
struct search_option result;
};
static inline void ab_update_parent_after_score(struct ab_frame *parent,
struct tt *tt,
struct move m,
double score)
{
if (score > parent->best_score) {
parent->best_score = score;
parent->best_move = m;
}
if (score > parent->alpha) {
parent->alpha = score;
}
if (parent->alpha >= parent->beta) {
struct search_option out = {
.score = parent->alpha,
.move = parent->best_move,
.depth = parent->depth,
.hash = parent->pos.hash,
.init = true,
.flag = TT_LOWER,
};
tt_insert(tt, parent->pos.hash, out);
parent->result = out;
/* mark parent as complete so it will be popped in st_wait_child. */
parent->stage = ST_WAIT_CHILD;
}
}
static static
struct search_option alphabeta_search(struct pos const* pos, struct search_option alphabeta_search(struct pos const* pos,
@@ -1871,139 +1932,231 @@ struct search_option alphabeta_search(struct pos const* pos,
double alpha, double alpha,
double beta) double beta)
{ {
if (depth <= 0) { struct {
return (struct search_option) { struct ab_frame stack[50];
.score = quiesce(pos, mailbox, us, alpha, beta, 0), size_t len;
/*.score = board_score_heuristic(pos),*/ } ab_stack = {0};
#define STACK_PUSH(s, x) \
do { \
assert(s.len < sizeof s.stack / sizeof s.stack[0]); \
(s.stack)[(s.len)++] = (x); \
} while (0)
#define STACK_TOP(s) &(s.stack)[(s.len) - 1]
#define STACK_POP(s) (assert(s.len > 0), s.stack[--(s.len)]);
#define STACK_EMPTY(s) (s.len == 0)
struct ab_frame root = {
.stage = ST_INIT,
.pos = *pos,
.us = us,
.depth = depth,
.mattr_filter = mattr_filter,
.alpha = alpha,
.beta = beta
};
my_memcpy(root.mailbox, mailbox, sizeof root.mailbox);
STACK_PUSH(ab_stack, root);
while (1) {
struct ab_frame *fr = STACK_TOP(ab_stack);
switch (fr->stage) {
case ST_INIT: {
if (fr->depth <= 0) {
fr->result = (struct search_option) {
.score = quiesce(&fr->pos, fr->mailbox, fr->us,
fr->alpha, fr->beta, 0),
.move = (struct move){0}, .move = (struct move){0},
.depth = 0, .depth = 0,
.hash = pos->hash, .hash = fr->pos.hash,
.init = true, .init = true,
.flag = TT_EXACT, .flag = TT_EXACT,
}; };
fr->stage = ST_WAIT_CHILD;
break;
} }
double const alpha_orig = alpha; fr->alpha_orig = fr->alpha;
struct search_option tte = tt_get(tt, pos->hash); fr->tte = tt_get(tt, fr->pos.hash);
if (fr->tte.init && fr->tte.hash == fr->pos.hash && fr->tte.depth >= fr->depth) {
if (fr->tte.flag == TT_EXACT) {
fr->result = fr->tte;
fr->stage = ST_WAIT_CHILD;
break;
} else if (fr->tte.flag == TT_LOWER) {
if (fr->tte.score > fr->alpha) {
fr->alpha = fr->tte.score;
}
} else if (fr->tte.flag == TT_UPPER) {
if (fr->tte.score < fr->beta) {
fr->beta = fr->tte.score;
}
}
if (tte.init && tte.hash == pos->hash && tte.depth >= depth) { if (fr->alpha >= fr->beta) {
if (tte.flag == TT_EXACT) { fr->result = fr->tte;
return tte; fr->stage = ST_WAIT_CHILD;
} else if (tte.flag == TT_LOWER) { break;
if (tte.score > alpha) alpha = tte.score;
} else if (tte.flag == TT_UPPER) {
if (tte.score < beta) beta = tte.score;
}
if (alpha >= beta) {
return tte;
} }
} }
struct move moves[MOVE_MAX];
size_t move_count = 0ULL;
all_moves(pos, us, &move_count, moves);
if (move_count == 0) { fr->move_count = 0;
/* TODO: reusing mate distances correctly needs ply normalization */ all_moves(&fr->pos, fr->us, &fr->move_count, fr->moves);
double score = 0;
if (attacks_to(pos, pos->pieces[us][PIECE_KING], 0ULL, 0ULL) != 0ULL) { /* checkmate or stalemate */
score = -(999.0 + (double)depth); if (fr->move_count == 0) {
double score = 0.0;
if (attacks_to(&fr->pos,
fr->pos.pieces[fr->us][PIECE_KING],
0ULL, 0ULL) != 0ULL) {
score = -(999.0 + (double)fr->depth);
} }
return (struct search_option) {
fr->result = (struct search_option) {
.score = score, .score = score,
.move = (struct move){0}, .move = (struct move){0},
.depth = depth, .depth = fr->depth,
.hash = pos->hash, .hash = fr->pos.hash,
.init = true, .init = true,
.flag = TT_EXACT, .flag = TT_EXACT,
}; };
fr->stage = ST_WAIT_CHILD;
break;
} }
for (size_t i = 0; i < move_count; ++i) { for (size_t i = 0; i < fr->move_count; ++i) {
move_compute_appeal(&moves[i], pos, us, mailbox); move_compute_appeal(&fr->moves[i], &fr->pos, fr->us, fr->mailbox);
} }
/* if TT had a best move for this position, search it first. */ /* put existing TT entry first */
if (tte.init && tte.hash == pos->hash) { if (fr->tte.init && fr->tte.hash == fr->pos.hash) {
for (size_t i = 0; i < move_count; ++i) { for (size_t i = 0; i < fr->move_count; ++i) {
if (moves[i].from == tte.move.from && moves[i].to == tte.move.to) { if (fr->moves[i].from == fr->tte.move.from &&
moves[i].appeal = APPEAL_MAX; fr->moves[i].to == fr->tte.move.to) {
fr->moves[i].appeal = APPEAL_MAX;
break; break;
} }
} }
} }
double best_score = -1e300; fr->best_score = -1e300;
struct move best_move = moves[0]; fr->best_move = fr->moves[0];
fr->stage = ST_LOOP;
} break;
while (move_count) { case ST_LOOP: {
struct move m = moves_linear_search(moves, &move_count); if (fr->result.init) {
fr->stage = ST_WAIT_CHILD;
if (mattr_filter && !(m.attr & mattr_filter)) { break;
continue;
}
/* TODO: make lean apply/undo mechanism instead of copying */
struct pos poscpy = *pos;
enum piece mailbox_cpy[SQ_INDEX_COUNT];
my_memcpy(mailbox_cpy, mailbox, sizeof mailbox_cpy);
size_t old_hist_length = hist->length;
enum move_result const r = board_move(&poscpy, hist, mailbox_cpy, m);
double score;
if (r == MR_STALEMATE || r == MR_REPEATS) {
score = 0.0;
} else {
score = -alphabeta_search(&poscpy,
hist,
tt,
mailbox_cpy,
opposite_player(us),
depth - 1,
mattr_filter,
-beta,
-alpha).score;
}
hist->length = old_hist_length;
if (score > best_score) {
best_score = score;
best_move = m;
}
if (score > alpha) {
alpha = score;
}
if (alpha >= beta) {
struct search_option out = {
.score = alpha,
.move = best_move,
.depth = depth,
.hash = pos->hash,
.init = true,
.flag = TT_LOWER,
};
tt_insert(tt, pos->hash, out);
return out;
}
} }
if (fr->move_count == 0) {
enum tt_flag flag = TT_EXACT; enum tt_flag flag = TT_EXACT;
if (best_score <= alpha_orig) flag = TT_UPPER; if (fr->best_score <= fr->alpha_orig) flag = TT_UPPER;
struct search_option out = { fr->result = (struct search_option) {
.score = best_score, .score = fr->best_score,
.move = best_move, .move = fr->best_move,
.depth = depth, .depth = fr->depth,
.hash = pos->hash, .hash = fr->pos.hash,
.init = true, .init = true,
.flag = flag, .flag = flag,
}; };
tt_insert(tt, pos->hash, out); tt_insert(tt, fr->pos.hash, fr->result);
fr->stage = ST_WAIT_CHILD;
break;
}
struct move m = moves_linear_search(fr->moves, &fr->move_count);
if (fr->mattr_filter && !(m.attr & fr->mattr_filter)) {
break;
}
fr->old_hist_length = hist->length;
struct pos child_pos = fr->pos;
enum piece child_mailbox[SQ_INDEX_COUNT];
my_memcpy(child_mailbox, fr->mailbox, sizeof child_mailbox);
enum move_result const r = board_move(&child_pos, hist, child_mailbox, m);
if (r == MR_STALEMATE || r == MR_REPEATS) {
hist->length = fr->old_hist_length;
ab_update_parent_after_score(fr, tt, m, 0.0);
break;
}
if (fr->depth - 1 <= 0) {
double score = -quiesce(&child_pos,
child_mailbox,
opposite_player(fr->us),
-fr->beta,
-fr->alpha,
0);
hist->length = fr->old_hist_length;
ab_update_parent_after_score(fr, tt, m, score);
break;
}
fr->pending_move = m;
fr->stage = ST_WAIT_CHILD;
struct ab_frame child = {0};
child.stage = ST_INIT;
child.pos = child_pos;
my_memcpy(child.mailbox, child_mailbox, sizeof child.mailbox);
child.us = opposite_player(fr->us);
child.depth = fr->depth - 1;
child.mattr_filter = fr->mattr_filter;
child.alpha = -fr->beta;
child.beta = -fr->alpha;
STACK_PUSH(ab_stack, child);
} break;
case ST_WAIT_CHILD: {
struct search_option out = fr->result;
STACK_POP(ab_stack);
if (STACK_EMPTY(ab_stack)) {
return out; return out;
}
struct ab_frame *parent = STACK_TOP(ab_stack);
if (parent->stage == ST_WAIT_CHILD) {
/* parent is waiting on a child (this frame), propagate */
double score = -out.score;
struct move m = parent->pending_move;
hist->length = parent->old_hist_length;
/* resume parent move loop. */
parent->stage = ST_LOOP;
ab_update_parent_after_score(parent, tt, m, score);
} else {
/* parent wasn't waiting: treat as a completed frame result being
propagated through a completion chain. */
parent->result = out;
parent->stage = ST_WAIT_CHILD;
}
} break;
default: {
/* unreachable */
assert(0);
__builtin_unreachable();
} break;
}
}
} }
static struct search_result {struct move move; double score;} static struct search_result {struct move move; double score;}