def get_eval( self, comment: str, turn: bool, ply: int, black_eval: List[float], white_eval: List[float] ) -> float: """ Returns move_eval with SPOV in pawn unit. """ move_eval = 0.0 if 'book' in comment.lower(): return move_eval if comment == '': if ply % 2: move_eval = -black_eval[-1] else: move_eval = white_eval[-1] return move_eval if self.tcec: value = comment.split('wv=')[1].split(',')[0] if 'M' in value: mate_num = int(value.split('M')[1]) # Todo: Get mate score of Lc0. move_eval = Mate(mate_num).score(mate_score=32000) / 100 move_eval = move_eval if turn else -move_eval else: move_eval = float(comment.split('wv=')[1].split(',')[0]) move_eval = move_eval if turn else -move_eval # Cutechess else: # No eval/depth comment, just time. if len(comment.split()) == 1: value = comment.split('s')[0] # {0} try: if ply % 2: move_eval = -black_eval[-1] else: move_eval = white_eval[-1] except ValueError: pass elif '+M' in comment or '-M' in comment: mate_num = int(comment.split('/')[0].split('M')[1]) move_eval = Mate(mate_num).score(mate_score=32000) move_eval = (move_eval if '+M' in comment else -move_eval) / 100 else: # Not {White mates} if '/' in comment: move_eval = float(comment.split('/')[0]) return move_eval
def test_not_investigating_major_advantage_to_mate_threat(self): a = Cp(900) b = Mate(5) self.assertFalse(should_investigate(a, b, board)) a = Cp(-900) b = Mate(-5) self.assertFalse(should_investigate(a, b, board))
def test_investigating_minor_advantage_to_mate(self): a = Cp(100) b = Mate(5) self.assertTrue(should_investigate(a, b, board)) a = Cp(-100) b = Mate(-5) self.assertTrue(should_investigate(a, b, board))
def test_investigating_major_advantage_to_getting_mated(self): a = Cp(700) b = Mate(-5) self.assertTrue(should_investigate(a, b, board)) a = Cp(-700) b = Mate(5) self.assertTrue(should_investigate(a, b, board))
def test_investigating_mate_threat_to_major_disadvantage(self): a = Mate(5) b = Cp(-700) self.assertTrue(should_investigate(a, b, board)) a = Mate(-5) b = Cp(700) self.assertTrue(should_investigate(a, b, board))
def test_investigating_mate_threat_to_even_position(self): a = Mate(5) b = Cp(0) self.assertTrue(should_investigate(a, b, board)) a = Mate(-5) b = Cp(0) self.assertTrue(should_investigate(a, b, board))
def test_investigating_mate_threat_to_getting_mated(self): a = Mate(1) b = Mate(-1) self.assertTrue(should_investigate(a, b, board)) a = Mate(-1) b = Mate(1) self.assertTrue(should_investigate(a, b, board))
def test_investigating_even_position_to_mate(self): a = Cp(0) b = Mate(5) self.assertTrue(should_investigate(a, b, board)) a = Cp(0) b = Mate(-5) self.assertTrue(should_investigate(a, b, board))
def is_valid_mate_in_one(pair: NextMovePair, engine: SimpleEngine) -> bool: if pair.best.score != Mate(1): return False non_mate_win_threshold = 0.6 if not pair.second or win_chances(pair.second.score) <= non_mate_win_threshold: return True if pair.second.score == Mate(1): # if there's more than one mate in one, gotta look if the best non-mating move is bad enough logger.debug('Looking for best non-mating move...') info = engine.analyse(pair.node.board(), multipv = 5, limit = pair_limit) for score in [pv["score"].pov(pair.winner) for pv in info]: if score < Mate(1) and win_chances(score) > non_mate_win_threshold: return False return True return False
def cruncher(thread_id: int): eval_nb = 0 db = pymongo.MongoClient()['puzzler'] bad_coll = db['puzzle2_bad_maybe'] play_coll = db['puzzle2_puzzle'] engine = SimpleEngine.popen_uci('./stockfish') engine.configure({'Threads': 4}) for doc in bad_coll.find({"bad": {"$exists": False}}): try: if ord(doc["_id"][4]) % threads != thread_id: continue doc = play_coll.find_one({'_id': doc['_id']}) if not doc: continue puzzle = read(doc) board = puzzle.mainline[len(puzzle.mainline) - 2].board() info = engine.analyse( board, multipv=5, limit=chess.engine.Limit(nodes=30_000_000)) bad = False for score in [pv["score"].pov(puzzle.pov) for pv in info]: if score < Mate(1) and score > Cp(250): bad = True # logger.info(puzzle.id) bad_coll.update_one({"_id": puzzle.id}, {"$set": { "bad": bad }}) except Exception as e: logger.error(e)
def analyze_position(server: Server, engine: SimpleEngine, node: GameNode, prev_score: Score, current_eval: PovScore) -> Union[Puzzle, Score]: board = node.board() winner = board.turn score = current_eval.pov(winner) if board.legal_moves.count() < 2: return score game_url = node.game().headers.get("Site") logger.debug("{} {} to {}".format(node.ply(), node.move.uci() if node.move else None, score)) if prev_score > Cp(400): logger.debug("{} Too much of a winning position to start with {} -> {}".format(node.ply(), prev_score, score)) return score if is_up_in_material(board, winner): logger.debug("{} already up in material {} {} {}".format(node.ply(), winner, material_count(board, winner), material_count(board, not winner))) return score elif score >= Mate(1) and not allow_one_mover: logger.debug("{} mate in one".format(node.ply())) return score elif score > mate_soon: logger.info("Mate {}#{} Probing...".format(game_url, node.ply())) if server.is_seen_pos(node): logger.info("Skip duplicate position") return score mate_solution = cook_mate(engine, copy.deepcopy(node), winner) server.set_seen(node.game()) return Puzzle(node, mate_solution) if mate_solution is not None else score elif score >= Cp(0) and win_chances(score) > win_chances(prev_score) + 0.5: if score < Cp(400) and material_diff(board, winner) > -1: logger.info("Not clearly winning and not from being down in material, aborting") return score logger.info("Advantage {}#{} {} -> {}. Probing...".format(game_url, node.ply(), prev_score, score)) if server.is_seen_pos(node): logger.info("Skip duplicate position") return score puzzle_node = copy.deepcopy(node) solution : Optional[List[NextMovePair]] = cook_advantage(engine, puzzle_node, winner) server.set_seen(node.game()) if not solution: return score while len(solution) % 2 == 0 or not solution[-1].second: if not solution[-1].second: logger.info("Remove final only-move") solution = solution[:-1] if not solution or (len(solution) == 1 and not allow_one_mover): logger.info("Discard one-mover") return score last = list(puzzle_node.mainline())[len(solution)] gain = material_diff(last.board(), winner) - material_diff(board, winner) if gain > 1 or ( len(solution) == 1 and win_chances(solution[0].best.score) > win_chances(solution[0].second.score) + 0.5): return Puzzle(node, [p.best.move for p in solution]) return score else: return score
def is_valid_attack(pair: NextMovePair) -> bool: if pair.second is None: return True if pair.best.score == Mate(1): return True if pair.best.score == Mate(2): return pair.second.score < Cp(500) if pair.best.score == Mate(3): return pair.second.score < Cp(300) if win_chances(pair.best.score) > win_chances(pair.second.score) + 0.5: return True # if best move is mate, and second move still good but doesn't win material, # then best move is valid attack if pair.best.score.is_mate() and pair.second.score < Cp(400): next_node = pair.node.add_variation(pair.second.move) return not "x" in next_node.san() return False
def cook_mate(engine: SimpleEngine, node: GameNode, winner: Color) -> Optional[List[Move]]: """ Recursively calculate mate solution """ if node.board().is_game_over(): return [] best_move, second_move = get_two_best_moves(engine, node.board(), winner) if node.board().turn == winner: logger.debug("Getting only mate move...") if best_move.score < mate_soon: logger.info( "Best move is not a mate, we're probably not searching deep enough" ) return None if second_move is not None and second_move.score > Cp(-300): logger.debug("Second best move is not terrible") return None else: logger.debug("Getting defensive move...") if best_move.score < Mate(1) and second_move is not None: much_worse = second_move.score == Mate( 1) and best_move.score < Mate(3) if not much_worse: logger.info("A second defensive move is not that worse") return None next_moves = cook_mate(engine, node.add_main_variation(best_move.move), winner) if next_moves is None: return None return [best_move.move] + next_moves
def test_not_ambiguous_equality_vs_significant_disadvantage(self): self.assertFalse(ambiguous_best_move([ Cp(0), Cp(-825), Cp(-1079), ])) self.assertFalse(ambiguous_best_move([ Cp(-36), Cp(-485), Cp(-504), ])) self.assertFalse(ambiguous_best_move([ Cp(0), Cp(-963), Mate(-12), ]))
def test_not_ambiguous_mate_vs_counter_mate(self): self.assertFalse(ambiguous_best_move([ Mate(1), Mate(-14), Mate(-11), ])) self.assertFalse(ambiguous_best_move([ Mate(-2), Mate(10), Mate(8), ]))
def analyze_game(engine: SimpleEngine, game: Game) -> Optional[Tuple[GameNode, List[Move], Kind]]: """ Evaluate the moves in a game looking for puzzles """ game_url = game.headers.get("Site", "?") logger.debug("Analyzing game {}...".format(game_url)) prev_score: Score = Cp(0) for node in game.mainline(): ev = node.eval() if not ev: logger.debug("Skipping game without eval on move {}".format( node.board().fullmove_number)) return None winner = node.board().turn score = ev.pov(winner) # was the opponent winning until their last move if prev_score > Cp(-300) or not is_down_in_material( node.board(), winner): pass elif mate_soon < score < Mate(1): logger.info("Mate found on {}#{}. Probing...".format( game_url, ply_of(node.board()))) solution = cook_mate(engine, node, winner) if solution is not None: return node, solution, Kind("mate") elif score > juicy_advantage: logger.info("Advantage found on {}#{}. Probing...".format( game_url, ply_of(node.board()))) solution = cook_advantage(engine, node, winner) if solution is not None: return node, solution, Kind("mate") else: print(score) prev_score = score return None
def cook_advantage(engine: SimpleEngine, node: GameNode, winner: Color) -> Optional[List[Move]]: """ Recursively calculate advantage solution """ best_move, second_move = get_two_best_moves(engine, node.board(), winner) if node.board().turn == winner: logger.debug("Getting only advantage move...") if best_move.score < juicy_advantage: logger.info( "Best move is not a juicy advantage, we're probably not searching deep enough" ) return None if second_move is not None and second_move.score > Cp(-300): logger.debug("Second best move is not terrible") return None else: logger.debug("Getting defensive move...") if best_move.score.is_mate(): logger.info("Expected advantage, got mate?!") return None if second_move is not None: much_worse = second_move.score < Mate(2) if not much_worse: logger.info("A second defensive move is not that worse") return None next_moves = cook_advantage(engine, node.add_main_variation(best_move.move), winner) if next_moves is None: return None return [best_move.move] + next_moves
def test_eval_last_move_no_blunder_mate_in_two(self): board = chess.Board( "Q2r4/1p1k4/1P3ppp/1Kp1r3/4p2b/1B3P2/2P2q2/8 w - - 6 44") game = Game() game.board = board game.player1 = FakeDiscordUser(id=1) game.player2 = FakeDiscordUser(id=2) game.current_player = game.player1 game.last_eval = Mate(2) chess_bot = Chess() chess_bot.games.append(game) result = asyncio.run(chess_bot.eval_last_move(game)) self.assertFalse(result["blunder"]) if chess_bot.is_stockfish_enabled(): self.assertEqual(result["mate_in"], 2) else: self.assertIsNone(result["mate_in"])
def test_puzzle_16(self): self.get_puzzle("kr6/p5pp/Q4np1/3p4/6P1/2P1qP2/PK4P1/3R3R w - - 1 26", Cp(-30), "b2a1", Mate(1), "e3c3")
def test_puzzle_10(self) -> None: self.get_puzzle("5rk1/pp3p2/1q1R3p/6p1/5pBb/2P4P/PPQ2PP1/3Rr1K1 w - - 6 26", Cp(-450), "g1h2", Mate(2), "h4g3 f2g3 b6g1")
def test_puzzle_9(self) -> None: self.get_puzzle("7k/p3r1bP/1p1rp2q/8/2PBB3/4P3/P3KQ2/6R1 b - - 0 38", Cp(-110), "e6e5", Mate(2), "f2f8 g7f8 g1g8")
def test_puzzle_3(self) -> None: # https://lichess.org/wQptHOX6/white#61 self.get_puzzle("1r4k1/5p1p/pr1p2p1/q2Bb3/2P5/P1R3PP/KB1R1Q2/8 b - - 1 31", Cp(-4), "e5c3", Mate(3), "f2f7 g8h8 f7f6 c3f6 b2f6")
def test_puzzle_1(self) -> None: # https://lichess.org/analysis/standard/3q1k2/p7/1p2Q2p/5P1K/1P4P1/P7/8/8_w_-_-_5_57#112 self.get_puzzle("3q1k2/p7/1p2Q2p/5P1K/1P4P1/P7/8/8 w - - 5 57", Cp(-1000), "h5g6", Mate(2), "d8g5 g6h7 g5g7")
def test_not_puzzle_15(self) -> None: # https://lichess.org/nq1x9tln/black#76 self.not_puzzle("3r4/8/2p2n2/7k/P1P4p/1P6/2K5/6R1 w - - 0 43", Cp(-1000), "b3b4", Mate(4))
def test_not_puzzle_13(self): self.not_puzzle("8/5p1k/4p1p1/4Q3/3Pp1Kp/4P2P/5qP1/8 w - - 2 44", Cp(-6360), "e5e4", Mate(1))
def go(self, command): self.send(command) d = {} for eline in iter(self.__engine__.stdout.readline, ''): line = eline.rstrip() if 'depth' in line and 'score' in line and 'multipv' in line: if 'score cp' in line: score = int(line.split('score cp')[1].split()[0]) elif 'score mate' in line: mate_num = int(line.split('score mate')[1].split()[0]) # Todo: Create a formula to convert mate score to cp score. score = Mate(mate_num).score(mate_score=32000) else: print('info string missing score, use 0.') sys.stdout.flush() score = 0 mpv = int(line.split('multipv')[1].split()[0]) pv = line.split(' pv ')[1].split()[0] d.update({mpv: [pv, score]}) if 'bestmove ' in eline: # Returns a move based on prng. K = self.K logging.info(f'K = {self.K}') top_score = d[1][1] / 100 cnt, num, moves, scores = 0, [], [], [] for k, v in d.items(): cnt += 1 num.append(cnt) moves.append(v[0]) scores.append(round(v[1] / 100, 2)) d = [] for i, s in enumerate(scores): d.append(round(s - top_score, 2)) p = proba(d, K) f = F(p) data = { 'num': num, 'move': moves, 'scores': scores, 'd(i)': d, 'P(i)': p, 'F(i)': f } df = pd.DataFrame(data) print(df.to_string(index=False)) sys.stdout.flush() move_num, rn = search_move_num(d, f) bestmove = moves[move_num - 1] bestscore = scores[move_num - 1] print( f'info string movenumber {move_num} randomnumber {rn} minf {min(f)} maxf {max(f)}' ) print(f'info score cp {int(bestscore*100)}') sys.stdout.flush() print(f'bestmove {bestmove}') sys.stdout.flush() break
def is_valid_defense(pair: NextMovePair) -> bool: return True if pair.second is None or pair.second.score == Mate(1): return True return win_chances(pair.second.score) > win_chances(pair.best.score) + 0.25
from chess import Move, Color from chess.engine import SimpleEngine, Mate, Cp, Score, PovScore from chess.pgn import Game, ChildNode from typing import List, Optional, Union from util import get_next_move_pair, material_count, material_diff, is_up_in_material, win_chances from server import Server version = 47 logger = logging.getLogger(__name__) logging.basicConfig(format='%(asctime)s %(levelname)-4s %(message)s', datefmt='%m/%d %H:%M') pair_limit = chess.engine.Limit(depth = 50, time = 30, nodes = 30_000_000) mate_defense_limit = chess.engine.Limit(depth = 15, time = 10, nodes = 10_000_000) mate_soon = Mate(15) def is_valid_mate_in_one(pair: NextMovePair, engine: SimpleEngine) -> bool: if pair.best.score != Mate(1): return False non_mate_win_threshold = 0.6 if not pair.second or win_chances(pair.second.score) <= non_mate_win_threshold: return True if pair.second.score == Mate(1): # if there's more than one mate in one, gotta look if the best non-mating move is bad enough logger.debug('Looking for best non-mating move...') info = engine.analyse(pair.node.board(), multipv = 5, limit = pair_limit) for score in [pv["score"].pov(pair.winner) for pv in info]: if score < Mate(1) and win_chances(score) > non_mate_win_threshold: return False return True
def test_not_puzzle_11(self) -> None: self.not_puzzle("2kr3r/ppp2pp1/1b6/1P2p3/4P3/P2B2P1/2P2PP1/R4RK1 w - - 0 18", Cp(20), "f1d1", Mate(4))
def test_not_puzzle_14(self) -> None: # https://lichess.org/nq1x9tln/black#76 self.not_puzzle("3R4/1Q2nk2/4p2p/4n3/BP3ppP/P7/5PP1/2r3K1 w - - 2 39", Cp(-1000), "g1h2", Mate(4))