@@ -32,7 +32,7 @@ public IterativeSearch(Board board, long maxNodes = long.MaxValue)
3232
3333 public IterativeSearch ( int searchDepth , Board board ) : this ( board )
3434 {
35- while ( ! GameOver && Depth < searchDepth )
35+ while ( Depth < searchDepth )
3636 SearchDeeper ( ) ;
3737 }
3838
@@ -43,35 +43,36 @@ public void SearchDeeper(Func<bool> killSwitch = null)
4343 _history . Scale ( ) ;
4444 StorePVinTT ( PrincipalVariation , Depth ) ;
4545 _killSwitch = new KillSwitch ( killSwitch ) ;
46- ( Score , PrincipalVariation ) = EvalPosition ( _root , Depth , SearchWindow . Infinite ) ;
46+ ( Score , PrincipalVariation ) = EvalPosition ( _root , 0 , Depth , SearchWindow . Infinite ) ;
4747 }
4848
4949 private void StorePVinTT ( Move [ ] pv , int depth )
5050 {
5151 Board position = new Board ( _root ) ;
52- foreach ( Move move in pv )
52+ for ( int ply = 0 ; ply < pv . Length ; ply ++ )
5353 {
54- Transpositions . Store ( position . ZobristHash , -- depth , SearchWindow . Infinite , Score , move ) ;
54+ Move move = pv [ ply ] ;
55+ Transpositions . Store ( position . ZobristHash , -- depth , ply , SearchWindow . Infinite , Score , move ) ;
5556 position . Play ( move ) ;
5657 }
5758 }
5859
59- private ( int Score , Move [ ] PV ) EvalPositionTT ( Board position , int depth , SearchWindow window )
60+ private ( int Score , Move [ ] PV ) EvalPositionTT ( Board position , int ply , int depth , SearchWindow window )
6061 {
61- if ( Transpositions . GetScore ( position . ZobristHash , depth , window , out int ttScore ) )
62+ if ( Transpositions . GetScore ( position . ZobristHash , depth , ply , window , out int ttScore ) )
6263 return ( ttScore , Array . Empty < Move > ( ) ) ;
6364
64- var result = EvalPosition ( position , depth , window ) ;
65- Transpositions . Store ( position . ZobristHash , depth , window , result . Score , default ) ;
65+ var result = EvalPosition ( position , ply , depth , window ) ;
66+ Transpositions . Store ( position . ZobristHash , depth , ply , window , result . Score , result . PV . Length > 0 ? result . PV [ 0 ] : default ) ;
6667 return result ;
6768 }
6869
69- private ( int Score , Move [ ] PV ) EvalPosition ( Board position , int depth , SearchWindow window )
70+ private ( int Score , Move [ ] PV ) EvalPosition ( Board position , int ply , int depth , SearchWindow window )
7071 {
7172 if ( depth <= 0 )
7273 {
7374 _mobilityBonus = Evaluation . ComputeMobility ( position ) ;
74- return ( QEval ( position , window ) , Array . Empty < Move > ( ) ) ;
75+ return ( QEval ( position , ply , window ) , Array . Empty < Move > ( ) ) ;
7576 }
7677
7778 NodesVisited ++ ;
@@ -80,16 +81,20 @@ private void StorePVinTT(Move[] pv, int depth)
8081
8182 Color color = position . SideToMove ;
8283 bool isChecked = position . IsChecked ( color ) ;
84+ //if the previous iteration found a mate we check the first few plys without null move to try and find the shortest mate or escape
85+ bool allowNullMove = Evaluation . IsCheckmate ( Score ) ? ( ply > Depth / 4 ) : true ;
8386
84- //should we try null move pruning?
85- if ( depth >= 2 && ! isChecked )
87+ //should we try null move pruning?
88+ if ( allowNullMove && depth >= 2 && ! isChecked && window . CanFailHigh ( color ) )
8689 {
8790 const int R = 2 ;
91+ //evaluate the position at reduced depth with a null-window around beta
92+ SearchWindow beta = window . GetUpperBound ( color ) ;
8893 //skip making a move
8994 Board nullChild = Playmaker . PlayNullMove ( position ) ;
90- //evaluate the position at reduced depth with a null-window around beta
91- ( int score , _ ) = EvalPositionTT ( nullChild , depth - R - 1 , window . GetUpperBound ( color ) ) ;
95+ ( int score , _ ) = EvalPositionTT ( nullChild , ply + 1 , depth - R - 1 , beta ) ;
9296 //is the evaluation "too good" despite null-move? then don't waste time on a branch that is likely going to fail-high
97+ //if the static eval look much worse the alpha also skip it
9398 if ( window . FailHigh ( score , color ) )
9499 return ( score , Array . Empty < Move > ( ) ) ;
95100 }
@@ -100,41 +105,39 @@ private void StorePVinTT(Move[] pv, int depth)
100105 foreach ( ( Move move , Board child ) in Playmaker . Play ( position , depth , _killers , _history ) )
101106 {
102107 expandedNodes ++ ;
108+ bool interesting = expandedNodes == 1 || isChecked || child . IsChecked ( child . SideToMove ) ;
103109
104- //moves after the PV node are unlikely to raise alpha. try to avoid a full evaluation!
105- if ( expandedNodes > 1 )
110+ //some near the leaves that appear hopeless can be skipped without evaluation
111+ if ( depth <= 4 && ! interesting )
106112 {
107- bool tactical = isChecked || child . IsChecked ( child . SideToMove ) ;
108-
109- //some moves are hopeless and can be skipped without deeper evaluation
110- if ( depth <= 4 && ! tactical )
111- {
112- int futilityMargin = ( int ) color * depth * MAX_GAIN_PER_PLY ;
113- if ( window . FailLow ( child . Score + futilityMargin , color ) )
114- continue ;
115- }
116-
117- //other moves are searched with a null-sized window and skipped if they don't raise alpha
118- if ( depth >= 2 )
119- {
120- //non-tactical late moves are searched at a reduced depth to make this test even faster!
121- int R = ( tactical || expandedNodes < 4 ) ? 0 : 2 ;
122- ( int score , _ ) = EvalPositionTT ( child , depth - R - 1 , window . GetLowerBound ( color ) ) ;
123- if ( window . FailLow ( score , color ) )
124- continue ;
125- }
113+ //if the static eval look much worse the alpha also skip it
114+ int futilityMargin = ( int ) color * depth * MAX_GAIN_PER_PLY ;
115+ if ( window . FailLow ( child . Score + futilityMargin , color ) )
116+ continue ;
117+ }
118+
119+ //moves after the PV node are unlikely to raise alpha.
120+ //avoid a full evaluation by searching with a null-sized window around alpha first
121+ //...we expect it to fail low but if it does not we have to research it!
122+ if ( depth >= 2 && expandedNodes > 1 )
123+ {
124+ //non-tactical late moves are searched at a reduced depth to make this test even faster!
125+ int R = ( interesting || expandedNodes < 4 ) ? 0 : 2 ;
126+ ( int score , _ ) = EvalPositionTT ( child , ply + 1 , depth - R - 1 , window . GetLowerBound ( color ) ) ;
127+ if ( window . FailLow ( score , color ) )
128+ continue ;
126129 }
127130
128- //this move is expected to raise alpha so we search at full depth!
129- var eval = EvalPositionTT ( child , depth - 1 , window ) ;
131+ //this move is expected to raise alpha so we search it at full depth!
132+ var eval = EvalPositionTT ( child , ply + 1 , depth - 1 , window ) ;
130133 if ( window . FailLow ( eval . Score , color ) )
131134 {
132135 _history . Bad ( position , move , depth ) ;
133136 continue ;
134137 }
135138
136139 //the position has a new best move and score!
137- Transpositions . Store ( position . ZobristHash , depth , window , eval . Score , move ) ;
140+ Transpositions . Store ( position . ZobristHash , depth , ply , window , eval . Score , move ) ;
138141 //set the PV to this move, followed by the PV of the childnode
139142 pv = Merge ( move , eval . PV ) ;
140143 //...and maybe we even get a beta cutoff
@@ -147,15 +150,21 @@ private void StorePVinTT(Move[] pv, int depth)
147150 _killers . Add ( move , depth ) ;
148151 }
149152
150- return ( window . GetScore ( color ) , pv ) ;
153+ return ( GetScore ( window , color ) , pv ) ;
151154 }
152155 }
153156
154157 //checkmate or draw?
155158 if ( expandedNodes == 0 )
156- return ( position . IsChecked ( color ) ? Evaluation . Checkmate ( color ) : 0 , Array . Empty < Move > ( ) ) ;
159+ return ( position . IsChecked ( color ) ? Evaluation . Checkmate ( color , ply ) : 0 , Array . Empty < Move > ( ) ) ;
157160
158- return ( window . GetScore ( color ) , pv ) ;
161+ return ( GetScore ( window , color ) , pv ) ;
162+ }
163+
164+ private static int GetScore ( SearchWindow window , Color color )
165+ {
166+ int score = window . GetScore ( color ) ;
167+ return score ;
159168 }
160169
161170 private static Move [ ] Merge ( Move move , Move [ ] pv )
@@ -166,7 +175,7 @@ private static Move[] Merge(Move move, Move[] pv)
166175 return result ;
167176 }
168177
169- private int QEval ( Board position , SearchWindow window )
178+ private int QEval ( Board position , int ply , SearchWindow window )
170179 {
171180 NodesVisited ++ ;
172181 if ( Aborted )
@@ -180,7 +189,7 @@ private int QEval(Board position, SearchWindow window)
180189 int standPatScore = position . Score + _mobilityBonus ;
181190 //Cut will raise alpha and perform beta cutoff when standPatScore is too good
182191 if ( window . Cut ( standPatScore , color ) )
183- return window . GetScore ( color ) ;
192+ return GetScore ( window , color ) ;
184193 }
185194
186195 int expandedNodes = 0 ;
@@ -189,7 +198,7 @@ private int QEval(Board position, SearchWindow window)
189198 {
190199 expandedNodes ++ ;
191200 //recursively evaluate the resulting position (after the capture) with QEval
192- int score = QEval ( child , window ) ;
201+ int score = QEval ( child , ply + 1 , window ) ;
193202
194203 //Cut will raise alpha and perform beta cutoff when the move is too good
195204 if ( window . Cut ( score , color ) )
@@ -198,14 +207,14 @@ private int QEval(Board position, SearchWindow window)
198207
199208 //checkmate?
200209 if ( expandedNodes == 0 && inCheck )
201- return Evaluation . Checkmate ( color ) ;
210+ return Evaluation . Checkmate ( color , ply ) ;
202211
203212 //stalemate?
204213 if ( expandedNodes == 0 && ! LegalMoves . HasMoves ( position ) )
205214 return 0 ;
206215
207216 //can't capture. We return the 'alpha' which may have been raised by "stand pat"
208- return window . GetScore ( color ) ;
217+ return GetScore ( window , color ) ;
209218 }
210219 }
211220}
0 commit comments