diff --git a/friendhaver/makefile b/friendhaver/makefile new file mode 100644 index 0000000..71f4397 --- /dev/null +++ b/friendhaver/makefile @@ -0,0 +1,24 @@ +EXE = mexx + +default: pgo-build + +pgo-build: + # STEP 0: Make sure there is no left-over profiling data from previous runs + rm -rf /tmp/pgo-data + + # STEP 1: Build the instrumented binaries + RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" \ + cargo build --release + + # STEP 2: Run the instrumented binaries with some typical data + ./target/release/mexx bench + + # STEP 3: Merge the `.profraw` files into a `.profdata` file + llvm-profdata merge -o /tmp/pgo-data/merged.profdata /tmp/pgo-data + + # STEP 4: Use the `.profdata` file for guiding optimizations + RUSTFLAGS="-Cprofile-use=/tmp/pgo-data/merged.profdata" \ + cargo build --release + + # STEP 5: Move the built binary to the final location + mv ./target/release/mexx $(EXE) diff --git a/friendhaver/src/commands/bench.rs b/friendhaver/src/commands/bench.rs new file mode 100644 index 0000000..b59c6ff --- /dev/null +++ b/friendhaver/src/commands/bench.rs @@ -0,0 +1,93 @@ +use std::str::FromStr; +use std::time; + +use uxi::Command; + +use super::Context; + +pub fn bench() -> Command { + Command::new(|_| { + const BENCH_FENS: &[&str] = &[ + "x-1-1-o/-1-1-1-/1-1-1-1/-1-1-1-/1-1-1-1/-1-1-1-/o-1-1-x x 0 1", + // "x-1-1-o/1-1-1-1/1-1-1-1/1-1-1-1/1-1-1-1/1-1-1-1/o-1-1-x x 0 1", + "x1-1-1o/2-1-2/-------/2-1-2/-------/2-1-2/o1-1-1x x 0 1", + // "x5o/1-----1/1-3-1/1-1-1-1/1-3-1/1-----1/o5x x 0 1", + "x-1-1-o/1-1-1-1/-1-1-1-/-1-1-1-/-1-1-1-/1-1-1-1/o-1-1-x x 0 1", + "x5o/1--1--1/1--1--1/7/1--1--1/1--1--1/o5x x 0 1", + // "x-3-o/1-1-1-1/1-1-1-1/3-3/1-1-1-1/1-1-1-1/o-3-x x 0 1", + // "x2-2o/3-3/3-3/-------/3-3/3-3/o2-2x x 0 1", + // "x2-2o/2-1-2/1-3-1/-2-2-/1-3-1/2-1-2/o2-2x x 0 1", + "x5o/7/7/7/7/7/o5x x 0 1", + "x5o/7/2-1-2/7/2-1-2/7/o5x x 0 1", + "x5o/7/3-3/2-1-2/3-3/7/o5x x 0 1", + "x2-2o/3-3/2---2/7/2---2/3-3/o2-2x x 0 1", + "x2-2o/3-3/7/--3--/7/3-3/o2-2x x 0 1", + "x1-1-1o/2-1-2/2-1-2/7/2-1-2/2-1-2/o1-1-1x x 0 1", + // "x5o/7/2-1-2/3-3/2-1-2/7/o5x x 0 1", + // "x5o/7/3-3/2---2/3-3/7/o5x x 0 1", + "x5o/2-1-2/1-3-1/7/1-3-1/2-1-2/o5x x 0 1", + "x5o/1-3-1/2-1-2/7/2-1-2/1-3-1/o5x x 0 1", + "2x3o/7/7/7/o6/5x1/6x o 2 2", + "5oo/7/x6/x6/7/7/o5x o 0 2", + "x5o/1x5/7/7/7/2o4/4x2 o 0 2", + "7/7/2x1o2/1x5/7/7/o5x o 0 2", + "7/7/1x4o/7/4x2/7/o6 o 3 2", + "x5o/7/6x/7/1o5/7/7 o 3 2", + "5oo/7/2x4/7/7/4x2/o6 o 1 2", + "x5o/7/7/3x3/7/1o5/o6 o 1 2", + "x5o/7/7/7/7/2x1x2/3x3 o 0 2", + "7/7/1x4o/7/7/4x2/o6 o 3 2", + "x5o/7/7/5x1/5x1/1o5/o6 o 0 2", + "6o/7/4x2/7/7/1o5/o5x o 1 2", + "x5o/x5o/7/7/7/6x/o5x o 0 2", + "4x1o/7/7/7/7/o6/o5x o 1 2", + "6o/7/x6/7/7/2o4/6x o 3 2", + "x5o/7/7/7/1o4x/7/5x1 o 2 2", + "x5o/6o/7/7/4x2/7/o6 o 1 2", + "7/7/1xx1o2/7/7/7/o5x o 0 2", + "2x3o/2x4/7/7/7/7/2o3x o 0 2", + "x5o/6o/7/7/4x2/3x3/o6 o 0 2", + "x5o/7/7/7/o3xx1/7/7 o 0 2", + "6o/6o/1x5/7/4x2/7/o6 o 1 2", + "7/7/4x1o/7/7/7/o5x o 3 2", + "4o2/7/2x4/7/7/7/o4xx o 0 2", + "2x3o/x6/7/7/7/o6/o5x o 1 2", + "6o/7/2x4/7/1o5/7/4x2 o 3 2", + "x6/4o2/7/7/6x/7/o6 o 3 2", + "x6/7/5o1/7/7/4x2/o6 o 3 2", + "x5o/1x4o/7/7/7/7/o3x2 o 0 2", + "xx4o/7/7/7/7/6x/oo4x o 0 2", + "x6/7/4x2/3x3/7/7/o5x o 2 2", + ]; + + let mut total_nodes = 0; + + let start = time::Instant::now(); + for (i, fen) in BENCH_FENS.iter().enumerate() { + println!("[#{}] {}", i + 1, fen); + let position = ataxx::Position::from_str(fen).unwrap(); + let mut searcher = + mcts::Searcher::new(position, mcts::policy::handcrafted, mcts::value::material); + let limits = mcts::Limits { + maxnodes: Some(50000), + maxdepth: Some(10), + movetime: None, + movestogo: None, + }; + + searcher.search(limits, &mut total_nodes); + } + let elapsed = start.elapsed().as_millis(); + + // Assert that the node-count hasn't changed unexpectedly. + debug_assert!(total_nodes == 2152064); + + println!( + "nodes {} nps {}", + total_nodes, + total_nodes as u128 * 1000 / elapsed + ); + + Ok(()) + }) +} diff --git a/friendhaver/src/search/mod.rs b/friendhaver/src/search/mod.rs index 1fe7281..489abc8 100644 --- a/friendhaver/src/search/mod.rs +++ b/friendhaver/src/search/mod.rs @@ -1,8 +1,12 @@ +use std::i16; + use tetka::games::{ - interface::{MoveType, PositionType}, - isolation, + interface::{BitBoard, MoveType, PositionType}, + isolation::{self, Move, Piece, Position}, }; +use derive_more::{Add, Neg, Sub}; + #[derive(Clone)] pub struct Searcher { position: isolation::Position, @@ -32,3 +36,65 @@ pub struct Limits { #[allow(unused)] pub movestogo: Option, } + +#[derive(Clone, Copy, Add, Sub, Neg, PartialEq, PartialOrd)] +pub struct Score(i16); + +impl Score { + const INF: Score = Score(i16::MAX - 1); + const NIL: Score = Score(i16::MAX); + const MAX_TILL_MATE: Score = Score(i16::MAX - 48); +} + +impl Searcher { + fn negamax( + &mut self, + position: isolation::Position, + mut alpha: Score, + beta: Score, + depth: u8, + ) -> Score { + if depth == 0 { + return Searcher::evaluate(&position); + } + + let moves = position.generate_moves::(); + if moves.is_empty() { + return -Score::INF; + } + + let mut best_move = Move::NULL; + let mut best_score = -Score::INF; + + for mov in moves.into_iter() { + let new_position = position.after_move::(mov); + let score = -self.negamax(new_position, -beta, -alpha, depth - 1); + + if score > best_score { + best_score = score; + + if score > alpha { + best_move = mov; + alpha = score; + + if score > beta { + return beta; + } + } + } + } + + best_score + } + + fn evaluate(position: &Position) -> Score { + let stm = position.side_to_move; + + Score( + ((position.piece_bb(Piece::Tile) + & isolation::BitBoard::singles(position.color_bb(stm))) + .count() + * 100) as i16, + ) + } +}