def _search(self, pos): """ Iterative deepening MTD-bi search """ self.nodes = 0 # In finished games, we could potentially go far enough to cause a recursion # limit exception. Hence we bound the ply. for depth in range(1, 1000): self.depth = depth # The inner loop is a binary search on the score of the position. # Inv: lower <= score <= upper # 'while lower != upper' would work, but play tests show a margin of 20 plays better. lower, upper = -MATE_UPPER, MATE_UPPER while lower < upper - EVAL_ROUGHNESS: gamma = (lower + upper + 1) // 2 score = self.bound(pos, gamma, depth) # Test for debugging search instability if not lower <= score <= upper: import tools print(__file__, 'search instability?', lower, upper, 'gamma score', gamma, score, 'depth', depth, 'pos', tools.renderFEN(pos)) if score >= gamma: lower = score if score < gamma: upper = score # We want to make sure the move to play hasn't been kicked out of the table, # So we make another call that must always fail high and thus produce a move. score = self.bound(pos, lower, depth) # Test for debugging tp_score assert score >= lower if self.tp_score.get((pos, depth, True)) is None: print("No score stored?", score) self.tp_score[(pos, depth, True)] = Entry(score, score) assert score == self.tp_score.get((pos, depth, True)).lower # Test for debugging tp_move arb_legal_move = lambda: next( (m for m in pos.gen_moves() if not any( pos.move(m).value(m1) >= MATE_LOWER for m1 in pos.move(m).gen_moves())), None) if self.tp_move.get(pos) is None: print('No move stored? Score: {}'.format(score)) self.tp_move[pos] = arb_legal_move() else: move = self.tp_move.get(pos) pos1 = pos.move(move) if any(pos1.value(m) >= MATE_LOWER for m in pos1.gen_moves()): import tools print('Returned illegal move? Score: {}'.format(score), 'move', tools.mrender(pos, move), 'pos', tools.renderFEN(pos)) self.tp_move[pos] = arb_legal_move() # Yield so the user may inspect the search yield
def allperft(f, depth=4, verbose=True): import gc lines = f.readlines() for d in range(1, depth+1): if verbose: print("Going to depth {}/{}".format(d, depth)) for line in lines: parts = line.split(';') if len(parts) <= d: continue if verbose: print(parts[0]) pos, score = tools.parseFEN(parts[0]), int(parts[d]) res = sum(1 for _ in tools.collect_tree_depth(tools.expand_position(pos), d)) if res != score: print('=========================================') print('ERROR at depth %d. Gave %d rather than %d' % (d, res, score)) print('=========================================') print(tools.renderFEN(pos,0)) for move in pos.gen_moves(): split = sum(1 for _ in tools.collect_tree_depth(tools.expand_position(pos.move(move)),1)) print('{}: {}'.format(tools.mrender(pos, move), split)) return False if verbose: print('') return True
def allperft(f, depth=4, verbose=True): import gc lines = f.readlines() for d in range(1, depth+1): if verbose: print("Going to depth {}/{}".format(d, depth)) for line in lines: parts = line.split(';') if verbose: print(parts[0]) pos, score = tools.parseFEN(parts[0]), int(parts[d]) res = sum(1 for _ in tools.collect_tree_depth(tools.expand_position(pos), d)) if res != score: print('=========================================') print('ERROR at depth %d. Gave %d rather than %d' % (d, res, score)) print('=========================================') print(tools.renderFEN(pos,0)) for move in pos.gen_moves(): split = sum(1 for _ in tools.collect_tree_depth(tools.expand_position(pos.move(move)),1)) print('{}: {}'.format(tools.mrender(pos, move), split)) return False if verbose: print('') return True
def test_fen(self): fen_file = os.path.join(os.path.dirname(__file__), 'tests/chessathome_openings.fen') for fen in open(fen_file): fen = fen.strip() pos = tools.parseFEN(fen) fen1 = tools.renderFEN(pos) self.assertEqual(fen, fen1, "Sunfish didn't correctly reproduce the FEN." + repr(pos))
def test_fen(self): fen_file = os.path.join(os.path.dirname(__file__), 'tests/chessathome_openings.fen') for fen in open(fen_file): fen = fen.strip() pos = tools.parseFEN(fen) fen1 = tools.renderFEN(pos) self.assertEqual( fen, fen1, "Sunfish didn't correctly reproduce the FEN." + repr(pos))
def main(): # test fen pos = tools.parseFEN(tools.FEN_INITIAL) assert (pos.board == elephantfish.initial) fen = tools.renderFEN(pos) assert (fen == tools.FEN_INITIAL) benchmark(5, 6) self_arena("elephantfish", "algorithms.elephantfish_improve", 100, 20, .1)
def test_fen2(self): initial = sunfish.Position(sunfish.initial, 0, (True,True), (True,True), 0, 0) for pos in tools.flatten_tree(tools.expand_position(initial),3): fen = tools.renderFEN(pos) self.assertEqual(fen.split()[1], 'wb'[tools.get_color(pos)], "Didn't read color correctly") pos1 = tools.parseFEN(fen) self.assertEqual(pos.board, pos1.board, "Sunfish didn't correctly reproduce the board") self.assertEqual(pos.wc, pos1.wc) self.assertEqual(pos.bc, pos1.bc) ep = pos.ep if not pos.board[pos.ep].isspace() else 0 ep1 = pos1.ep if not pos1.board[pos1.ep].isspace() else 0 kp = pos.kp if not pos.board[pos.kp].isspace() else 0 kp1 = pos1.kp if not pos1.board[pos1.kp].isspace() else 0 self.assertEqual(ep, ep1) self.assertEqual(kp, kp1)
def test_fen2(self): initial = sunfish.Position(sunfish.initial, 0, (True, True), (True, True), 0, 0) for pos in tools.flatten_tree(tools.expand_position(initial), 3): fen = tools.renderFEN(pos) self.assertEqual(fen.split()[1], 'wb'[tools.get_color(pos)], "Didn't read color correctly") pos1 = tools.parseFEN(fen) self.assertEqual(pos.board, pos1.board, "Sunfish didn't correctly reproduce the board") self.assertEqual(pos.wc, pos1.wc) self.assertEqual(pos.bc, pos1.bc) ep = pos.ep if not pos.board[pos.ep].isspace() else 0 ep1 = pos1.ep if not pos1.board[pos1.ep].isspace() else 0 kp = pos.kp if not pos.board[pos.kp].isspace() else 0 kp1 = pos1.kp if not pos1.board[pos1.kp].isspace() else 0 self.assertEqual(ep, ep1) self.assertEqual(kp, kp1)
def make_request(self, url, lastmove='', nextmove=''): fen = renderFEN(self.pos).split()[0] if lastmove is None: lastmove = '' if nextmove is None: nextmove = '' if self.pc == "black": fen = fen[::-1] try: response = requests.post(url, json={ 'fen': fen, 'lastmove': lastmove, 'arrows': nextmove }) except: response = None return response
def play(version1_version2_secs_plus_fen): ''' returns 1 if fish1 won, 0 for draw and -1 otherwise ''' version1, version2, secs, plus, fen = version1_version2_secs_plus_fen modules = [importlib.import_module(version1), importlib.import_module(version2)] searchers = [] for module in modules: if hasattr(module, 'Searcher'): searchers.append(module.Searcher()) else: searchers.append(module) times = [secs, secs] efactor = [1, 1] pos = tools.parseFEN(fen) seen = set() for d in range(200): moves_remain = 30 use = times[d%2]/moves_remain + plus # Use a bit more time, if we have more on the clock than our opponent use += (times[d%2] - times[(d+1)%2])/10 use = max(use, plus) t = time.time() m, score = searchers[d%2].search(pos, use*efactor[d%2]) efactor[d%2] *= (use/(time.time() - t))**.5 times[d%2] -= time.time() - t times[d%2] += plus #print('Used {:.2} rather than {:.2}. Off by {:.2}. Remaining: {}' #.format(time.time()-t, use, (time.time()-t)/use, times[d%2])) if times[d%2] < 0: print('{} ran out of time'.format(version2 if d%2 == 1 else version1)) return version1 if d%2 == 1 else version2 pass if m is None: print('Game not done, but no move? Score', score) name = version1 if d%2 == 0 else version2 print(version1, tools.renderFEN(pos)) assert False # Test move is_dead = lambda pos: any(pos.value(m) >= sunfish.MATE_LOWER for m in pos.gen_moves()) if is_dead(pos.move(m)): name = version1 if d%2 == 0 else version2 print('{} made an illegal move {} in position {}. Depth {}, Score {}'. format(name, tools.mrender(pos,m), tools.renderFEN(pos), searchers[d%2].depth, score)) assert False # Make the move pos = pos.move(m) # Test repetition draws # This is by far the most common type of draw if pos in seen: #print('Rep time at end', times) return None seen.add(pos) any_moves = not all(is_dead(pos.move(m)) for m in pos.gen_moves()) in_check = is_dead(pos.nullmove()) if not any_moves: if not in_check: # This is actually a bit interesting. Why would we ever throw away a win like this? name = version1 if d%2 == 0 else version2 print('{} stalemated? depth {} {}'.format( name, searchers[d%2].depth, tools.renderFEN(pos))) if score != 0: print('it got the wrong score: {} != 0'.format(score)) return None else: name = version1 if d%2 == 0 else version2 if score < sunfish.MATE_LOWER: print('{} mated, but did not realize. Only scored {} in position {}, depth {}'.format(name, score, tools.renderFEN(pos), searchers[d%2].depth)) return name print('Game too long', tools.renderFEN(pos)) return None
def svg(self): board = chess.Board(renderFEN(self.hist[-1])) lastmove = chess.Move.from_uci( self.lastmove) if self.lastmove else self.lastmove svg = chess.svg.board(board=board, lastmove=lastmove) return svg
def play(version1_version2_secs_plus_fen): ''' returns 1 if fish1 won, 0 for draw and -1 otherwise ''' version1, version2, secs, plus, fen = version1_version2_secs_plus_fen modules = [importlib.import_module(version1), importlib.import_module(version2)] searchers = [] for module in modules: if hasattr(module, 'Searcher'): searchers.append(module.Searcher()) else: searchers.append(module) times = [secs, secs] efactor = [1, 1] pos = tools.parseFEN(fen) seen = set() for d in range(200): moves_remain = 30 use = times[d%2]/moves_remain + plus # Use a bit more time, if we have more on the clock than our opponent use += (times[d%2] - times[(d+1)%2])/10 use = max(use, plus) t = time.time() m, score, depth = tools.search(searchers[d%2], pos, use*efactor[d%2]) efactor[d%2] *= (use/(time.time() - t))**.5 times[d%2] -= time.time() - t times[d%2] += plus #print('Used {:.2} rather than {:.2}. Off by {:.2}. Remaining: {}' #.format(time.time()-t, use, (time.time()-t)/use, times[d%2])) if times[d%2] < 0: print('{} ran out of time'.format(version2 if d%2 == 1 else version1)) return version1 if d%2 == 1 else version2 pass if m is None: print('Game not done, but no move? Score', score) name = version1 if d%2 == 0 else version2 print(version1, tools.renderFEN(pos)) assert False # Test move is_dead = lambda pos: any(pos.value(m) >= amwafish.MATE_LOWER for m in pos.gen_moves()) if is_dead(pos.move(m)): name = version1 if d%2 == 0 else version2 print('{} made an illegal move {} in position {}. Depth {}, Score {}'. format(name, tools.mrender(pos,m), tools.renderFEN(pos), depth, score)) return version2 if d%2 == 0 else version1 #assert False # Make the move pos = pos.move(m) # Test repetition draws # This is by far the most common type of draw if pos in seen: #print('Rep time at end', times) return None seen.add(pos) any_moves = not all(is_dead(pos.move(m)) for m in pos.gen_moves()) in_check = is_dead(pos.nullmove()) if not any_moves: if not in_check: # This is actually a bit interesting. Why would we ever throw away a win like this? name = version1 if d%2 == 0 else version2 print('{} stalemated? depth {} {}'.format( name, depth, tools.renderFEN(pos))) if score != 0: print('it got the wrong score: {} != 0'.format(score)) return None else: name = version1 if d%2 == 0 else version2 if score < amwafish.MATE_LOWER: print('{} mated, but did not realize. Only scored {} in position {}, depth {}'.format(name, score, tools.renderFEN(pos), depth)) return name print('Game too long', tools.renderFEN(pos)) return None
def main(eng): parser = argparse.ArgumentParser() parser.add_argument('module', help='chessEngine.py file (without .py)', type=str, default='crapbox', nargs='?') args = parser.parse_args() ''' look at this later sunfish = importlib.import_module(args.module) if args.tables is not None: pst_module = importlib.import_module(args.tables) sunfish.pst = pst_module.pst logging.basicConfig(filename='sunfish.log', level=logging.DEBUG) ''' out = Unbuffered(sys.stdout) def output(line): print(line) #print(line, file=out) #logging.debug(line) pos = tools.parseFEN(tools.FEN_INITIAL) ''' searcher = sunfish.Searcher() ''' color = WHITE our_time, opp_time = 1000, 1000 # time in centi-seconds show_thinking = False stack = [] while True: if stack: smove = stack.pop() else: smove = input() logging.debug(f'>>> {smove} ') if smove == 'quit': break elif smove == 'uci': output('id name Cristiceps') output('id author Benjamin Christie') output('uciok') elif smove == 'isready': output('readyok') elif smove == 'ucinewgame': stack.append('position fen ' + tools.FEN_INITIAL) # syntax specified in UCI # position [fen | startpos ] moves .... elif smove.startswith('position'): params = smove.split(' ') idx = smove.find('moves') if idx >= 0: moveslist = smove[idx:].split()[1:] else: moveslist = [] if params[1] == 'fen': if idx >= 0: fenpart = smove[:idx] else: fenpart = smove _, _, fen = fenpart.split(' ', 2) elif params[1] == 'startpos': fen = tools.FEN_INITIAL else: pass pos = tools.parseFEN(fen) color = WHITE if fen.split()[1] == 'w' else BLACK for move in moveslist: pos = pos.move(tools.mparse(color, move)) color = 1 - color elif smove.startswith('go'): # default options depth = 1000 movetime = -1 _, *params = smove.split(' ') for param, val in zip(*2 * (iter(params), )): if param == 'depth': depth = int(val) if param == 'movetime': movetime = int(val) if param == 'wtime': our_time = int(val) if param == 'btime': opp_time = int(val) moves_remain = 40 start = time.time() ponder = None renderedFen = tools.renderFEN(pos) ''' eng = customEngine.Engine() currMove = str(eng.getRandomMove(renderedFen)) ''' currMove = str(eng.getMove(renderedFen)) output("info currmove " + currMove) time.sleep(0.1) output("bestmove " + currMove) ''' if sdepth >= depth: break ''' elif smove.startswith('stop'): output("bestmove " + currMove) elif smove.startswith('time'): our_time = int(smove.split()[1]) elif smove.startswith('otim'): opp_time = int(smove.split()[1]) else: pass