use std::cmp::Ordering;
use std::collections::HashMap;
use crate::board::Position;
use crate::board::movegen::{LegalAll, MoveListGen};
use crate::book::{
BookCandidate, BookDatabase, BookDatabaseEntry, BookEntryMetadata, BookError, BookKey,
BookMoveMetadata, BookPosition, book_key_from_position,
};
use crate::types::{Color, Move};
#[allow(dead_code)]
const BOOK_VALUE_INF: i16 = i16::MAX;
const BOOK_VALUE_NONE: i16 = i16::MIN;
const BOOK_VALUE_MATE: i16 = 32000;
const BOOK_VALUE_MAX: i16 = BOOK_VALUE_MATE;
const BOOK_VALUE_MIN: i16 = -BOOK_VALUE_MATE;
const BOOK_DEPTH_MAX: u16 = 9999;
const BOOK_DEPTH_PERPETUAL_CHECKED: u16 = BOOK_DEPTH_MAX - 1; const BOOK_DEPTH_PERPETUAL_CHECK: u16 = BOOK_DEPTH_MAX - 2;
const BOOK_MAX_PLY: u16 = 256;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ValueDepth {
pub(crate) value: i16,
pub(crate) depth: u16,
}
impl ValueDepth {
pub(crate) const fn new(value: i16, depth: u16) -> Self {
Self { value, depth }
}
}
impl Ord for ValueDepth {
fn cmp(&self, other: &Self) -> Ordering {
if self.value != other.value {
return self.value.cmp(&other.value);
}
let self_pc = self.depth == BOOK_DEPTH_PERPETUAL_CHECK;
let other_pc = other.depth == BOOK_DEPTH_PERPETUAL_CHECK;
match (self_pc, other_pc) {
(true, true) => return Ordering::Equal,
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
(false, false) => {}
}
if self.value >= 0 {
other.depth.cmp(&self.depth)
} else {
self.depth.cmp(&other.depth)
}
}
}
impl PartialOrd for ValueDepth {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn make_vd_for_parent(vd: ValueDepth) -> ValueDepth {
ValueDepth::new(vd.value.wrapping_neg(), (vd.depth + 1).min(BOOK_DEPTH_MAX))
}
fn leaf_value_depth(score: Option<i32>) -> ValueDepth {
let value = match score {
None => BOOK_VALUE_NONE,
Some(v) => v.clamp(i32::from(BOOK_VALUE_MIN), i32::from(BOOK_VALUE_MAX)) as i16,
};
ValueDepth::new(value, 0)
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum GraphMove {
Leaf { mv: Move, vd: ValueDepth },
Child { mv: Move, next: u32 },
}
impl GraphMove {
fn mv(&self) -> Move {
match self {
Self::Leaf { mv, .. } | Self::Child { mv, .. } => *mv,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct GraphNode {
moves: Vec<GraphMove>,
vd: ValueDepth,
color: Color,
sfen: String,
original_ply: Option<u32>,
const_node: bool,
checked: bool,
check_loop: bool,
}
pub(crate) struct BookGraph {
nodes: Vec<GraphNode>,
stats: BookGraphStats,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct BookGraphStats {
pub(crate) converged_moves: u64,
pub(crate) const_nodes: u64,
pub(crate) in_check: u64,
pub(crate) check_loop_nodes: u64,
}
impl BookGraph {
pub(crate) fn build(database: &BookDatabase) -> Result<Self, BookError> {
if !cfg!(feature = "hash-128") {
return Err(BookError::Unsupported(
"book solver requires the `hash-128` feature (128bit position hash); \
64bit hashing would merge distinct positions",
));
}
let mut nodes: Vec<GraphNode> = Vec::with_capacity(database.len());
let mut key_to_index: HashMap<BookKey, u32> = HashMap::with_capacity(database.len());
let mut stats = BookGraphStats::default();
for entry in database.entries() {
let sfen = entry.position().sfen();
let (black_pos, white_pos, color) = orientations(sfen)?;
let key = book_key_from_position(&white_pos);
let checked = !black_pos.checkers().is_empty();
if checked {
stats.in_check += 1;
}
let index = u32::try_from(nodes.len())
.map_err(|_| BookError::Unsupported("book solver node count exceeds u32"))?;
key_to_index.insert(key, index);
let mut moves = Vec::with_capacity(entry.candidates().len());
for candidate in entry.candidates() {
let mut mv = candidate.mv();
if mv == Move::MOVE_NONE {
continue;
}
if color == Color::WHITE {
mv = flip_move(mv)?;
}
moves.push(GraphMove::Leaf { mv, vd: leaf_value_depth(candidate.score()) });
}
nodes.push(GraphNode {
moves,
vd: ValueDepth::new(BOOK_VALUE_NONE, 0),
color,
sfen: sfen.to_string(),
original_ply: entry.position().original_ply(),
const_node: false,
checked,
check_loop: checked,
});
}
for node in &mut nodes {
let (black_pos, _white, _color) = orientations(&node.sfen)?;
let book_moves = std::mem::take(&mut node.moves);
let mut moves: Vec<GraphMove> = Vec::new();
for mv in MoveListGen::<LegalAll>::new(&black_pos).iter() {
let next_key = black_pos.key_after_move(*mv);
if let Some(&child) = key_to_index.get(&next_key) {
if !book_moves.iter().any(|bm| bm.mv() == *mv) {
stats.converged_moves += 1;
}
moves.push(GraphMove::Child { mv: *mv, next: child });
}
}
node.const_node = moves.is_empty();
for book_move in &book_moves {
let mv = book_move.mv();
if !moves.iter().any(|m| m.mv() == mv) {
moves.push(*book_move);
}
}
node.moves = moves;
}
Ok(Self { nodes, stats })
}
#[allow(dead_code)]
pub(crate) const fn stats(&self) -> BookGraphStats {
self.stats
}
pub(crate) fn remove_const_nodes(&mut self) {
for node in &mut self.nodes {
node.const_node = false;
}
for _ in 0..BOOK_MAX_PLY {
let count = self.remove_const_nodes_once();
self.stats.const_nodes += count;
if count == 0 {
break;
}
}
}
fn remove_const_nodes_once(&mut self) -> u64 {
let mut count = 0;
for i in 0..self.nodes.len() {
if self.nodes[i].const_node {
continue;
}
let all_const = self.nodes[i].moves.iter().all(|m| match m {
GraphMove::Leaf { .. } => true,
GraphMove::Child { next, .. } => self.nodes[*next as usize].const_node,
});
if !all_const {
continue;
}
let vd = self.bestvd_for_parent(i);
self.nodes[i].vd = vd;
self.nodes[i].const_node = true;
count += 1;
}
count
}
fn bestvd_for_parent(&self, node_index: usize) -> ValueDepth {
let mut best: Option<ValueDepth> = None;
for m in &self.nodes[node_index].moves {
let vd = match m {
GraphMove::Leaf { vd, .. } => *vd,
GraphMove::Child { next, .. } => self.nodes[*next as usize].vd,
};
if best.is_none_or(|b| vd > b) {
best = Some(vd);
}
}
let best = best.unwrap_or(ValueDepth::new(BOOK_VALUE_NONE, BOOK_DEPTH_MAX));
make_vd_for_parent(best)
}
pub(crate) fn extract_check_loop(&mut self) {
for _ in 0..BOOK_MAX_PLY {
let mut updated = 0u64;
for i in 0..self.nodes.len() {
if !self.nodes[i].check_loop {
continue;
}
if !self.two_ply_has_check_loop(i) {
self.nodes[i].check_loop = false;
updated += 1;
}
}
if updated == 0 {
break;
}
}
self.stats.check_loop_nodes =
self.nodes.iter().filter(|node| node.check_loop).count() as u64;
let seeds: Vec<usize> =
(0..self.nodes.len()).filter(|&i| self.nodes[i].check_loop).collect();
for i in seeds {
let next_indices: Vec<u32> = self.nodes[i]
.moves
.iter()
.filter_map(|m| match m {
GraphMove::Child { next, .. } => Some(*next),
GraphMove::Leaf { .. } => None,
})
.collect();
for next in next_indices {
let has_check_loop_grandchild =
self.nodes[next as usize].moves.iter().any(|m| match m {
GraphMove::Child { next: nn, .. } => self.nodes[*nn as usize].check_loop,
GraphMove::Leaf { .. } => false,
});
if has_check_loop_grandchild {
self.nodes[next as usize].check_loop = true;
}
}
}
}
fn two_ply_has_check_loop(&self, node_index: usize) -> bool {
for m in &self.nodes[node_index].moves {
let GraphMove::Child { next, .. } = m else {
continue;
};
for m2 in &self.nodes[*next as usize].moves {
let GraphMove::Child { next: nn, .. } = m2 else {
continue;
};
if self.nodes[*nn as usize].check_loop {
return true;
}
}
}
false
}
pub(crate) fn init_cycle_nodes(&mut self) {
for node in &mut self.nodes {
if node.const_node {
continue;
}
node.vd = if node.check_loop {
let value = if node.checked { BOOK_VALUE_MIN } else { BOOK_VALUE_MAX };
ValueDepth::new(value, BOOK_DEPTH_MAX)
} else {
ValueDepth::new(0, BOOK_DEPTH_MAX)
};
}
}
pub(crate) fn propagate_all_nodes(&mut self) -> Result<(), BookError> {
let check_loop_nodes: Vec<u32> =
(0..self.nodes.len()).filter(|&i| self.nodes[i].check_loop).map(|i| i as u32).collect();
for _ in 0..(BOOK_MAX_PLY + 100) {
let updating = self.propagate_all_nodes_once();
self.dfs_for_check_loop_nodes(&check_loop_nodes)?;
if updating == 0 {
break;
}
}
Ok(())
}
fn propagate_all_nodes_once(&mut self) -> u64 {
let mut count = 0;
for i in 0..self.nodes.len() {
if self.nodes[i].const_node || self.nodes[i].check_loop {
continue;
}
let mut best = self.bestvd_for_parent(i);
if best.depth > BOOK_MAX_PLY {
best.depth = BOOK_DEPTH_MAX;
}
if self.nodes[i].vd != best {
count += 1;
}
self.nodes[i].vd = best;
}
count
}
fn dfs_for_check_loop_nodes(&mut self, check_loop_nodes: &[u32]) -> Result<(), BookError> {
let mut trajectory: Vec<u32> = Vec::new();
for &node_index in check_loop_nodes {
let vd = self.dfs_for_check_loop_node(node_index, &mut trajectory, 0)?;
self.nodes[node_index as usize].vd = vd;
}
Ok(())
}
fn dfs_for_check_loop_node(
&self,
node_index: u32,
trajectory: &mut Vec<u32>,
depth: u32,
) -> Result<ValueDepth, BookError> {
let node = &self.nodes[node_index as usize];
if !node.check_loop {
return Ok(node.vd);
}
if trajectory.contains(&node_index) {
return Ok(if node.checked {
ValueDepth::new(-BOOK_VALUE_MAX, BOOK_DEPTH_PERPETUAL_CHECK)
} else {
ValueDepth::new(BOOK_VALUE_MAX, BOOK_DEPTH_PERPETUAL_CHECKED)
});
}
if depth as usize > self.nodes.len() + 16 {
return Err(BookError::InvalidData(
"book solver check-loop DFS exceeded depth guard".into(),
));
}
trajectory.push(node_index);
let mut best: Option<ValueDepth> = None;
for move_index in 0..node.moves.len() {
let vd = match self.nodes[node_index as usize].moves[move_index] {
GraphMove::Leaf { vd, .. } => vd,
GraphMove::Child { next, .. } => {
self.dfs_for_check_loop_node(next, trajectory, depth + 1)?
}
};
if best.is_none_or(|b| vd > b) {
best = Some(vd);
}
}
if let Some(pos) = trajectory.iter().rposition(|&x| x == node_index) {
trajectory.remove(pos);
}
let best = best.unwrap_or(ValueDepth::new(BOOK_VALUE_NONE, BOOK_DEPTH_MAX));
Ok(make_vd_for_parent(best))
}
pub(crate) fn project(&self, shrink: bool) -> Result<BookDatabase, BookError> {
let mut entries: Vec<BookDatabaseEntry> = Vec::with_capacity(self.nodes.len());
for node in &self.nodes {
let mut moves: Vec<(Move, ValueDepth)> = Vec::with_capacity(node.moves.len());
for m in &node.moves {
match m {
GraphMove::Leaf { mv, vd } => moves.push((*mv, *vd)),
GraphMove::Child { mv, next } => {
let child = &self.nodes[*next as usize];
let mut vd = child.vd;
if child.check_loop && child.checked {
vd.depth = BOOK_DEPTH_PERPETUAL_CHECK;
}
moves.push((*mv, vd));
}
}
}
moves.sort_by_key(|(_, vd)| core::cmp::Reverse(*vd));
let best_value = moves.first().map(|(_, vd)| vd.value);
let best_depth = moves.first().map(|(_, vd)| vd.depth);
let mut candidates: Vec<BookCandidate> = Vec::with_capacity(moves.len());
for (i, (mv, vd)) in moves.iter().enumerate() {
if shrink && best_value != Some(vd.value) {
continue;
}
let mut out_vd = *vd;
if i > 0 && best_value == Some(vd.value) && best_depth != Some(vd.depth) {
out_vd.value = out_vd.value.saturating_sub(1);
}
let out_mv = if node.color == Color::WHITE { flip_move(*mv)? } else { *mv };
candidates.push(BookCandidate::new(
out_mv,
None,
Some(i32::from(out_vd.value)),
Some(u32::from(out_vd.depth)),
None,
None,
BookMoveMetadata::new(),
));
}
let position = BookPosition::from_sfen(&node.sfen, node.original_ply)?;
entries.push(BookDatabaseEntry::new(position, candidates, BookEntryMetadata::new()));
}
Ok(BookDatabase::from_entries_unchecked(entries))
}
}
fn orientations(sfen: &str) -> Result<(Position, Position, Color), BookError> {
let original =
Position::from_sfen(sfen).map_err(|err| BookError::InvalidData(err.to_string()))?;
let color = original.turn();
let flipped = Position::from_sfen(&original.to_sfen_flipped(None))
.map_err(|err| BookError::InvalidData(err.to_string()))?;
let (black_pos, white_pos) = match color {
Color::BLACK => (original, flipped),
Color::WHITE => (flipped, original),
};
Ok((black_pos, white_pos, color))
}
fn flip_move(mv: Move) -> Result<Move, BookError> {
if !mv.is_normal() {
return Ok(mv);
}
let to = mv.to_sq().flip();
if mv.is_drop() {
let piece_type = mv.dropped_piece().ok_or_else(|| {
BookError::InvalidData(format!("invalid book solver drop move: 0x{:04x}", mv.raw()))
})?;
Ok(Move::drop(piece_type, to))
} else {
let from = mv.from_sq().flip();
Ok(if mv.is_promotion() { Move::promotion(from, to) } else { Move::normal(from, to) })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PetaShockOptions {
shrink: bool,
}
impl Default for PetaShockOptions {
fn default() -> Self {
Self::new()
}
}
impl PetaShockOptions {
#[must_use]
pub const fn new() -> Self {
Self { shrink: false }
}
#[must_use]
pub const fn with_shrink(mut self, shrink: bool) -> Self {
self.shrink = shrink;
self
}
#[must_use]
pub const fn shrink(self) -> bool {
self.shrink
}
}
pub fn solve_peta_shock_book(
database: &BookDatabase,
options: &PetaShockOptions,
) -> Result<BookDatabase, BookError> {
let mut graph = BookGraph::build(database)?;
graph.remove_const_nodes();
graph.extract_check_loop();
graph.init_cycle_nodes();
graph.propagate_all_nodes()?;
graph.project(options.shrink())
}
#[cfg(all(test, feature = "hash-128"))]
mod tests {
use super::*;
use crate::board::{self, InitialPosition};
use crate::book::{YaneuraOuBook, YaneuraOuBookDiagnostics, YaneuraOuDb2016WriteOptions};
use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use std::time::{SystemTime, UNIX_EPOCH};
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
fn norm(sfen: &str) -> String {
Position::from_sfen(sfen).unwrap().to_sfen(None)
}
fn child(parent_sfen: &str, usi: &str) -> String {
let mut pos = Position::from_sfen(parent_sfen).unwrap();
pos.apply_move(Move::from_usi(usi).unwrap());
pos.to_sfen(None)
}
fn cand(usi: &str, score: Option<i32>) -> BookCandidate {
BookCandidate::new(
Move::from_usi(usi).unwrap(),
None,
score,
None,
None,
None,
BookMoveMetadata::new(),
)
}
type EntrySpec<'a> = (&'a str, &'a [(&'a str, Option<i32>)]);
fn make_db(entries: &[EntrySpec]) -> BookDatabase {
let es = entries
.iter()
.map(|(sfen, moves)| {
let position = BookPosition::from_sfen(sfen, Some(1)).unwrap();
let candidates = moves.iter().map(|(usi, score)| cand(usi, *score)).collect();
BookDatabaseEntry::new(position, candidates, BookEntryMetadata::new())
})
.collect();
BookDatabase::try_from_entries(es).unwrap()
}
fn candidates_of(db: &BookDatabase, sfen: &str) -> Vec<(String, Option<i32>, Option<u32>)> {
let entry = db.entry_by_sfen(sfen).unwrap().expect("entry");
entry.candidates().iter().map(|c| (c.mv().to_usi(), c.score(), c.depth())).collect()
}
fn temp_path() -> PathBuf {
let stamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
let seq = TEMP_COUNTER.fetch_add(1, AtomicOrdering::Relaxed);
std::env::temp_dir()
.join(format!("rsshogi-book-solver-{}-{stamp}-{seq}.db", std::process::id()))
}
#[test]
fn test_build_discovers_convergence_edge_for_unrecorded_move() {
board::init();
let r = InitialPosition::Standard.to_sfen().to_string();
let a = child(&r, "7g7f");
let db = make_db(&[(&r, &[]), (&a, &[("3c3d", Some(100))])]);
let graph = BookGraph::build(&db).unwrap();
let stats = graph.stats();
assert_eq!(stats.converged_moves, 1);
assert_eq!(stats.in_check, 0);
}
#[test]
fn test_build_marks_checked_node() {
board::init();
let checked_sfen = norm("4k4/9/9/9/4r4/9/9/9/4K4 b - 1");
let db = make_db(&[(&checked_sfen, &[("5i6i", Some(0))])]);
let graph = BookGraph::build(&db).unwrap();
assert_eq!(graph.stats().in_check, 1);
}
#[test]
fn test_acyclic_minimax_propagates_negamax_values() {
board::init();
let r = InitialPosition::Standard.to_sfen().to_string();
let a = child(&r, "7g7f");
let db = make_db(&[(&r, &[("7g7f", None)]), (&a, &[("3c3d", Some(100))])]);
let solved = solve_peta_shock_book(&db, &PetaShockOptions::new()).unwrap();
let a_norm = norm(&a);
assert_eq!(candidates_of(&solved, &a_norm), vec![("3c3d".to_string(), Some(100), Some(0))]);
let r_norm = norm(&r);
assert_eq!(
candidates_of(&solved, &r_norm),
vec![("7g7f".to_string(), Some(-100), Some(1))]
);
}
#[test]
fn test_minimax_chooses_best_child_with_depth_tiebreak() {
board::init();
let r = InitialPosition::Standard.to_sfen().to_string();
let a = child(&r, "7g7f"); let b = child(&r, "2g2f"); let db = make_db(&[
(&r, &[("7g7f", None), ("2g2f", None)]),
(&a, &[("3c3d", Some(50))]),
(&b, &[("3c3d", Some(200))]),
]);
let solved = solve_peta_shock_book(&db, &PetaShockOptions::new()).unwrap();
let r_norm = norm(&r);
let cands = candidates_of(&solved, &r_norm);
assert_eq!(cands[0].0, "7g7f");
assert_eq!(cands[0].1, Some(-50));
assert_eq!(cands[1].0, "2g2f");
assert_eq!(cands[1].1, Some(-200));
}
#[test]
fn test_simple_repetition_converges_to_draw_zero() {
board::init();
let s0 = norm("4k4/9/9/9/9/9/9/9/4K4 b - 1");
let s1 = child(&s0, "5i4i");
let s2 = child(&s1, "5a4a");
let s3 = child(&s2, "4i5i");
let back = child(&s3, "4a5a");
let strip_ply = |s: &str| s.rsplit_once(' ').unwrap().0.to_string();
assert_eq!(strip_ply(&back), strip_ply(&s0), "4 手で元局面へ戻る repetition のはず");
let db = make_db(&[
(&s0, &[("5i4i", None)]),
(&s1, &[("5a4a", None)]),
(&s2, &[("4i5i", None)]),
(&s3, &[("4a5a", None)]),
]);
let solved = solve_peta_shock_book(&db, &PetaShockOptions::new()).unwrap();
for s in [&s0, &s1, &s2, &s3] {
let cands = candidates_of(&solved, &norm(s));
assert!(!cands.is_empty(), "cycle node に手が無い: {s}");
for (_, score, _) in &cands {
assert_eq!(*score, Some(0), "千日手なら value 0 のはず: {s}");
}
}
}
#[test]
fn test_perpetual_check_marks_sentinel_depth_and_terminates() {
board::init();
let p0 = norm("4k4/9/9/9/4R4/9/9/9/4K4 w - 1");
let p1 = child(&p0, "5a4a");
let p2 = child(&p1, "5e4e");
let p3 = child(&p2, "4a5a");
let back = child(&p3, "4e5e");
let strip_ply = |s: &str| s.rsplit_once(' ').unwrap().0.to_string();
assert_eq!(strip_ply(&back), strip_ply(&p0), "連続王手 4 手で元局面へ戻るはず");
let db = make_db(&[
(&p0, &[("5a4a", None)]),
(&p1, &[("5e4e", None)]),
(&p2, &[("4a5a", None)]),
(&p3, &[("4e5e", None)]),
]);
let graph = BookGraph::build(&db).unwrap();
assert_eq!(graph.stats().in_check, 2);
let solved = solve_peta_shock_book(&db, &PetaShockOptions::new()).unwrap();
let p1_cands = candidates_of(&solved, &norm(&p1));
assert!(
p1_cands.iter().any(|(_, _, depth)| *depth == Some(9997)),
"連続王手の千日手へ入る手は depth 9997 で印付けされるはず: {p1_cands:?}"
);
}
#[test]
fn test_unevaluated_child_does_not_propagate_phantom_win() {
board::init();
let r = InitialPosition::Standard.to_sfen().to_string();
let a = child(&r, "7g7f");
let b = child(&r, "2g2f");
let db = make_db(&[
(&r, &[("7g7f", None), ("2g2f", None)]),
(&a, &[("3c3d", None)]),
(&b, &[("3c3d", Some(50))]),
]);
let solved = solve_peta_shock_book(&db, &PetaShockOptions::new()).unwrap();
let cands = candidates_of(&solved, &norm(&r));
assert_eq!(cands[0].0, "2g2f");
assert_eq!(cands[0].1, Some(-50));
assert_eq!(cands[1].0, "7g7f");
assert_eq!(cands[1].1, Some(i32::from(i16::MIN)));
assert!(cands.iter().all(|(_, score, _)| *score != Some(i32::from(i16::MAX))));
}
#[test]
fn test_empty_child_node_does_not_propagate_phantom_win() {
board::init();
let r = InitialPosition::Standard.to_sfen().to_string();
let a = child(&r, "7g7f");
let db = make_db(&[(&r, &[("7g7f", None)]), (&a, &[])]);
let solved = solve_peta_shock_book(&db, &PetaShockOptions::new()).unwrap();
let cands = candidates_of(&solved, &norm(&r));
assert_eq!(cands.len(), 1);
assert_eq!(cands[0].0, "7g7f");
assert_eq!(cands[0].1, Some(i32::from(i16::MIN)));
}
#[test]
fn test_end_to_end_writer_peta_shock_profile() {
board::init();
let r = InitialPosition::Standard.to_sfen().to_string();
let a = child(&r, "7g7f");
let db = make_db(&[(&r, &[("7g7f", None)]), (&a, &[("3c3d", Some(100))])]);
let solved = solve_peta_shock_book(&db, &PetaShockOptions::new()).unwrap();
let text =
solved.to_yaneuraou_db2016_string(&YaneuraOuDb2016WriteOptions::peta_shock()).unwrap();
for line in text.lines().filter(|l| !l.starts_with('#') && !l.starts_with("sfen")) {
let fields: Vec<&str> = line.split_whitespace().collect();
assert_eq!(fields.len(), 4, "4 列のはず: {line}");
assert_eq!(fields[1], "none", "ponder は常に none: {line}");
}
let path = temp_path();
fs::write(&path, &text).unwrap();
let book = YaneuraOuBook::open(&path).unwrap();
assert!(matches!(
book.diagnostics(),
YaneuraOuBookDiagnostics::Sorted { complete: true, .. }
));
fs::remove_file(path).unwrap();
}
#[test]
fn test_shrink_keeps_only_best_value_moves() {
board::init();
let r = InitialPosition::Standard.to_sfen().to_string();
let a = child(&r, "7g7f");
let b = child(&r, "2g2f");
let db = make_db(&[
(&r, &[("7g7f", None), ("2g2f", None)]),
(&a, &[("3c3d", Some(50))]),
(&b, &[("3c3d", Some(200))]),
]);
let full = solve_peta_shock_book(&db, &PetaShockOptions::new()).unwrap();
assert_eq!(candidates_of(&full, &norm(&r)).len(), 2);
let shrunk =
solve_peta_shock_book(&db, &PetaShockOptions::new().with_shrink(true)).unwrap();
let cands = candidates_of(&shrunk, &norm(&r));
assert_eq!(cands.len(), 1);
assert_eq!(cands[0].0, "7g7f");
}
}