2023-10-30 14:46:27 +01:00
import re
import sys
import subprocess
from shutil import which
from pathlib import Path
from argparse import ArgumentParser , Namespace
def main ( args : Namespace ) - > int :
2024-04-13 16:28:48 +02:00
if args . silent :
print = lambda * _ : . . .
2024-04-20 23:47:57 +02:00
else :
print = __builtins__ . print
2023-10-30 14:46:27 +01:00
print ( " Nimfish move validator v0.0.1 by nocturn9x " )
try :
STOCKFISH = ( args . stockfish or Path ( which ( " stockfish " ) ) ) . resolve ( strict = True )
except Exception as e :
print ( f " Could not locate stockfish executable -> { type ( e ) . __name__ } : { e } " )
2024-04-13 16:28:48 +02:00
return 2
2023-10-30 14:46:27 +01:00
try :
2024-04-23 01:50:56 +02:00
NIMFISH = ( args . nimfish or Path ( which ( " nimfish " ) ) ) . resolve ( strict = True )
2023-10-30 14:46:27 +01:00
except Exception as e :
print ( f " Could not locate nimfish executable -> { type ( e ) . __name__ } : { e } " )
2024-04-13 16:28:48 +02:00
return 2
2023-10-30 14:46:27 +01:00
print ( f " Starting Stockfish engine at { STOCKFISH . as_posix ( ) !r} " )
stockfish_process = subprocess . Popen ( STOCKFISH ,
stdout = subprocess . PIPE ,
stderr = subprocess . STDOUT ,
stdin = subprocess . PIPE ,
2024-04-08 20:28:31 +02:00
encoding = " u8 " ,
text = True ,
bufsize = 1
2023-10-30 14:46:27 +01:00
)
print ( f " Starting Nimfish engine at { NIMFISH . as_posix ( ) !r} " )
nimfish_process = subprocess . Popen ( NIMFISH ,
stdout = subprocess . PIPE ,
stderr = subprocess . STDOUT ,
stdin = subprocess . PIPE ,
2024-04-08 20:28:31 +02:00
encoding = " u8 " ,
text = True ,
bufsize = 1
)
2023-10-30 15:26:48 +01:00
print ( f " Setting position to { ( args . fen if args . fen else ' rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 ' ) !r} " )
2023-10-30 14:46:27 +01:00
if args . fen :
nimfish_process . stdin . write ( f " position fen { args . fen } \n " )
stockfish_process . stdin . write ( f " position fen { args . fen } \n " )
else :
nimfish_process . stdin . write ( " position startpos \n " )
stockfish_process . stdin . write ( " position startpos \n " )
print ( f " Engines started, beginning search to depth { args . ply } " )
nimfish_process . stdin . write ( f " go perft { args . ply } { ' bulk ' if args . bulk else ' ' } \n " )
stockfish_process . stdin . write ( f " go perft { args . ply } \n " )
2024-04-13 16:28:48 +02:00
stockfish_output , stockfish_error = stockfish_process . communicate ( )
nimfish_output , nimfish_error = nimfish_process . communicate ( )
if nimfish_process . returncode != 0 :
print ( f " Nimfish crashed, stderr output below: \n { nimfish_error } " )
if stockfish_process . returncode != 0 :
print ( f " Stockfish crashed, stderr below: \n { stockfish_error } " )
if not all ( [ stockfish_process . returncode == 0 , nimfish_process . returncode == 0 ] ) :
return 3
2023-10-30 14:46:27 +01:00
positions = {
" all " : { } ,
" stockfish " : { } ,
" nimfish " : { }
}
pattern = re . compile ( r " (?P<source>[a-h][1-8])(?P<target>[a-h][1-8])(?P<promotion>b|n|q|r)?: \ s(?P<nodes>[0-9]+) " , re . MULTILINE )
for ( source , target , promotion , nodes ) in pattern . findall ( stockfish_output ) :
move = f " { source } { target } { promotion } "
positions [ " all " ] [ move ] = [ int ( nodes ) ]
positions [ " stockfish " ] [ move ] = int ( nodes )
for ( source , target , promotion , nodes ) in pattern . findall ( nimfish_output ) :
move = f " { source } { target } { promotion } "
2023-11-01 19:07:09 +01:00
if move in positions [ " all " ] :
positions [ " all " ] [ move ] . append ( int ( nodes ) )
else :
positions [ " all " ] [ move ] = [ int ( nodes ) ]
2023-10-30 14:46:27 +01:00
positions [ " nimfish " ] [ move ] = int ( nodes )
2023-10-30 15:26:48 +01:00
missing = {
2023-10-30 14:46:27 +01:00
# Are in nimfish but not in stockfish
" nimfish " : [ ] ,
# Are in stockfish but not in nimfish
" stockfish " : [ ]
}
2023-10-30 15:26:48 +01:00
# What mistakes did Nimfish do?
mistakes = set ( )
2023-10-30 14:46:27 +01:00
for move , nodes in positions [ " all " ] . items ( ) :
2023-10-30 15:26:48 +01:00
if move not in positions [ " stockfish " ] :
missing [ " nimfish " ] . append ( move )
continue
elif move not in positions [ " nimfish " ] :
missing [ " stockfish " ] . append ( move )
continue
if nodes [ 0 ] != nodes [ 1 ] :
mistakes . add ( move )
2024-04-09 17:46:30 +02:00
mistakes = sorted ( list ( mistakes ) )
2023-10-30 14:46:27 +01:00
total_nodes = { " stockfish " : sum ( positions [ " stockfish " ] [ move ] for move in positions [ " stockfish " ] ) ,
" nimfish " : sum ( positions [ " nimfish " ] [ move ] for move in positions [ " nimfish " ] ) }
total_difference = total_nodes [ " stockfish " ] - total_nodes [ " nimfish " ]
2024-04-08 20:28:31 +02:00
print ( f " Stockfish searched { total_nodes [ ' stockfish ' ] } node { ' ' if total_nodes [ ' stockfish ' ] == 1 else ' s ' } " )
print ( f " Nimfish searched { total_nodes [ ' nimfish ' ] } node { ' ' if total_nodes [ ' nimfish ' ] == 1 else ' s ' } " )
2023-10-31 23:06:27 +01:00
2023-10-30 14:46:27 +01:00
if total_difference > 0 :
2024-04-09 17:46:30 +02:00
print ( f " Nimfish searched { total_difference } fewer node { ' ' if total_difference == 1 else ' s ' } than Stockfish " )
2023-10-31 23:06:27 +01:00
elif total_difference < 0 :
total_difference = abs ( total_difference )
print ( f " Nimfish searched { total_difference } more node { ' ' if total_difference == 1 else ' s ' } than Stockfish " )
2023-10-30 15:26:48 +01:00
else :
print ( " Node count is identical " )
pattern = re . compile ( r " (?: \ s \ s- \ sCaptures: \ s(?P<captures>[0-9]+)) \ n "
r " (?: \ s \ s- \ sChecks: \ s(?P<checks>[0-9]+)) \ n "
r " (?: \ s \ s- \ sE \ .P: \ s(?P<enPassant>[0-9]+)) \ n "
r " (?: \ s \ s- \ sCheckmates: \ s(?P<checkmates>[0-9]+)) \ n "
r " (?: \ s \ s- \ sCastles: \ s(?P<castles>[0-9]+)) \ n "
r " (?: \ s \ s- \ sPromotions: \ s(?P<promotions>[0-9]+)) " ,
re . MULTILINE )
extra : re . Match | None = None
2023-10-30 17:46:06 +01:00
if not args . bulk :
2023-10-30 15:26:48 +01:00
extra = pattern . search ( nimfish_output )
2023-10-31 23:06:27 +01:00
missed_total = len ( missing [ ' stockfish ' ] ) + len ( missing [ ' nimfish ' ] )
2023-10-30 15:26:48 +01:00
if missing [ " stockfish " ] or missing [ " nimfish " ] or mistakes :
2023-10-31 23:06:27 +01:00
print ( f " Found { missed_total } missed move { ' ' if missed_total == 1 else ' s ' } and { len ( mistakes ) } counting mistake { ' ' if len ( mistakes ) == 1 else ' s ' } , more info below: " )
2023-10-30 14:46:27 +01:00
if args . bulk :
print ( " Note: Nimfish was run in bulk-counting mode, so a detailed breakdown of each move type is not available. "
" To fix this, re-run the program without the --bulk option " )
2023-10-30 15:26:48 +01:00
if extra :
print ( f " Breakdown by move type: " )
2024-04-08 20:28:31 +02:00
print ( f " - Captures: { extra . group ( ' captures ' ) } " )
print ( f " - Checks: { extra . group ( ' checks ' ) } " )
print ( f " - En Passant: { extra . group ( ' enPassant ' ) } " )
print ( f " - Checkmates: { extra . group ( ' checkmates ' ) } " )
print ( f " - Castles: { extra . group ( ' castles ' ) } " )
print ( f " - Promotions: { extra . group ( ' promotions ' ) } " )
2023-10-30 15:26:48 +01:00
2023-10-30 14:46:27 +01:00
elif not args . bulk :
print ( " Unable to locate move breakdown in Nimfish output " )
2023-10-30 15:26:48 +01:00
if missing [ " stockfish " ] or missing [ " nimfish " ] :
2023-10-31 23:06:27 +01:00
print ( " \n Move count breakdown: " )
2023-10-30 15:26:48 +01:00
if missing [ " stockfish " ] :
2023-10-31 23:06:27 +01:00
print ( " Legal moves missed: " )
2023-10-30 15:26:48 +01:00
for move in missing [ " stockfish " ] :
print ( f " - { move } : { positions [ ' stockfish ' ] [ move ] } " )
if missing [ " nimfish " ] :
2024-04-08 20:28:31 +02:00
print ( " \n Illegal moves generated: " )
2023-10-30 15:26:48 +01:00
for move in missing [ " nimfish " ] :
print ( f " - { move } : { positions [ ' nimfish ' ] [ move ] } " )
if mistakes :
2024-04-08 20:28:31 +02:00
print ( " \n Counting mistakes made: " )
2023-10-30 15:26:48 +01:00
for move in mistakes :
2023-10-31 23:06:27 +01:00
missed = positions [ " stockfish " ] [ move ] - positions [ " nimfish " ] [ move ]
print ( f " - { move } : expected { positions [ ' stockfish ' ] [ move ] } , got { positions [ ' nimfish ' ] [ move ] } ( { ' - ' if missed > 0 else ' + ' } { abs ( missed ) } ) " )
2024-04-13 16:28:48 +02:00
return 1
2023-10-30 14:46:27 +01:00
else :
2023-10-31 23:06:27 +01:00
print ( " No discrepancies detected " )
2024-04-13 16:28:48 +02:00
return 0
2023-10-30 14:46:27 +01:00
2023-10-30 15:26:48 +01:00
2023-10-30 14:46:27 +01:00
if __name__ == " __main__ " :
2024-04-13 16:28:48 +02:00
parser = ArgumentParser ( description = " Automatically compare perft results between Nimfish and Stockfish " )
2023-10-30 14:46:27 +01:00
parser . add_argument ( " --fen " , " -f " , type = str , default = " " , help = " The FEN string of the position to start from (empty string means the initial one). Defaults to ' ' " )
parser . add_argument ( " --ply " , " -d " , type = int , required = True , help = " The depth to stop at, expressed in plys (half-moves) " )
2024-04-13 16:28:48 +02:00
parser . add_argument ( " --bulk " , action = " store_true " , help = " Enable bulk-counting for Nimfish (much faster) " , default = False )
2023-10-30 14:46:27 +01:00
parser . add_argument ( " --stockfish " , type = Path , help = " Path to the stockfish executable. Defaults to ' ' (detected automatically) " , default = None )
2023-11-13 09:52:37 +01:00
parser . add_argument ( " --nimfish " , type = Path , help = " Path to the nimfish executable. Defaults to ' ' (detected automatically) " , default = None )
2024-04-13 16:28:48 +02:00
parser . add_argument ( " --silent " , action = " store_true " , help = " Disable all output (a return code of 0 means the test was successful) " , default = False )
2024-04-23 01:50:56 +02:00
try :
sys . exit ( main ( parser . parse_args ( ) ) )
except KeyboardInterrupt :
sys . exit ( 255 )