use std::sync::LazyLock;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, Ordering};
use crate::position::{BoardEffects, Position};
use crate::types::{Color, Piece, PieceType, Square, Value};
pub const DEFAULT_PASS_RIGHT_VALUE_EARLY: i32 = 0;
pub const DEFAULT_PASS_RIGHT_VALUE_LATE: i32 = 0;
const EARLY_PLY: u16 = 40;
const LATE_PLY: u16 = 120;
static PASS_RIGHT_VALUE_EARLY: AtomicI32 = AtomicI32::new(DEFAULT_PASS_RIGHT_VALUE_EARLY);
static PASS_RIGHT_VALUE_LATE: AtomicI32 = AtomicI32::new(DEFAULT_PASS_RIGHT_VALUE_LATE);
pub fn get_pass_right_value() -> i32 {
PASS_RIGHT_VALUE_LATE.load(Ordering::Relaxed)
}
pub fn set_pass_right_value(value: i32) {
PASS_RIGHT_VALUE_EARLY.store(value, Ordering::Relaxed);
PASS_RIGHT_VALUE_LATE.store(value, Ordering::Relaxed);
}
pub fn set_pass_right_value_phased(early: i32, late: i32) {
PASS_RIGHT_VALUE_EARLY.store(early, Ordering::Relaxed);
PASS_RIGHT_VALUE_LATE.store(late, Ordering::Relaxed);
}
#[inline]
fn compute_pass_right_value(ply: u16, early_value: i32, late_value: i32) -> i32 {
if ply <= EARLY_PLY {
early_value
} else if ply >= LATE_PLY {
late_value
} else {
let ratio = (ply - EARLY_PLY) as i32;
let range = (LATE_PLY - EARLY_PLY) as i32;
early_value + (late_value - early_value) * ratio / range
}
}
#[inline]
fn pass_right_value_by_ply(ply: u16) -> i32 {
let early_value = PASS_RIGHT_VALUE_EARLY.load(Ordering::Relaxed);
let late_value = PASS_RIGHT_VALUE_LATE.load(Ordering::Relaxed);
compute_pass_right_value(ply, early_value, late_value)
}
#[inline]
pub fn evaluate_pass_rights(pos: &Position, ply: u16) -> Value {
if !pos.is_pass_rights_enabled() {
return Value::ZERO;
}
let us = pos.side_to_move();
let them = !us;
let our_rights = pos.pass_rights(us) as i32;
let their_rights = pos.pass_rights(them) as i32;
let value = pass_right_value_by_ply(ply);
Value::new((our_rights - their_rights) * value)
}
const DEFAULT_PASS_MOVE_BONUS: i32 = 0;
static PASS_MOVE_BONUS: AtomicI32 = AtomicI32::new(DEFAULT_PASS_MOVE_BONUS);
#[inline]
pub fn get_pass_move_bonus() -> i32 {
PASS_MOVE_BONUS.load(Ordering::Relaxed)
}
pub fn set_pass_move_bonus(value: i32) {
PASS_MOVE_BONUS.store(value, Ordering::Relaxed);
}
#[inline]
pub fn get_scaled_pass_move_bonus(_ply: i32) -> i32 {
PASS_MOVE_BONUS.load(Ordering::Relaxed)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MaterialLevel {
Lv1,
Lv2,
Lv3,
Lv4,
Lv7,
Lv8,
Lv9,
}
impl MaterialLevel {
pub fn from_value(v: u8) -> Option<Self> {
match v {
1 => Some(MaterialLevel::Lv1),
2 => Some(MaterialLevel::Lv2),
3 => Some(MaterialLevel::Lv3),
4 => Some(MaterialLevel::Lv4),
7 => Some(MaterialLevel::Lv7),
8 => Some(MaterialLevel::Lv8),
9 => Some(MaterialLevel::Lv9),
_ => None,
}
}
pub fn value(self) -> u8 {
match self {
MaterialLevel::Lv1 => 1,
MaterialLevel::Lv2 => 2,
MaterialLevel::Lv3 => 3,
MaterialLevel::Lv4 => 4,
MaterialLevel::Lv7 => 7,
MaterialLevel::Lv8 => 8,
MaterialLevel::Lv9 => 9,
}
}
}
pub const DEFAULT_MATERIAL_LEVEL: MaterialLevel = MaterialLevel::Lv9;
static MATERIAL_LEVEL: AtomicU8 = AtomicU8::new(9);
static MATERIAL_ENABLED: AtomicBool = AtomicBool::new(false);
pub fn is_material_enabled() -> bool {
MATERIAL_ENABLED.load(Ordering::Relaxed)
}
pub fn get_material_level() -> MaterialLevel {
let v = MATERIAL_LEVEL.load(Ordering::Relaxed);
debug_assert!(
MaterialLevel::from_value(v).is_some(),
"Invalid MaterialLevel value in AtomicU8: {v}"
);
MaterialLevel::from_value(v).unwrap_or(DEFAULT_MATERIAL_LEVEL)
}
pub fn set_material_level(level: MaterialLevel) {
MATERIAL_LEVEL.store(level.value(), Ordering::Relaxed);
MATERIAL_ENABLED.store(true, Ordering::Relaxed);
}
pub fn disable_material() {
MATERIAL_ENABLED.store(false, Ordering::Relaxed);
}
pub fn material_needs_board_effects() -> bool {
matches!(
get_material_level(),
MaterialLevel::Lv3
| MaterialLevel::Lv4
| MaterialLevel::Lv7
| MaterialLevel::Lv8
| MaterialLevel::Lv9
)
}
pub(crate) fn base_piece_value(pt: PieceType) -> i32 {
match pt {
PieceType::Pawn => 90,
PieceType::Lance => 315,
PieceType::Knight => 405,
PieceType::Silver => 495,
PieceType::Bishop => 855,
PieceType::Rook => 990,
PieceType::Gold => 540,
PieceType::King => 15000,
PieceType::ProPawn => 540,
PieceType::ProLance => 540,
PieceType::ProKnight => 540,
PieceType::ProSilver => 540,
PieceType::Horse => 945,
PieceType::Dragon => 1395,
}
}
#[inline]
pub(crate) fn signed_piece_value(pc: Piece) -> i32 {
if pc.is_none() {
return 0;
}
let sign = if pc.color() == Color::Black { 1 } else { -1 };
sign * base_piece_value(pc.piece_type())
}
#[inline]
pub(crate) fn hand_piece_value(color: Color, pt: PieceType) -> i32 {
let sign = if color == Color::Black { 1 } else { -1 };
sign * base_piece_value(pt)
}
pub fn compute_material_value(pos: &Position) -> Value {
let mut score = 0i32;
for sq in pos.occupied().iter() {
score += signed_piece_value(pos.piece_on(sq));
}
for color in [Color::Black, Color::White] {
let hand = pos.hand(color);
for pt in PieceType::HAND_PIECES {
score += hand.count(pt) as i32 * hand_piece_value(color, pt);
}
}
Value::new(score)
}
#[inline]
fn dist(a: Square, b: Square) -> usize {
let df = (a.file().index() as i32 - b.file().index() as i32).unsigned_abs() as usize;
let dr = (a.rank().index() as i32 - b.rank().index() as i32).unsigned_abs() as usize;
df.max(dr)
}
const fn make_effect_values(base: i32) -> [i32; 9] {
let mut arr = [0; 9];
let mut i = 0;
while i < 9 {
arr[i] = base * 1024 / (i as i32 + 1);
i += 1;
}
arr
}
const LV3_OUR_EFFECT_VALUE: [i32; 9] = make_effect_values(68);
const LV3_THEIR_EFFECT_VALUE: [i32; 9] = make_effect_values(96);
const LV4_OUR_EFFECT_VALUE: [i32; 9] = make_effect_values(85);
const LV4_THEIR_EFFECT_VALUE: [i32; 9] = make_effect_values(98);
const LV7_OUR_EFFECT_VALUE: [i32; 9] = make_effect_values(83);
const LV7_THEIR_EFFECT_VALUE: [i32; 9] = make_effect_values(92);
static MULTI_EFFECT_VALUE: LazyLock<[i32; 11]> = LazyLock::new(|| {
let mut arr = [0i32; 11];
for (m, value) in arr.iter_mut().enumerate().skip(1) {
*value = (6365.0 - 0.8525f64.powi((m as i32) - 1) * 5341.0) as i32;
}
arr
});
struct Lv7Tables {
our_effect_table: [[[i32; 3]; Square::NUM]; Square::NUM],
their_effect_table: [[[i32; 3]; Square::NUM]; Square::NUM],
}
static LV7_TABLES: LazyLock<Lv7Tables> = LazyLock::new(|| {
let mv = &*MULTI_EFFECT_VALUE;
let mut our_effect_table = [[[0i32; 3]; Square::NUM]; Square::NUM];
let mut their_effect_table = [[[0i32; 3]; Square::NUM]; Square::NUM];
for king_sq in Square::all() {
for sq in Square::all() {
let d = dist(sq, king_sq);
for m in 0..3 {
our_effect_table[king_sq.index()][sq.index()][m] =
mv[m] * LV7_OUR_EFFECT_VALUE[d] / (1024 * 1024);
their_effect_table[king_sq.index()][sq.index()][m] =
mv[m] * LV7_THEIR_EFFECT_VALUE[d] / (1024 * 1024);
}
}
}
Lv7Tables {
our_effect_table,
their_effect_table,
}
});
const OUR_EFFECT_TO_OUR_PIECE: [i32; 3] = [0, 33, 43];
const THEIR_EFFECT_TO_OUR_PIECE: [i32; 3] = [0, 113, 122];
const KING_POS_BONUS: [i32; 81] = [
875, 655, 830, 680, 770, 815, 720, 945, 755, 605, 455, 610, 595, 730, 610, 600, 590, 615, 565,
640, 555, 525, 635, 565, 440, 600, 575, 520, 515, 580, 420, 640, 535, 565, 500, 510, 220, 355,
240, 375, 340, 335, 305, 275, 320, 500, 530, 560, 445, 510, 395, 455, 490, 410, 345, 275, 250,
355, 295, 280, 420, 235, 135, 335, 370, 385, 255, 295, 200, 265, 305, 305, 255, 225, 245, 295,
200, 320, 275, 70, 200,
];
const OUR_EFFECT_RATE: [i32; 10] = [1120, 1872, 112, 760, 744, 880, 1320, 600, 904, 1024];
const THEIR_EFFECT_RATE: [i32; 10] = [1056, 1714, 1688, 1208, 248, 240, 496, 816, 928, 1024];
fn king_pos_bonus(color: Color, sq: Square) -> i32 {
let target_sq = if color == Color::Black {
sq
} else {
sq.inverse()
};
let idx = (8 - target_sq.file().index()) + target_sq.rank().index() * 9;
let bonus = KING_POS_BONUS[idx];
if color == Color::Black { bonus } else { -bonus }
}
fn direction_of(king: Square, sq: Square) -> usize {
let mut df = sq.file().index() as i32 - king.file().index() as i32;
let dr = sq.rank().index() as i32 - king.rank().index() as i32;
if df > 0 {
df = -df;
}
if df == 0 && dr == 0 {
return 9;
}
if df == 0 && dr < 0 {
return 0;
}
if df > dr && dr < 0 {
return 1;
}
if df == dr && dr < 0 {
return 2;
}
if df < dr && dr < 0 {
return 3;
}
if df < 0 && dr == 0 {
return 4;
}
if df < -dr && dr > 0 {
return 5;
}
if df == -dr && dr > 0 {
return 6;
}
if df == 0 && dr > 0 {
return 8;
}
if df > -dr && dr > 0 {
return 7;
}
unreachable!("Unexpected direction calculation: df={df}, dr={dr}");
}
#[inline]
fn clamp_effect(count: u8, max: usize) -> usize {
usize::min(count as usize, max)
}
fn eval_lv1(pos: &Position) -> i32 {
pos.state().material_value.raw()
}
fn eval_lv2(pos: &Position) -> i32 {
let mut score = pos.state().material_value.raw();
for sq in pos.occupied().iter() {
let pc = pos.piece_on(sq);
score -= signed_piece_value(pc) * 104 / 1024;
}
score
}
fn eval_lv3(pos: &Position, effects: &BoardEffects) -> i32 {
let mut score = pos.state().material_value.raw();
let king_b = pos.king_square(Color::Black);
let king_w = pos.king_square(Color::White);
for sq in Square::all() {
let e_b = effects.effect(Color::Black, sq) as i32;
let e_w = effects.effect(Color::White, sq) as i32;
let d_b = dist(sq, king_b);
let d_w = dist(sq, king_w);
let s_b = e_b * LV3_OUR_EFFECT_VALUE[d_b] / 1024 - e_w * LV3_THEIR_EFFECT_VALUE[d_b] / 1024;
let s_w = e_w * LV3_OUR_EFFECT_VALUE[d_w] / 1024 - e_b * LV3_THEIR_EFFECT_VALUE[d_w] / 1024;
score += s_b;
score -= s_w;
let pc = pos.piece_on(sq);
if pc.is_some() {
score -= signed_piece_value(pc) * 104 / 1024;
}
}
score
}
fn eval_lv4(pos: &Position, effects: &BoardEffects) -> i32 {
let mut score = pos.state().material_value.raw();
let king_b = pos.king_square(Color::Black);
let king_w = pos.king_square(Color::White);
let mv = &*MULTI_EFFECT_VALUE;
for sq in Square::all() {
let e_b = clamp_effect(effects.effect(Color::Black, sq), 10);
let e_w = clamp_effect(effects.effect(Color::White, sq), 10);
let d_b = dist(sq, king_b);
let d_w = dist(sq, king_w);
let s_b = mv[e_b] * LV4_OUR_EFFECT_VALUE[d_b] / (1024 * 1024)
- mv[e_w] * LV4_THEIR_EFFECT_VALUE[d_b] / (1024 * 1024);
let s_w = mv[e_w] * LV4_OUR_EFFECT_VALUE[d_w] / (1024 * 1024)
- mv[e_b] * LV4_THEIR_EFFECT_VALUE[d_w] / (1024 * 1024);
score += s_b;
score -= s_w;
let pc = pos.piece_on(sq);
if pc.is_some() {
score -= signed_piece_value(pc) * 104 / 1024;
}
}
score
}
fn eval_lv7(pos: &Position, effects: &BoardEffects) -> i32 {
eval_lv7_like(pos, effects, false, false)
}
fn eval_lv8(pos: &Position, effects: &BoardEffects) -> i32 {
eval_lv7_like(pos, effects, true, false)
}
fn eval_lv9(pos: &Position, effects: &BoardEffects) -> i32 {
eval_lv7_like(pos, effects, true, true)
}
fn eval_lv7_like(
pos: &Position,
effects: &BoardEffects,
use_king_bonus: bool,
use_direction: bool,
) -> i32 {
let mut score = pos.state().material_value.raw();
let king_b = pos.king_square(Color::Black);
let king_w = pos.king_square(Color::White);
let inv_king_w = king_w.inverse();
let tables = &*LV7_TABLES;
for sq in Square::all() {
let m1 = clamp_effect(effects.effect(Color::Black, sq), 2);
let m2 = clamp_effect(effects.effect(Color::White, sq), 2);
let pc = pos.piece_on(sq);
let mut local = 0i32;
if use_direction {
let dir_b = direction_of(king_b, sq);
let inv_sq = sq.inverse();
let dir_w = direction_of(inv_king_w, inv_sq);
local += tables.our_effect_table[king_b.index()][sq.index()][m1]
* OUR_EFFECT_RATE[dir_b]
/ 1024;
local -= tables.their_effect_table[king_b.index()][sq.index()][m2]
* THEIR_EFFECT_RATE[dir_b]
/ 1024;
local -= tables.our_effect_table[inv_king_w.index()][inv_sq.index()][m2]
* OUR_EFFECT_RATE[dir_w]
/ 1024;
local += tables.their_effect_table[inv_king_w.index()][inv_sq.index()][m1]
* THEIR_EFFECT_RATE[dir_w]
/ 1024;
} else {
local += tables.our_effect_table[king_b.index()][sq.index()][m1];
local -= tables.their_effect_table[king_b.index()][sq.index()][m2];
let inv_sq = sq.inverse();
local -= tables.our_effect_table[inv_king_w.index()][inv_sq.index()][m2];
local += tables.their_effect_table[inv_king_w.index()][inv_sq.index()][m1];
}
for color in [Color::Black, Color::White] {
let king_sq = if color == Color::Black {
king_b
} else {
king_w
};
if dist(sq, king_sq) == 1 {
let effect_us = if color == Color::Black { m1 } else { m2 };
let delta = if effect_us <= 1 {
if pc.is_none() || pc.color() != color {
11
} else {
-20
}
} else if pc.is_none() || pc.color() != color {
0
} else {
-11
};
local -= delta * if color == Color::Black { 1 } else { -1 };
}
}
if pc.is_none() {
} else if pc.piece_type() == PieceType::King {
if use_king_bonus {
local += king_pos_bonus(pc.color(), sq);
}
} else {
let pv = signed_piece_value(pc);
local -= pv * 104 / 1024;
let effect_us = if pc.color() == Color::Black { m1 } else { m2 };
let effect_them = if pc.color() == Color::Black { m2 } else { m1 };
local += pv * OUR_EFFECT_TO_OUR_PIECE[effect_us] / 4096;
local -= pv * THEIR_EFFECT_TO_OUR_PIECE[effect_them] / 4096;
}
score += local;
}
score
}
pub fn evaluate_material(pos: &Position) -> Value {
let level = get_material_level();
let raw = match level {
MaterialLevel::Lv1 => eval_lv1(pos),
MaterialLevel::Lv2 => eval_lv2(pos),
MaterialLevel::Lv3 => {
let effects = pos.board_effects();
eval_lv3(pos, effects)
}
MaterialLevel::Lv4 => {
let effects = pos.board_effects();
eval_lv4(pos, effects)
}
MaterialLevel::Lv7 => {
let effects = pos.board_effects();
eval_lv7(pos, effects)
}
MaterialLevel::Lv8 => {
let effects = pos.board_effects();
eval_lv8(pos, effects)
}
MaterialLevel::Lv9 => {
let effects = pos.board_effects();
eval_lv9(pos, effects)
}
};
if pos.side_to_move() == Color::Black {
Value::new(raw)
} else {
Value::new(-raw)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::position::SFEN_HIRATE;
#[test]
fn test_material_eval_hirate() {
let mut pos = Position::new();
pos.set_sfen(SFEN_HIRATE).unwrap();
let value = evaluate_material(&pos);
assert!(value.raw().abs() < 200);
}
#[test]
fn test_material_level_value_roundtrip() {
let levels = [
MaterialLevel::Lv1,
MaterialLevel::Lv2,
MaterialLevel::Lv3,
MaterialLevel::Lv4,
MaterialLevel::Lv7,
MaterialLevel::Lv8,
MaterialLevel::Lv9,
];
for level in levels {
let value = level.value();
let restored = MaterialLevel::from_value(value).unwrap();
assert_eq!(level, restored);
}
}
#[test]
fn test_material_level_invalid_values() {
assert!(MaterialLevel::from_value(0).is_none());
assert!(MaterialLevel::from_value(5).is_none()); assert!(MaterialLevel::from_value(6).is_none()); assert!(MaterialLevel::from_value(10).is_none());
}
#[test]
fn test_get_set_material_level() {
let original = get_material_level();
let original_enabled = is_material_enabled();
set_material_level(MaterialLevel::Lv1);
assert_eq!(get_material_level(), MaterialLevel::Lv1);
assert!(is_material_enabled());
set_material_level(MaterialLevel::Lv9);
assert_eq!(get_material_level(), MaterialLevel::Lv9);
assert!(is_material_enabled());
disable_material();
assert!(!is_material_enabled());
if original_enabled {
set_material_level(original);
}
}
#[test]
fn test_compute_pass_right_value_early() {
const TEST_EARLY: i32 = 50;
const TEST_LATE: i32 = 200;
assert_eq!(compute_pass_right_value(0, TEST_EARLY, TEST_LATE), TEST_EARLY);
assert_eq!(compute_pass_right_value(20, TEST_EARLY, TEST_LATE), TEST_EARLY);
assert_eq!(compute_pass_right_value(40, TEST_EARLY, TEST_LATE), TEST_EARLY);
}
#[test]
fn test_compute_pass_right_value_late() {
const TEST_EARLY: i32 = 50;
const TEST_LATE: i32 = 200;
assert_eq!(compute_pass_right_value(120, TEST_EARLY, TEST_LATE), TEST_LATE);
assert_eq!(compute_pass_right_value(150, TEST_EARLY, TEST_LATE), TEST_LATE);
assert_eq!(compute_pass_right_value(200, TEST_EARLY, TEST_LATE), TEST_LATE);
}
#[test]
fn test_compute_pass_right_value_interpolation() {
const TEST_EARLY: i32 = 50;
const TEST_LATE: i32 = 200;
let mid_ply = (EARLY_PLY + LATE_PLY) / 2; let expected_mid = (TEST_EARLY + TEST_LATE) / 2;
assert_eq!(compute_pass_right_value(mid_ply, TEST_EARLY, TEST_LATE), expected_mid);
let ply_60 = compute_pass_right_value(60, TEST_EARLY, TEST_LATE);
assert!(ply_60 > TEST_EARLY);
assert!(ply_60 < TEST_LATE);
let ply_100 = compute_pass_right_value(100, TEST_EARLY, TEST_LATE);
assert!(ply_100 > ply_60);
assert!(ply_100 < TEST_LATE);
}
#[test]
fn test_pass_right_value_global_and_evaluation() {
use crate::types::Color;
let orig_early = PASS_RIGHT_VALUE_EARLY.load(std::sync::atomic::Ordering::Relaxed);
let orig_late = PASS_RIGHT_VALUE_LATE.load(std::sync::atomic::Ordering::Relaxed);
let orig_level = get_material_level();
set_pass_right_value_phased(50, 200);
assert_eq!(pass_right_value_by_ply(0), 50, "Early value should be 50");
assert_eq!(pass_right_value_by_ply(200), 200, "Late value should be 200");
set_pass_right_value(150);
assert_eq!(pass_right_value_by_ply(0), 150, "Both should be 150");
assert_eq!(pass_right_value_by_ply(200), 150, "Both should be 150");
set_material_level(MaterialLevel::Lv9);
set_pass_right_value_phased(200, 50);
let mut pos = Position::new();
pos.set_startpos_with_pass_rights(2, 2);
let pass_eval1 = evaluate_pass_rights(&pos, pos.game_ply() as u16);
assert_eq!(pass_eval1.raw(), 0, "Equal pass rights should give 0 eval");
pos.do_pass_move();
assert_eq!(pos.side_to_move(), Color::White);
assert_eq!(pos.pass_rights(Color::Black), 1);
assert_eq!(pos.pass_rights(Color::White), 2);
let material_eval = evaluate_material(&pos);
let pass_eval2 = evaluate_pass_rights(&pos, pos.game_ply() as u16);
assert_eq!(pass_eval2.raw(), 200, "White should have +200cp pass rights advantage");
assert_eq!(
material_eval.raw(),
0,
"Material eval should be 0 for starting position (pass rights added separately)"
);
let combined_eval = material_eval + pass_eval2;
let negamax_combined = -combined_eval.raw();
assert!(
negamax_combined < 0,
"Negamax combined score should be negative: got {negamax_combined}"
);
set_pass_right_value_phased(orig_early, orig_late);
set_material_level(orig_level);
}
}