/
Search.cs
1515 lines (1252 loc) · 62.1 KB
/
Search.cs
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
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Key = System.UInt64;
#if PRIMITIVE
using ValueT = System.Int32;
using MoveT = System.Int32;
#endif
internal static class Search
{
internal static SignalsType Signals;
internal static LimitsType Limits;
internal static List<RootMove> RootMoves = new List<RootMove>();
internal static Position RootPos;
internal static StateInfoWrapper SetupStates;
internal static uint PVIdx;
private static readonly EasyMoveManager EasyMove = new EasyMoveManager();
private static double BestMoveChanges;
private static readonly ValueT[] DrawValue = new ValueT[Color.COLOR_NB];
private static HistoryStats History = new HistoryStats();
private static CounterMovesHistoryStats CounterMovesHistory = new CounterMovesHistoryStats();
private static MovesStats Countermoves = new MovesStats();
/// check_time() is called by the timer thread when the timer triggers. It is
/// used to print debug info and, more importantly, to detect when we are out of
/// available time and thus stop the search.
// Futility and reductions lookup tables, initialized at startup
private static readonly int[,] FutilityMoveCounts = new int[2, 16]; // [improving][depth]
private static readonly Depth[,,,] Reductions = new Depth[2, 2, 64, 64]; // [pv][improving][depth][moveNumber]
internal static void check_time()
{
var elapsed = TimeManagement.elapsed();
if (elapsed >= 1000)
{
//TODO: enable db_print?
//dbg_print();
}
// An engine may not stop pondering until told so by the GUI
if (Limits.ponder)
{
return;
}
if (Limits.use_time_management())
{
var stillAtFirstMove = Signals.firstRootMove && !Signals.failedLowAtRoot
&& elapsed > TimeManagement.available()*75/100;
if (stillAtFirstMove || elapsed > TimeManagement.maximum() - 2*TimerThread.Resolution)
{
Signals.stop = true;
}
}
else if (Limits.movetime != 0 && elapsed >= Limits.movetime)
{
Signals.stop = true;
}
else if (Limits.nodes != 0)
{
long nodes = RootPos.nodes_searched();
// Loop across all split points and sum accumulated SplitPoint nodes plus
// all the currently active positions nodes.
// FIXME: Racy...
foreach (var th in ThreadPool.threads)
{
for (var i = 0; i < th.splitPointsSize; ++i)
{
var sp = th.splitPoints[i];
ThreadHelper.lock_grab(sp.spinLock);
nodes += sp.nodes;
nodes = ThreadPool.threads.Where((t, idx) => (sp.slavesMask & (1u << idx)) != 0 && t.activePosition != null).Aggregate(nodes, (current, t) => current + t.activePosition.nodes_searched());
ThreadHelper.lock_release(sp.spinLock);
}
}
if (nodes >= (long) Limits.nodes)
{
Signals.stop = true;
}
}
}
/// Search::reset() clears all search memory, to obtain reproducible search results
internal static void reset()
{
TranspositionTable.clear();
History = new HistoryStats();
CounterMovesHistory = new CounterMovesHistoryStats();
Countermoves = new MovesStats();
}
/// Search::think() is the external interface to Stockfish's search, and is
/// called by the main thread when the program receives the UCI 'go' command. It
/// searches from RootPos and at the end prints the "bestmove" to output.
internal static void think()
{
var us = RootPos.side_to_move();
TimeManagement.init(Limits, us, RootPos.game_ply(), DateTime.Now);
int contempt = int.Parse(OptionMap.Instance["Contempt"].v)*Value.PawnValueEg/100; // From centipawns
DrawValue[us] = Value.VALUE_DRAW - contempt;
DrawValue[Color.opposite(us)] = Value.VALUE_DRAW + contempt;
Tablebases.Hits = 0;
Tablebases.RootInTB = false;
Tablebases.UseRule50 = bool.Parse(OptionMap.Instance["Syzygy50MoveRule"].v);
Tablebases.ProbeDepth = int.Parse(OptionMap.Instance["SyzygyProbeDepth"].v)*Depth.ONE_PLY_C;
Tablebases.Cardinality = int.Parse(OptionMap.Instance["SyzygyProbeLimit"].v);
// Skip TB probing when no TB found: !TBLargest . !Tablebases.Cardinality
if (Tablebases.Cardinality > Tablebases.MaxCardinality)
{
Tablebases.Cardinality = Tablebases.MaxCardinality;
Tablebases.ProbeDepth = Depth.DEPTH_ZERO_C;
}
if (RootMoves.Count == 0)
{
RootMoves.Add(new RootMove(Move.MOVE_NONE));
Output.WriteLine(
$"info depth 0 score {UCI.value(RootPos.checkers() != 0 ? -Value.VALUE_MATE : Value.VALUE_DRAW)}");
}
else
{
if (Tablebases.Cardinality >= RootPos.count(PieceType.ALL_PIECES, Color.WHITE)
+ RootPos.count(PieceType.ALL_PIECES, Color.BLACK))
{
// If the current root position is in the tablebases then RootMoves
// contains only moves that preserve the draw or win.
Tablebases.RootInTB = Tablebases.root_probe(RootPos, RootMoves, Tablebases.Score);
if (Tablebases.RootInTB)
Tablebases.Cardinality = 0; // Do not probe tablebases during the search
else // If DTZ tables are missing, use WDL tables as a fallback
{
// Filter out moves that do not preserve a draw or win
Tablebases.RootInTB = Tablebases.root_probe_wdl(RootPos, RootMoves, Tablebases.Score);
// Only probe during search if winning
if (Tablebases.Score <= Value.VALUE_DRAW)
Tablebases.Cardinality = 0;
}
if (Tablebases.RootInTB)
{
Tablebases.Hits = RootMoves.Count;
if (!Tablebases.UseRule50)
Tablebases.Score = Tablebases.Score > Value.VALUE_DRAW
? Value.VALUE_MATE - _.MAX_PLY - 1
: Tablebases.Score < Value.VALUE_DRAW
? -Value.VALUE_MATE + _.MAX_PLY + 1
: Value.VALUE_DRAW;
}
}
foreach (var th in ThreadPool.threads)
{
th.maxPly = 0;
th.notify_one(); // Wake up all the threads
}
ThreadPool.timer.run = true;
ThreadPool.timer.notify_one(); // Start the recurring timer
id_loop(RootPos); // Let's start searching !
ThreadPool.timer.run = false;
}
// When playing in 'nodes as time' mode, subtract the searched nodes from
// the available ones before to exit.
if (Limits.npmsec != 0)
{
TimeManagement.availableNodes += Limits.inc[us] - RootPos.nodes_searched();
}
// When we reach the maximum depth, we can arrive here without a raise of
// Signals.stop. However, if we are pondering or in an infinite search,
// the UCI protocol states that we shouldn't print the best move before the
// GUI sends a "stop" or "ponderhit" command. We therefore simply wait here
// until the GUI sends one of those commands (which also raises Signals.stop).
if (!Signals.stop && (Limits.ponder || Limits.infinite != 0))
{
Signals.stopOnPonderhit = true;
//RootPos.this_thread().wait_for(Signals.stop);
ThreadHelper.lock_grab(RootPos.this_thread().spinlock /*mutex*/);
while (!Signals.stop)
{
ThreadHelper.cond_wait(RootPos.this_thread().sleepCondition, RootPos.this_thread().spinlock /*mutex*/);
}
ThreadHelper.lock_release(RootPos.this_thread().spinlock /*mutex*/);
}
Output.Write($"bestmove {UCI.move(RootMoves[0].pv[0], RootPos.is_chess960())}");
if (RootMoves[0].pv.Count > 1 || RootMoves[0].extract_ponder_from_tt(RootPos))
{
Output.Write($" ponder {UCI.move(RootMoves[0].pv[1], RootPos.is_chess960())}");
}
Output.WriteLine();
}
// id_loop() is the main iterative deepening loop. It calls search() repeatedly
// with increasing depth until the allocated thinking time has been consumed,
// user stops the search, or the maximum search depth is reached.
private static void id_loop(Position pos)
{
var stack = new Stack[_.MAX_PLY + 4];
for (var idx = 0; idx < stack.Length; idx++)
{
stack[idx] = new Stack();
}
var ss = new StackArrayWrapper(stack, 2); // To allow referencing (ss-2) and (ss+2)
ValueT alpha, delta;
var easyMove = EasyMove.get(pos.key());
EasyMove.clear();
//TODO: need to memset?
//Math.Memset(ss - 2, 0, 5 * sizeof(Stack));
var depth = Depth.DEPTH_ZERO;
BestMoveChanges = 0;
var bestValue = delta = alpha = -Value.VALUE_INFINITE;
var beta = Value.VALUE_INFINITE;
TranspositionTable.new_search();
var multiPV = uint.Parse(OptionMap.Instance["MultiPV"].v);
var skill = new Skill(int.Parse(OptionMap.Instance["Skill Level"].v));
// When playing with strength handicap enable MultiPV search that we will
// use behind the scenes to retrieve a set of possible moves.
if (skill.enabled())
{
multiPV = Math.Max(multiPV, 4);
multiPV = Math.Max(multiPV, 4);
multiPV = Math.Max(multiPV, 4);
}
multiPV = (uint) Math.Min(multiPV, RootMoves.Count);
// Iterative deepening loop until requested to stop or target depth reached;
while (++depth < _.MAX_PLY && !Signals.stop && (Limits.depth == 0 || depth <= Limits.depth))
{
// Age out PV variability metric
BestMoveChanges *= 0.5;
// Save the last iteration's scores before first PV line is searched and
// all the move scores except the (new) PV are set to -VALUE_INFINITE.
foreach (var rm in RootMoves)
{
rm.previousScore = rm.score;
}
// MultiPV loop. We perform a full root search for each PV line
for (PVIdx = 0; PVIdx < multiPV && !Signals.stop; ++PVIdx)
{
// Reset aspiration window starting size
if (depth >= 5*Depth.ONE_PLY_C)
{
delta = Value.Create(16);
alpha = Value.Create(Math.Max(RootMoves[(int) PVIdx].previousScore - delta, -Value.VALUE_INFINITE));
beta = Value.Create(Math.Min(RootMoves[(int) PVIdx].previousScore + delta, Value.VALUE_INFINITE));
}
// Start with a small aspiration window and, in the case of a fail
// high/low, re-search with a bigger window until we're not failing
// high/low anymore.
while (true)
{
bestValue = search(NodeType.Root, false, pos, ss, alpha, beta, depth, false);
// Bring the best move to the front. It is critical that sorting
// is done with a stable algorithm because all the values but the
// first and eventually the new best one are set to -VALUE_INFINITE
// and we want to keep the same order for all the moves except the
// new PV that goes to the front. Note that in case of MultiPV
// search the already searched PV lines are preserved.
//TODO: Check for stable sort replacement
Utils.stable_sort(RootMoves, (int) PVIdx, RootMoves.Count);
//std::stable_sort(RootMoves.begin() + PVIdx, RootMoves.end());
// Write PV back to transposition table in case the relevant
// entries have been overwritten during the search.
for (var i = 0; i <= PVIdx; ++i)
{
RootMoves[i].insert_pv_in_tt(pos);
}
// If search has been stopped break immediately. Sorting and
// writing PV back to TT is safe because RootMoves is still
// valid, although it refers to previous iteration.
if (Signals.stop)
{
break;
}
// When failing high/low give some update (without cluttering
// the UI) before a re-search.
if (multiPV == 1 && (bestValue <= alpha || bestValue >= beta) && TimeManagement.elapsed() > 3000)
{
Output.WriteLine(UCI.pv(pos, depth, alpha, beta));
}
// In case of failing low/high increase aspiration window and
// re-search, otherwise exit the loop.
if (bestValue <= alpha)
{
beta = (alpha + beta)/2;
alpha = Value.Create(Math.Max(bestValue - delta, -Value.VALUE_INFINITE));
Signals.failedLowAtRoot = true;
Signals.stopOnPonderhit = false;
}
else if (bestValue >= beta)
{
alpha = (alpha + beta)/2;
beta = Value.Create(Math.Min(bestValue + delta, Value.VALUE_INFINITE));
}
else
{
break;
}
delta += delta/2;
Debug.Assert(alpha >= -Value.VALUE_INFINITE && beta <= Value.VALUE_INFINITE);
}
// Sort the PV lines searched so far and update the GUI
//TODO: Check for stable sort replacement
Utils.stable_sort(RootMoves, 0, (int) PVIdx + 1);
//std::stable_sort(RootMoves.begin(), RootMoves.begin() + PVIdx + 1);
if (Signals.stop)
{
Output.WriteLine($"info nodes {RootPos.nodes_searched()} time {TimeManagement.elapsed()}");
}
else if (PVIdx + 1 == multiPV || TimeManagement.elapsed() > 3000)
{
Output.WriteLine(UCI.pv(pos, depth, alpha, beta));
}
}
// If skill level is enabled and time is up, pick a sub-optimal best move
if (skill.enabled() && skill.time_to_pick(depth))
{
skill.pick_best(multiPV);
}
// Have we found a "mate in x"?
if (Limits.mate != 0 && bestValue >= Value.VALUE_MATE_IN_MAX_PLY
&& Value.VALUE_MATE - bestValue <= 2*Limits.mate)
{
Signals.stop = true;
}
// Do we have time for the next iteration? Can we stop searching now?
if (Limits.use_time_management())
{
if (!Signals.stop && !Signals.stopOnPonderhit)
{
// Take some extra time if the best move has changed
if (depth > 4*Depth.ONE_PLY && multiPV == 1)
{
TimeManagement.pv_instability(BestMoveChanges);
}
// Stop the search if only one legal move is available or all
// of the available time has been used or we matched an easyMove
// from the previous search and just did a fast verification.
if (RootMoves.Count == 1 || TimeManagement.elapsed() > TimeManagement.available()
|| (RootMoves[0].pv[0] == easyMove && BestMoveChanges < 0.03
&& TimeManagement.elapsed() > TimeManagement.available()/10))
{
// If we are allowed to ponder do not stop the search now but
// keep pondering until the GUI sends "ponderhit" or "stop".
if (Limits.ponder)
{
Signals.stopOnPonderhit = true;
}
else
{
Signals.stop = true;
}
}
}
if (RootMoves[0].pv.Count >= 3)
{
EasyMove.update(pos, RootMoves[0].pv);
}
else
{
EasyMove.clear();
}
}
}
// Clear any candidate easy move that wasn't stable for the last search
// iterations; the second condition prevents consecutive fast moves.
if (EasyMove.stableCnt < 6 || TimeManagement.elapsed() < TimeManagement.available())
{
EasyMove.clear();
}
// If skill level is enabled, swap best PV line with the sub-optimal one
if (skill.enabled())
{
var foundIdx = RootMoves.FindIndex(rootMove1 => rootMove1.pv[0] == skill.best_move(multiPV));
Debug.Assert(foundIdx >= 0);
var rootMove = RootMoves[0];
RootMoves[0] = RootMoves[foundIdx];
RootMoves[foundIdx] = rootMove;
}
}
/// Search::perft() is our utility to verify move generation. All the leaf nodes
/// up to the given depth are generated and counted and the sum returned.
internal static long perft(bool Root, Position pos, Depth depth)
{
var st = new StateInfo();
long nodes = 0;
var ci = new CheckInfo(pos);
var leaf = (depth == 2*Depth.ONE_PLY);
var ml = new MoveList(GenType.LEGAL, pos);
for (var index = ml.begin(); index < ml.end(); index++)
{
var m = ml.moveList.table[index];
long cnt;
if (Root && depth <= Depth.ONE_PLY_C)
{
cnt = 1;
nodes++;
}
else
{
pos.do_move(m, st, pos.gives_check(m, ci));
cnt = leaf ? new MoveList(GenType.LEGAL, pos).size() : perft(false, pos, depth - Depth.ONE_PLY);
nodes += cnt;
pos.undo_move(m);
}
if (Root)
{
Output.WriteLine($"{UCI.move(m, pos.is_chess960())}: {cnt}");
}
}
return nodes;
}
/// Search::init() is called during startup to initialize various lookup tables
// Razoring and futility margin based on depth
private static ValueT razor_margin(Depth d)
{
return Value.Create(512 + 32* (int)d);
}
private static ValueT futility_margin(Depth d)
{
return Value.Create(200* (int)d);
}
private static Depth reduction(bool PvNode, bool i, Depth d, int mn)
{
return Reductions[PvNode ? 1 : 0, i ? 1 : 0, Math.Min(d, 63*Depth.ONE_PLY_C), Math.Min(mn, 63)];
}
internal static void init()
{
double[][] K = {new[] {0.83, 2.25}, new[] {0.50, 3.00}};
for (var pv = 0; pv <= 1; ++pv)
for (var imp = 0; imp <= 1; ++imp)
for (var d = 1; d < 64; ++d)
for (var mc = 1; mc < 64; ++mc)
{
var r = K[pv][0] + Math.Log(d)*Math.Log(mc)/K[pv][1];
if (r >= 1.5)
Reductions[pv, imp, d, mc] = new Depth((int) (r*Depth.ONE_PLY_C));
// Increase reduction when eval is not improving
if (pv == 0 && imp == 0 && Reductions[pv, imp, d, mc] >= 2*Depth.ONE_PLY_C)
Reductions[pv, imp, d, mc] += Depth.ONE_PLY;
}
for (var d = 0; d < 16; ++d)
{
FutilityMoveCounts[0, d] = (int) (2.4 + 0.773*Math.Pow(d + 0.00, 1.8));
FutilityMoveCounts[1, d] = (int) (2.9 + 1.045*Math.Pow(d + 0.49, 1.8));
}
}
// value_from_tt() is the inverse of value_to_tt(): It adjusts a mate score
// from the transposition table (which refers to the plies to mate/be mated
// from current position) to "plies to mate/be mated from the root".
private static ValueT value_from_tt(ValueT v, int ply)
{
return v == Value.VALUE_NONE
? Value.VALUE_NONE
: v >= Value.VALUE_MATE_IN_MAX_PLY
? v - ply
: v <= Value.VALUE_MATED_IN_MAX_PLY ? v + ply : v;
}
// search<>() is the main search function for both PV and non-PV nodes and for
// normal and SplitPoint nodes. When called just after a split point the search
// is simpler because we have already probed the hash table, done a null move
// search, and searched the first move before splitting, so we don't have to
// repeat all this work again. We also don't need to store anything to the hash
// table here: This is taken care of after we return from the split point.
private static ValueT search(NodeType NT, bool SpNode, Position pos, StackArrayWrapper ss, ValueT alpha, ValueT beta,
Depth depth, bool cutNode)
{
Utils.WriteToLog($"search(NT={(int) NT}, SpNode={(SpNode ? 1 : 0)}, pos={pos.key()}, ss, alpha={alpha}, beta={beta}, depth={(int) depth}, cutNode={(cutNode ? 1 : 0)})");
var RootNode = NT == NodeType.Root;
var PvNode = RootNode || NT == NodeType.PV;
Debug.Assert(-Value.VALUE_INFINITE <= alpha && alpha < beta && beta <= Value.VALUE_INFINITE);
Debug.Assert(PvNode || (alpha == beta - 1));
Debug.Assert(depth > Depth.DEPTH_ZERO);
var st = new StateInfo();
TTEntry tte;
SplitPoint splitPoint = null;
ulong posKey = 0;
MoveT ttMove, move, excludedMove, bestMove;
ValueT bestValue, value, ttValue, eval;
bool ttHit;
int moveCount = 0;
int quietCount = 0;
var stack = ss[ss.current];
var stackPlus1 = ss[ss.current + 1];
var stackPlus2 = ss[ss.current + 2];
var stackMinus1 = ss[ss.current - 1];
var stackMinus2 = ss[ss.current - 2];
// Step 1. Initialize node
var thisThread = pos.this_thread();
bool inCheck = pos.checkers() != 0;
if (SpNode)
{
splitPoint = stack.splitPoint;
bestMove = Move.Create(splitPoint.bestMove);
bestValue = Value.Create(splitPoint.bestValue);
tte = new TTEntry();
ttMove = excludedMove = Move.MOVE_NONE;
ttValue = Value.VALUE_NONE;
Debug.Assert(splitPoint.bestValue > -Value.VALUE_INFINITE && splitPoint.moveCount > 0);
goto moves_loop;
}
moveCount = quietCount = stack.moveCount = 0;
bestValue = -Value.VALUE_INFINITE;
stack.ply = stackMinus1.ply + 1;
// Used to send selDepth info to GUI
if (PvNode && thisThread.maxPly < stack.ply)
thisThread.maxPly = stack.ply;
if (!RootNode)
{
// Step 2. Check for aborted search and immediate draw
if (Signals.stop || pos.is_draw() || stack.ply >= _.MAX_PLY)
return stack.ply >= _.MAX_PLY && !inCheck
? Eval.evaluate(false, pos)
: DrawValue[pos.side_to_move()];
// Step 3. Mate distance pruning. Even if we mate at the next move our score
// would be at best mate_in(ss.ply+1), but if alpha is already bigger because
// a shorter mate was found upward in the tree then there is no need to search
// because we will never beat the current alpha. Same logic but with reversed
// signs applies also in the opposite condition of being mated instead of giving
// mate. In this case return a fail-high score.
alpha = Value.Create(Math.Max(Value.mated_in(stack.ply), alpha));
beta = Value.Create(Math.Min(Value.mate_in(stack.ply + 1), beta));
if (alpha >= beta)
return alpha;
}
Debug.Assert(0 <= stack.ply && stack.ply < _.MAX_PLY);
stack.currentMove = stack.ttMove = stackPlus1.excludedMove = bestMove = Move.MOVE_NONE;
stackPlus1.skipEarlyPruning = false;
stackPlus1.reduction = Depth.DEPTH_ZERO;
stackPlus2.killers0 = stackPlus2.killers1 = Move.MOVE_NONE;
// Step 4. Transposition table lookup
// We don't want the score of a partial search to overwrite a previous full search
// TT value, so we use a different position key in case of an excluded move.
excludedMove = stack.excludedMove;
posKey = excludedMove != 0 ? pos.exclusion_key() : pos.key();
tte = TranspositionTable.probe(posKey, out ttHit);
stack.ttMove = ttMove = RootNode ? RootMoves[(int) PVIdx].pv[0] : ttHit ? tte.move() : Move.MOVE_NONE;
ttValue = ttHit ? value_from_tt(tte.value(), stack.ply) : Value.VALUE_NONE;
// At non-PV nodes we check for a fail high/low. We don't prune at PV nodes
if (!PvNode
&& ttHit
&& tte.depth() >= depth
&& ttValue != Value.VALUE_NONE // Only in case of TT access race
&& (ttValue >= beta
? (tte.bound() & Bound.BOUND_LOWER) != 0
: (tte.bound() & Bound.BOUND_UPPER) != 0))
{
stack.currentMove = ttMove; // Can be Move.MOVE_NONE
// If ttMove is quiet, update killers, history, counter move on TT hit
if (ttValue >= beta && ttMove != 0 && !pos.capture_or_promotion(ttMove))
update_stats(pos, ss, ttMove, depth, null, 0);
return ttValue;
}
// Step 4a. Tablebase probe
if (!RootNode && Tablebases.Cardinality != 0)
{
var piecesCnt = pos.count(PieceType.ALL_PIECES, Color.WHITE) + pos.count(PieceType.ALL_PIECES, Color.BLACK);
if (piecesCnt <= Tablebases.Cardinality
&& (piecesCnt < Tablebases.Cardinality || depth >= Tablebases.ProbeDepth)
&& pos.rule50_count() == 0)
{
var found = 0;
var v = Tablebases.probe_wdl(pos, ref found);
if (found != 0)
{
Tablebases.Hits++;
var drawScore = Tablebases.UseRule50 ? 1 : 0;
value = v < -drawScore
? -Value.VALUE_MATE + _.MAX_PLY + stack.ply
: v > drawScore
? Value.VALUE_MATE - _.MAX_PLY - stack.ply
: Value.VALUE_DRAW + 2*v*drawScore;
tte.save(posKey, value_to_tt(value, stack.ply), Bound.BOUND_EXACT,
new Depth(Math.Min(_.MAX_PLY - Depth.ONE_PLY_C, (int)depth + 6*Depth.ONE_PLY_C)),
Move.MOVE_NONE, Value.VALUE_NONE, TranspositionTable.generation());
return value;
}
}
}
// Step 5. Evaluate the position statically
if (inCheck)
{
stack.staticEval = Value.VALUE_NONE;
goto moves_loop;
}
if (ttHit)
{
// Never assume anything on values stored in TT
if ((stack.staticEval = eval = tte.eval()) == Value.VALUE_NONE)
eval = stack.staticEval = Eval.evaluate(false, pos);
// Can ttValue be used as a better position evaluation?
if (ttValue != Value.VALUE_NONE)
if ((tte.bound() & (ttValue > eval ? Bound.BOUND_LOWER : Bound.BOUND_UPPER)) != 0)
eval = ttValue;
}
else
{
eval = stack.staticEval =
stackMinus1.currentMove != Move.MOVE_NULL
? Eval.evaluate(false, pos)
: -stackMinus1.staticEval + 2*Eval.Tempo;
tte.save(posKey, Value.VALUE_NONE, Bound.BOUND_NONE, Depth.DEPTH_NONE, Move.MOVE_NONE,
stack.staticEval, TranspositionTable.generation());
}
if (stack.skipEarlyPruning)
goto moves_loop;
// Step 6. Razoring (skipped when in check)
if (!PvNode
&& depth < 4*Depth.ONE_PLY
&& eval + razor_margin(depth) <= alpha
&& ttMove == Move.MOVE_NONE)
{
if (depth <= Depth.ONE_PLY_C
&& eval + razor_margin(3*Depth.ONE_PLY) <= alpha)
return qsearch(NodeType.NonPV, false, pos, ss, alpha, beta, Depth.DEPTH_ZERO);
var ralpha = alpha - razor_margin(depth);
var v = qsearch(NodeType.NonPV, false, pos, ss, ralpha, ralpha + 1, Depth.DEPTH_ZERO);
if (v <= ralpha)
return v;
}
// Step 7. Futility pruning: child node (skipped when in check)
if (!RootNode
&& depth < 7*Depth.ONE_PLY
&& eval - futility_margin(depth) >= beta
&& eval < Value.VALUE_KNOWN_WIN // Do not return unproven wins
&& pos.non_pawn_material(pos.side_to_move())!=0)
return eval - futility_margin(depth);
// Step 8. Null move search with verification search (is omitted in PV nodes)
if (!PvNode
&& depth >= 2*Depth.ONE_PLY_C
&& eval >= beta
&& pos.non_pawn_material(pos.side_to_move())!=0)
{
stack.currentMove = Move.MOVE_NULL;
Debug.Assert(eval - beta >= 0);
// Null move dynamic reduction based on depth and value
var R = ((823 + 67*depth)/256 + Math.Min((eval - beta)/Value.PawnValueMg, 3))*(int) Depth.ONE_PLY;
pos.do_null_move(st);
stackPlus1.skipEarlyPruning = true;
var nullValue = depth - R < Depth.ONE_PLY
? -qsearch(NodeType.NonPV, false, pos, new StackArrayWrapper(ss.table, ss.current + 1), -beta, -beta + 1,
Depth.DEPTH_ZERO)
: -search(NodeType.NonPV, false, pos, new StackArrayWrapper(ss.table, ss.current + 1), -beta, -beta + 1,
depth - R, !cutNode);
stackPlus1.skipEarlyPruning = false;
pos.undo_null_move();
if (nullValue >= beta)
{
// Do not return unproven mate scores
if (nullValue >= Value.VALUE_MATE_IN_MAX_PLY)
nullValue = beta;
if (depth < 12*Depth.ONE_PLY && Math.Abs(beta) < Value.VALUE_KNOWN_WIN)
return nullValue;
// Do verification search at high depths
stack.skipEarlyPruning = true;
var v = depth - R < Depth.ONE_PLY
? qsearch(NodeType.NonPV, false, pos, ss, beta - 1, beta, Depth.DEPTH_ZERO)
: search(NodeType.NonPV, false, pos, ss, beta - 1, beta, depth - R, false);
stack.skipEarlyPruning = false;
if (v >= beta)
return nullValue;
}
}
// Step 9. ProbCut (skipped when in check)
// If we have a very good capture (i.e. SEE > seeValues[captured_piece_type])
// and a reduced search returns a value much above beta, we can (almost) safely
// prune the previous move.
if (!PvNode
&& depth >= 5*Depth.ONE_PLY_C
&& Math.Abs(beta) < Value.VALUE_MATE_IN_MAX_PLY)
{
var rbeta = Value.Create(Math.Min(beta + 200, Value.VALUE_INFINITE));
var rdepth = depth - 4*Depth.ONE_PLY;
Debug.Assert(rdepth >= Depth.ONE_PLY_C);
Debug.Assert(stackMinus1.currentMove != Move.MOVE_NONE);
Debug.Assert(stackMinus1.currentMove != Move.MOVE_NULL);
var mp2 = new MovePicker(pos, ttMove, History, CounterMovesHistory,
Value.PieceValue[(int) Phase.MG][pos.captured_piece_type()]);
var ci2 = new CheckInfo(pos);
while ((move = mp2.next_move(false)) != Move.MOVE_NONE)
if (pos.legal(move, ci2.pinned))
{
stack.currentMove = move;
pos.do_move(move, st, pos.gives_check(move, ci2));
value =
-search(NodeType.NonPV, false, pos, new StackArrayWrapper(ss.table, ss.current + 1), -rbeta,
-rbeta + 1, rdepth, !cutNode);
pos.undo_move(move);
if (value >= rbeta)
return value;
}
}
// Step 10. Internal iterative deepening (skipped when in check)
if (depth >= (PvNode ? 5*Depth.ONE_PLY_C : 8*Depth.ONE_PLY_C)
&& ttMove == 0
&& (PvNode || stack.staticEval + 256 >= beta))
{
var d = depth - 2*Depth.ONE_PLY - (PvNode ? Depth.DEPTH_ZERO : depth/4);
stack.skipEarlyPruning = true;
search(PvNode ? NodeType.PV : NodeType.NonPV, false, pos, ss, alpha, beta, d, true);
stack.skipEarlyPruning = false;
tte = TranspositionTable.probe(posKey, out ttHit);
ttMove = ttHit ? tte.move() : Move.MOVE_NONE;
}
moves_loop: // When in check and at SpNode search starts from here
var prevMoveSq = Move.to_sq(stackMinus1.currentMove);
var countermove = Countermoves.table[pos.piece_on(prevMoveSq), prevMoveSq];
var mp = new MovePicker(pos, ttMove, depth, History, CounterMovesHistory, countermove, ss);
var ci = new CheckInfo(pos);
value = bestValue; // Workaround a bogus 'uninitialized' warning under gcc
var improving = stack.staticEval >= stackMinus2.staticEval
|| stack.staticEval == Value.VALUE_NONE
|| stackMinus2.staticEval == Value.VALUE_NONE;
var singularExtensionNode = !RootNode
&& !SpNode
&& depth >= 8*Depth.ONE_PLY_C
&& ttMove != Move.MOVE_NONE
/* && ttValue != Value.VALUE_NONE Already implicit in the next condition */
&& Math.Abs(ttValue) < Value.VALUE_KNOWN_WIN
&& excludedMove == 0// Recursive singular search is not allowed
&& ((tte.bound() & Bound.BOUND_LOWER) != 0)
&& tte.depth() >= depth - 3*Depth.ONE_PLY_C;
var quietsSearched = new MoveT[64];
// Step 11. Loop through moves
// Loop through all pseudo-legal moves until no moves remain or a beta cutoff occurs
while ((move = mp.next_move(SpNode)) != Move.MOVE_NONE)
{
Utils.WriteToLog($"mp.next_move = {(int) move}");
Debug.Assert(Move.is_ok(move));
if (move == excludedMove)
continue;
// At root obey the "searchmoves" option and skip moves not listed in Root
// Move List. As a consequence any illegal move is also skipped. In MultiPV
// mode we also skip PV moves which have been already searched.
if (RootNode && RootMoves.All(rootMove => rootMove.pv[0] != move))
continue;
if (SpNode)
{
// Shared counter cannot be decremented later if the move turns out to be illegal
if (!pos.legal(move, ci.pinned))
continue;
stack.moveCount = moveCount = ++splitPoint.moveCount;
ThreadHelper.lock_release(splitPoint.spinLock);
}
else
stack.moveCount = ++moveCount;
if (RootNode)
{
Signals.firstRootMove = (moveCount == 1);
if (thisThread == ThreadPool.main() && TimeManagement.elapsed() > 3000)
Output.WriteLine(
$"info depth {depth/Depth.ONE_PLY} currmove {UCI.move(move, pos.is_chess960())} currmovenumber {moveCount + PVIdx}");
}
if (PvNode)
stackPlus1.pv = new List<MoveT>();
var extension = Depth.DEPTH_ZERO;
var captureOrPromotion = pos.capture_or_promotion(move);
var givesCheck = Move.type_of(move) == MoveType.NORMAL && ci.dcCandidates == 0
? Bitboard.AndWithSquare(ci.checkSquares[Piece.type_of(pos.piece_on(Move.from_sq(move)))], Move.to_sq(move))!=0
: pos.gives_check(move, ci);
// Step 12. Extend checks
if (givesCheck && pos.see_sign(move) >= Value.VALUE_ZERO)
extension = Depth.ONE_PLY;
// Singular extension search. If all moves but one fail low on a search of
// (alpha-s, beta-s), and just one fails high on (alpha, beta), then that move
// is singular and should be extended. To verify this we do a reduced search
// on all the other moves but the ttMove and if the result is lower than
// ttValue minus a margin then we extend the ttMove.
if (singularExtensionNode
&& move == ttMove
&& extension == 0
&& pos.legal(move, ci.pinned))
{
var rBeta = ttValue - 2*depth/Depth.ONE_PLY;
stack.excludedMove = move;
stack.skipEarlyPruning = true;
value = search(NodeType.NonPV, false, pos, ss, rBeta - 1, rBeta, depth/2, cutNode);
stack.skipEarlyPruning = false;
stack.excludedMove = Move.MOVE_NONE;
if (value < rBeta)
extension = Depth.ONE_PLY;
}
// Update the current move (this must be done after singular extension search)
var newDepth = depth - Depth.ONE_PLY + extension;
// Step 13. Pruning at shallow depth
if (!RootNode
&& !captureOrPromotion
&& !inCheck
&& !givesCheck
&& !pos.advanced_pawn_push(move)
&& bestValue > Value.VALUE_MATED_IN_MAX_PLY)
{
// Move count based pruning
if (depth < 16*Depth.ONE_PLY
&& moveCount >= FutilityMoveCounts[improving ? 1 : 0, depth])
{
if (SpNode)
ThreadHelper.lock_grab(splitPoint.spinLock);
continue;
}
var predictedDepth = newDepth - reduction(PvNode, improving, depth, moveCount);
// Futility pruning: parent node
if (predictedDepth < 7*Depth.ONE_PLY)
{
var futilityValue = stack.staticEval + futility_margin(predictedDepth) + 256;
if (futilityValue <= alpha)
{
bestValue = Value.Create(Math.Max(bestValue, futilityValue));
if (SpNode)
{
ThreadHelper.lock_grab(splitPoint.spinLock);
if (bestValue > splitPoint.bestValue)
splitPoint.bestValue = bestValue;
}
continue;
}
}
// Prune moves with negative SEE at low depths
if (predictedDepth < 4*Depth.ONE_PLY && pos.see_sign(move) < Value.VALUE_ZERO)
{
if (SpNode)
ThreadHelper.lock_grab(splitPoint.spinLock);
continue;
}
}
// Speculative prefetch as early as possible
//prefetch(TT.first_entry(pos.key_after(move)));
// Check for legality just before making the move
if (!RootNode && !SpNode && !pos.legal(move, ci.pinned))
{
stack.moveCount = --moveCount;
continue;
}
stack.currentMove = move;
// Step 14. Make the move
pos.do_move(move, st, givesCheck);
// Step 15. Reduced depth search (LMR). If the move fails high it will be
// re-searched at full depth.
bool doFullDepthSearch;