diff --git a/PYTHON/lichess_bot/tools/generate_blunder_tests.py b/PYTHON/lichess_bot/tools/generate_blunder_tests.py index 61e3dca..345c7d7 100755 --- a/PYTHON/lichess_bot/tools/generate_blunder_tests.py +++ b/PYTHON/lichess_bot/tools/generate_blunder_tests.py @@ -327,47 +327,12 @@ def append_cases_to_unified_test( def _process_single_log(log_path: str) -> int: """Process a single log file. Returns 0 on success, non-zero otherwise.""" - try: - with open(log_path, encoding="utf-8") as fh: - text = fh.read() - except FileNotFoundError: - logging.exception(f"Log file not found: {log_path}") - return 2 - - try: - blunders = parse_columns_for_blunders(text) - except Exception: - logging.exception(f"Error parsing Columns in {os.path.basename(log_path)}") - return 2 - if not blunders: - logging.warning( - f"No blunders found in Columns section: {os.path.basename(log_path)}" - ) - return 1 - - pgn_text = extract_pgn(text) - if not pgn_text: - logging.warning(f"No PGN section found: {os.path.basename(log_path)}") - return 1 - - try: - cases = fen_and_uci_for_blunders(pgn_text, blunders) - except Exception: - logging.exception( - f"Error converting SAN to UCI in {os.path.basename(log_path)}" - ) - return 2 - if not cases: - logging.warning( - f"Failed to reconstruct any blunder positions " - f"from PGN: {os.path.basename(log_path)}" - ) - return 1 - base = os.path.basename(log_path) - m = re.search(r"game_([A-Za-z0-9]+)\.log$", base) - game_id = m.group(1) if m else os.path.splitext(base)[0] + result = _parse_and_extract_blunders(log_path, base) + if isinstance(result, int): + return result # Error code + cases, game_id = result # Always append to the unified test file unified = os.path.join( os.path.dirname(__file__), "..", "tests", "test_blunders_all.py" @@ -381,6 +346,73 @@ def _process_single_log(log_path: str) -> int: return 0 +def _parse_and_extract_blunders( + log_path: str, base: str +) -> int | tuple[list[tuple[str, str, str, Blunder]], str]: + """Parse log file and extract blunder cases. + + Returns error code or (cases, game_id). + """ + text, err = _read_log_file(log_path) + if err is not None or text is None: + return err if err is not None else 2 + + blunders, err = _parse_blunders(text, base) + if err is not None or blunders is None: + return err if err is not None else 2 + + cases, err = _extract_cases(text, blunders, base) + if err is not None or cases is None: + return err if err is not None else 2 + + m = re.search(r"game_([A-Za-z0-9]+)\.log$", base) + game_id = m.group(1) if m else os.path.splitext(base)[0] + return cases, game_id + + +def _read_log_file(log_path: str) -> tuple[str | None, int | None]: + """Read log file contents. Returns (text, None) or (None, error_code).""" + try: + with open(log_path, encoding="utf-8") as fh: + return fh.read(), None + except FileNotFoundError: + logging.exception(f"Log file not found: {log_path}") + return None, 2 + + +def _parse_blunders(text: str, base: str) -> tuple[list[Blunder] | None, int | None]: + """Parse blunders from text. Returns (blunders, None) or (None, error_code).""" + try: + blunders = parse_columns_for_blunders(text) + except Exception: + logging.exception(f"Error parsing Columns in {base}") + return None, 2 + if not blunders: + logging.warning(f"No blunders found in Columns section: {base}") + return None, 1 + return blunders, None + + +def _extract_cases( + text: str, blunders: list[Blunder], base: str +) -> tuple[list[tuple[str, str, str, Blunder]] | None, int | None]: + """Extract FEN/UCI cases from PGN. Returns (cases, None) or (None, error_code).""" + pgn_text = extract_pgn(text) + if not pgn_text: + logging.warning(f"No PGN section found: {base}") + return None, 1 + + try: + cases = fen_and_uci_for_blunders(pgn_text, blunders) + except Exception: + logging.exception(f"Error converting SAN to UCI in {base}") + return None, 2 + if not cases: + logging.warning(f"Failed to reconstruct any blunder positions from PGN: {base}") + return None, 1 + return cases, None + + def main(argv: list[str]) -> int: """Process log files and generate blunder test cases.""" script_dir = os.path.dirname(__file__) diff --git a/PYTHON/randomJPG/generateJpeg.py b/PYTHON/randomJPG/generateJpeg.py index 74318b9..c207ef8 100644 --- a/PYTHON/randomJPG/generateJpeg.py +++ b/PYTHON/randomJPG/generateJpeg.py @@ -1,6 +1,7 @@ """Generate random colorful JPEG images with configurable parameters.""" import argparse +from dataclasses import dataclass from datetime import datetime, timezone import logging import os @@ -13,24 +14,22 @@ logging.basicConfig(level=logging.INFO) MAX_IMAGE_SIZE = 1000 -def generate_bloated_jpeg( - size: int, - color_list: list[str], - block_size: int, - output_path: str, - quality: int, - image_index: int, - folder: str, -) -> str: - """Generates a random JPEG image with given size, list of colors, and block size. +@dataclass +class ImageConfig: + """Configuration for generating a bloated JPEG image.""" + + size: int + color_list: list[str] + block_size: int + output_path: str + quality: int + + +def generate_bloated_jpeg(config: ImageConfig, image_index: int, folder: str) -> str: + """Generates a random JPEG image with given configuration. Args: - size: Size of the image (both width and height, - must be divisible by block_size). - color_list: List of colors in hex format. - block_size: Size of the pixel blocks. - output_path: Output path for the JPEG image. - quality: Quality setting for the JPEG image (0-100). + config: Image generation configuration. image_index: Index of the image for unique naming. folder: Folder to save the image. @@ -38,28 +37,29 @@ def generate_bloated_jpeg( Path to the generated image. """ # Ensure size is divisible by block_size and does not exceed MAX_IMAGE_SIZE - if size > MAX_IMAGE_SIZE or size % block_size != 0: + if config.size > MAX_IMAGE_SIZE or config.size % config.block_size != 0: msg = ( f"Size must be {MAX_IMAGE_SIZE} pixels or less and divisible by block_size" ) raise ValueError(msg) # Create a new image - image = Image.new("RGB", (size, size)) + image = Image.new("RGB", (config.size, config.size)) pixels = image.load() # Convert hex colors to RGB rgb_colors = [ - tuple(int(color[i : i + 2], 16) for i in (1, 3, 5)) for color in color_list + tuple(int(color[i : i + 2], 16) for i in (1, 3, 5)) + for color in config.color_list ] # Fill the image with block_size x block_size pixel squares # of random colors from the list - for y in range(0, size, block_size): - for x in range(0, size, block_size): + for y in range(0, config.size, config.block_size): + for x in range(0, config.size, config.block_size): color = random.choice(rgb_colors) - for i in range(block_size): - for j in range(block_size): + for i in range(config.block_size): + for j in range(config.block_size): pixels[x + i, y + j] = color # Create the folder if it does not exist @@ -69,11 +69,12 @@ def generate_bloated_jpeg( # Generate unique output path unique_output_path = os.path.join( folder, - f"{os.path.splitext(output_path)[0]}_{image_index}{os.path.splitext(output_path)[1]}", + f"{os.path.splitext(config.output_path)[0]}_{image_index}" + f"{os.path.splitext(config.output_path)[1]}", ) # Save the image with specified quality to maximize file size - image.save(unique_output_path, "JPEG", quality=quality, optimize=False) + image.save(unique_output_path, "JPEG", quality=config.quality, optimize=False) return unique_output_path @@ -149,14 +150,13 @@ if __name__ == "__main__": logging.info(f" Output folder: {folder}") # Generate the specified number of images + config = ImageConfig( + size=args.size, + color_list=args.colors, + block_size=args.block_size, + output_path=args.output_path, + quality=args.quality, + ) for i in range(1, args.num_images + 1): - output_path = generate_bloated_jpeg( - args.size, - args.colors, - args.block_size, - args.output_path, - args.quality, - i, - folder, - ) + output_path = generate_bloated_jpeg(config, i, folder) logging.info(f"Image {i} saved to {os.path.abspath(output_path)}") diff --git a/PYTHON/stockfish_analysis/analyze_chess_game.py b/PYTHON/stockfish_analysis/analyze_chess_game.py index ff91aa4..a7e439e 100755 --- a/PYTHON/stockfish_analysis/analyze_chess_game.py +++ b/PYTHON/stockfish_analysis/analyze_chess_game.py @@ -109,6 +109,16 @@ CP_LOSS_INACCURACY = 99 CP_LOSS_MISTAKE = 299 +# Centipawn loss thresholds for move classification +_CP_LOSS_BANDS = [ + (CP_LOSS_BEST, "Best"), + (CP_LOSS_EXCELLENT, "Excellent"), + (CP_LOSS_GOOD, "Good"), + (CP_LOSS_INACCURACY, "Inaccuracy"), + (CP_LOSS_MISTAKE, "Mistake"), +] + + def classify_cp_loss(cp_loss: int | None) -> str: """Classify move quality using Lichess-like centipawn loss bands. @@ -123,16 +133,9 @@ def classify_cp_loss(cp_loss: int | None) -> str: """ if cp_loss is None: return "Unknown" - if cp_loss <= CP_LOSS_BEST: - return "Best" - if cp_loss <= CP_LOSS_EXCELLENT: - return "Excellent" - if cp_loss <= CP_LOSS_GOOD: - return "Good" - if cp_loss <= CP_LOSS_INACCURACY: - return "Inaccuracy" - if cp_loss <= CP_LOSS_MISTAKE: - return "Mistake" + for threshold, classification in _CP_LOSS_BANDS: + if cp_loss <= threshold: + return classification return "Blunder" diff --git a/poker-modifier-app/poker_modifier_app.py b/poker-modifier-app/poker_modifier_app.py index cb0c4f5..3351a48 100644 --- a/poker-modifier-app/poker_modifier_app.py +++ b/poker-modifier-app/poker_modifier_app.py @@ -521,24 +521,34 @@ class PokerModifierApp: def setup_gui(self) -> None: """Create and configure the main GUI window.""" - # Create main window + self._setup_main_window() + main_frame = self._create_main_frame() + self._create_title(main_frame) + self._create_settings_frame(main_frame) + self._create_result_display(main_frame) + self._create_buttons(main_frame) + self._create_statistics_frame(main_frame) + + def _setup_main_window(self) -> None: + """Initialize the main Tk window.""" self.root = tk.Tk() self.root.title("🃏 Texas Hold'em Modifier") self.root.geometry("650x750") self.root.configure(bg="#0f4c3a") self.root.resizable(True, True) - - # Configure style style = ttk.Style() style.theme_use("clam") - # Main container + def _create_main_frame(self) -> tk.Frame: + """Create and return the main container frame.""" main_frame = tk.Frame(self.root, bg="#0f4c3a", padx=20, pady=20) main_frame.pack(fill=tk.BOTH, expand=True) + return main_frame - # Title + def _create_title(self, parent: tk.Frame) -> None: + """Create the title label.""" title_label = tk.Label( - main_frame, + parent, text="🃏 Texas Hold'em Modifier", font=("Arial", 24, "bold"), fg="#ffd700", @@ -546,9 +556,13 @@ class PokerModifierApp: ) title_label.pack(pady=(0, 20)) - # Settings frame + def _create_settings_frame(self, parent: tk.Frame) -> None: + """Create the settings frame. + + Includes probability, debug, and game length controls. + """ settings_frame = tk.LabelFrame( - main_frame, + parent, text="Settings", font=("Arial", 12, "bold"), fg="#ffd700", @@ -558,8 +572,13 @@ class PokerModifierApp: ) settings_frame.pack(fill=tk.X, pady=(0, 20), padx=10, ipady=10) - # Probability setting - prob_frame = tk.Frame(settings_frame, bg="#1a6b4d") + self._create_probability_controls(settings_frame) + self._create_debug_controls(settings_frame) + self._create_length_controls(settings_frame) + + def _create_probability_controls(self, parent: tk.Widget) -> None: + """Create the probability slider and label.""" + prob_frame = tk.Frame(parent, bg="#1a6b4d") prob_frame.pack(fill=tk.X, padx=10, pady=5) tk.Label( @@ -596,11 +615,11 @@ class PokerModifierApp: ) self.prob_label.pack(side=tk.RIGHT) - # Debug controls frame - debug_frame = tk.Frame(settings_frame, bg="#1a6b4d") + def _create_debug_controls(self, parent: tk.Widget) -> None: + """Create the debug mode checkbox and force endgame button.""" + debug_frame = tk.Frame(parent, bg="#1a6b4d") debug_frame.pack(fill=tk.X, padx=10, pady=5) - # Debug mode toggle self.debug_var = tk.BooleanVar(value=False) debug_check = tk.Checkbutton( debug_frame, @@ -616,7 +635,6 @@ class PokerModifierApp: ) debug_check.pack(side=tk.LEFT, padx=(0, 15)) - # Force endgame button (only visible in debug mode) self.force_endgame_button = tk.Button( debug_frame, text="Force Endgame", @@ -629,8 +647,9 @@ class PokerModifierApp: ) # Initially hidden - # Game length setting - length_frame = tk.Frame(settings_frame, bg="#1a6b4d") + def _create_length_controls(self, parent: tk.Widget) -> None: + """Create the game length slider and label.""" + length_frame = tk.Frame(parent, bg="#1a6b4d") length_frame.pack(fill=tk.X, padx=10, pady=5) tk.Label( @@ -667,14 +686,14 @@ class PokerModifierApp: ) self.length_label.pack(side=tk.RIGHT) - # Result display frame + def _create_result_display(self, parent: tk.Frame) -> None: + """Create the result display frame.""" self.result_frame = tk.Frame( - main_frame, bg="#2d2d2d", relief=tk.RIDGE, bd=3, height=150 + parent, bg="#2d2d2d", relief=tk.RIDGE, bd=3, height=150 ) self.result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 20), padx=10) self.result_frame.pack_propagate(False) - # Initial result text self.result_label = tk.Label( self.result_frame, text="Click 'Start Round' to begin!", @@ -686,11 +705,11 @@ class PokerModifierApp: ) self.result_label.pack(expand=True, fill=tk.BOTH, padx=20, pady=20) - # Button frame for Start and Reset - button_frame = tk.Frame(main_frame, bg="#0f4c3a") + def _create_buttons(self, parent: tk.Frame) -> None: + """Create the start and reset buttons.""" + button_frame = tk.Frame(parent, bg="#0f4c3a") button_frame.pack(fill=tk.X, pady=(0, 20), padx=10) - # Start button self.start_button = tk.Button( button_frame, text="Start Round", @@ -708,7 +727,6 @@ class PokerModifierApp: side=tk.LEFT, fill=tk.X, expand=True, ipady=10, padx=(0, 5) ) - # Reset button self.reset_button = tk.Button( button_frame, text="Reset Game", @@ -724,8 +742,9 @@ class PokerModifierApp: ) self.reset_button.pack(side=tk.RIGHT, ipady=10, padx=(5, 0)) - # Statistics frame - stats_frame = tk.Frame(main_frame, bg="#0f4c3a") + def _create_statistics_frame(self, parent: tk.Frame) -> None: + """Create the statistics display frame with rounds, modifiers, and phase.""" + stats_frame = tk.Frame(parent, bg="#0f4c3a") stats_frame.pack(fill=tk.X, padx=10) # Rounds played diff --git a/pyproject.toml b/pyproject.toml index b02d915..6cae21b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,10 +36,6 @@ ignore = [ # Relaxed for script-heavy repository "PTH", # Use pathlib instead of os.path - too invasive for existing code "C901", # Function is too complex - many scripts have complex logic - "PLR0912", # Too many branches - relaxed for scripts - "PLR0915", # Too many statements - relaxed for scripts - "PLR0911", # Too many return statements - relaxed - "PLR0913", # Too many arguments - relaxed "BLE001", # Blind except - will fix critical ones manually "S603", # subprocess without shell - known pattern "S607", # start-process with partial path - acceptable @@ -93,6 +89,12 @@ unfixable = [] ] "PYTHON/lichess_bot/main.py" = [ "PERF203", # Try-except needed for stream/move error handling in loops + "PLR0912", # Complex nested game event handling with many branches + "PLR0915", # Long function handling complete game lifecycle +] +"PYTHON/stockfish_analysis/analyze_chess_game.py" = [ + "PLR0912", # Complex main() with many argument combinations and analysis modes + "PLR0915", # Long main() handling complete analysis workflow ] "PYTHON/randomize_numbers/random_digits.py" = [ "PERF203", # Try-except needed for parsing user input in loop