dpv007 commited on
Commit
b7046d9
·
verified ·
1 Parent(s): 00266cb

Upload folder using huggingface_hub

Browse files
model_int8.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8e9978d354d36850b50e91f1efe65042c93aa9c34471e3e6044b4240d9592737
3
+ size 473391
neural_engine/Cargo.toml CHANGED
@@ -10,3 +10,4 @@ serde = { version = "1.0", features = ["derive"] }
10
  serde_json = "1.0"
11
  rand = "0.8.5"
12
  rand_distr = "0.4.3"
 
 
10
  serde_json = "1.0"
11
  rand = "0.8.5"
12
  rand_distr = "0.4.3"
13
+ shakmaty-syzygy = "0.24.0"
neural_engine/src/main.rs CHANGED
@@ -7,6 +7,10 @@ use shakmaty::zobrist::{Zobrist64, ZobristHash};
7
  use ort::session::Session;
8
  use ort::session::builder::GraphOptimizationLevel;
9
  use rand_distr::{Dirichlet, Distribution};
 
 
 
 
10
 
11
  #[derive(Clone)]
12
  struct MCTSNode {
@@ -27,6 +31,27 @@ impl MCTSNode {
27
  }
28
  }
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  fn add_dirichlet_noise(node: &mut MCTSNode, alpha: f32, epsilon: f32) {
31
  if node.children.len() <= 1 { return; }
32
  let dirichlet = Dirichlet::new(&vec![alpha; node.children.len()]).unwrap();
@@ -56,6 +81,27 @@ fn material_difference(board: &Chess, color: shakmaty::Color) -> i32 {
56
  evaluate_material(board, color) - evaluate_material(board, other)
57
  }
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  fn quiescence_search(board: &Chess, mut alpha: i32, beta: i32, depth: u8) -> i32 {
60
  let in_check = board.is_check();
61
  let stand_pat = material_difference(board, board.turn());
@@ -71,7 +117,9 @@ fn quiescence_search(board: &Chess, mut alpha: i32, beta: i32, depth: u8) -> i32
71
  }
72
  }
73
 
74
- let legals = board.legal_moves();
 
 
75
  for m in &legals {
76
  if m.is_capture() || in_check {
77
  let mut next_board = board.clone();
@@ -88,6 +136,51 @@ fn quiescence_search(board: &Chess, mut alpha: i32, beta: i32, depth: u8) -> i32
88
  alpha
89
  }
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  fn load_vocab() -> (HashMap<String, i64>, HashMap<i64, String>) {
92
  let data = fs::read_to_string("vocab.json").expect("Unable to read vocab.json. Please run build_engine.bat first to generate it!");
93
  let vocab: HashMap<String, i64> = serde_json::from_str(&data).expect("JSON format error");
@@ -161,6 +254,7 @@ fn mcts_simulate(
161
  vocab: &HashMap<String, i64>,
162
  inv_vocab: &HashMap<i64, String>,
163
  nn_cache: &mut HashMap<u64, (HashMap<String, f32>, f32)>,
 
164
  ) -> f32 {
165
  if board.is_game_over() {
166
  if board.is_checkmate() {
@@ -169,6 +263,18 @@ fn mcts_simulate(
169
  return 0.0; // Draw
170
  }
171
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  if node.children.is_empty() {
173
  // Expand
174
  let legals = board.legal_moves();
@@ -250,7 +356,7 @@ fn mcts_simulate(
250
  history.push(best_action.clone());
251
 
252
  let child_node = node.children.get_mut(&best_action).unwrap();
253
- let val = mcts_simulate(child_node, next_board, history, session, vocab, inv_vocab, nn_cache);
254
 
255
  history.pop();
256
 
@@ -269,15 +375,21 @@ fn main() {
269
  // Ensure ONNX library is set up implicitly by ort
270
  let _ = ort::init().with_name("neural_engine").commit();
271
 
272
- let mut session = if Path::new("model.onnx").exists() {
273
  Some(Session::builder().unwrap()
274
  .with_optimization_level(GraphOptimizationLevel::Level3).unwrap()
275
  .with_intra_threads(4).unwrap()
276
- .commit_from_file("model.onnx").unwrap())
277
  } else {
278
  None
279
  };
280
 
 
 
 
 
 
 
281
  let (vocab, inv_vocab) = load_vocab();
282
 
283
  let mut nn_cache: HashMap<u64, (HashMap<String, f32>, f32)> = HashMap::new();
@@ -397,61 +509,139 @@ fn main() {
397
  movetime_ms = 100;
398
  }
399
 
 
 
 
 
 
 
 
 
 
400
  if let Some(ref mut sess) = session {
401
  let start_time = std::time::Instant::now();
402
  // Leave a small buffer (5% or minimum 50ms) to ensure we reply in time
403
  let buffer = (movetime_ms as f64 * 0.05) as u128;
404
- let safe_movetime = if movetime_ms > buffer { movetime_ms - buffer } else { movetime_ms };
405
- let limit = std::time::Duration::from_millis(safe_movetime as u64);
406
-
407
  if global_root.children.is_empty() {
408
- mcts_simulate(&mut global_root, board.clone(), &mut history_moves, sess, &vocab, &inv_vocab, &mut nn_cache);
409
  }
410
  add_dirichlet_noise(&mut global_root, 0.3, 0.25);
411
 
412
  let mut iter_count = 0;
413
 
414
  // Loop continuously until time expires
415
- while start_time.elapsed() < limit {
416
- mcts_simulate(&mut global_root, board.clone(), &mut history_moves, sess, &vocab, &inv_vocab, &mut nn_cache);
 
 
417
  iter_count += 1;
418
 
419
- // Stream UCI info every 100 iterations so !eval works
420
  if iter_count % 100 == 0 {
421
  let mut best_eval = 0.0;
422
  let mut best_visits = -1;
 
423
  let mut best_action = String::new();
 
424
  for (action, child) in &global_root.children {
425
- if (child.visit_count as i32) > best_visits {
426
- best_visits = child.visit_count as i32;
 
 
427
  best_action = action.clone();
428
  if child.visit_count > 0 {
429
  best_eval = child.value_sum / (child.visit_count as f32);
430
  }
 
 
431
  }
432
  }
433
- let cp = (best_eval * 1000.0) as i32;
434
  let time_ms = start_time.elapsed().as_millis();
435
- // We mock depth as 10 since MCTS doesn't use iterative deepening
436
- println!("info depth 10 nodes {} score cp {} time {} pv {}", iter_count, cp, time_ms, best_action);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
  }
438
  }
439
 
 
 
 
 
 
 
 
440
  let mut best_action = String::new();
441
- let mut best_visits = -1;
442
  let mut best_eval = 0.0;
443
- for (action, child) in &global_root.children {
444
- if (child.visit_count as i32) > best_visits {
445
- best_visits = child.visit_count as i32;
446
- best_action = action.clone();
447
- if child.visit_count > 0 {
448
- best_eval = child.value_sum / (child.visit_count as f32);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  }
450
  }
451
  }
452
  if !best_action.is_empty() {
453
  let cp = (best_eval * 1000.0) as i32;
454
- println!("info score cp {}", cp);
 
 
 
 
 
455
  println!("bestmove {}", best_action);
456
  } else {
457
  println!("bestmove 0000"); // fallback
 
7
  use ort::session::Session;
8
  use ort::session::builder::GraphOptimizationLevel;
9
  use rand_distr::{Dirichlet, Distribution};
10
+ use shakmaty_syzygy::{Tablebase, Wdl, AmbiguousWdl};
11
+
12
+ mod polyglot;
13
+ use polyglot::PolyglotBook;
14
 
15
  #[derive(Clone)]
16
  struct MCTSNode {
 
31
  }
32
  }
33
 
34
+ fn extract_pv(node: &MCTSNode) -> Vec<String> {
35
+ let mut pv = Vec::new();
36
+ let mut current = node;
37
+ while !current.children.is_empty() {
38
+ let mut best_action = String::new();
39
+ let mut best_visits = -1;
40
+ for (action, child) in &current.children {
41
+ if (child.visit_count as i32) > best_visits {
42
+ best_visits = child.visit_count as i32;
43
+ best_action = action.clone();
44
+ }
45
+ }
46
+ if best_action.is_empty() || best_visits == 0 {
47
+ break;
48
+ }
49
+ pv.push(best_action.clone());
50
+ current = current.children.get(&best_action).unwrap();
51
+ }
52
+ pv
53
+ }
54
+
55
  fn add_dirichlet_noise(node: &mut MCTSNode, alpha: f32, epsilon: f32) {
56
  if node.children.len() <= 1 { return; }
57
  let dirichlet = Dirichlet::new(&vec![alpha; node.children.len()]).unwrap();
 
81
  evaluate_material(board, color) - evaluate_material(board, other)
82
  }
83
 
84
+ fn get_piece_value(role: shakmaty::Role) -> i32 {
85
+ match role {
86
+ shakmaty::Role::Pawn => 1,
87
+ shakmaty::Role::Knight => 3,
88
+ shakmaty::Role::Bishop => 3,
89
+ shakmaty::Role::Rook => 5,
90
+ shakmaty::Role::Queen => 9,
91
+ shakmaty::Role::King => 100,
92
+ }
93
+ }
94
+
95
+ fn score_move(m: &shakmaty::Move) -> i32 {
96
+ if m.is_capture() {
97
+ let victim = m.capture().map(|r| get_piece_value(r)).unwrap_or(0);
98
+ let attacker = get_piece_value(m.role());
99
+ 1000 + victim * 10 - attacker
100
+ } else {
101
+ 0
102
+ }
103
+ }
104
+
105
  fn quiescence_search(board: &Chess, mut alpha: i32, beta: i32, depth: u8) -> i32 {
106
  let in_check = board.is_check();
107
  let stand_pat = material_difference(board, board.turn());
 
117
  }
118
  }
119
 
120
+ let mut legals: Vec<_> = board.legal_moves().into_iter().collect();
121
+ legals.sort_by_cached_key(|m| -score_move(m));
122
+
123
  for m in &legals {
124
  if m.is_capture() || in_check {
125
  let mut next_board = board.clone();
 
136
  alpha
137
  }
138
 
139
+ fn tactical_search(board: &Chess, mut alpha: i32, beta: i32, depth: u8, qs_depth: u8) -> i32 {
140
+ if depth == 0 {
141
+ return quiescence_search(board, alpha, beta, qs_depth);
142
+ }
143
+
144
+ if board.is_game_over() {
145
+ if board.is_checkmate() { return -9999; }
146
+ return 0;
147
+ }
148
+
149
+ let mut legals: Vec<_> = board.legal_moves().into_iter().collect();
150
+ legals.sort_by_cached_key(|m| -score_move(m));
151
+
152
+ let mut best_score = -99999;
153
+ let mut first = true;
154
+
155
+ for m in &legals {
156
+ let mut next_board = board.clone();
157
+ next_board.play_unchecked(m);
158
+
159
+ let score = if first {
160
+ -tactical_search(&next_board, -beta, -alpha, depth - 1, qs_depth)
161
+ } else {
162
+ let mut val = -tactical_search(&next_board, -alpha - 1, -alpha, depth - 1, qs_depth);
163
+ if val > alpha && val < beta {
164
+ val = -tactical_search(&next_board, -beta, -alpha, depth - 1, qs_depth);
165
+ }
166
+ val
167
+ };
168
+
169
+ first = false;
170
+ if score > best_score {
171
+ best_score = score;
172
+ }
173
+ if best_score > alpha {
174
+ alpha = best_score;
175
+ }
176
+ if alpha >= beta {
177
+ break;
178
+ }
179
+ }
180
+ best_score
181
+ }
182
+
183
+
184
  fn load_vocab() -> (HashMap<String, i64>, HashMap<i64, String>) {
185
  let data = fs::read_to_string("vocab.json").expect("Unable to read vocab.json. Please run build_engine.bat first to generate it!");
186
  let vocab: HashMap<String, i64> = serde_json::from_str(&data).expect("JSON format error");
 
254
  vocab: &HashMap<String, i64>,
255
  inv_vocab: &HashMap<i64, String>,
256
  nn_cache: &mut HashMap<u64, (HashMap<String, f32>, f32)>,
257
+ tablebases: &mut Tablebase<Chess>,
258
  ) -> f32 {
259
  if board.is_game_over() {
260
  if board.is_checkmate() {
 
263
  return 0.0; // Draw
264
  }
265
 
266
+ // Probe Syzygy Endgame Tablebases for positions with <= 7 pieces
267
+ if board.board().occupied().count() <= 7 {
268
+ if let Ok(wdl) = tablebases.probe_wdl(&board) {
269
+ let tb_value = match wdl {
270
+ AmbiguousWdl::Win | AmbiguousWdl::CursedWin | AmbiguousWdl::MaybeWin => 0.99,
271
+ AmbiguousWdl::Loss | AmbiguousWdl::BlessedLoss | AmbiguousWdl::MaybeLoss => -0.99,
272
+ AmbiguousWdl::Draw => 0.0,
273
+ };
274
+ return tb_value;
275
+ }
276
+ }
277
+
278
  if node.children.is_empty() {
279
  // Expand
280
  let legals = board.legal_moves();
 
356
  history.push(best_action.clone());
357
 
358
  let child_node = node.children.get_mut(&best_action).unwrap();
359
+ let val = mcts_simulate(child_node, next_board, history, session, vocab, inv_vocab, nn_cache, tablebases);
360
 
361
  history.pop();
362
 
 
375
  // Ensure ONNX library is set up implicitly by ort
376
  let _ = ort::init().with_name("neural_engine").commit();
377
 
378
+ let mut session = if Path::new("model_int8.onnx").exists() {
379
  Some(Session::builder().unwrap()
380
  .with_optimization_level(GraphOptimizationLevel::Level3).unwrap()
381
  .with_intra_threads(4).unwrap()
382
+ .commit_from_file("model_int8.onnx").unwrap())
383
  } else {
384
  None
385
  };
386
 
387
+ let mut tablebases = Tablebase::<Chess>::new();
388
+ let _ = tablebases.add_directory("syzygy");
389
+
390
+ // Load polyglot opening book if available
391
+ let polyglot_book = PolyglotBook::new("book.bin").ok();
392
+
393
  let (vocab, inv_vocab) = load_vocab();
394
 
395
  let mut nn_cache: HashMap<u64, (HashMap<String, f32>, f32)> = HashMap::new();
 
509
  movetime_ms = 100;
510
  }
511
 
512
+ // --- 1. Polyglot Opening Book Check ---
513
+ if let Some(book) = &polyglot_book {
514
+ let hash = board.zobrist_hash::<Zobrist64>(shakmaty::EnPassantMode::Legal).0;
515
+ if let Some(book_move) = book.lookup(hash) {
516
+ println!("bestmove {}", book_move);
517
+ continue; // Skip MCTS completely
518
+ }
519
+ }
520
+
521
  if let Some(ref mut sess) = session {
522
  let start_time = std::time::Instant::now();
523
  // Leave a small buffer (5% or minimum 50ms) to ensure we reply in time
524
  let buffer = (movetime_ms as f64 * 0.05) as u128;
525
+ let mut safe_movetime = if movetime_ms > buffer { movetime_ms - buffer } else { movetime_ms };
 
 
526
  if global_root.children.is_empty() {
527
+ mcts_simulate(&mut global_root, board.clone(), &mut history_moves, sess, &vocab, &inv_vocab, &mut nn_cache, &mut tablebases);
528
  }
529
  add_dirichlet_noise(&mut global_root, 0.3, 0.25);
530
 
531
  let mut iter_count = 0;
532
 
533
  // Loop continuously until time expires
534
+ let mut extended = false;
535
+
536
+ while (start_time.elapsed().as_millis() as u128) < safe_movetime {
537
+ mcts_simulate(&mut global_root, board.clone(), &mut history_moves, sess, &vocab, &inv_vocab, &mut nn_cache, &mut tablebases);
538
  iter_count += 1;
539
 
540
+ // Stream UCI info and check Dynamic Time Allocation every 100 iterations
541
  if iter_count % 100 == 0 {
542
  let mut best_eval = 0.0;
543
  let mut best_visits = -1;
544
+ let mut second_best_visits = -1;
545
  let mut best_action = String::new();
546
+
547
  for (action, child) in &global_root.children {
548
+ let v = child.visit_count as i32;
549
+ if v > best_visits {
550
+ second_best_visits = best_visits;
551
+ best_visits = v;
552
  best_action = action.clone();
553
  if child.visit_count > 0 {
554
  best_eval = child.value_sum / (child.visit_count as f32);
555
  }
556
+ } else if v > second_best_visits {
557
+ second_best_visits = v;
558
  }
559
  }
560
+
561
  let time_ms = start_time.elapsed().as_millis();
562
+ let cp = (best_eval * 1000.0) as i32;
563
+
564
+ // --- 2. Dynamic Time Management ---
565
+ if time_ms as f64 > (safe_movetime as f64 * 0.15) && best_visits > 500 {
566
+ // Early Stopping: Move is overwhelmingly obvious
567
+ if second_best_visits > 0 && best_visits > 10 * second_best_visits {
568
+ println!("info string Early stopping: move is obvious.");
569
+ break;
570
+ }
571
+ }
572
+
573
+ if !extended && time_ms as f64 > (safe_movetime as f64 * 0.8) {
574
+ // Time Extension: Position is critical and unsure
575
+ if second_best_visits > 0 && best_visits < (second_best_visits as f64 * 1.2) as i32 {
576
+ let extra_time = (safe_movetime as f64 * 0.5) as u128;
577
+ let max_allowed = wtime / 10; // Avoid flagging
578
+
579
+ if wtime == 0 || (safe_movetime + extra_time < max_allowed) {
580
+ safe_movetime += extra_time;
581
+ println!("info string Extending search time for critical position. New limit: {}ms", safe_movetime);
582
+ extended = true;
583
+ }
584
+ }
585
+ }
586
+ let pv_moves = extract_pv(&global_root);
587
+ let pv_str = pv_moves.join(" ");
588
+ let depth = std::cmp::max(1, pv_moves.len());
589
+ let nps = if time_ms > 0 { (iter_count as u128 * 1000) / time_ms } else { 0 };
590
+ println!("info depth {} nodes {} nps {} score cp {} time {} pv {}", depth, iter_count, nps, cp, time_ms, pv_str);
591
  }
592
  }
593
 
594
+ // TACTICAL SAFETY NET
595
+ let mut candidates: Vec<(&String, &MCTSNode)> = global_root.children.iter().collect();
596
+ // Sort candidates by visit_count descending
597
+ candidates.sort_by(|a, b| b.1.visit_count.cmp(&a.1.visit_count));
598
+
599
+ let current_mat = material_difference(&board, board.turn());
600
+
601
  let mut best_action = String::new();
 
602
  let mut best_eval = 0.0;
603
+
604
+ for (action, child) in candidates {
605
+ let m_res = shakmaty::uci::Uci::from_ascii(action.as_bytes()).unwrap().to_move(&board);
606
+ if let Ok(m) = m_res {
607
+ let mut next_board = board.clone();
608
+ next_board.play_unchecked(&m);
609
+
610
+ // 3 plies of full search, then 4 plies of quiescence (starting from depth 0)
611
+ let tact_score = -tactical_search(&next_board, -9999, 9999, 3, 0);
612
+
613
+ // If it doesn't unconditionally drop >= 2 points of material, ACCEPT IT
614
+ if tact_score > current_mat - 2 {
615
+ best_action = action.clone();
616
+ if child.visit_count > 0 {
617
+ best_eval = child.value_sum / (child.visit_count as f32);
618
+ }
619
+ break;
620
+ }
621
+ }
622
+ }
623
+
624
+ // Fallback to highest visited move if ALL top moves drop material (e.g. inevitable capture)
625
+ if best_action.is_empty() {
626
+ let mut best_visits = -1;
627
+ for (action, child) in &global_root.children {
628
+ if (child.visit_count as i32) > best_visits {
629
+ best_visits = child.visit_count as i32;
630
+ best_action = action.clone();
631
+ if child.visit_count > 0 {
632
+ best_eval = child.value_sum / (child.visit_count as f32);
633
+ }
634
  }
635
  }
636
  }
637
  if !best_action.is_empty() {
638
  let cp = (best_eval * 1000.0) as i32;
639
+ let time_ms = start_time.elapsed().as_millis();
640
+ let pv_moves = extract_pv(&global_root);
641
+ let pv_str = pv_moves.join(" ");
642
+ let depth = std::cmp::max(1, pv_moves.len());
643
+ let nps = if time_ms > 0 { (iter_count as u128 * 1000) / time_ms } else { 0 };
644
+ println!("info depth {} nodes {} nps {} score cp {} time {} pv {}", depth, iter_count, nps, cp, time_ms, pv_str);
645
  println!("bestmove {}", best_action);
646
  } else {
647
  println!("bestmove 0000"); // fallback
neural_engine/src/polyglot.rs ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::fs::File;
2
+ use std::io::{self, Read, Seek, SeekFrom};
3
+ use rand::Rng;
4
+
5
+ #[derive(Debug, Clone, Copy)]
6
+ pub struct PolyglotEntry {
7
+ pub key: u64,
8
+ pub mov: u16,
9
+ pub weight: u16,
10
+ pub learn: u32,
11
+ }
12
+
13
+ pub struct PolyglotBook {
14
+ entries: Vec<PolyglotEntry>,
15
+ }
16
+
17
+ impl PolyglotBook {
18
+ pub fn new(path: &str) -> io::Result<Self> {
19
+ let mut file = File::open(path)?;
20
+ let mut buffer = Vec::new();
21
+ file.read_to_end(&mut buffer)?;
22
+
23
+ let mut entries = Vec::with_capacity(buffer.len() / 16);
24
+ for chunk in buffer.chunks_exact(16) {
25
+ let key = u64::from_be_bytes(chunk[0..8].try_into().unwrap());
26
+ let mov = u16::from_be_bytes(chunk[8..10].try_into().unwrap());
27
+ let weight = u16::from_be_bytes(chunk[10..12].try_into().unwrap());
28
+ let learn = u32::from_be_bytes(chunk[12..16].try_into().unwrap());
29
+
30
+ entries.push(PolyglotEntry { key, mov, weight, learn });
31
+ }
32
+
33
+ Ok(PolyglotBook { entries })
34
+ }
35
+
36
+ pub fn lookup(&self, key: u64) -> Option<String> {
37
+ // Binary search the entries for the first match
38
+ let start_idx = self.entries.binary_search_by_key(&key, |e| e.key).ok()?;
39
+
40
+ // The binary search might hit any entry in a block of identical keys.
41
+ // Find the start of the block.
42
+ let mut first = start_idx;
43
+ while first > 0 && self.entries[first - 1].key == key {
44
+ first -= 1;
45
+ }
46
+
47
+ // Find all moves for this key
48
+ let mut valid_moves = Vec::new();
49
+ let mut i = first;
50
+ while i < self.entries.len() && self.entries[i].key == key {
51
+ if self.entries[i].weight > 0 {
52
+ valid_moves.push(self.entries[i]);
53
+ }
54
+ i += 1;
55
+ }
56
+
57
+ if valid_moves.is_empty() {
58
+ return None;
59
+ }
60
+
61
+ // Weighted random choice
62
+ let total_weight: u32 = valid_moves.iter().map(|e| e.weight as u32).sum();
63
+ if total_weight == 0 {
64
+ // Fallback to random if all weights are somehow 0 (already handled above but just in case)
65
+ let chosen = valid_moves[rand::thread_rng().gen_range(0..valid_moves.len())];
66
+ return Some(polyglot_to_uci(chosen.mov));
67
+ }
68
+
69
+ let mut rng = rand::thread_rng();
70
+ let mut random_weight = rng.gen_range(0..total_weight);
71
+
72
+ for entry in valid_moves {
73
+ if random_weight < entry.weight as u32 {
74
+ return Some(polyglot_to_uci(entry.mov));
75
+ }
76
+ random_weight -= entry.weight as u32;
77
+ }
78
+
79
+ None
80
+ }
81
+ }
82
+
83
+ pub fn polyglot_to_uci(mov: u16) -> String {
84
+ let to_file = mov & 7;
85
+ let to_rank = (mov >> 3) & 7;
86
+ let from_file = (mov >> 6) & 7;
87
+ let from_rank = (mov >> 9) & 7;
88
+ let promotion = (mov >> 12) & 7;
89
+
90
+ let from_sq = format!("{}{}", (b'a' + from_file as u8) as char, (b'1' + from_rank as u8) as char);
91
+ let to_sq = format!("{}{}", (b'a' + to_file as u8) as char, (b'1' + to_rank as u8) as char);
92
+
93
+ let prom_str = match promotion {
94
+ 1 => "n",
95
+ 2 => "b",
96
+ 3 => "r",
97
+ 4 => "q",
98
+ _ => "",
99
+ };
100
+
101
+ // Polyglot encodes castling as King captures Rook.
102
+ // e1h1 becomes O-O, which in standard UCI is e1g1.
103
+ // Let's normalize it to standard UCI.
104
+ let is_castling = match (from_sq.as_str(), to_sq.as_str()) {
105
+ ("e1", "h1") => Some("e1g1"),
106
+ ("e1", "a1") => Some("e1c1"),
107
+ ("e8", "h8") => Some("e8g8"),
108
+ ("e8", "a8") => Some("e8c8"),
109
+ _ => None,
110
+ };
111
+
112
+ if let Some(castling_move) = is_castling {
113
+ castling_move.to_string()
114
+ } else {
115
+ format!("{}{}{}", from_sq, to_sq, prom_str)
116
+ }
117
+ }