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 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_major_advantage_to_even_position(self): a = Cp(700) b = Cp(0) self.assertTrue(should_investigate(a, b, board)) a = Cp(-700) b = Cp(0) 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_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 test_not_investigating_even_position(self): board = Board("4k3/8/3n4/3N4/8/8/4K3/8 w - - 0 1") a = Cp(0) b = Cp(0) self.assertFalse(should_investigate(a, b, board)) a = Cp(9) b = Cp(9) self.assertFalse(should_investigate(a, b, board))
def test_investigating_moderate_score_changes(self): score_changes = [ [0, 200], [50, 200], [-50, 200], ] for a, b in score_changes: a = Cp(a) b = Cp(b) self.assertTrue(should_investigate(a, b, board))
def test_not_ambiguous_slight_advantage_vs_significant_disadvantage(self): self.assertFalse(ambiguous_best_move([ Cp(146), Cp(-405), ])) self.assertFalse(ambiguous_best_move([ Cp(149), Cp(-458), Cp(-543), ]))
def test_investigating_major_score_changes(self): score_changes = [ [0, 500], [100, 500], [100, -100], ] for a, b in score_changes: a = Cp(a) b = Cp(b) self.assertTrue(should_investigate(a, b, board))
def test_not_investigating_insignificant_score_changes(self): score_changes = [ [0, 0], [-50, 50], [50, -50], [-70, -70], [70, 70], ] for a, b in score_changes: a = Cp(a) b = Cp(b) self.assertFalse(should_investigate(a, b, board))
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 cook_advantage(engine: SimpleEngine, node: ChildNode, winner: Color) -> Optional[List[NextMovePair]]: if node.board().is_repetition(2): logger.debug("Found repetition, canceling") return None next = get_next_move(engine, node, winner) if not next: logger.debug("No next move") return [] if next.best.score.is_mate(): logger.debug("Expected advantage, got mate?!") return None if next.best.score < Cp(200): logger.debug("Not winning enough, aborting") return None follow_up = cook_advantage(engine, node.add_main_variation(next.best.move), winner) if follow_up is None: return None return [next] + follow_up
def find_puzzle_candidates(game: Game, scan_depth=SCAN_DEPTH) -> List[Puzzle]: """ finds puzzle candidates from a chess game """ log(Color.DIM, "Scanning game for puzzles (depth: %d)..." % scan_depth) prev_score = Cp(0) puzzles = [] i = 0 node = game while not node.is_end(): next_node = node.variation(0) next_board = next_node.board() cur_score = AnalysisEngine.best_move(next_board, scan_depth).score board = node.board() highlight_move = False if should_investigate(prev_score, cur_score, board): highlight_move = True puzzle = Puzzle( board, next_node.move, ) puzzles.append(puzzle) log_move(board, next_node.move, cur_score, highlight=highlight_move) prev_score = cur_score node = next_node i += 1 return puzzles
def analyze_game(server: Server, engine: SimpleEngine, game: Game, args: argparse.Namespace) -> Optional[Puzzle]: logger.debug("Analyzing game {}...".format(game.headers.get("Site"))) prev_score: Score = Cp(20) for node in game.mainline(): current_eval = node.eval() if not current_eval: logger.debug("Skipping game without eval on ply {}".format( node.ply())) return None result = analyze_position(server, engine, node, prev_score, current_eval, args) if isinstance(result, Puzzle): return result prev_score = -result logger.debug("Found nothing from {}".format(game.headers.get("Site"))) return None
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 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 test_not_ambiguous_significant_advantage_vs_slight_advantage(self): self.assertFalse(ambiguous_best_move([ Cp(379), Cp(78), Cp(77), ])) self.assertFalse(ambiguous_best_move([ Cp(365), Cp(110), Cp(95), ])) self.assertFalse(ambiguous_best_move([ Cp(-683), Cp(-81), Cp(-65), ]))
def test_not_ambiguous_equality_vs_slight_disadvantage(self): self.assertFalse(ambiguous_best_move([ Cp(3), Cp(-131), Cp(-200), ])) self.assertFalse(ambiguous_best_move([ Cp(3), Cp(-170), Cp(-371), ])) self.assertFalse(ambiguous_best_move([ Cp(0), Cp(-282), Cp(-293), ]))
def should_resign(self, move_list): """ Have ai decide if it wishes to resign or offer a draw, returning true if it wishes to resign """ # If the best move is still pretty bad, then resign best_move = move_list[0] move_score = self.get_move_score(best_move) # TODO may still need to fine tune if move_score > Cp(2000): return True return False
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 analyze_game(self, game: Game, tier: int) -> Optional[Puzzle]: logger.debug(f'Analyzing tier {tier} {game.headers.get("Site")}...') prev_score: Score = Cp(20) seen_epds: Set[str] = set() board = game.board() skip_until_irreversible = False for node in game.mainline(): if skip_until_irreversible: if board.is_irreversible(node.move): skip_until_irreversible = False seen_epds.clear() else: board.push(node.move) continue current_eval = node.eval() if not current_eval: logger.debug("Skipping game without eval on ply {}".format( node.ply())) return None board.push(node.move) epd = board.epd() if epd in seen_epds: skip_until_irreversible = True continue seen_epds.add(epd) if board.castling_rights != maximum_castling_rights(board): continue result = self.analyze_position(node, prev_score, current_eval, tier) if isinstance(result, Puzzle): return result prev_score = -result logger.debug("Found nothing from {}".format(game.headers.get("Site"))) return None
def test_eval_last_move_no_blunder_no_mate(self): board = chess.Board() board.push_san("g4") board.push_san("e5") game = Game() game.board = board game.player1 = FakeDiscordUser(id=1) game.player2 = FakeDiscordUser(id=2) game.current_player = game.player1 game.last_eval = Cp(0) chess_bot = Chess() chess_bot.games.append(game) chess_bot.stockfish_limit["time"] = 2 result = asyncio.run(chess_bot.eval_last_move(game)) self.assertFalse(result["blunder"]) self.assertIsNone(result["mate_in"])
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 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 cook_advantage(engine: SimpleEngine, node: ChildNode, winner: Color) -> Optional[List[NextMovePair]]: board = node.board() if board.is_repetition(2): logger.debug("Found repetition, canceling") return None pair = get_next_pair(engine, node, winner) if not pair: return [] if pair.best.score < Cp(200): logger.debug("Not winning enough, aborting") return None follow_up = cook_advantage(engine, node.add_main_variation(pair.best.move), winner) if follow_up is None: return None return [pair] + follow_up
def test_eval_last_move_lost_position_blunders_mate(self): board = chess.Board( "Q1kr4/1p6/1P3ppp/1Kp1r3/4p2b/1B3P2/2P2q2/8 b - - 5 43") game = Game() game.board = board game.player1 = FakeDiscordUser(id=1) game.player2 = FakeDiscordUser(id=2) game.current_player = game.player1 game.last_eval = Cp(1000) chess_bot = Chess() chess_bot.games.append(game) result = asyncio.run(chess_bot.eval_last_move(game)) if chess_bot.is_stockfish_enabled(): self.assertTrue(result["blunder"]) self.assertEqual(result["mate_in"], -2) else: self.assertFalse(result["blunder"]) self.assertIsNone(result["mate_in"])
def test_eval_last_move_last_move_blunder_mate_in_one(self): board = chess.Board() board.push_san("g4") board.push_san("e5") board.push_san("f4") game = Game() game.board = board game.player1 = FakeDiscordUser(id=1) game.player2 = FakeDiscordUser(id=2) game.current_player = game.player1 game.last_eval = Cp(0) chess_bot = Chess() chess_bot.games.append(game) result = asyncio.run(chess_bot.eval_last_move(game)) if chess_bot.is_stockfish_enabled(): self.assertTrue(result["blunder"]) self.assertEqual(result["mate_in"], 1) else: self.assertFalse(result["blunder"]) self.assertIsNone(result["mate_in"])