rshogi-core 0.2.4

A high-performance shogi engine core library with NNUE evaluation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
//! 探索統計(search-stats feature有効時のみ)
//!
//! 探索中の各種枝刈りの発生回数を記録し、チューニングやデバッグに使用する。

/// 深度別統計の最大深度
#[cfg(feature = "search-stats")]
pub(super) const STATS_MAX_DEPTH: usize = 32;

/// 探索統計カウンタ
///
/// 各枝刈りの発生回数を記録し、チューニングやデバッグに使用する。
/// `search-stats` featureが有効な場合のみコンパイルされる。
#[cfg(feature = "search-stats")]
#[derive(Debug, Clone)]
pub struct SearchStats {
    /// 総ノード数(探索関数の呼び出し回数)
    pub nodes_searched: u64,
    /// LMR適用回数
    pub lmr_applied: u64,
    /// LMRによる再探索回数
    pub lmr_research: u64,
    /// Move Loop内の枝刈り回数(LMP, Futility, SEE, History等の合計)
    pub move_loop_pruned: u64,
    /// Futility Pruning(静的評価による枝刈り)回数
    pub futility_pruned: u64,
    /// NMP(Null Move Pruning)試行回数
    pub nmp_attempted: u64,
    /// NMPによる枝刈り成功回数
    pub nmp_cutoff: u64,
    /// NMPスキップ理由: cut_node == false
    pub nmp_skip_not_cut_node: u64,
    /// NMPスキップ理由: in_check
    pub nmp_skip_in_check: u64,
    /// NMPスキップ理由: static_eval < beta - margin
    pub nmp_skip_eval_low: u64,
    /// NMPスキップ理由: excluded_move あり
    pub nmp_skip_excluded: u64,
    /// NMPスキップ理由: prev_move が null/pass
    pub nmp_skip_prev_null: u64,
    /// NMP候補ノード数(cut_node && !in_check && !excluded のノード)
    pub nmp_candidate_nodes: u64,
    /// Razoring適用回数
    pub razoring_applied: u64,
    /// ProbCut試行回数
    pub probcut_attempted: u64,
    /// ProbCutによる枝刈り成功回数
    pub probcut_cutoff: u64,
    /// Singular Extension適用回数
    pub singular_extension: u64,
    /// Multi-Cut発動回数
    pub multi_cut: u64,
    /// TT(置換表)カットオフ回数
    pub tt_cutoff: u64,
    /// 深度別ノード数(depth 0-31)
    pub nodes_by_depth: [u64; STATS_MAX_DEPTH],
    /// 深度別TTカットオフ数
    pub tt_cutoff_by_depth: [u64; STATS_MAX_DEPTH],
    /// 深度別TTプローブ数
    pub tt_probe_by_depth: [u64; STATS_MAX_DEPTH],
    /// 深度別TTヒット数
    pub tt_hit_by_depth: [u64; STATS_MAX_DEPTH],
    /// 深度別TT深度不足でカットオフ失敗
    pub tt_fail_depth_by_depth: [u64; STATS_MAX_DEPTH],
    /// 深度別TTバウンド不適合でカットオフ失敗
    pub tt_fail_bound_by_depth: [u64; STATS_MAX_DEPTH],
    /// LMRでdepth 1に遷移したノード数(親の深度別)
    pub lmr_to_depth1_from: [u64; STATS_MAX_DEPTH],
    /// depth 1での全子ノード数(統計用)
    pub depth1_children_total: u64,
    /// depth 1でTTカットオフされた子ノード数
    pub depth1_children_tt_cut: u64,
    /// 深度別TT書き込み数
    pub tt_write_by_depth: [u64; STATS_MAX_DEPTH],
    /// 深度別Razoring適用回数
    pub razoring_by_depth: [u64; STATS_MAX_DEPTH],
    /// 深度別Futility Pruning適用回数
    pub futility_by_depth: [u64; STATS_MAX_DEPTH],
    /// 深度別NMPカットオフ回数
    pub nmp_cutoff_by_depth: [u64; STATS_MAX_DEPTH],
    /// 深度別first move cutoff回数(Move Ordering品質)
    pub first_move_cutoff_by_depth: [u64; STATS_MAX_DEPTH],
    /// 深度別カットオフ回数(first move cutoff rate計算用)
    pub cutoff_by_depth: [u64; STATS_MAX_DEPTH],
    /// 深度別のカットオフ時move_count合計(平均計算用)
    pub move_count_sum_by_depth: [u64; STATS_MAX_DEPTH],
    /// LMR削減量(r/1024)のヒストグラム(0-15+)
    pub lmr_reduction_histogram: [u64; 16],
    /// LMR適用後の新深度別ノード数
    pub lmr_new_depth_histogram: [u64; STATS_MAX_DEPTH],

    // =============================================================================
    // 静止探索(qsearch)統計
    // =============================================================================
    /// 静止探索ノード数
    pub qs_nodes: u64,
    /// 静止探索 TT ヒット数
    pub qs_tt_hit: u64,
    /// 静止探索 TT カットオフ数
    pub qs_tt_cutoff: u64,
    /// stand pat(静的評価で即時 beta カット)回数
    pub qs_stand_pat_cutoff: u64,
    /// 生成された手の総数
    pub qs_moves_generated: u64,
    /// 実際に探索された手の数
    pub qs_moves_searched: u64,
    /// SEE による枝刈り数(capture && !see_ge(0))
    pub qs_see_pruned: u64,
    /// Futility Pruning 数(静止探索内)
    pub qs_futility_pruned: u64,
    /// History による枝刈り数(cont_score + pawn_score <= 5868)
    pub qs_history_pruned: u64,
    /// SEE マージンによる枝刈り数(!see_ge(-74))
    pub qs_see_margin_pruned: u64,
    /// 王手回避時のノード数
    pub qs_in_check_nodes: u64,

    // =============================================================================
    // LMR cut_node 分析
    // =============================================================================
    /// cut_node での LMR 適用回数
    pub lmr_cut_node_applied: u64,
    /// cut_node での LMR depth 1 遷移回数
    pub lmr_cut_node_to_depth1: u64,
    /// 非 cut_node での LMR 適用回数
    pub lmr_non_cut_node_applied: u64,
    /// 非 cut_node での LMR depth 1 遷移回数
    pub lmr_non_cut_node_to_depth1: u64,
}

#[cfg(feature = "search-stats")]
impl Default for SearchStats {
    fn default() -> Self {
        Self {
            nodes_searched: 0,
            lmr_applied: 0,
            lmr_research: 0,
            move_loop_pruned: 0,
            futility_pruned: 0,
            nmp_attempted: 0,
            nmp_cutoff: 0,
            nmp_skip_not_cut_node: 0,
            nmp_skip_in_check: 0,
            nmp_skip_eval_low: 0,
            nmp_skip_excluded: 0,
            nmp_skip_prev_null: 0,
            nmp_candidate_nodes: 0,
            razoring_applied: 0,
            probcut_attempted: 0,
            probcut_cutoff: 0,
            singular_extension: 0,
            multi_cut: 0,
            tt_cutoff: 0,
            nodes_by_depth: [0; STATS_MAX_DEPTH],
            tt_cutoff_by_depth: [0; STATS_MAX_DEPTH],
            tt_probe_by_depth: [0; STATS_MAX_DEPTH],
            tt_hit_by_depth: [0; STATS_MAX_DEPTH],
            tt_fail_depth_by_depth: [0; STATS_MAX_DEPTH],
            tt_fail_bound_by_depth: [0; STATS_MAX_DEPTH],
            lmr_to_depth1_from: [0; STATS_MAX_DEPTH],
            depth1_children_total: 0,
            depth1_children_tt_cut: 0,
            tt_write_by_depth: [0; STATS_MAX_DEPTH],
            razoring_by_depth: [0; STATS_MAX_DEPTH],
            futility_by_depth: [0; STATS_MAX_DEPTH],
            nmp_cutoff_by_depth: [0; STATS_MAX_DEPTH],
            first_move_cutoff_by_depth: [0; STATS_MAX_DEPTH],
            cutoff_by_depth: [0; STATS_MAX_DEPTH],
            move_count_sum_by_depth: [0; STATS_MAX_DEPTH],
            lmr_reduction_histogram: [0; 16],
            lmr_new_depth_histogram: [0; STATS_MAX_DEPTH],
            // qsearch 統計
            qs_nodes: 0,
            qs_tt_hit: 0,
            qs_tt_cutoff: 0,
            qs_stand_pat_cutoff: 0,
            qs_moves_generated: 0,
            qs_moves_searched: 0,
            qs_see_pruned: 0,
            qs_futility_pruned: 0,
            qs_history_pruned: 0,
            qs_see_margin_pruned: 0,
            qs_in_check_nodes: 0,
            // LMR cut_node 分析
            lmr_cut_node_applied: 0,
            lmr_cut_node_to_depth1: 0,
            lmr_non_cut_node_applied: 0,
            lmr_non_cut_node_to_depth1: 0,
        }
    }
}

#[cfg(feature = "search-stats")]
impl SearchStats {
    /// 統計をリセット
    pub fn reset(&mut self) {
        *self = Self::default();
    }

    /// 統計をフォーマットして文字列として返す
    pub fn format_report(&self) -> String {
        let mut report = String::new();
        report.push_str("=== Search Statistics ===\n");
        report.push_str(&format!("Nodes searched:      {:>12}\n", self.nodes_searched));
        report.push_str(&format!("TT cutoffs:          {:>12}\n", self.tt_cutoff));
        report.push_str("--- Pre-Move Pruning ---\n");
        report.push_str(&format!("NMP attempted:       {:>12}\n", self.nmp_attempted));
        report.push_str(&format!("NMP cutoffs:         {:>12}\n", self.nmp_cutoff));
        if self.nmp_candidate_nodes > 0 {
            let success_rate = self.nmp_cutoff as f64 / self.nmp_attempted.max(1) as f64 * 100.0;
            report.push_str(&format!("NMP success rate:    {:>11.1}%\n", success_rate));
            report.push_str("  --- NMP Skip Reasons ---\n");
            report.push_str(&format!("  Candidate nodes:   {:>12}\n", self.nmp_candidate_nodes));
            report.push_str(&format!("  Skip (not cut):    {:>12}\n", self.nmp_skip_not_cut_node));
            report.push_str(&format!("  Skip (in check):   {:>12}\n", self.nmp_skip_in_check));
            report.push_str(&format!("  Skip (eval low):   {:>12}\n", self.nmp_skip_eval_low));
            report.push_str(&format!("  Skip (excluded):   {:>12}\n", self.nmp_skip_excluded));
            report.push_str(&format!("  Skip (prev null):  {:>12}\n", self.nmp_skip_prev_null));
        }
        report.push_str(&format!("Razoring:            {:>12}\n", self.razoring_applied));
        report.push_str(&format!("Futility (static):   {:>12}\n", self.futility_pruned));
        report.push_str(&format!("ProbCut attempted:   {:>12}\n", self.probcut_attempted));
        report.push_str(&format!("ProbCut cutoffs:     {:>12}\n", self.probcut_cutoff));
        report.push_str("--- Move Loop ---\n");
        report.push_str(&format!("Move loop pruned:    {:>12}\n", self.move_loop_pruned));
        report.push_str(&format!("LMR applied:         {:>12}\n", self.lmr_applied));
        report.push_str(&format!("LMR re-search:       {:>12}\n", self.lmr_research));
        report.push_str("--- Extensions ---\n");
        report.push_str(&format!("Singular extension:  {:>12}\n", self.singular_extension));
        report.push_str(&format!("Multi-cut:           {:>12}\n", self.multi_cut));
        // 深度別ノード数(ノード数が0より大きい深度のみ表示)
        report.push_str("--- Nodes by Depth ---\n");
        for (d, &count) in self.nodes_by_depth.iter().enumerate() {
            if count > 0 {
                let tt_cut = self.tt_cutoff_by_depth[d];
                let tt_rate = if count > 0 {
                    (tt_cut as f64 / count as f64 * 100.0) as u32
                } else {
                    0
                };
                report.push_str(&format!(
                    "  depth {:>2}: {:>10} nodes, {:>8} TT cuts ({:>2}%)\n",
                    d, count, tt_cut, tt_rate
                ));
            }
        }
        // TT詳細統計(depth 1のみ詳細表示)
        report.push_str("--- TT Details (depth 1) ---\n");
        let probe = self.tt_probe_by_depth[1];
        let hit = self.tt_hit_by_depth[1];
        let cut = self.tt_cutoff_by_depth[1];
        let fail_depth = self.tt_fail_depth_by_depth[1];
        let fail_bound = self.tt_fail_bound_by_depth[1];
        if probe > 0 {
            report.push_str(&format!(
                "  Probes: {}, Hits: {} ({:.1}%), Cuts: {} ({:.1}%)\n",
                probe,
                hit,
                hit as f64 / probe as f64 * 100.0,
                cut,
                cut as f64 / probe as f64 * 100.0
            ));
            report
                .push_str(&format!("  Fail reasons: depth={}, bound={}\n", fail_depth, fail_bound));
        }
        // depth 1への遷移元分析
        report.push_str("--- LMR to Depth 1 Sources ---\n");
        for (d, &count) in self.lmr_to_depth1_from.iter().enumerate() {
            if count > 0 {
                report.push_str(&format!("  from depth {:>2}: {:>8} nodes\n", d, count));
            }
        }
        // TT書き込み統計
        report.push_str("--- TT Writes by Depth ---\n");
        for (d, &count) in self.tt_write_by_depth.iter().enumerate() {
            if count > 0 {
                let probe = self.tt_probe_by_depth[d];
                let ratio = if probe > 0 {
                    format!("{:.1}x", count as f64 / probe as f64)
                } else {
                    "-".to_string()
                };
                report.push_str(&format!(
                    "  depth {:>2}: {:>8} writes (probe ratio: {})\n",
                    d, count, ratio
                ));
            }
        }
        // 早期リターン統計(depth別)
        report.push_str("--- Early Return by Depth ---\n");
        for d in 0..STATS_MAX_DEPTH {
            let razoring = self.razoring_by_depth[d];
            let futility = self.futility_by_depth[d];
            let nmp = self.nmp_cutoff_by_depth[d];
            let nodes = self.nodes_by_depth[d];
            if razoring > 0 || futility > 0 || nmp > 0 {
                report.push_str(&format!(
                    "  depth {:>2}: razoring={:>6}, futility={:>6}, nmp={:>6} (nodes={})\n",
                    d, razoring, futility, nmp, nodes
                ));
            }
        }
        // Move Ordering品質統計(depth別)
        report.push_str("--- Move Ordering Quality (First Move Cutoff Rate) ---\n");
        for d in 0..STATS_MAX_DEPTH {
            let first_cut = self.first_move_cutoff_by_depth[d];
            let total_cut = self.cutoff_by_depth[d];
            if total_cut > 0 {
                let rate = first_cut as f64 / total_cut as f64 * 100.0;
                report.push_str(&format!(
                    "  depth {:>2}: {:>6}/{:>6} ({:>5.1}%)\n",
                    d, first_cut, total_cut, rate
                ));
            }
        }
        // カットオフ時のmove_count平均(depth別)
        report.push_str("--- Average Move Count at Cutoff ---\n");
        for d in 0..STATS_MAX_DEPTH {
            let total_cut = self.cutoff_by_depth[d];
            let move_count_sum = self.move_count_sum_by_depth[d];
            if total_cut > 0 {
                let avg = move_count_sum as f64 / total_cut as f64;
                report.push_str(&format!(
                    "  depth {:>2}: {:>6.2} avg ({} cutoffs)\n",
                    d, avg, total_cut
                ));
            }
        }
        // LMR削減量のヒストグラム
        report.push_str("--- LMR Reduction Histogram (r/1024) ---\n");
        for (r, &count) in self.lmr_reduction_histogram.iter().enumerate() {
            if count > 0 {
                let label = if r == 15 {
                    "15+".to_string()
                } else {
                    format!("{:>2}", r)
                };
                report.push_str(&format!(
                    "  r={}: {:>8} ({:>5.1}%)\n",
                    label,
                    count,
                    count as f64 / self.lmr_applied as f64 * 100.0
                ));
            }
        }
        // LMR適用後の新深度別ノード数
        report.push_str("--- LMR New Depth Distribution ---\n");
        for d in 0..STATS_MAX_DEPTH {
            let count = self.lmr_new_depth_histogram[d];
            if count > 0 {
                report.push_str(&format!(
                    "  new_depth {:>2}: {:>8} ({:>5.1}%)\n",
                    d,
                    count,
                    count as f64 / self.lmr_applied as f64 * 100.0
                ));
            }
        }

        // =============================================================================
        // 静止探索(qsearch)統計
        // =============================================================================
        report.push_str("--- Quiescence Search Statistics ---\n");
        report.push_str(&format!("QS nodes:            {:>12}\n", self.qs_nodes));
        if self.qs_nodes > 0 {
            let qs_nodes = self.qs_nodes as f64;
            report.push_str(&format!(
                "  In-check nodes:    {:>12} ({:.1}%)\n",
                self.qs_in_check_nodes,
                self.qs_in_check_nodes as f64 / qs_nodes * 100.0
            ));
            report.push_str(&format!(
                "  TT hit:            {:>12} ({:.1}%)\n",
                self.qs_tt_hit,
                self.qs_tt_hit as f64 / qs_nodes * 100.0
            ));
            report.push_str(&format!(
                "  TT cutoff:         {:>12} ({:.1}%)\n",
                self.qs_tt_cutoff,
                self.qs_tt_cutoff as f64 / qs_nodes * 100.0
            ));
            report.push_str(&format!(
                "  Stand-pat cutoff:  {:>12} ({:.1}%)\n",
                self.qs_stand_pat_cutoff,
                self.qs_stand_pat_cutoff as f64 / qs_nodes * 100.0
            ));
            report.push_str(&format!(
                "  Moves generated:   {:>12} ({:.1} avg/node)\n",
                self.qs_moves_generated,
                self.qs_moves_generated as f64 / qs_nodes
            ));
            report.push_str(&format!(
                "  Moves searched:    {:>12} ({:.1} avg/node)\n",
                self.qs_moves_searched,
                self.qs_moves_searched as f64 / qs_nodes
            ));
        }
        // 静止探索内の枝刈り統計
        let qs_total_pruned = self.qs_see_pruned
            + self.qs_futility_pruned
            + self.qs_history_pruned
            + self.qs_see_margin_pruned;
        if qs_total_pruned > 0 {
            report.push_str("  --- QS Pruning ---\n");
            report.push_str(&format!("    SEE (capture):   {:>12}\n", self.qs_see_pruned));
            report.push_str(&format!("    Futility:        {:>12}\n", self.qs_futility_pruned));
            report.push_str(&format!("    History:         {:>12}\n", self.qs_history_pruned));
            report.push_str(&format!("    SEE margin:      {:>12}\n", self.qs_see_margin_pruned));
        }
        // =============================================================================
        // LMR cut_node 分析
        // =============================================================================
        report.push_str("--- LMR Cut Node Analysis ---\n");
        if self.lmr_cut_node_applied > 0 {
            let cut_rate =
                self.lmr_cut_node_to_depth1 as f64 / self.lmr_cut_node_applied as f64 * 100.0;
            report.push_str(&format!(
                "  cut_node:     {:>8} LMR, {:>8} to d1 ({:.1}%)\n",
                self.lmr_cut_node_applied, self.lmr_cut_node_to_depth1, cut_rate
            ));
        }
        if self.lmr_non_cut_node_applied > 0 {
            let non_cut_rate = self.lmr_non_cut_node_to_depth1 as f64
                / self.lmr_non_cut_node_applied as f64
                * 100.0;
            report.push_str(&format!(
                "  non_cut_node: {:>8} LMR, {:>8} to d1 ({:.1}%)\n",
                self.lmr_non_cut_node_applied, self.lmr_non_cut_node_to_depth1, non_cut_rate
            ));
        }

        report
    }
}

// =============================================================================
// 統計マクロ
// =============================================================================

/// 統計カウンタをインクリメントするマクロ(feature有効時のみ実行)
/// SearchState への参照を受け取り、stats フィールドへアクセス
#[cfg(feature = "search-stats")]
macro_rules! inc_stat {
    ($st:expr, $field:ident) => {
        $st.stats.$field += 1;
    };
}

#[cfg(not(feature = "search-stats"))]
macro_rules! inc_stat {
    ($self:expr, $field:ident) => {};
}

/// 深度別統計をカウントするマクロ(feature有効時のみ実行)
/// SearchState への参照を受け取り、stats フィールドへアクセス
#[cfg(feature = "search-stats")]
macro_rules! inc_stat_by_depth {
    ($st:expr, $field:ident, $depth:expr) => {
        let d = ($depth as usize).min($crate::search::stats::STATS_MAX_DEPTH - 1);
        $st.stats.$field[d] += 1;
    };
}

#[cfg(not(feature = "search-stats"))]
macro_rules! inc_stat_by_depth {
    ($self:expr, $field:ident, $depth:expr) => {};
}

// マクロを search モジュール内で使えるようにする
pub(super) use inc_stat;
pub(super) use inc_stat_by_depth;