From 08dfd062b73a889fe4a9af3e9d1a00c0507f45b3 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Fri, 22 Aug 2025 21:19:10 +0200 Subject: [PATCH] feat: record who played who and date --- PYTHON/lichess_bot/.bot_version | 2 +- PYTHON/lichess_bot/engine.py | 126 ++++++++++++++++++++++++-------- PYTHON/lichess_bot/main.py | 37 ++++++++++ 3 files changed, 133 insertions(+), 32 deletions(-) diff --git a/PYTHON/lichess_bot/.bot_version b/PYTHON/lichess_bot/.bot_version index e440e5c..bf0d87a 100644 --- a/PYTHON/lichess_bot/.bot_version +++ b/PYTHON/lichess_bot/.bot_version @@ -1 +1 @@ -3 \ No newline at end of file +4 \ No newline at end of file diff --git a/PYTHON/lichess_bot/engine.py b/PYTHON/lichess_bot/engine.py index f88d3d4..f088397 100644 --- a/PYTHON/lichess_bot/engine.py +++ b/PYTHON/lichess_bot/engine.py @@ -223,11 +223,23 @@ class RandomEngine: alpha = stand_pat # Explore captures to avoid horizon effect + # Consider only captures/promotions and order by SEE to reduce blunders + capture_moves: list[tuple[int, chess.Move]] = [] for move in self._ordered_moves(board): if time.time() >= self._deadline: break if not board.is_capture(move) and not move.promotion: continue + try: + capture_moves.append((int(self._see_value(board, move)), move)) + except Exception: + capture_moves.append((0, move)) + + capture_moves.sort(key=lambda t: t[0], reverse=True) + + for _, move in capture_moves: + if time.time() >= self._deadline: + break board.push(move) score = -self._quiescence(board, -beta, -alpha, start) board.pop() @@ -238,70 +250,67 @@ class RandomEngine: return alpha def _ordered_moves(self, board: chess.Board): - # Simple move ordering: captures/promotions first, then checks + # Move ordering that mixes tactical SEE with simple heuristics def score_move(m: chess.Move) -> int: s = 0 - if board.is_capture(m): + is_cap = board.is_capture(m) + if is_cap: s += 1000 if m.promotion: s += 800 try: if board.gives_check(m): - s += 100 + s += 120 + except Exception: + pass + + # SEE: reward good captures and avoid obviously losing moves + try: + see = int(self._see_value(board, m)) + if is_cap or see < 0: + s += max(-600, min(600, see)) except Exception: pass - # Early-game heuristics to avoid silly shuffles early = self._is_early_game(board) piece = board.piece_at(m.from_square) if piece: - # Strongly prefer castling when legal if board.is_castling(m): - s += 500 - # Prefer developing minors from back rank + s += 650 if piece.piece_type in (chess.KNIGHT, chess.BISHOP): - if chess.square_rank(m.from_square) in (0, 7) and not board.is_capture(m): - s += 80 - # Penalize knights to the rim early (unless it's a capture or check) + if chess.square_rank(m.from_square) in (0, 7) and not is_cap: + s += 90 if early and piece.piece_type == chess.KNIGHT: to_file = chess.square_file(m.to_square) - if to_file in (0, 7) and not board.is_capture(m): - s -= 120 - # Discourage king walks when not castling + if to_file in (0, 7) and not is_cap: + s -= 140 if piece.piece_type == chess.KING and early and not board.is_castling(m): - s -= 400 - # Mildly discourage rook moves very early if both knights and bishops are undeveloped + s -= 450 if piece.piece_type == chess.ROOK and early and self._most_minors_undeveloped(board, piece.color): - s -= 120 - # Discourage early queen sorties unless forcing - if piece.piece_type == chess.QUEEN and early and not board.is_capture(m): + s -= 140 + if piece.piece_type == chess.QUEEN and early and not is_cap: try: gives_check = board.gives_check(m) except Exception: gives_check = False if not gives_check: - s -= 90 - # Discourage moving the same piece twice very early if not forcing - if early and not board.is_capture(m) and not board.is_castling(m): + s -= 120 + if early and not is_cap and not board.is_castling(m): if not self._is_start_square(piece.piece_type, piece.color, m.from_square): - # unless stepping into center to_center = chess.square_file(m.to_square) in (3, 4) and chess.square_rank(m.to_square) in (2, 3, 4, 5) if not to_center: - s -= 60 - # Penalize weakening f-pawn pushes early - if piece.piece_type == chess.PAWN and early and not board.is_capture(m): + s -= 70 + if piece.piece_type == chess.PAWN and early and not is_cap: from_file = chess.square_file(m.from_square) from_rank = chess.square_rank(m.from_square) to_rank = chess.square_rank(m.to_square) - # f2->f3 or f7->f6 if from_file == 5: if piece.color == chess.WHITE and from_rank == 1 and to_rank == 2: - s -= 120 + s -= 140 if piece.color == chess.BLACK and from_rank == 6 and to_rank == 5: - s -= 120 - # small bonus for central pawn advances + s -= 140 if chess.square_file(m.to_square) in (3, 4): - s += 40 + s += 50 return s moves = list(board.legal_moves) @@ -365,8 +374,11 @@ class RandomEngine: # Piece-square tendencies (small) pst = self._pst_score(board) + # Hanging/loose pieces penalty + hanging_pen = self._hanging_pieces_penalty(board) + # Aggregate white-centric score then convert to side-to-move via negamax - white_score = material - dp_pen + mobility_term + center_score + rook_file_bonus + safety + pst + white_score = material - dp_pen + mobility_term + center_score + rook_file_bonus + safety + pst - hanging_pen return white_score if board.turn == chess.WHITE else -white_score def _opening_book_move(self, board: chess.Board) -> Optional[chess.Move]: @@ -502,3 +514,55 @@ class RandomEngine: if (file_idx, rank_idx) in {(4, 7), (3, 7)}: return -10 return 0 + + # --- Tactical helpers --- + def _see_value(self, board: chess.Board, move: chess.Move) -> int: + """Static Exchange Evaluation for a move in centipawns. + + Positive is good for the side to move. Uses python-chess SEE when available. + """ + if hasattr(board, "see"): + return int(board.see(move)) + # Fallback MVV/LVA approximation + victim = board.piece_at(move.to_square) + attacker = board.piece_at(move.from_square) + if not attacker: + return 0 + gain = 0 + if victim: + gain += self.piece_values.get(victim.piece_type, 0) + gain -= self.piece_values.get(attacker.piece_type, 0) + return gain + + def _hanging_pieces_penalty(self, board: chess.Board) -> int: + """Penalty for pieces that can be captured for non-negative SEE by the opponent.""" + pen_white = 0 + pen_black = 0 + # Evaluate from a neutral board state without mutating turn logic + for sq, pc in board.piece_map().items(): + if pc.piece_type == chess.KING: + continue + opp = not pc.color + # If opponent has a legal capture on this square with SEE >= 0, penalize + attackers = board.attackers(opp, sq) + if not attackers: + continue + bad = False + for a in attackers: + m = chess.Move(a, sq) + if m in board.legal_moves: + try: + see_gain = self._see_value(board, m) + except Exception: + see_gain = self.piece_values.get(pc.piece_type, 0) - 1 + if see_gain >= 0: + bad = True + break + if bad: + val = int(self.piece_values.get(pc.piece_type, 0) * 0.33) + if pc.color == chess.WHITE: + pen_white += val + else: + pen_black += val + # Convert to white-centric score + return pen_white - pen_black diff --git a/PYTHON/lichess_bot/main.py b/PYTHON/lichess_bot/main.py index 4b3670e..140a404 100644 --- a/PYTHON/lichess_bot/main.py +++ b/PYTHON/lichess_bot/main.py @@ -53,6 +53,11 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No my_ms = None opp_ms = None inc_ms = 0 + # Meta info for logging/PGN + game_date_iso: Optional[str] = None + white_name: Optional[str] = None + black_name: Optional[str] = None + site_url: Optional[str] = None try: for event in api.stream_game_events(game_id): et = event.get("type") @@ -69,6 +74,18 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No # Discover my color from gameFull white_id = event["white"].get("id") black_id = event["black"].get("id") + white_name = event["white"].get("name") or white_id or "?" + black_name = event["black"].get("name") or black_id or "?" + # Set site and date if available + try: + # Lichess event may include 'createdAt' ms epoch + created_ms = event.get("createdAt") or event.get("createdAtDate") + if created_ms: + import datetime + game_date_iso = datetime.datetime.utcfromtimestamp(int(created_ms)/1000).strftime("%Y.%m.%d") + except Exception: + pass + site_url = f"https://lichess.org/{game_id}" me = api.get_my_user_id() if me == white_id: color = "white" @@ -158,6 +175,14 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No # Record the bot version in the PGN headers try: game.headers["BotVersion"] = f"v{bot_version}" + if site_url: + game.headers["Site"] = site_url + if game_date_iso: + game.headers["Date"] = game_date_iso + if white_name: + game.headers["White"] = white_name + if black_name: + game.headers["Black"] = black_name except Exception: pass with open(game_log_path, "a") as lf: @@ -250,7 +275,19 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No # If PGN marker not found (unexpected), append at end insert_idx = len(content) + # Prepend meta information block for easier parsing later + meta_lines = [] + if game_date_iso: + meta_lines.append(f"Date: {game_date_iso}") + if white_name or black_name: + meta_lines.append(f"Players: {white_name or '?'} vs {black_name or '?'}") + if meta_lines: + meta_block = "\n".join(meta_lines) + "\n" + else: + meta_block = "" + analysis_block = ( + (meta_block if meta_block else "") + "ANALYSIS:\n" + analysis_text.rstrip() + "\n\n" ) new_content = content[:insert_idx] + analysis_block + content[insert_idx:]