use std::io::{self, BufRead, Write};
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::JoinHandle;
use std::time::Duration;
use sekirei_core::{
board::Board,
color::Color,
nnue::load_weights,
search::{SearchConfig, SpeculativeSearcher},
sfen::{move_to_usi, parse_position_cmd},
tt::Tt,
};
const ENGINE_NAME: &str = "Sekirei";
const ENGINE_AUTHOR: &str = "ke.tanabe@gmail.com";
const DEFAULT_HASH_MB: usize = 64;
fn main() {
if let Some(path) = std::env::args().nth(1) {
match load_weights(Path::new(&path)) {
Ok(()) => eprintln!("info string NNUE weights loaded from {path}"),
Err(e) => eprintln!("info string weight load failed ({path}): {e}"),
}
}
let stdin = io::stdin();
let stdout = io::stdout();
let mut hash_mb = DEFAULT_HASH_MB;
let mut searcher = make_searcher(hash_mb);
let mut eval_file: Option<String> = None;
let mut move_overhead_ms: u64 = 50;
let mut board = Board::startpos();
let mut search_abort: Option<Arc<AtomicBool>> = None;
let mut search_handle: Option<JoinHandle<()>> = None;
for raw in stdin.lock().lines() {
let Ok(line) = raw else { break };
let line = line.trim().to_string();
if line.is_empty() {
continue;
}
let (cmd, rest) = line
.split_once(' ')
.map(|(c, r)| (c, r.trim()))
.unwrap_or((&line, ""));
match cmd {
"usi" => {
println!("id name {ENGINE_NAME}");
println!("id author {ENGINE_AUTHOR}");
println!("option name Hash type spin default {DEFAULT_HASH_MB} min 1 max 2048");
println!("option name Threads type spin default 0 min 0 max 512");
println!("option name MoveOverhead type spin default 50 min 0 max 5000");
println!("option name Ponder type check default false");
println!("option name EvalFile type string default ");
println!("usiok");
stdout.lock().flush().ok();
}
"isready" => {
if let Some(ref path) = eval_file
&& !sekirei_core::nnue::weights_active()
{
match sekirei_core::nnue::load_weights(Path::new(path)) {
Ok(()) => println!("info string NNUE weights loaded from {path}"),
Err(e) => println!("info string weight load failed: {e}"),
}
}
println!("readyok");
stdout.lock().flush().ok();
}
"setoption" => {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.get(1) == Some(&"Hash")
&& let Some(mb) = parts.get(3).and_then(|s| s.parse().ok())
{
hash_mb = mb;
searcher = make_searcher(hash_mb);
} else if parts.get(1) == Some(&"Threads") {
if let Some(n) = parts.get(3).and_then(|s| s.parse::<usize>().ok()) {
let _ = rayon::ThreadPoolBuilder::new()
.num_threads(n)
.build_global();
}
} else if parts.get(1) == Some(&"MoveOverhead") {
if let Some(n) = parts.get(3).and_then(|s| s.parse().ok()) {
move_overhead_ms = n;
}
} else if parts.get(1) == Some(&"EvalFile") {
if let Some(val) = rest.split_once("value ").map(|(_, v)| v.trim())
&& !val.is_empty()
{
eval_file = Some(val.to_string());
}
}
}
"usinewgame" => {
if let Some(a) = search_abort.take() {
a.store(true, Ordering::Relaxed);
}
if let Some(h) = search_handle.take() {
h.join().ok();
}
board = Board::startpos();
}
"position" => match parse_position_cmd(rest) {
Ok(b) => board = b,
Err(e) => eprintln!("position error: {e}"),
},
"go" => {
if let Some(prev) = search_abort.take() {
prev.store(true, Ordering::Relaxed);
}
if let Some(h) = search_handle.take() {
h.join().ok();
}
let pondering = rest.split_whitespace().any(|t| t == "ponder");
let config = parse_go(rest, board.side_to_move, move_overhead_ms, pondering);
let abort = searcher.abort_flag();
search_abort = Some(abort);
let searcher2 = Arc::clone(&searcher);
let mut board2 = board.clone();
search_handle = Some(std::thread::spawn(move || {
let info = searcher2.search(&mut board2, config);
let elapsed_ms = info.elapsed.as_millis().max(1) as u64;
let nps = info.nodes.saturating_mul(1000) / elapsed_ms;
if let Some(m) = info.best_move {
println!(
"info depth {} score cp {} nodes {} nps {} time {} hashfull {} pv {}",
info.depth,
info.score,
info.nodes,
nps,
elapsed_ms,
info.hashfull,
move_to_usi(m)
);
}
let best = info
.best_move
.map(move_to_usi)
.unwrap_or_else(|| "resign".to_string());
println!("bestmove {best}");
io::stdout().lock().flush().ok();
}));
}
"stop" => {
if let Some(a) = search_abort.take() {
a.store(true, Ordering::Relaxed);
}
if let Some(h) = search_handle.take() {
h.join().ok();
}
}
"ponderhit" => {
if let Some(a) = search_abort.take() {
a.store(true, Ordering::Relaxed);
}
if let Some(h) = search_handle.take() {
h.join().ok();
}
}
"gameover" => {}
"quit" => {
if let Some(a) = search_abort.take() {
a.store(true, Ordering::Relaxed);
}
if let Some(h) = search_handle.take() {
h.join().ok();
}
break;
}
_ => {
eprintln!("unknown command: '{cmd}'");
}
}
}
}
fn make_searcher(hash_mb: usize) -> Arc<SpeculativeSearcher> {
Arc::new(SpeculativeSearcher::new(Tt::new(hash_mb), 3))
}
fn parse_go(args: &str, side: Color, overhead_ms: u64, pondering: bool) -> SearchConfig {
let mut btime: Option<u64> = None;
let mut wtime: Option<u64> = None;
let mut byoyomi: Option<u64> = None;
let mut movetime: Option<u64> = None;
let mut depth: Option<u32> = None;
let mut infinite = false;
let tokens: Vec<&str> = args.split_whitespace().collect();
let mut i = 0;
while i < tokens.len() {
match tokens[i] {
"btime" => {
i += 1;
btime = tokens.get(i).and_then(|s| s.parse().ok());
}
"wtime" => {
i += 1;
wtime = tokens.get(i).and_then(|s| s.parse().ok());
}
"byoyomi" => {
i += 1;
byoyomi = tokens.get(i).and_then(|s| s.parse().ok());
}
"movetime" => {
i += 1;
movetime = tokens.get(i).and_then(|s| s.parse().ok());
}
"depth" => {
i += 1;
depth = tokens.get(i).and_then(|s| s.parse().ok());
}
"infinite" => {
infinite = true;
}
_ => {}
}
i += 1;
}
let has_clock = btime.is_some() || wtime.is_some() || byoyomi.is_some() || movetime.is_some();
let time_limit = if infinite || pondering {
None
} else if let Some(mt) = movetime {
Some(Duration::from_millis(
mt.saturating_sub(overhead_ms).max(50),
))
} else if depth.is_some() && !has_clock {
None } else if has_clock {
let our_time = match side {
Color::Black => btime.unwrap_or(0),
Color::White => wtime.unwrap_or(0),
};
let byo_ms = byoyomi.unwrap_or(0);
let divisor = if our_time < 30_000 { 15 } else { 30 };
let from_main = if our_time > 0 { our_time / divisor } else { 0 };
let from_byo = byo_ms * 13 / 20;
let alloc = if our_time < 5_000 && byo_ms > 0 {
from_byo
} else {
from_main.max(from_byo)
};
let alloc = alloc.saturating_sub(overhead_ms).max(50);
Some(Duration::from_millis(alloc))
} else {
None };
SearchConfig {
max_depth: depth.unwrap_or(50),
time_limit,
}
}