use rand::Rng;
use crate::types::{Depth, Move, Value};
use super::RootMoves;
#[derive(Clone, Copy, Debug)]
pub struct SkillOptions {
pub skill_level: i32,
pub uci_limit_strength: bool,
pub uci_elo: i32,
}
impl Default for SkillOptions {
fn default() -> Self {
Self {
skill_level: 20,
uci_limit_strength: false,
uci_elo: 0,
}
}
}
#[derive(Clone, Debug)]
pub struct Skill {
level: f64,
pub best: Move,
}
impl Skill {
pub fn from_options(opts: &SkillOptions) -> Self {
const LOWEST_ELO: i32 = 1320;
const HIGHEST_ELO: i32 = 3190;
let level = if opts.uci_limit_strength && opts.uci_elo != 0 {
let e = (opts.uci_elo - LOWEST_ELO) as f64 / (HIGHEST_ELO - LOWEST_ELO) as f64;
(((37.2473 * e - 40.8525) * e + 22.2943) * e - 0.311438).clamp(0.0, 19.0)
} else {
opts.skill_level as f64
};
Self {
level,
best: Move::NONE,
}
}
pub fn enabled(&self) -> bool {
self.level < 20.0
}
pub fn time_to_pick(&self, depth: Depth) -> bool {
depth == 1 + self.level as Depth
}
pub fn pick_best<R: Rng + ?Sized>(
&mut self,
root_moves: &RootMoves,
multi_pv: usize,
rng: &mut R,
) -> Move {
if root_moves.is_empty() || multi_pv == 0 {
return Move::NONE;
}
let capped_multi_pv = multi_pv.min(root_moves.len());
let top_score = root_moves[0].score.raw();
let last_score = root_moves[capped_multi_pv - 1].score.raw();
let delta = (top_score - last_score).min(100);
let weakness = 120.0 - 2.0 * self.level;
let weakness_int = weakness.max(1.0) as u32;
let mut max_score = Value::new(-32001).raw();
let mut best_move = root_moves[0].mv();
for rm in root_moves.iter().take(capped_multi_pv) {
let rand_term = rng.random::<u32>() % weakness_int;
let push = ((weakness * (top_score - rm.score.raw()) as f64)
+ delta as f64 * rand_term as f64)
/ 128.0;
let candidate = rm.score.raw() + push as i32;
if candidate >= max_score {
max_score = candidate;
best_move = rm.mv();
}
}
self.best = best_move;
best_move
}
pub fn pick_best_from_pairs<R: Rng + ?Sized>(
&mut self,
top_moves: &[(Move, Value)],
rng: &mut R,
) -> Move {
if top_moves.is_empty() {
return Move::NONE;
}
let capped_len = top_moves.len().min(4); let top_score = top_moves[0].1.raw();
let last_score = top_moves[capped_len - 1].1.raw();
let delta = (top_score - last_score).min(100);
let weakness = 120.0 - 2.0 * self.level;
let weakness_int = weakness.max(1.0) as u32;
let mut max_score = Value::new(-32001).raw();
let mut best_move = top_moves[0].0;
for (mv, score) in top_moves.iter().take(capped_len) {
let rand_term = rng.random::<u32>() % weakness_int;
let push = ((weakness * (top_score - score.raw()) as f64)
+ delta as f64 * rand_term as f64)
/ 128.0;
let candidate = score.raw() + push as i32;
if candidate >= max_score {
max_score = candidate;
best_move = *mv;
}
}
self.best = best_move;
best_move
}
}
#[cfg(test)]
mod tests {
use rand::RngCore;
use crate::search::RootMove;
use crate::types::Move;
use super::*;
#[derive(Clone)]
struct FixedSeqRng {
data: Vec<u32>,
idx: usize,
}
impl FixedSeqRng {
fn new(seq: &[u32]) -> Self {
Self {
data: seq.to_vec(),
idx: 0,
}
}
fn next_val(&mut self) -> u32 {
let v = self.data.get(self.idx).copied().unwrap_or(0);
self.idx = (self.idx + 1) % self.data.len().max(1);
v
}
}
impl RngCore for FixedSeqRng {
fn next_u32(&mut self) -> u32 {
self.next_val()
}
fn next_u64(&mut self) -> u64 {
self.next_val() as u64
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
for chunk in dest.chunks_mut(8) {
let bytes = self.next_u64().to_le_bytes();
let len = chunk.len().min(8);
chunk[..len].copy_from_slice(&bytes[..len]);
}
}
}
#[test]
fn skill_enabled_flag() {
let s = Skill::from_options(&SkillOptions {
skill_level: 10,
..Default::default()
});
assert!(s.enabled());
let s = Skill::from_options(&SkillOptions {
skill_level: 20,
..Default::default()
});
assert!(!s.enabled());
}
#[test]
fn pick_best_prefers_weaker_move_with_high_weakness() {
let mut rng = FixedSeqRng::new(&[0, 119, 119, 119]); let mut skill = Skill::from_options(&SkillOptions {
skill_level: 0,
..Default::default()
});
let root_moves = RootMoves::from_vec(
vec![
(300, "7g7f"),
(50, "2g2f"), (0, "3g3f"),
(-50, "8h7g"),
]
.into_iter()
.map(|(score, mv)| {
let mut rm = RootMove::new(Move::from_usi(mv).unwrap());
rm.score = Value::new(score);
rm
})
.collect(),
);
let best = skill.pick_best(&root_moves, 4, &mut rng);
assert_eq!(best, Move::from_usi("2g2f").unwrap());
}
}