Skip to content
This repository has been archived by the owner on Oct 8, 2024. It is now read-only.

Commit

Permalink
feat: move ordering now considers promotions and enemy attacks
Browse files Browse the repository at this point in the history
feat: time constraints now incorporate winc/binc
  • Loading branch information
dannyhammer committed Aug 16, 2024
1 parent ba4f651 commit fdabadc
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 110 deletions.
18 changes: 10 additions & 8 deletions brogle/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,6 @@ impl Write for Engine {
}

/// Represents a custom command that can be sent to this engine.
#[derive(Clone, Debug)]
pub enum EngineCommand {
/// For displaying the list of available commands.
Help,
Expand Down Expand Up @@ -594,18 +593,21 @@ impl UciEngine for Engine {
let timeout = if let Some(movetime) = options.move_time {
movetime
} else {
// Otherwise, search based on time remaining
let time_remaining = if self.game.current_player().is_white() {
options.w_time.unwrap_or(Duration::MAX)
} else if self.game.current_player().is_black() {
options.b_time.unwrap_or(Duration::MAX)
// Otherwise, search based on time remaining and increment
let (time, inc) = if self.game.current_player().is_white() {
(options.w_time, options.w_inc)
} else {
Duration::MAX
(options.b_time, options.b_inc)
};

time_remaining / 20 // 5% of time remaining
let (time, inc) = (time.unwrap_or(Duration::MAX), inc.unwrap_or(Duration::ZERO));

// 5% of time remaining + time increment
time / 20 + inc
};

// eprintln!("Starting search for {timeout:?}");

// Clone the arcs for whether we're searching and our found results
let is_searching = Arc::clone(&self.is_searching);
let sender = self.sender.clone().unwrap();
Expand Down
81 changes: 12 additions & 69 deletions brogle/src/search/searcher.rs
Original file line number Diff line number Diff line change
@@ -1,64 +1,13 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use std::{ops::Neg, time::Instant};
use std::time::Instant;

use anyhow::{bail, Result};
use brogle_core::{Game, Move, PieceKind};

use crate::{value_of, Evaluator, Score, INF, MATE};

/// The result of a search, containing the best move, score, and other metadata.
#[derive(Debug, Eq, Clone)]
pub struct SearchResult {
/// The best move found during this search. If `None`, then no valid move was found (i.e. when mated).
pub bestmove: Option<Move>,
/// The score of the best move. A higher score is better for the score player.
pub score: Score,
}

impl SearchResult {
pub fn new(bestmove: Move) -> Self {
Self {
bestmove: Some(bestmove),
..Default::default()
}
}
}

impl Default for SearchResult {
/// A default search result has no best move and a Very Bad score.
fn default() -> Self {
Self {
bestmove: None,
score: -INF, // Initially, our score is Very Bad
}
}
}

impl PartialEq for SearchResult {
/// Search results are compared by their `score` fields.
fn eq(&self, other: &Self) -> bool {
self.score.eq(&other.score)
}
}

impl PartialOrd for SearchResult {
/// Search results are compared by their `score` fields.
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.score.partial_cmp(&other.score)
}
}

impl Neg for SearchResult {
type Output = Self;
/// Negating a search result just negates its score.
fn neg(mut self) -> Self::Output {
self.score = self.score.neg();
self
}
}

pub struct SearchData {
pub nodes_searched: usize,
pub score: Score,
Expand Down Expand Up @@ -135,7 +84,6 @@ impl<'a> Searcher<'a> {
let new_pos = self.game.clone().with_move_made(mv);

if new_pos.is_repetition() || new_pos.can_draw_by_fifty() {
// eprintln!("Repetition in Search after {mv} on {}", new_pos.fen());
continue;
}

Expand Down Expand Up @@ -259,7 +207,6 @@ impl<'a> Searcher<'a> {
}

Ok(best)
// Ok(alpha)
}

fn quiescence(
Expand Down Expand Up @@ -291,16 +238,10 @@ impl<'a> Searcher<'a> {

// Only search captures
for i in 0..captures.len() {
// for mv in moves.iter().filter(|mv| mv.is_capture()) {
let mv = captures[i];

// Make the score move on the position, getting a new position in return
let new_pos = game.clone().with_move_made(mv);
if new_pos.is_repetition() {
// eprintln!("Repetition in QSearch after {mv} on {}", new_pos.fen());
continue;
}
// self.result.nodes_searched += 1;

// Recursively search our opponent's responses
let score = -self.quiescence(&new_pos, ply + 1, -beta, -alpha)?;
Expand All @@ -314,6 +255,7 @@ impl<'a> Searcher<'a> {
self.starttime.elapsed(),
);
}

if score > best {
best = score;

Expand All @@ -340,7 +282,7 @@ impl<'a> Searcher<'a> {
}
}

// TODO: Refactor this into its own function and verify that its values are good: https://discord.com/channels/719576389245993010/719576389690589244/1268914745298391071
// TODO: verify the values are good: https://discord.com/channels/719576389245993010/719576389690589244/1268914745298391071
fn mvv_lva(kind: PieceKind, captured: PieceKind) -> i32 {
10 * value_of(captured) - value_of(kind)
}
Expand All @@ -361,15 +303,16 @@ fn score_move(game: &Game, mv: &Move) -> i32 {
}

// Promoting is also a good idea
// if let Some(promotion) = mv.promotion() {
// score += value_of(promotion);
// }
if let Some(promotion) = mv.promotion() {
score += value_of(promotion);
}

// Going somewhere attacked by an opponent is not a good idea
// let attacks = game.attacks_by_color(game.current_player().opponent());
// if attacks.get(mv.to()) {
// score -= value_of(kind);
// }
// Going somewhere attacked by an opponent is not a good idea, but may be necessary,
// so negate it, but not by much
let attacks = game.attacks_by_color(game.current_player().opponent());
if attacks.get(mv.to()) {
score -= value_of(kind) / 10;
}

-score // We're sorting, so a lower number is better
}
26 changes: 26 additions & 0 deletions brogle_core/brogle_types/src/tile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,32 @@ impl Tile {
}
}

/// Iterating over [`Tile`]s increases their internal counter by 1.
///
/// # Example
/// ```
/// # use brogle_types::Tile;
/// assert_eq!(Tile::A1.next(), Some(Tile::B1));
/// assert_eq!(Tile::H3.next(), Some(Tile::A4));
/// assert_eq!(Tile::H8.next(), None);
/// ```
pub fn next(self) -> Option<Self> {
(self.0 < Self::MAX).then(|| Self::from_bits_unchecked(self.0 + 1))
}

/// Iterating backwards over [`Tile`]s decreases their internal counter by 1.
///
/// # Example
/// ```
/// # use brogle_types::Tile;
/// assert_eq!(Tile::A1.prev(), None);
/// assert_eq!(Tile::A4.prev(), Some(Tile::H3));
/// assert_eq!(Tile::H8.prev(), Some(Tile::G8));
/// ```
pub fn prev(self) -> Option<Self> {
(self.0 > Self::MIN).then(|| Self::from_bits_unchecked(self.0 - 1))
}

/// Fetches the inner index value of the [`Tile`], which represented as a [`u8`].
///
/// # Example
Expand Down
19 changes: 10 additions & 9 deletions brogle_core/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use brogle_core::*;

/// All contents of this file should be ignored. I just use this `main` to test small things in `brogle_core`.
fn main() {
/*
let mut game = Game::default();
println!("pos: {}\nkey: {}", game.position(), game.key());
game.make_move(Move::from_uci(&game, "b1a3").unwrap());
Expand All @@ -13,20 +12,22 @@ fn main() {
println!("pos: {}\nkey: {}", game.position(), game.key());
game.make_move(Move::from_uci(&game, "a6b8").unwrap());
println!("pos: {}\nkey: {}", game.position(), game.key());
*/

// let fen = FEN_STARTPOS;
// let moves = ["b1a3", "b8a6", "a3b1", "a6b8"];
// let fen = "k7/8/8/8/3p4/8/4P3/K7 w - - 0 1"; // Testing hash keys with en passant
// let moves = ["e2e4", "d4e3"];
let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
let moves = ["e1g1"];
let mut game = Game::from_fen(fen).unwrap();
// let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
// let moves = ["e1g1"];

for mv in moves {
game.make_move(Move::from_uci(&game, mv).unwrap());
// println!("repetition? {}", game.is_repetition());
}
// let fen = FEN_KIWIPETE;
// let pos = Position::from_fen(fen).unwrap();

// let movegen = MoveGen::new(pos);

// for mv in movegen {
// println!("{mv}");
// }

// let mut game = Game::default();
// game.make_move(Move::from_uci(&game, "b1a3").unwrap());
Expand Down
94 changes: 70 additions & 24 deletions brogle_core/src/movegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,72 @@ use super::{

include!("blobs/magics.rs"); // TODO: Make these into blobs

/*
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct MoveGen {
pub(crate) position: Position,
moves: ArrayVec<Move, MAX_NUM_MOVES>,
to_mask: Bitboard,
from_mask: Bitboard,
current: Tile,
checkers: Bitboard,
checkmask: Bitboard,
pinmask_ortho: Bitboard,
pinmask_diag: Bitboard,
}
impl MoveGen {
pub fn new(position: Position) -> Self {
let color = position.current_player();
let king_tile = position.king(color).to_tile_unchecked();
let mut discoverable_checks = Bitboard::EMPTY_BOARD;
let checkers = self.compute_attacks_to(&position, king_tile, color.opponent());
let pinmasks = self.compute_pinmasks_for(&position, king_tile, color);
// These are the rays containing the King and his Checkers
// They are used to prevent the King from retreating along a line he is checked on!
// Note: A pawn can't generate a discoverable check, as it can only capture 1 square away.
for checker in checkers & !position.kind(PieceKind::Pawn) {
discoverable_checks |= ray_containing(king_tile, checker) ^ checker.bitboard();
}
Self {
position,
moves: ArrayVec::default(),
to_mask: Bitboard::FULL_BOARD,
from_mask: Bitboard::FULL_BOARD,
current: Tile::A1,
checkers: Bitboard::EMPTY_BOARD,
checkmask: Bitboard::FULL_BOARD,
pinmask_ortho: Bitboard::EMPTY_BOARD,
pinmask_diag: Bitboard::EMPTY_BOARD,
}
}
pub fn generate_moves_from(&mut self, tile: Tile) -> Option<Move> {
let _piece = self.position.piece_at(tile)?;
None
}
}
impl Iterator for MoveGen {
type Item = Move;
fn next(&mut self) -> Option<Self::Item> {
self.current = self.current.next()?;
self.generate_moves_from(self.current)
}
}
impl Deref for MoveGen {
type Target = Position;
fn deref(&self) -> &Self::Target {
&self.position
}
}
*/

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MoveGenerator {
pub(crate) position: Position,
Expand Down Expand Up @@ -101,26 +167,6 @@ impl MoveGenerator {
self.checkers().population() > 1
}

// pub fn is_in_checkmate(&self) -> bool {
// self.is_in_check() && self.num_legal_moves == 0
// }

// pub fn order_moves<K: Ord>(&mut self, f: impl FnMut(&Move) -> K) {
// self.legal_moves[..self.num_legal_moves].sort_by_cached_key(f);
// }

// pub fn num_legal_moves(&self) -> usize {
// self.num_legal_moves
// }

// pub fn legal_moves(&self) -> &[Move] {
// &self.legal_moves[..self.num_legal_moves]
// }

// pub fn legal_moves_mut(&mut self) -> &mut [Move] {
// &mut self.legal_moves[..self.num_legal_moves]
// }

pub fn legal_captures(&self) -> ArrayVec<Move, MAX_NUM_MOVES> {
self.legal_moves()
.into_iter()
Expand Down Expand Up @@ -169,14 +215,13 @@ impl MoveGenerator {
// eprintln!("CHECKMASK:\n{checkmask:?}");
// eprintln!("ENEMY_OR_EMPTY:\n{enemy_or_empty:?}");

// For sliding pieces, we need a blocker mask to compute pseudo-legal moves
// Pawns are... weird
self.compute_pawn_moves(color, checkmask, &mut moves);

// For sliding pieces, we need a blocker mask to compute pseudo-legal moves
self.compute_normal_piece_moves(color, king_tile, checkmask, &mut moves);
self.compute_king_moves(color, self.occupied(), &mut moves); // TODO: legal_mask for King?

// Pawns are... weird
self.compute_pawn_moves(color, checkmask, &mut moves);

moves
}

Expand Down Expand Up @@ -388,6 +433,7 @@ impl MoveGenerator {
) {
// Loop over every tile containing this piece
let normal_pieces = self.color(color) ^ self.king(color) ^ self.pawns(color);

// for from in self.piece_parts(color, PieceKind::Knight) {
for from in normal_pieces {
let pseudo_legal = self.attacks_by_tile(from);
Expand Down

0 comments on commit fdabadc

Please sign in to comment.