7
7
from functools import partial
8
8
print = partial (print , flush = True )
9
9
10
+ version = 'sunfish nnue'
11
+
10
12
###############################################################################
11
13
# A small neural network to evaluate positions
12
14
###############################################################################
@@ -105,11 +107,13 @@ def features(board):
105
107
}
106
108
107
109
# Constants for tuning search
108
- #EVAL_ROUGHNESS = 13
109
- #QS_LIMIT = 200
110
- #QS_CAPTURE, QS_SINGLE, QS_DOUBLE = range(3)
111
- #QS_TYPE = QS_CAPTURE
112
- #debug = False
110
+ EVAL_ROUGHNESS = 13
111
+
112
+ # minifier-hide start
113
+ opt_ranges = dict (
114
+ EVAL_ROUGHNESS = (0 , 50 ),
115
+ )
116
+ # minifier-hide end
113
117
114
118
115
119
###############################################################################
@@ -258,11 +262,7 @@ def is_capture(self, move):
258
262
# to last forever (until python stackoverflows.) Thus we need to either
259
263
# dampen the eval function, or like here, reduce QS search to captures
260
264
# only. Well, captures plus promotions.
261
- return (
262
- self .board [move .j ] != "."
263
- or abs (move .j - self .kp ) < 2
264
- or self .board [move .i ] == "P" and (A8 <= move .j <= H8 or move .j == self .ep )
265
- )
265
+ return self .board [move .j ] != "." or abs (move .j - self .kp ) < 2 or move .prom
266
266
267
267
def compute_value (self ):
268
268
#relu6 = lambda x: np.minimum(np.maximum(x, 0), 6)
@@ -288,7 +288,6 @@ def hash(self):
288
288
# return (self.wf + self.bf).sum()
289
289
# return self._replace(wf=0, bf=0)
290
290
291
-
292
291
###############################################################################
293
292
# Search logic
294
293
###############################################################################
@@ -359,11 +358,7 @@ def bound(self, pos, gamma, depth, root=True):
359
358
def moves ():
360
359
# First try not moving at all. We only do this if there is at least one major
361
360
# piece left on the board, since otherwise zugzwangs are too dangerous.
362
- # It doesn't make sense to use this function for depth 2, since it will take us
363
- # to depth max(0, d-2)=0, meaning reducing by two. So it's not actually the
364
- # opponents turn. This seems like it should be a major bug?
365
- #if (depth >= 3 or depth == 1) and not root and any(c in pos.board for c in "NBRQ"):
366
- if depth >= 3 and not root :
361
+ if depth > 2 and not root and any (c in pos .board for c in "NBRQ" ):
367
362
yield None , - self .bound (pos .rotate (nullmove = True ), 1 - gamma , depth - 3 , False )
368
363
# For QSearch we have a different kind of null-move, namely we can just stop
369
364
# and not capture anything else.
@@ -387,9 +382,8 @@ def mvv_lva(move):
387
382
return score
388
383
389
384
killer = self .tp_move .get (pos .hash ())
390
- if killer :
391
- if depth > 0 or pos .is_capture (killer ):
392
- yield killer , - self .bound (pos .move (killer ), 1 - gamma , depth - 1 , False )
385
+ if killer and (depth > 0 or pos .is_capture (killer )):
386
+ yield killer , - self .bound (pos .move (killer ), 1 - gamma , depth - 1 , False )
393
387
394
388
# Then all the other moves
395
389
# moves = [(move, pos.move(move)) for move in pos.gen_moves()]
@@ -430,24 +424,12 @@ def mvv_lva(move):
430
424
self .tp_move [pos .hash ()] = move
431
425
break
432
426
433
- # Stalemate checking is a bit tricky: Say we failed low, because
434
- # we can't (legally) move and so the (real) score is -infty.
435
- # At the next depth we are allowed to just return r, -infty <= r < gamma,
436
- # which is normally fine.
437
- # However, what if gamma = -10 and we don't have any legal moves?
438
- # Then the score is actaully a draw and we should fail high!
439
- # Thus, if best < gamma and best < 0 we need to double check what we are doing.
440
- # This doesn't prevent sunfish from making a move that results in stalemate,
441
- # but only if depth == 1, so that's probably fair enough.
442
- # (Btw, at depth 1 we can also mate without realizing.)
443
- if best < gamma and best < 0 and depth > 0 :
444
- # A position is dead if the curent player has a move that captures the king
445
- is_dead = lambda pos : any (
446
- pos .move (m ).score <= - MATE_LOWER for m in pos .gen_moves ()
447
- )
448
- if all (is_dead (pos .move (m )) for m in pos .gen_moves ()):
449
- in_check = is_dead (pos .rotate (nullmove = True ))
450
- best = - MATE_UPPER if in_check else 0
427
+ # Stalemate checking
428
+ if depth > 0 and best == - MATE_UPPER :
429
+ flipped = pos .rotate (nullmove = True )
430
+ # Hopefully this is already in the TT because of null-move
431
+ in_check = self .bound (flipped , MATE_UPPER , 0 ) == MATE_UPPER
432
+ best = - MATE_LOWER if in_check else 0
451
433
452
434
# Table part 2
453
435
self .tp_score [pos .hash (), depth , root ] = (
@@ -477,7 +459,7 @@ def search(self, history):
477
459
# 'while lower != upper' would work, but play tests show a margin of 20 plays
478
460
# better.
479
461
lower , upper = - MATE_UPPER , MATE_UPPER
480
- while lower < upper - 1 :
462
+ while lower < upper - EVAL_ROUGHNESS :
481
463
score = self .bound (pos , gamma , depth )
482
464
if score >= gamma :
483
465
lower = score
@@ -504,11 +486,20 @@ def render(i):
504
486
505
487
wf , bf = features (initial )
506
488
hist = [Position (initial , 0 , wf , bf , (True , True ), (True , True ), 0 , 0 )]
489
+
490
+
491
+ # minifier-hide start
492
+ import sys , tools .uci
493
+ tools .uci .run (sys .modules [__name__ ], hist [- 1 ])
494
+ sys .exit ()
495
+ # minifier-hide end
496
+
497
+
507
498
searcher = Searcher ()
508
499
while True :
509
500
args = input ().split ()
510
501
if args [0 ] == "uci" :
511
- print (f"id name sunfish nnue " )
502
+ print (f"id name { version } " )
512
503
print ("uciok" )
513
504
514
505
elif args [0 ] == "isready" :
0 commit comments