Skip to content

Commit

Permalink
Pre-compute move flags
Browse files Browse the repository at this point in the history
Instead of computing these every time we create a move, we can compute
them at compile time.

We also adopt a trick from Simbelmyne (transmuting) to allow checking
directly against the enum, which simplifies the checking code.

Elo   | 2.07 +- 4.01 (95%)
SPRT  | 8.0+0.08s Threads=1 Hash=16MB
LLR   | 2.99 (-2.94, 2.94) [-5.00, 0.00]
Games | N: 12910 W: 3615 L: 3538 D: 5757
Penta | [318, 1527, 2746, 1488, 376]
  • Loading branch information
jgilchrist committed Nov 17, 2024
1 parent 290b333 commit 02a7ca9
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 77 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Add countermove history (2.66 +- 2.03)
* Use separate functions for scoring tacticals vs. quiets (2.64 +- 4.43)
* Enable LTO (2.09 +- 3.32)
* Pre-compute move flags (2.07 +- 4.01)
* Use arrayvec for MoveList (1.05 +- 3.35)

### Misc
Expand Down
151 changes: 74 additions & 77 deletions src/chess/moves.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,85 +63,90 @@ pub struct Move(NonZeroU16);

const SRC_MASK: u16 = 0b0000_0000_0011_1111;
const DST_MASK: u16 = 0b0000_1111_1100_0000;
const CAPTURE_BIT_MASK: u16 = 0b0001_0000_0000_0000;
const PROMOTION_BIT_MASK: u16 = 0b0010_0000_0000_0000;
const FIRST_FLAG_MASK: u16 = 0b0100_0000_0000_0000;
const SECOND_FLAG_MASK: u16 = 0b1000_0000_0000_0000;
const FLAGS_MASK: u16 = FIRST_FLAG_MASK | SECOND_FLAG_MASK;

const DST_SHIFT: usize = 6;
const CAPTURE_BIT_SHIFT: usize = 12;
const PROMOTION_BIT_SHIFT: usize = 13;
const FIRST_FLAG_SHIFT: usize = 14;
const FLAGS_SHIFT: usize = 14;
const SECOND_FLAG_SHIFT: usize = 15;

#[inline]
const fn src_bits(src: Square) -> u16 {
src.idx() as u16
#[repr(u8)]
#[derive(PartialEq, Eq)]
enum Flags {
Quiet = 0b0000,
Castle = flag_bits(true, false),
Capture = CAPTURE_FLAG_BIT,
EnPassant = CAPTURE_FLAG_BIT | flag_bits(true, false),
PromoteToBishop = PROMOTION_FLAG_BIT | flag_bits(false, false),
PromoteToKnight = PROMOTION_FLAG_BIT | flag_bits(false, true),
PromoteToRook = PROMOTION_FLAG_BIT | flag_bits(true, false),
PromoteToQueen = PROMOTION_FLAG_BIT | flag_bits(true, true),
CaptureAndPromoteToBishop = CAPTURE_FLAG_BIT | PROMOTION_FLAG_BIT | flag_bits(false, false),
CaptureAndPromoteToKnight = CAPTURE_FLAG_BIT | PROMOTION_FLAG_BIT | flag_bits(false, true),
CaptureAndPromoteToRook = CAPTURE_FLAG_BIT | PROMOTION_FLAG_BIT | flag_bits(true, false),
CaptureAndPromoteToQueen = CAPTURE_FLAG_BIT | PROMOTION_FLAG_BIT | flag_bits(true, true),
}

#[inline]
const fn dst_bits(dst: Square) -> u16 {
(dst.idx() as u16) << DST_SHIFT
impl Flags {
// Trick from Simbelmyne - rather than checking individual bits, we can transmute and check everything at once
fn from_u8(flags: u8) -> Self {
unsafe { std::mem::transmute::<u8, Self>(flags) }
}
}

#[inline]
const fn capture_bit() -> u16 {
1u16 << CAPTURE_BIT_SHIFT
}
const CAPTURE_FLAG_BIT: u8 = 0b0001;
const PROMOTION_FLAG_BIT: u8 = 0b0010;

#[inline]
const fn promotion_bit() -> u16 {
1u16 << PROMOTION_BIT_SHIFT
}
const CAPTURE_BIT_MASK: u16 = 0b0001_0000_0000_0000;
const PROMOTION_BIT_MASK: u16 = 0b0010_0000_0000_0000;

#[inline]
const fn flag_bits(f1: bool, f2: bool) -> u16 {
(f1 as u16) << FIRST_FLAG_SHIFT | (f2 as u16) << SECOND_FLAG_SHIFT
const fn flag_bits(f1: bool, f2: bool) -> u8 {
((f1 as u8) << 2) | ((f2 as u8) << 3)
}

#[inline]
const fn promotion_flag_bits(promotion_piece_kind: PromotionPieceKind) -> u16 {
(match promotion_piece_kind {
PromotionPieceKind::Queen => 0b00u16,
PromotionPieceKind::Rook => 0b01u16,
PromotionPieceKind::Knight => 0b10u16,
PromotionPieceKind::Bishop => 0b11u16,
}) << FLAGS_SHIFT
}
const DST_SHIFT: usize = 6;
const FLAGS_SHIFT: usize = 12;

impl Move {
#[inline]
const fn new(data: u16) -> Self {
const fn new(src: Square, dst: Square, flags: Flags) -> Self {
// It's impossible for us to create a move with '0' data. In order to do that
// we'd need both the source and destination squares to be A1 (0).
Self(unsafe { NonZeroU16::new_unchecked(data) })
Self(unsafe {
NonZeroU16::new_unchecked(
(src.idx() as u16)
| (dst.idx() as u16) << DST_SHIFT
| ((flags as u16) << FLAGS_SHIFT),
)
})
}

#[inline]
pub const fn quiet(src: Square, dst: Square) -> Self {
Self::new(src_bits(src) | dst_bits(dst))
Self::new(src, dst, Flags::Quiet)
}

#[inline]
pub const fn capture(src: Square, dst: Square) -> Self {
Self::new(src_bits(src) | dst_bits(dst) | capture_bit())
Self::new(src, dst, Flags::Capture)
}

#[inline]
pub const fn castles(src: Square, dst: Square) -> Self {
Self::new(src_bits(src) | dst_bits(dst) | flag_bits(true, false))
Self::new(src, dst, Flags::Castle)
}

#[inline]
pub const fn en_passant(src: Square, dst: Square) -> Self {
Self::new(src_bits(src) | dst_bits(dst) | capture_bit() | flag_bits(true, false))
Self::new(src, dst, Flags::EnPassant)
}

#[inline]
pub const fn quiet_promotion(src: Square, dst: Square, promotion: PromotionPieceKind) -> Self {
Self::new(src_bits(src) | dst_bits(dst) | promotion_bit() | promotion_flag_bits(promotion))
Self::new(
src,
dst,
match promotion {
PromotionPieceKind::Bishop => Flags::PromoteToBishop,
PromotionPieceKind::Knight => Flags::PromoteToKnight,
PromotionPieceKind::Rook => Flags::PromoteToRook,
PromotionPieceKind::Queen => Flags::PromoteToQueen,
},
)
}

#[inline]
Expand All @@ -151,11 +156,14 @@ impl Move {
promotion: PromotionPieceKind,
) -> Self {
Self::new(
src_bits(src)
| dst_bits(dst)
| capture_bit()
| promotion_bit()
| promotion_flag_bits(promotion),
src,
dst,
match promotion {
PromotionPieceKind::Bishop => Flags::CaptureAndPromoteToBishop,
PromotionPieceKind::Knight => Flags::CaptureAndPromoteToKnight,
PromotionPieceKind::Rook => Flags::CaptureAndPromoteToRook,
PromotionPieceKind::Queen => Flags::CaptureAndPromoteToQueen,
},
)
}

Expand All @@ -174,48 +182,43 @@ impl Move {
Square::from_index(((self.data() & DST_MASK) >> DST_SHIFT) as u8)
}

#[inline]
fn flags(self) -> Flags {
Flags::from_u8((self.0.get() >> FLAGS_SHIFT) as u8)
}

#[inline]
pub fn is_capture(self) -> bool {
(self.data() & CAPTURE_BIT_MASK) == CAPTURE_BIT_MASK
}

#[allow(unused)]
#[inline]
pub fn is_promotion(self) -> bool {
(self.data() & PROMOTION_BIT_MASK) == PROMOTION_BIT_MASK
}

#[inline]
pub fn promotion(self) -> Option<PromotionPieceKind> {
if !self.is_promotion() {
return None;
use PromotionPieceKind::*;

match self.flags() {
Flags::PromoteToBishop | Flags::CaptureAndPromoteToBishop => Some(Bishop),
Flags::PromoteToKnight | Flags::CaptureAndPromoteToKnight => Some(Knight),
Flags::PromoteToRook | Flags::CaptureAndPromoteToRook => Some(Rook),
Flags::PromoteToQueen | Flags::CaptureAndPromoteToQueen => Some(Queen),
_ => None,
}

let flag_bits = (self.data() & FLAGS_MASK) >> FLAGS_SHIFT;

Some(match flag_bits {
0b00 => PromotionPieceKind::Queen,
0b01 => PromotionPieceKind::Rook,
0b10 => PromotionPieceKind::Knight,
0b11 => PromotionPieceKind::Bishop,
_ => unreachable!(),
})
}

#[inline]
pub fn is_quiet(self) -> bool {
!self.is_promotion() && !self.is_capture()
}

#[inline]
pub fn is_en_passant(self) -> bool {
self.is_capture()
&& !self.is_promotion()
&& (self.data() & FIRST_FLAG_MASK) == FIRST_FLAG_MASK
self.flags() == Flags::EnPassant
}

#[inline]
pub fn is_castling(self) -> bool {
self.is_quiet() && (self.data() & FIRST_FLAG_MASK) == FIRST_FLAG_MASK
self.flags() == Flags::Castle
}
}

Expand Down Expand Up @@ -259,7 +262,6 @@ mod tests {
let mv = Move::quiet(A1, B1);
assert_eq!(mv.src(), A1);
assert_eq!(mv.dst(), B1);
assert!(mv.is_quiet());
assert!(mv.promotion().is_none());
assert!(!mv.is_capture());
assert!(!mv.is_castling());
Expand All @@ -269,7 +271,6 @@ mod tests {
#[test]
fn test_quiet_promotion() {
let mv = Move::quiet_promotion(A1, B1, PromotionPieceKind::Queen);
assert!(!mv.is_quiet());
assert_eq!(mv.promotion(), Some(PromotionPieceKind::Queen));
assert!(!mv.is_capture());
assert!(!mv.is_castling());
Expand All @@ -279,7 +280,6 @@ mod tests {
#[test]
fn test_capture() {
let mv = Move::capture(A1, B1);
assert!(!mv.is_quiet());
assert!(mv.promotion().is_none());
assert!(mv.is_capture());
assert!(!mv.is_castling());
Expand All @@ -289,7 +289,6 @@ mod tests {
#[test]
fn test_capture_promotion() {
let mv = Move::capture_promotion(A1, B1, PromotionPieceKind::Queen);
assert!(!mv.is_quiet());
assert_eq!(mv.promotion(), Some(PromotionPieceKind::Queen));
assert!(mv.is_capture());
assert!(!mv.is_castling());
Expand All @@ -299,7 +298,6 @@ mod tests {
#[test]
fn test_castles() {
let mv = Move::castles(A1, B1);
assert!(mv.is_quiet());
assert!(mv.promotion().is_none());
assert!(!mv.is_capture());
assert!(mv.is_castling());
Expand All @@ -309,7 +307,6 @@ mod tests {
#[test]
fn test_en_passant() {
let mv = Move::en_passant(A1, B1);
assert!(!mv.is_quiet());
assert!(mv.promotion().is_none());
assert!(mv.is_capture());
assert!(!mv.is_castling());
Expand Down

0 comments on commit 02a7ca9

Please sign in to comment.