diff --git a/lib/chess/board.rs b/lib/chess/board.rs index a84a7f76..c987ee75 100644 --- a/lib/chess/board.rs +++ b/lib/chess/board.rs @@ -2,8 +2,8 @@ use crate::chess::*; use crate::util::{Assume, Integer}; use derive_more::{Debug, Display, Error}; use std::fmt::{self, Formatter, Write}; +use std::io::Write as _; use std::str::{self, FromStr}; -use std::{io::Write as _, ops::Index}; /// The chess board. #[derive(Debug, Clone, Eq, PartialEq, Hash)] @@ -133,36 +133,12 @@ impl Board { /// Toggles a piece on a square. #[inline(always)] pub fn toggle(&mut self, p: Piece, sq: Square) { - debug_assert!(self[sq].is_none_or(|q| p == q)); + debug_assert!(self.piece_on(sq).is_none_or(|q| p == q)); self.colors[p.color() as usize] ^= sq.bitboard(); self.roles[p.role() as usize] ^= sq.bitboard(); } } -/// Retrieves the [`Piece`] at a given [`Square`], if any. -impl Index for Board { - type Output = Option; - - #[inline(always)] - fn index(&self, sq: Square) -> &Self::Output { - match self.piece_on(sq) { - Some(Piece::WhitePawn) => &Some(Piece::WhitePawn), - Some(Piece::WhiteKnight) => &Some(Piece::WhiteKnight), - Some(Piece::WhiteBishop) => &Some(Piece::WhiteBishop), - Some(Piece::WhiteRook) => &Some(Piece::WhiteRook), - Some(Piece::WhiteQueen) => &Some(Piece::WhiteQueen), - Some(Piece::WhiteKing) => &Some(Piece::WhiteKing), - Some(Piece::BlackPawn) => &Some(Piece::BlackPawn), - Some(Piece::BlackKnight) => &Some(Piece::BlackKnight), - Some(Piece::BlackBishop) => &Some(Piece::BlackBishop), - Some(Piece::BlackRook) => &Some(Piece::BlackRook), - Some(Piece::BlackQueen) => &Some(Piece::BlackQueen), - Some(Piece::BlackKing) => &Some(Piece::BlackKing), - None => &None, - } - } -} - impl Display for Board { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let mut skip = 0; @@ -173,7 +149,7 @@ impl Display for Board { buffer[0] = if sq.rank() == Rank::First { b' ' } else { b'/' }; } - match self[sq] { + match self.piece_on(sq) { None => skip += 1, Some(p) => { buffer[1] = buffer[0]; @@ -321,35 +297,35 @@ mod tests { #[proptest] fn iter_returns_pieces_and_squares(b: Board) { for (p, sq) in b.iter() { - assert_eq!(b[sq], Some(p)); + assert_eq!(b.piece_on(sq), Some(p)); } } #[proptest] fn by_color_returns_squares_occupied_by_pieces_of_a_color(b: Board, c: Color) { for sq in b.by_color(c) { - assert_eq!(b[sq].map(|p| p.color()), Some(c)); + assert_eq!(b.piece_on(sq).map(|p| p.color()), Some(c)); } } #[proptest] fn by_color_returns_squares_occupied_by_pieces_of_a_role(b: Board, r: Role) { for sq in b.by_role(r) { - assert_eq!(b[sq].map(|p| p.role()), Some(r)); + assert_eq!(b.piece_on(sq).map(|p| p.role()), Some(r)); } } #[proptest] fn by_piece_returns_squares_occupied_by_a_piece(b: Board, p: Piece) { for sq in b.by_piece(p) { - assert_eq!(b[sq], Some(p)); + assert_eq!(b.piece_on(sq), Some(p)); } } #[proptest] fn king_returns_square_occupied_by_a_king(b: Board, c: Color) { if let Some(sq) = b.king(c) { - assert_eq!(b[sq], Some(Piece::new(Role::King, c))); + assert_eq!(b.piece_on(sq), Some(Piece::new(Role::King, c))); } } @@ -362,37 +338,35 @@ mod tests { } #[proptest] - fn toggle_removes_piece_from_square(mut b: Board, #[filter(#b[#sq].is_some())] sq: Square) { - let p = b[sq].unwrap(); + fn toggle_removes_piece_from_square( + mut b: Board, + #[filter(#b.piece_on(#sq).is_some())] sq: Square, + ) { + let p = b.piece_on(sq).unwrap(); b.toggle(p, sq); - assert_eq!(b[sq], None); + assert_eq!(b.piece_on(sq), None); } #[proptest] fn toggle_places_piece_on_square( mut b: Board, - #[filter(#b[#sq].is_none())] sq: Square, + #[filter(#b.piece_on(#sq).is_none())] sq: Square, p: Piece, ) { b.toggle(p, sq); - assert_eq!(b[sq], Some(p)); + assert_eq!(b.piece_on(sq), Some(p)); } #[proptest] #[should_panic] fn toggle_panics_if_square_occupied_by_other_piece( mut b: Board, - #[filter(#b[#sq].is_some())] sq: Square, - #[filter(Some(#p) != #b[#sq])] p: Piece, + #[filter(#b.piece_on(#sq).is_some())] sq: Square, + #[filter(Some(#p) != #b.piece_on(#sq))] p: Piece, ) { b.toggle(p, sq); } - #[proptest] - fn board_can_be_indexed_by_square(b: Board, sq: Square) { - assert_eq!(b[sq], b.piece_on(sq)); - } - #[proptest] fn parsing_printed_board_is_an_identity(b: Board) { assert_eq!(b.to_string().parse(), Ok(b)); diff --git a/lib/chess/position.rs b/lib/chess/position.rs index 3dffdd02..1a2d1f58 100644 --- a/lib/chess/position.rs +++ b/lib/chess/position.rs @@ -4,8 +4,7 @@ use arrayvec::{ArrayVec, CapacityError}; use derive_more::{Debug, Display, Error, From}; use std::fmt::{self, Formatter}; use std::hash::{Hash, Hasher}; -use std::num::NonZeroU32; -use std::str::FromStr; +use std::{num::NonZeroU32, ops::Index, str::FromStr}; #[cfg(test)] use proptest::{prelude::*, sample::*}; @@ -652,6 +651,30 @@ impl Position { } } +/// Retrieves the [`Piece`] at a given [`Square`], if any. +impl Index for Position { + type Output = Option; + + #[inline(always)] + fn index(&self, sq: Square) -> &Self::Output { + match self.board.piece_on(sq) { + Some(Piece::WhitePawn) => &Some(Piece::WhitePawn), + Some(Piece::WhiteKnight) => &Some(Piece::WhiteKnight), + Some(Piece::WhiteBishop) => &Some(Piece::WhiteBishop), + Some(Piece::WhiteRook) => &Some(Piece::WhiteRook), + Some(Piece::WhiteQueen) => &Some(Piece::WhiteQueen), + Some(Piece::WhiteKing) => &Some(Piece::WhiteKing), + Some(Piece::BlackPawn) => &Some(Piece::BlackPawn), + Some(Piece::BlackKnight) => &Some(Piece::BlackKnight), + Some(Piece::BlackBishop) => &Some(Piece::BlackBishop), + Some(Piece::BlackRook) => &Some(Piece::BlackRook), + Some(Piece::BlackQueen) => &Some(Piece::BlackQueen), + Some(Piece::BlackKing) => &Some(Piece::BlackKing), + None => &None, + } + } +} + impl Display for Position { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Display::fmt(&self.board, f) @@ -736,7 +759,7 @@ mod tests { #[proptest] fn occupied_returns_non_empty_squares(pos: Position) { for sq in pos.occupied() { - assert_ne!(pos.board[sq], None); + assert_ne!(pos[sq], None); } } @@ -747,7 +770,7 @@ mod tests { #[proptest] fn king_returns_square_occupied_by_a_king(pos: Position, c: Color) { - assert_eq!(pos.board[pos.king(c)], Some(Piece::new(Role::King, c))); + assert_eq!(pos[pos.king(c)], Some(Piece::new(Role::King, c))); } #[proptest] @@ -832,12 +855,12 @@ mod tests { assert_ne!(pos, prev); assert_ne!(pos.turn(), prev.turn()); - assert_eq!(pos.board[m.whence()], None); + assert_eq!(pos[m.whence()], None); assert_eq!( - pos.board[m.whither()], + pos[m.whither()], m.promotion() .map(|r| Piece::new(r, prev.turn())) - .or_else(|| prev.board[m.whence()]) + .or_else(|| prev[m.whence()]) ); assert_eq!( diff --git a/lib/nnue.rs b/lib/nnue.rs index 509cac27..b11e698f 100644 --- a/lib/nnue.rs +++ b/lib/nnue.rs @@ -27,8 +27,8 @@ pub use value::*; /// [NNUE]: https://www.chessprogramming.org/NNUE #[derive(Debug, Clone, Eq, PartialEq, Hash)] struct Nnue { - ft: Transformer, - psqt: Transformer, + ft: Affine, + psqt: Linear, hidden: [Hidden<{ Positional::LEN }>; Material::LEN], } @@ -74,12 +74,12 @@ impl Nnue { } #[inline(always)] - fn psqt() -> &'static Transformer { + fn psqt() -> &'static Linear { unsafe { &NNUE.get().as_ref_unchecked().psqt } } #[inline(always)] - fn ft() -> &'static Transformer { + fn ft() -> &'static Affine { unsafe { &NNUE.get().as_ref_unchecked().ft } } diff --git a/lib/nnue/evaluator.rs b/lib/nnue/evaluator.rs index 2a06a60e..b8b67ffc 100644 --- a/lib/nnue/evaluator.rs +++ b/lib/nnue/evaluator.rs @@ -1,6 +1,6 @@ use crate::chess::{Color, Move, ParsePositionError, Perspective, Piece, Position, Role, Square}; -use crate::nnue::{Accumulator, Feature, Material, Positional, Value}; -use crate::util::Integer; +use crate::nnue::{Accumulator, Feature, Material, Nnue, Positional, Value}; +use crate::util::{Assume, Integer}; use arrayvec::ArrayVec; use derive_more::{Debug, Deref, Display}; use std::str::FromStr; @@ -12,14 +12,14 @@ use proptest::prelude::*; #[derive(Debug, Display, Clone, Eq, PartialEq, Hash, Deref)] #[debug("Evaluator({self})")] #[display("{pos}")] -pub struct Evaluator { +pub struct Evaluator { #[deref] pos: Position, - acc: T, + acc: (Material, Positional), } #[cfg(test)] -impl Arbitrary for Evaluator { +impl Arbitrary for Evaluator { type Parameters = (); type Strategy = BoxedStrategy; @@ -34,10 +34,11 @@ impl Default for Evaluator { } } -impl Evaluator { +impl Evaluator { /// Constructs the evaluator from a [`Position`]. pub fn new(pos: Position) -> Self { - let mut acc = T::default(); + let mut acc: (Material, Positional) = Default::default(); + for side in Color::iter() { let ksq = pos.king(side); for (p, s) in pos.iter() { @@ -48,13 +49,6 @@ impl Evaluator { Evaluator { pos, acc } } - /// The [`Position`]'s evaluation. - pub fn evaluate(&self) -> Value { - let phase = (self.occupied().len() - 1) / 4; - let value = self.acc.evaluate(self.turn(), phase) >> 7; - value.saturate() - } - /// Play a [null-move]. /// /// [null-move]: https://www.chessprogramming.org/Null_Move @@ -83,17 +77,13 @@ impl Evaluator { for side in sides { let ksq = self.king(side); - let old = Piece::new(role, turn); - let new = Piece::new(promotion.unwrap_or(role), turn); - self.acc.replace( - side, - Feature::new(side, ksq, old, wc), - Feature::new(side, ksq, new, wt), - ); - - if let Some((r, s)) = capture { + let old = Feature::new(side, ksq, Piece::new(role, turn), wc); + let new = Feature::new(side, ksq, Piece::new(promotion.unwrap_or(role), turn), wt); + self.acc.replace(side, old, new); + + if let Some((r, sq)) = capture { let victim = Piece::new(r, !turn); - self.acc.remove(side, Feature::new(side, ksq, victim, s)); + self.acc.remove(side, Feature::new(side, ksq, victim, sq)); } else if role == Role::King && (wt - wc).abs() == 2 { let rook = Piece::new(Role::Rook, turn); let (wc, wt) = if wt > wc { @@ -102,31 +92,68 @@ impl Evaluator { (Square::A1.perspective(turn), Square::D1.perspective(turn)) }; - self.acc.replace( - side, - Feature::new(side, ksq, rook, wc), - Feature::new(side, ksq, rook, wt), - ); + let old = Feature::new(side, ksq, rook, wc); + let new = Feature::new(side, ksq, rook, wt); + self.acc.replace(side, old, new); } } } -} -impl Evaluator { - /// The [`Position`]'s material evaluator. - pub fn material(&self) -> Evaluator { - Evaluator { - pos: self.pos.clone(), - acc: self.acc.0.clone(), + /// Estimates the material gain of a move. + pub fn gain(&self, m: Move) -> Value { + let psqt = Nnue::psqt(); + let turn = self.turn(); + let promotion = m.promotion(); + let (wc, wt) = (m.whence(), m.whither()); + let role = self[wc].assume().role(); + let phase = (self.occupied().len() - 1 - m.is_capture() as usize) / 4; + let mut deltas = [0i32, 0i32]; + + for (delta, side) in deltas.iter_mut().zip([turn, !turn]) { + let ksq = self.king(side); + + let old = Feature::new(side, ksq, Piece::new(role, turn), wc); + *delta -= psqt.get(old.cast::()).assume().get(phase).assume(); + + let new = Feature::new(side, ksq, Piece::new(promotion.unwrap_or(role), turn), wt); + *delta += psqt.get(new.cast::()).assume().get(phase).assume(); + + if m.is_capture() { + let (victim, target) = match self[wt] { + Some(p) => (p, wt), + None => ( + Piece::new(Role::Pawn, !turn), + Square::new(wt.file(), wc.rank()), + ), + }; + + let cap = Feature::new(side, ksq, victim, target); + *delta -= psqt.get(cap.cast::()).assume().get(phase).assume(); + } else if role == Role::King && (wt - wc).abs() == 2 { + let rook = Piece::new(Role::Rook, turn); + let (wc, wt) = if wt > wc { + (Square::H1.perspective(turn), Square::F1.perspective(turn)) + } else { + (Square::A1.perspective(turn), Square::D1.perspective(turn)) + }; + + let old = Feature::new(side, ksq, rook, wc); + *delta -= psqt.get(old.cast::()).assume().get(phase).assume(); + + let new = Feature::new(side, ksq, rook, wt); + *delta += psqt.get(new.cast::()).assume().get(phase).assume(); + } } + + let value = (deltas[0] - deltas[1]) >> 7; + value.saturate() } - /// The [`Position`]'s positional evaluator. - pub fn positional(&self) -> Evaluator { - Evaluator { - pos: self.pos.clone(), - acc: self.acc.1.clone(), - } + /// The [`Position`]'s evaluation. + pub fn evaluate(&self) -> Value { + let phase = (self.occupied().len() - 1) / 4; + let value = self.acc.evaluate(self.turn(), phase) >> 7; + value.saturate() } } diff --git a/lib/nnue/transformer.rs b/lib/nnue/transformer.rs index 20805af9..c9c0ece7 100644 --- a/lib/nnue/transformer.rs +++ b/lib/nnue/transformer.rs @@ -1,5 +1,6 @@ use crate::nnue::Feature; use crate::util::{AlignTo64, Assume, Integer}; +use derive_more::derive::{Deref, DerefMut}; use std::ops::{Add, AddAssign, Sub, SubAssign}; #[cfg(test)] @@ -8,15 +9,14 @@ use proptest::{prelude::*, sample::Index}; #[cfg(test)] use std::ops::Range; -/// A feature transformer. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Transformer { - pub(super) bias: AlignTo64<[T; N]>, +/// A linear feature transformer. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deref, DerefMut)] +pub struct Linear { pub(super) weight: AlignTo64<[[T; N]; Feature::LEN]>, } #[cfg(test)] -impl Arbitrary for Box> { +impl Arbitrary for Box> { type Parameters = Range; type Strategy = BoxedStrategy; @@ -25,10 +25,6 @@ impl Arbitrary for Box> { .prop_map(move |rng| { let mut transformer = unsafe { Self::new_zeroed().assume_init() }; - for v in transformer.bias.iter_mut() { - *v = rng.index((end - start) as _) as i16 + start - } - for v in &mut transformer.weight.iter_mut().flatten() { *v = rng.index((end - start) as _) as i16 + start } @@ -40,14 +36,14 @@ impl Arbitrary for Box> { } } -impl Transformer +impl Linear where - T: Copy + Add + AddAssign + Sub + SubAssign, + T: Default + Copy + Add + AddAssign + Sub + SubAssign, { /// A fresh accumulator. #[inline(always)] pub fn fresh(&self) -> [T; N] { - *self.bias + [Default::default(); N] } /// Updates the accumulator by adding features. @@ -79,6 +75,47 @@ where } } +/// An affine feature transformer. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deref)] +pub struct Affine { + pub(super) bias: AlignTo64<[T; N]>, + #[deref] + pub(super) weight: Linear, +} + +#[cfg(test)] +impl Arbitrary for Box> { + type Parameters = Range; + type Strategy = BoxedStrategy; + + fn arbitrary_with(range @ Range { start, end }: Self::Parameters) -> Self::Strategy { + (any_with::>>(range), any::()) + .prop_map(move |(linear, rng)| { + let mut transformer = unsafe { Self::new_zeroed().assume_init() }; + + transformer.weight = *linear; + for v in transformer.bias.iter_mut() { + *v = rng.index((end - start) as _) as i16 + start + } + + transformer + }) + .no_shrink() + .boxed() + } +} + +impl Affine +where + T: Default + Copy + Add + AddAssign + Sub + SubAssign, +{ + /// A fresh accumulator. + #[inline(always)] + pub fn fresh(&self) -> [T; N] { + *self.bias + } +} + #[cfg(test)] mod tests { use super::*; @@ -86,13 +123,13 @@ mod tests { use test_strategy::proptest; #[proptest] - fn fresh_accumulator_equals_bias(#[any(-128i16..128)] t: Box>) { + fn fresh_accumulator_equals_bias(#[any(-128i16..128)] t: Box>) { assert_eq!(t.fresh(), *t.bias); } #[proptest] fn add_updates_accumulator( - #[any(-128i16..128)] t: Box>, + #[any(-128i16..128)] t: Box>, ft: Feature, #[strategy(uniform3(-128..128i16))] prev: [i16; 3], ) { @@ -111,7 +148,7 @@ mod tests { #[proptest] fn remove_updates_accumulator( - #[any(-128..128i16)] t: Box>, + #[any(-128..128i16)] t: Box>, ft: Feature, #[strategy(uniform3(-128..128i16))] prev: [i16; 3], ) { diff --git a/lib/search/driver.rs b/lib/search/driver.rs index eff39b0e..4de5220c 100644 --- a/lib/search/driver.rs +++ b/lib/search/driver.rs @@ -102,7 +102,6 @@ impl Driver { #[cfg(test)] mod tests { use super::*; - use crate::{chess::Move, nnue::Value}; use test_strategy::proptest; #[proptest] diff --git a/lib/search/engine.rs b/lib/search/engine.rs index 465c4237..12879e1b 100644 --- a/lib/search/engine.rs +++ b/lib/search/engine.rs @@ -271,15 +271,12 @@ impl Engine { .map(|m| { if Some(m) == transposed.moves().next() { (m, Value::upper()) + } else if !m.is_quiet() { + (m, pos.gain(m)) } else if killers.contains(m) { (m, Value::new(25)) - } else if m.is_quiet() { - (m, Value::lower() / 2 + self.history.get(m, pos.turn())) } else { - let mut next = pos.material(); - let material = next.evaluate(); - next.play(m); - (m, -next.evaluate() - material) + (m, Value::lower() / 2 + self.history.get(m, pos.turn())) } }) .collect(); @@ -458,7 +455,6 @@ impl Engine { #[cfg(test)] mod tests { use super::*; - use crate::chess::Move; use proptest::{prop_assume, sample::Selector}; use test_strategy::proptest; diff --git a/lib/uci.rs b/lib/uci.rs index 89856346..f515f3da 100644 --- a/lib/uci.rs +++ b/lib/uci.rs @@ -205,10 +205,8 @@ impl + Unpin, O: Sink + Unpin> Uci { ["eval"] => { let pos = &self.position; let turn = self.position.turn(); - let mat = pos.material().evaluate().perspective(turn); - let psn = pos.positional().evaluate().perspective(turn); - let val = pos.evaluate().perspective(turn); - let info = format!("info material {mat:+} positional {psn:+} value {val:+}"); + let value = pos.evaluate().perspective(turn); + let info = format!("info value {value:+}"); self.output.send(info).await?; }