Skip to content

Commit 8ab0770

Browse files
Merge pull request #95 from DeveloperPaul123/feature/lmr
bench: 3997113
2 parents 9ef102c + 170f671 commit 8ab0770

File tree

6 files changed

+299
-4
lines changed

6 files changed

+299
-4
lines changed

chess/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Created Date: Friday, August 16th 2024
55
* Author: Paul Tsouchlos (DeveloperPaul123) ([email protected])
66
* -----
7-
* Last Modified: Tue Nov 26 2024
7+
* Last Modified: Wed Apr 23 2025
88
* -----
99
* Copyright (c) 2024 Paul Tsouchlos (DeveloperPaul123)
1010
* GNU General Public License v3.0 or later

engine/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ pub mod hce_values;
66
pub mod history_table;
77
mod inplace_incremental_sort;
88
pub mod input_handler;
9+
mod lmr;
910
mod move_order;
1011
pub(crate) mod node_types;
1112
pub mod phased_score;
1213
pub mod score;
1314
pub mod search;
1415
pub mod search_thread;
16+
pub(crate) mod table;
1517
pub mod traits;
1618
pub mod ttable;
1719
pub mod tuneable;

engine/src/lmr.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use crate::tuneable::{LMR_OFFSET, LMR_SCALING_FACTOR};
2+
3+
/// LMR (Late Move Reduction) formula for calculating the reduction factor
4+
/// based on the natural logarithm of the current depth and the number of moves made.
5+
///
6+
/// # Arguments
7+
///
8+
/// - `depth` - The current depth in the search tree.
9+
/// - `move_count` - The number of moves made so far.
10+
///
11+
/// # Returns
12+
///
13+
/// A floating-point value representing the reduction factor.
14+
pub(crate) fn formula(depth: usize, move_count: usize) -> f64 {
15+
let d_ln = (depth as f64).ln();
16+
let mvs_ln = (move_count as f64).ln();
17+
if d_ln.is_finite() && mvs_ln.is_finite() {
18+
LMR_OFFSET + (d_ln * mvs_ln) / LMR_SCALING_FACTOR
19+
} else {
20+
0_f64
21+
}
22+
}
23+
24+
#[cfg(test)]
25+
mod tests {
26+
use super::*;
27+
28+
#[test]
29+
fn test_lmr_formula() {
30+
let depth = 5;
31+
let move_count = 10;
32+
let result = formula(depth, move_count);
33+
assert!(result.is_finite());
34+
assert!(result > 0.0);
35+
}
36+
37+
#[test]
38+
fn test_lmr_formula_zero_depth() {
39+
let depth = 0;
40+
let move_count = 10;
41+
let result = formula(depth, move_count);
42+
assert_eq!(result, LMR_OFFSET);
43+
}
44+
45+
#[test]
46+
fn test_lmr_formula_zero_moves() {
47+
let depth = 5;
48+
let move_count = 0;
49+
let result = formula(depth, move_count);
50+
assert_eq!(result, LMR_OFFSET);
51+
}
52+
}

engine/src/search.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Created Date: Thursday, November 21st 2024
55
* Author: Paul Tsouchlos (DeveloperPaul123) ([email protected])
66
* -----
7-
* Last Modified: Thu Apr 17 2025
7+
* Last Modified: Wed Apr 23 2025
88
* -----
99
* Copyright (c) 2024 Paul Tsouchlos (DeveloperPaul123)
1010
* GNU General Public License v3.0 or later
@@ -34,9 +34,11 @@ use crate::{
3434
evaluation::ByteKnightEvaluation,
3535
history_table::HistoryTable,
3636
inplace_incremental_sort::InplaceIncrementalSort,
37+
lmr,
3738
move_order::MoveOrder,
3839
node_types::{NodeType, NonPvNode, PvNode, RootNode},
3940
score::{LargeScoreType, Score, ScoreType},
41+
table::Table,
4042
traits::Eval,
4143
ttable::{self, TranspositionTableEntry},
4244
tuneable::{
@@ -156,6 +158,7 @@ pub struct Search<'search_lifetime> {
156158
parameters: SearchParameters,
157159
eval: ByteKnightEvaluation,
158160
stop_flag: Option<Arc<AtomicBool>>,
161+
lmr_table: Table<f64, 32_000>,
159162
}
160163

161164
impl<'a> Search<'a> {
@@ -164,6 +167,10 @@ impl<'a> Search<'a> {
164167
ttable: &'a mut TranspositionTable,
165168
history_table: &'a mut HistoryTable,
166169
) -> Self {
170+
// Initialize our LMR table as a 2D array of our LMR formula for depth and moves played
171+
let mut table = Table::<f64, 32_000>::new(MAX_DEPTH as usize, MAX_MOVE_LIST_SIZE);
172+
table.fill(lmr::formula);
173+
167174
Search {
168175
transposition_table: ttable,
169176
history_table,
@@ -172,6 +179,7 @@ impl<'a> Search<'a> {
172179
parameters: parameters.clone(),
173180
eval: ByteKnightEvaluation::default(),
174181
stop_flag: None,
182+
lmr_table: table,
175183
}
176184
}
177185

@@ -391,6 +399,7 @@ impl<'a> Search<'a> {
391399
let mut best_score = -Score::INF;
392400
let mut best_move = None;
393401

402+
let lmr_reduction = 1;
394403
// loop through all moves
395404
for (i, mv) in move_iter.into_iter().enumerate() {
396405
// make the move
@@ -400,8 +409,13 @@ impl<'a> Search<'a> {
400409
if Node::PV && i == 0 {
401410
-self.negamax::<PvNode>(board, depth - 1, ply + 1, -beta, -alpha_use)
402411
} else {
412+
let reduction = if mv.is_quiet() && depth >= 3 && board.full_move_number() >= 3 {
413+
(lmr_reduction as f64 + self.lmr_table.at(depth as usize, i).expect("LMR value uninitialized")).floor() as i16
414+
} else {
415+
1
416+
};
403417
// search with a null window
404-
let temp_score = -self.negamax::<NonPvNode>(board, depth - 1, ply + 1, -alpha_use - 1, -alpha_use);
418+
let temp_score = -self.negamax::<NonPvNode>(board, depth - reduction, ply + 1, -alpha_use - 1, -alpha_use);
405419
// if it fails, we need to do a full re-search
406420
if temp_score > alpha_use && temp_score < beta {
407421
-self.negamax::<NonPvNode>(board, depth - 1, ply + 1, -beta, -alpha_use)

engine/src/table.rs

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
use std::fmt::Display;
2+
3+
use arrayvec::ArrayVec;
4+
5+
/// A generic 2D table for general use in chess engines.
6+
#[derive(Debug, Clone)]
7+
pub(crate) struct Table<T, const CAP: usize> {
8+
data: ArrayVec<T, CAP>,
9+
width: usize,
10+
height: usize,
11+
}
12+
13+
impl<T, const CAP: usize> Table<T, CAP> {
14+
/// Creates a new table with the specified width and height.
15+
/// The table is initialized with a capacity of `CAP` elements.
16+
/// The width and height are the number of columns and rows respectively.
17+
///
18+
/// # Arguments
19+
///
20+
/// - `width` - The number of columns in the table.
21+
/// - `height` - The number of rows in the table.
22+
///
23+
/// # Returns
24+
///
25+
/// A new instance of `Table<T, CAP>`.
26+
///
27+
/// # Panics
28+
///
29+
/// This function will panic if width*height > CAP.
30+
pub fn new(width: usize, height: usize) -> Self {
31+
assert!(
32+
width * height <= CAP,
33+
"Table capacity exceeded: {} > {}",
34+
width * height,
35+
CAP
36+
);
37+
38+
Self {
39+
data: ArrayVec::<T, CAP>::new(),
40+
width,
41+
height,
42+
}
43+
}
44+
45+
/// Insert an element at the given row and column.
46+
///
47+
/// # Arguments
48+
///
49+
/// - `value` - The value to insert.
50+
/// - `row` - The row index.
51+
/// - `col` - The column index.
52+
pub fn insert(&mut self, value: T, row: usize, col: usize) {
53+
assert!(
54+
row < self.rows() && col < self.cols(),
55+
"Invalid row or column index"
56+
);
57+
self.data.insert(self.index(row, col), value);
58+
}
59+
60+
/// Get a mutable reference to the element at the given row and column, if it exists.
61+
///
62+
/// # Arguments
63+
///
64+
/// - `row` - The row index.
65+
/// - `col` - The column index.
66+
///
67+
/// # Returns
68+
///
69+
/// An `Option<&T>` that contains a reference to the element at the specified row and column, or `None` if it does not exist.
70+
pub fn at(&self, row: usize, col: usize) -> Option<&T> {
71+
self.data.get(self.index(row, col))
72+
}
73+
74+
/// Get a slice of the given row.
75+
///
76+
/// # Arguments
77+
///
78+
/// - `row` - The row index.
79+
///
80+
/// # Returns
81+
///
82+
/// A slice of the row at the specified index.
83+
pub fn row(&self, row: usize) -> &[T] {
84+
let start_idx = self.index(row, 0);
85+
let end_idx = self.index(row, self.cols());
86+
// note, this is an inclusive range
87+
self.data.get(start_idx..end_idx).expect("Invalid range")
88+
}
89+
90+
/// Returns the number of rows in the table; a.k.a, the height.
91+
pub fn rows(&self) -> usize {
92+
self.height
93+
}
94+
95+
/// Returns the number of columns in the table; a.k.a, the width.
96+
pub fn cols(&self) -> usize {
97+
self.width
98+
}
99+
100+
/// Fills the table with values generated by the provided function.
101+
///
102+
/// # Arguments
103+
///
104+
/// - `value_producer` - A function that takes the row and column indices and returns a value of type `T`.
105+
pub fn fill<ValueProducer: Fn(usize, usize) -> T>(&mut self, value_producer: ValueProducer) {
106+
for row in 0..self.rows() {
107+
for col in 0..self.cols() {
108+
self.insert(value_producer(row, col), row, col);
109+
}
110+
}
111+
}
112+
113+
/// Helper to calculate the index with a given row and column.
114+
fn index(&self, row: usize, col: usize) -> usize {
115+
row * self.width + col
116+
}
117+
}
118+
119+
impl<T: Display, const CAP: usize> Display for Table<T, CAP> {
120+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121+
let mut output = "".to_string();
122+
123+
for row in 0..self.rows() {
124+
let row_data = self.row(row);
125+
for (col, item) in row_data.iter().enumerate() {
126+
output.push_str(format!("{}", item).as_str());
127+
if col < self.cols() - 1 {
128+
output.push(',');
129+
output.push(' ');
130+
}
131+
}
132+
133+
if row < self.rows() - 1 {
134+
output.push('\n');
135+
}
136+
}
137+
138+
write!(f, "{}", output)
139+
}
140+
}
141+
142+
#[cfg(test)]
143+
mod tests {
144+
use crate::table::Table;
145+
146+
#[test]
147+
fn initialize_and_fill() {
148+
const SIZE: usize = 8;
149+
let iota = |row: usize, col: usize| -> usize { row * SIZE + col };
150+
151+
let mut table = Table::<usize, 64>::new(8, 8);
152+
table.fill(iota);
153+
154+
for row in 0..table.rows() {
155+
for col in 0..table.cols() {
156+
let val = table.at(row, col);
157+
assert!(val.is_some());
158+
assert_eq!(*val.unwrap(), iota(row, col));
159+
}
160+
}
161+
}
162+
163+
#[test]
164+
#[should_panic]
165+
fn expect_panic_with_cap_mismatch() {
166+
const SIZE: usize = 8;
167+
let _ = Table::<usize, SIZE>::new(8, 8);
168+
}
169+
170+
#[test]
171+
#[should_panic]
172+
fn insert_invalid_index() {
173+
const SIZE: usize = 8;
174+
let mut table = Table::<usize, SIZE>::new(8, 8);
175+
table.insert(0, 0, 10);
176+
}
177+
178+
#[test]
179+
fn index_calculation() {
180+
const SIZE: usize = 64;
181+
let table = Table::<usize, SIZE>::new(8, 8);
182+
let mut i = 0;
183+
for row in 0..table.rows() {
184+
for col in 0..table.cols() {
185+
assert_eq!(table.index(row, col), i);
186+
i += 1;
187+
}
188+
}
189+
}
190+
191+
#[test]
192+
fn read_row() {
193+
const SIZE: usize = 64;
194+
let mut table = Table::<usize, SIZE>::new(8, 8);
195+
table.fill(|_, col| col);
196+
197+
for row in 0..table.rows() {
198+
let row_data = table.row(row);
199+
assert!(row_data.len() == table.cols());
200+
for (i, item) in row_data.iter().enumerate() {
201+
assert_eq!(*item, i);
202+
}
203+
}
204+
}
205+
206+
#[test]
207+
fn verify_formatting() {
208+
const SIZE: usize = 64;
209+
let mut table = Table::<usize, SIZE>::new(8, 8);
210+
table.fill(|row, col| row * 8 + col);
211+
let formatted = format!("{}", table);
212+
println!("{}", formatted);
213+
let expected = "0, 1, 2, 3, 4, 5, 6, 7\n\
214+
8, 9, 10, 11, 12, 13, 14, 15\n\
215+
16, 17, 18, 19, 20, 21, 22, 23\n\
216+
24, 25, 26, 27, 28, 29, 30, 31\n\
217+
32, 33, 34, 35, 36, 37, 38, 39\n\
218+
40, 41, 42, 43, 44, 45, 46, 47\n\
219+
48, 49, 50, 51, 52, 53, 54, 55\n\
220+
56, 57, 58, 59, 60, 61, 62, 63";
221+
222+
assert_eq!(formatted, expected);
223+
}
224+
}

engine/src/tuneable.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Created Date: Wednesday, December 11th 2024
55
* Author: Paul Tsouchlos (DeveloperPaul123) ([email protected])
66
* -----
7-
* Last Modified: Mon Apr 14 2025
7+
* Last Modified: Wed Apr 23 2025
88
* -----
99
* Copyright (c) 2024 Paul Tsouchlos (DeveloperPaul123)
1010
* GNU General Public License v3.0 or later
@@ -25,3 +25,6 @@ pub(crate) const IIR_DEPTH_REDUCTION: ScoreType = 1;
2525

2626
pub(crate) const NMP_MIN_DEPTH: ScoreType = 3;
2727
pub(crate) const NMP_DEPTH_REDUCTION: ScoreType = 2;
28+
29+
pub(crate) const LMR_OFFSET: f64 = 0.0;
30+
pub(crate) const LMR_SCALING_FACTOR: f64 = 3.0;

0 commit comments

Comments
 (0)