From 742356d295812d06a78c57130b9b344a4c6eb3fd Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 30 Nov 2025 14:25:35 +0100 Subject: [PATCH] fix: enforce 88-char line length limit (E501) - Fixed all 119 line-too-long errors across Python files - Broke long strings, comments, and docstrings into multiline format - All pre-commit hooks now pass with strict 88-char limit --- PYTHON/extractLinks/main.py | 9 +- PYTHON/keyboardCoop/main.py | 32 +- PYTHON/lichess_bot/engine.py | 17 +- PYTHON/lichess_bot/lichess_api.py | 17 +- PYTHON/lichess_bot/main.py | 190 ++++++++--- PYTHON/lichess_bot/tests/conftest.py | 4 +- PYTHON/lichess_bot/tests/test_puzzles.py | 16 +- .../tools/generate_blunder_tests.py | 89 +++-- PYTHON/randomJPG/generateJpeg.py | 28 +- PYTHON/randomize_numbers/random_digits.py | 8 +- PYTHON/scapeWebsite/scrape_comics.py | 4 +- PYTHON/screen_locker/screen_lock.py | 32 +- PYTHON/split/split_x_into_n_symmetrically.py | 5 +- .../stockfish_analysis/analyze_chess_game.py | 144 ++++++-- PYTHON/tagDivider/tagDivider.py | 20 +- articles/test_server_api.py | 8 +- poker-modifier-app/poker_modifier_app.py | 313 ++++++++++++++---- pyproject.toml | 2 +- 18 files changed, 712 insertions(+), 226 deletions(-) diff --git a/PYTHON/extractLinks/main.py b/PYTHON/extractLinks/main.py index 252dc36..7769548 100755 --- a/PYTHON/extractLinks/main.py +++ b/PYTHON/extractLinks/main.py @@ -52,12 +52,17 @@ def extract_hosts_from_html(html_text: str) -> list[str]: def main() -> int: - ap = argparse.ArgumentParser(description="Extract hosts from hrefs in an HTML file.") + ap = argparse.ArgumentParser( + description="Extract hosts from hrefs in an HTML file." + ) ap.add_argument("input_html", help="Path to input HTML file") ap.add_argument( "output_txt", nargs="?", - help="Path to output text file (defaults to _links.txt in the same directory)", + help=( + "Path to output text file " + "(defaults to _links.txt in the same directory)" + ), ) args = ap.parse_args() diff --git a/PYTHON/keyboardCoop/main.py b/PYTHON/keyboardCoop/main.py index 112dfcc..8e514a8 100644 --- a/PYTHON/keyboardCoop/main.py +++ b/PYTHON/keyboardCoop/main.py @@ -87,7 +87,9 @@ class KeyboardCoopGame: def load_dictionary(self): """Load dictionary from words_dictionary.json file.""" try: - dictionary_path = os.path.join(os.path.dirname(__file__), "words_dictionary.json") + dictionary_path = os.path.join( + os.path.dirname(__file__), "words_dictionary.json" + ) with open(dictionary_path, encoding="utf-8") as f: dictionary_data = json.load(f) # Convert to set for faster lookup (we only need the keys) @@ -147,7 +149,10 @@ class KeyboardCoopGame: "good", } except json.JSONDecodeError: - print("Warning: Error reading words_dictionary.json, using fallback dictionary") + print( + "Warning: Error reading words_dictionary.json, " + "using fallback dictionary" + ) return { "cat", "dog", @@ -280,13 +285,19 @@ class KeyboardCoopGame: self.current_word += letter # Update available letters to include adjacent letters AND the same letter - adjacent_letters = set(self.key_adjacency[letter]) if letter in self.key_adjacency else set() + adjacent_letters = ( + set(self.key_adjacency[letter]) + if letter in self.key_adjacency + else set() + ) adjacent_letters.add(letter) # Allow selecting the same letter again self.available_letters = adjacent_letters # Switch player self.current_player = 1 - self.current_player - self.message = f"Player {self.current_player + 1}: Choose an adjacent letter!" + self.message = ( + f"Player {self.current_player + 1}: Choose an adjacent letter!" + ) # If no valid moves available, force word submission if not self.available_letters: @@ -297,7 +308,10 @@ class KeyboardCoopGame: if len(self.current_word) >= 3 and self.is_valid_word(self.current_word): points = self.calculate_score(len(self.current_word)) self.score += points - self.message = f"'{self.current_word}' is valid! +{points} points (Total: {self.score}) - New keyboard!" + self.message = ( + f"'{self.current_word}' is valid! +{points} points " + f"(Total: {self.score}) - New keyboard!" + ) # Randomize keyboard layout after scoring self.generate_random_keyboard() @@ -357,7 +371,9 @@ class KeyboardCoopGame: self.screen.blit(title, (30, 20)) # Current word - word_text = self.font.render(f"Current Word: {self.current_word.upper()}", True, TEXT_COLOR) + word_text = self.font.render( + f"Current Word: {self.current_word.upper()}", True, TEXT_COLOR + ) self.screen.blit(word_text, (30, 50)) # Score @@ -366,7 +382,9 @@ class KeyboardCoopGame: # Current player player_color = PLAYER_COLORS[self.current_player] - player_text = self.font.render(f"Current Player: {self.current_player + 1}", True, player_color) + player_text = self.font.render( + f"Current Player: {self.current_player + 1}", True, player_color + ) self.screen.blit(player_text, (30, 100)) # Message diff --git a/PYTHON/lichess_bot/engine.py b/PYTHON/lichess_bot/engine.py index 0eeaa29..6ab4d8c 100644 --- a/PYTHON/lichess_bot/engine.py +++ b/PYTHON/lichess_bot/engine.py @@ -39,7 +39,9 @@ class RandomEngine: ) ) self.engine_path = engine_path or default_path - if not os.path.isfile(self.engine_path) or not os.access(self.engine_path, os.X_OK): + if not os.path.isfile(self.engine_path) or not os.access( + self.engine_path, os.X_OK + ): msg = ( f"C engine not found or not executable at '{self.engine_path}'. " "Build it first (make -C C/lichess_random_engine)." @@ -65,7 +67,9 @@ class RandomEngine: return (proc.stdout or "").strip() def choose_move(self, board: chess.Board) -> chess.Move: - mv, _ = self.choose_move_with_explanation(board, time_budget_sec=self.max_time_sec) + mv, _ = self.choose_move_with_explanation( + board, time_budget_sec=self.max_time_sec + ) return mv def choose_move_with_explanation( @@ -77,7 +81,8 @@ class RandomEngine: return None, "no_legal_moves" args = ["--fen", board.fen()] + [m.uci() for m in legal] - # Optionally pass a seed for reproducibility when desired; keep default behavior otherwise. + # Optionally pass a seed for reproducibility when desired; + # keep default behavior otherwise. # We deliberately avoid adding annotations here per request. output = self._call_engine(args, timeout=max(0.1, time_budget_sec)) @@ -103,7 +108,7 @@ class RandomEngine: *, time_budget_sec: float, ) -> tuple[float, str, chess.Move | None, str]: - """Ask the C engine to explain the current move list and analyze a specific candidate. + """Ask the C engine to explain and analyze a specific candidate. Returns (candidate_score, candidate_expl, best_move, best_expl) where explanations are concise JSON snippets from the engine. All logic is @@ -113,7 +118,9 @@ class RandomEngine: if not legal: return 0.0, "no_legal_moves", None, "no_best_move" - args = ["--fen", board.fen(), "--explain", "--analyze", proposed_move_uci] + [m.uci() for m in legal] + args = ["--fen", board.fen(), "--explain", "--analyze", proposed_move_uci] + [ + m.uci() for m in legal + ] out = self._call_engine(args, timeout=max(0.1, time_budget_sec)) # Try to parse the engine's JSON explanation diff --git a/PYTHON/lichess_bot/lichess_api.py b/PYTHON/lichess_bot/lichess_api.py index 32a9e8a..fa87744 100644 --- a/PYTHON/lichess_bot/lichess_api.py +++ b/PYTHON/lichess_bot/lichess_api.py @@ -22,7 +22,9 @@ class LichessAPI: } ) - def _request(self, method: str, url: str, *, raise_for_status: bool = False, **kwargs) -> requests.Response: + def _request( + self, method: str, url: str, *, raise_for_status: bool = False, **kwargs + ) -> requests.Response: """Wrapper around session.request that logs every request/response. - Logs start (method+URL) and end (status, elapsed). @@ -47,7 +49,10 @@ class LichessAPI: except Exception: snippet = None if snippet: - logging.warning(f"HTTP {method} {url} -> {status} in {elapsed:.2f}s body='{snippet}'") + logging.warning( + f"HTTP {method} {url} -> {status} " + f"in {elapsed:.2f}s body='{snippet}'" + ) else: logging.warning(f"HTTP {method} {url} -> {status} in {elapsed:.2f}s") else: @@ -63,7 +68,9 @@ class LichessAPI: try: # Use NDJSON Accept and no timeout for long-lived stream headers = {"Accept": "application/x-ndjson"} - with self._request("GET", url, headers=headers, stream=True, timeout=None) as r: + with self._request( + "GET", url, headers=headers, stream=True, timeout=None + ) as r: r.raise_for_status() backoff = 0.5 # reset on success for line in r.iter_lines(decode_unicode=True): @@ -91,7 +98,9 @@ class LichessAPI: data = {"reason": reason} self._request("POST", url, data=data, timeout=30, raise_for_status=True) - def join_game_stream(self, game_id: str, my_color: str | None) -> tuple[chess.Board, str]: + def join_game_stream( + self, game_id: str, my_color: str | None + ) -> tuple[chess.Board, str]: """Deprecated: use stream_game_events and parse initial state there.""" # Fallback to initial behavior for compatibility url = f"{LICHESS_API}/api/board/game/stream/{game_id}" diff --git a/PYTHON/lichess_bot/main.py b/PYTHON/lichess_bot/main.py index cf37e2f..11266b3 100644 --- a/PYTHON/lichess_bot/main.py +++ b/PYTHON/lichess_bot/main.py @@ -38,7 +38,8 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No logging.info(f"Starting game thread for {game_id} [bot v{bot_version}]") board = chess.Board() color: str | None = my_color - # Track how many moves we have already processed; start at -1 so we act on the first state (0 moves) + # Track how many moves we have already processed; + # start at -1 so we act on the first state (0 moves) last_handled_len = -1 # Prepare a per-game log file game_log_path = os.path.join(os.getcwd(), f"lichess_bot_game_{game_id}.log") @@ -69,8 +70,16 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No moves = state.get("moves", "") status = state.get("status") # clocks are in milliseconds if present - my_ms = state.get("wtime") if color == "white" else state.get("btime") - opp_ms = state.get("btime") if color == "white" else state.get("wtime") + my_ms = ( + state.get("wtime") + if color == "white" + else state.get("btime") + ) + opp_ms = ( + state.get("btime") + if color == "white" + else state.get("wtime") + ) inc_ms = state.get("winc") or state.get("binc") or 0 # Discover my color from gameFull white_id = event["white"].get("id") @@ -80,13 +89,15 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No # Set site and date if available try: # Lichess event may include 'createdAt' ms epoch - created_ms = event.get("createdAt") or event.get("createdAtDate") + 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" - ) + 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}" @@ -111,9 +122,14 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No moves_list = moves.split() if moves else [] new_len = len(moves_list) - logging.info(f"Game {game_id}: event={et}, moves={new_len}, color={color}") + logging.info( + f"Game {game_id}: event={et}, moves={new_len}, color={color}" + ) if new_len == last_handled_len: - logging.debug(f"Game {game_id}: position unchanged (len={new_len}), skipping") + logging.debug( + f"Game {game_id}: position unchanged " + f"(len={new_len}), skipping" + ) continue # Rebuild board from moves @@ -125,56 +141,90 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No logging.debug(f"Game {game_id}: could not apply move {m}") if color is None: - logging.info(f"Game {game_id}: color unknown yet; waiting for gameFull") - # Do not mark this position handled on gameFull; wait for authoritative gameState + logging.info( + f"Game {game_id}: color unknown yet; " + "waiting for gameFull" + ) + # Do not mark this position handled on gameFull; + # wait for authoritative gameState if et == "gameState": last_handled_len = new_len continue is_white_turn = board.turn - my_turn = (is_white_turn and color == "white") or ((not is_white_turn) and color == "black") - logging.info(f"Game {game_id}: turn={'white' if is_white_turn else 'black'}, my_turn={my_turn}") + my_turn = (is_white_turn and color == "white") or ( + (not is_white_turn) and color == "black" + ) + logging.info( + f"Game {game_id}: " + f"turn={'white' if is_white_turn else 'black'}, " + f"my_turn={my_turn}" + ) # Move policy: # - Always move on 'gameState' (authoritative) # - Also allow moving on the initial 'gameFull' when there are # zero moves and it's our turn. This avoids stalling at game # start when Lichess doesn't immediately send a 'gameState' # for 0 moves. - allow_move = (et == "gameState") or (et == "gameFull" and new_len == 0) + allow_move = (et == "gameState") or ( + et == "gameFull" and new_len == 0 + ) if my_turn and allow_move: - # Compute a per-move time budget (seconds) based on remaining time - # Heuristic: use min( max_time_sec, max(0.05, 0.6 * my_time_left/remaining_moves + inc) ) + # Compute a per-move time budget (seconds) based on + # remaining time. + # Heuristic: use min( max_time_sec, + # max(0.05, 0.6 * my_time_left/remaining_moves + inc) ) # Estimate remaining moves as 30 - ply/2 bounded to [10, 60] - est_moves_left = max(10, min(60, 30 - board.fullmove_number // 2)) + est_moves_left = max( + 10, min(60, 30 - board.fullmove_number // 2) + ) time_left_sec = (my_ms or 0) / 1000.0 inc_sec = (inc_ms or 0) / 1000.0 - budget = 0.6 * (time_left_sec / max(1, est_moves_left)) + 0.5 * inc_sec + budget = ( + 0.6 * (time_left_sec / max(1, est_moves_left)) + + 0.5 * inc_sec + ) # Spend more time per move (requested): double the budget budget *= 2.0 # Keep within reasonable bounds budget = max(0.05, min(engine.max_time_sec, budget)) - move, reason = engine.choose_move_with_explanation(board, time_budget_sec=budget) + move, reason = engine.choose_move_with_explanation( + board, time_budget_sec=budget + ) if move is None: - logging.info(f"Game {game_id}: no legal moves (game likely over)") + logging.info( + f"Game {game_id}: no legal moves (game likely over)" + ) break try: - # Double-check legality just before sending to avoid 400s when state changed. + # Double-check legality just before sending + # to avoid 400s when state changed. if move not in board.legal_moves: - logging.info(f"Game {game_id}: selected move no longer legal; skipping send") + logging.info( + f"Game {game_id}: selected move " + "no longer legal; skipping send" + ) else: logging.info( f"Game {game_id}: playing {move.uci()} " - f"(budget={budget:.2f}s, my_time_left={time_left_sec:.1f}s, " + f"(budget={budget:.2f}s, " + f"my_time_left={time_left_sec:.1f}s, " f"inc={inc_sec:.2f}s)" ) if game_log_path: with open(game_log_path, "a") as lf: - lf.write(f"ply {last_handled_len + 1}: {move.uci()}\n{reason}\n\n") + lf.write( + f"ply {last_handled_len + 1}: " + f"{move.uci()}\n{reason}\n\n" + ) api.make_move(game_id, move) except Exception as e: - logging.warning(f"Game {game_id}: move {move.uci()} failed: {e}") - # Mark this position as handled on authoritative gameState, or after we've - # actually attempted a move (including the first move on gameFull len=0). + logging.warning( + f"Game {game_id}: " f"move {move.uci()} failed: {e}" + ) + # Mark this position as handled on authoritative + # gameState, or after we've actually attempted a move + # (including the first move on gameFull len=0). if et == "gameState" or (my_turn and allow_move): last_handled_len = new_len if status in {"mate", "resign", "stalemate", "timeout", "draw"}: @@ -204,10 +254,13 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No pass with open(game_log_path, "a") as lf: lf.write("\nPGN:\n") - exporter = chess.pgn.StringExporter(headers=True, variations=False, comments=False) + exporter = chess.pgn.StringExporter( + headers=True, variations=False, comments=False + ) lf.write(game.accept(exporter)) lf.write("\n") - # After PGN is written, run analysis and save it to the same file (inserted before PGN) + # After PGN is written, run analysis and save it + # to the same file (inserted before PGN) if game_log_path: analysis_text: str | None = None try: @@ -223,7 +276,10 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No except Exception: total_plies = 0 - logging.info(f"Game {game_id}: starting post-game analysis ({total_plies} plies)") + logging.info( + f"Game {game_id}: starting post-game " + f"analysis ({total_plies} plies)" + ) # Run analyzer unbuffered and stream output for progress proc = subprocess.Popen( [sys.executable, "-u", analyze_script, game_log_path], @@ -243,16 +299,22 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No if m: # Count as one analyzed ply analyzed += 1 - left = max(0, (total_plies or 0) - analyzed) if total_plies else "?" + left = ( + max(0, (total_plies or 0) - analyzed) + if total_plies + else "?" + ) if total_plies: pct = analyzed / total_plies * 100.0 logging.info( f"Game {game_id}: analysis progress " - f"{analyzed}/{total_plies} ({pct:.0f}%), left {left}" + f"{analyzed}/{total_plies} " + f"({pct:.0f}%), left {left}" ) else: logging.info( - f"Game {game_id}: analysis progress {analyzed} plies (total unknown)" + f"Game {game_id}: analysis progress " + f"{analyzed} plies (total unknown)" ) # Capture any remaining stderr and ensure process ends @@ -261,28 +323,37 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No ret = proc.wait() analysis_text = "".join(lines) if ret != 0: - logging.warning(f"Game {game_id}: analysis script exited with code {ret}") + logging.warning( + f"Game {game_id}: analysis script " + f"exited with code {ret}" + ) if stderr_text: analysis_text += "\n[stderr]\n" + stderr_text logging.info(f"Game {game_id}: analysis complete") else: logging.info( - f"Game {game_id}: analysis script not found at {analyze_script}; skipping analysis" + f"Game {game_id}: analysis script not found " + f"at {analyze_script}; skipping analysis" ) except Exception as e: logging.debug(f"Game {game_id}: analysis run failed: {e}") - # Insert analysis before the PGN section so future runs can still parse PGN cleanly + # Insert analysis before the PGN section so future runs + # can still parse PGN cleanly if analysis_text: try: - with open(game_log_path, encoding="utf-8", errors="replace") as f: + with open( + game_log_path, encoding="utf-8", errors="replace" + ) as f: content = f.read() # Find the start of the 'PGN:' line insert_idx = 0 p = content.find("\nPGN:\n") if p != -1: - insert_idx = p + 1 # start of the line after the preceding newline + insert_idx = ( + p + 1 + ) # start of the line after the preceding newline elif content.startswith("PGN:\n"): insert_idx = 0 else: @@ -294,20 +365,32 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No 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 '?'}") + meta_lines.append( + f"Players: {white_name or '?'} " + f"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" + (meta_block if meta_block else "") + + "ANALYSIS:\n" + + analysis_text.rstrip() + + "\n\n" + ) + new_content = ( + content[:insert_idx] + + analysis_block + + content[insert_idx:] ) - new_content = content[:insert_idx] + analysis_block + content[insert_idx:] with open(game_log_path, "w", encoding="utf-8") as f: f.write(new_content) except Exception as e: - logging.debug(f"Game {game_id}: could not write analysis to log: {e}") + logging.debug( + f"Game {game_id}: could not write analysis to log: {e}" + ) except Exception as e: logging.debug(f"Game {game_id}: could not write PGN: {e}") logging.info(f"Ending game thread for {game_id}") @@ -324,19 +407,30 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No variant = challenge.get("variant", {}).get("key", "standard") speed = challenge.get("speed") perf_ok = speed in {"bullet", "blitz", "rapid", "classical"} - not_corr = challenge.get("speed") != "correspondence" or not decline_correspondence + not_corr = ( + challenge.get("speed") != "correspondence" + or not decline_correspondence + ) if variant == "standard" and perf_ok and not_corr: logging.info(f"Accepting challenge {ch_id} ({speed})") api.accept_challenge(ch_id) else: - logging.info(f"Declining challenge {ch_id} (variant={variant}, speed={speed})") + logging.info( + f"Declining challenge {ch_id} " + f"(variant={variant}, speed={speed})" + ) api.decline_challenge(ch_id) elif event.get("type") == "gameStart": game_id = event["game"]["id"] # Spin up a game thread - if game_id not in game_threads or not game_threads[game_id].is_alive(): - t = threading.Thread(target=handle_game, args=(game_id,), name=f"game-{game_id}") + if ( + game_id not in game_threads + or not game_threads[game_id].is_alive() + ): + t = threading.Thread( + target=handle_game, args=(game_id,), name=f"game-{game_id}" + ) t.daemon = True game_threads[game_id] = t t.start() @@ -355,7 +449,9 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No def main(): parser = argparse.ArgumentParser(description="Run a minimal Lichess bot") - parser.add_argument("--log-level", default="INFO", help="Logging level (default: INFO)") + parser.add_argument( + "--log-level", default="INFO", help="Logging level (default: INFO)" + ) parser.add_argument( "--decline-correspondence", action="store_true", diff --git a/PYTHON/lichess_bot/tests/conftest.py b/PYTHON/lichess_bot/tests/conftest.py index a8d4ae2..4c4dc4d 100644 --- a/PYTHON/lichess_bot/tests/conftest.py +++ b/PYTHON/lichess_bot/tests/conftest.py @@ -15,4 +15,6 @@ def pytest_ignore_collect(collection_path: Path, config): This lets us keep historical files in the repo without collecting them. """ basename = collection_path.name - return bool(basename.startswith("test_blunders_") and basename != "test_blunders_all.py") + return bool( + basename.startswith("test_blunders_") and basename != "test_blunders_all.py" + ) diff --git a/PYTHON/lichess_bot/tests/test_puzzles.py b/PYTHON/lichess_bot/tests/test_puzzles.py index 0fbe3c3..e5e69e0 100644 --- a/PYTHON/lichess_bot/tests/test_puzzles.py +++ b/PYTHON/lichess_bot/tests/test_puzzles.py @@ -26,13 +26,16 @@ def _load_top_puzzles(csv_path: str, limit: int = 8) -> list[tuple[str, str]]: @pytest.mark.parametrize( ("fen", "moves_str"), - _load_top_puzzles(os.path.join(os.path.dirname(__file__), "lichess_db_puzzle.csv"), limit=8), + _load_top_puzzles( + os.path.join(os.path.dirname(__file__), "lichess_db_puzzle.csv"), limit=8 + ), ) def test_puzzle_engine_follow_solution(fen: str, moves_str: str): board = chess.Board(fen) eng = RandomEngine(max_time_sec=1.0) - # Moves are space-separated UCIs alternating sides starting from side-to-move in the FEN + # Moves are space-separated UCIs alternating sides + # starting from side-to-move in the FEN solution_moves = moves_str.split() step = 0 for uci in solution_moves: @@ -41,11 +44,14 @@ def test_puzzle_engine_follow_solution(fen: str, moves_str: str): mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=0.5) assert mv is not None, f"No move returned at step {step}.\nExplanation: {expl}" - # If engine move differs from solution, fail immediately but provide analysis of the correct move + # If engine move differs from solution, fail immediately + # but provide analysis of the correct move if mv.uci() != uci: # Ask the engine to analyze the correct move for debug - score_cp, proposed_expl, best_mv, best_expl = eng.evaluate_proposed_move_with_suggestion( - board, uci, time_budget_sec=0.5 + score_cp, proposed_expl, best_mv, best_expl = ( + eng.evaluate_proposed_move_with_suggestion( + board, uci, time_budget_sec=0.5 + ) ) details = [ f"Puzzle failed at step {step}.", diff --git a/PYTHON/lichess_bot/tools/generate_blunder_tests.py b/PYTHON/lichess_bot/tools/generate_blunder_tests.py index fcb61d2..81e431a 100755 --- a/PYTHON/lichess_bot/tools/generate_blunder_tests.py +++ b/PYTHON/lichess_bot/tools/generate_blunder_tests.py @@ -22,7 +22,8 @@ Usage examples: python PYTHON/lichess_bot/tools/generate_blunder_tests.py OVmR29MI # Process an explicit file path - python PYTHON/lichess_bot/tools/generate_blunder_tests.py /path/to/lichess_bot_game_xxxxx.log + python PYTHON/lichess_bot/tools/generate_blunder_tests.py \ + /path/to/lichess_bot_game_xxxxx.log It will create files like: PYTHON/lichess_bot/tests/test_blunders_.py @@ -70,7 +71,8 @@ def parse_columns_for_blunders(text: str) -> list[Blunder]: continue # Split by 2+ spaces to get columns parts = re.split(r"\s{2,}", ln.strip()) - # Expected columns: ply, side, move, played_eval, best_eval, loss, class, best_suggestion + # Expected columns: + # ply, side, move, played_eval, best_eval, loss, class, best_suggestion if len(parts) < 8: continue try: @@ -85,8 +87,9 @@ def parse_columns_for_blunders(text: str) -> list[Blunder]: # Require best suggestion to be provided; if it's missing, raise if not best_suggestion_san: msg = ( - f"Missing best_suggestion in Columns for blunder row: ply={ply} side={side} move={move_san}.\n" - f"Raw line: '{ln.strip()}'" + f"Missing best_suggestion in Columns " + f"for blunder row: ply={ply} side={side} " + f"move={move_san}.\nRaw line: '{ln.strip()}'" ) raise ValueError(msg) blunders.append( @@ -119,7 +122,9 @@ def san_list_from_game(game: chess.pgn.Game) -> list[str]: return san_moves -def fen_and_uci_for_blunders(pgn_text: str, blunders: list[Blunder]) -> list[tuple[str, str, str, Blunder]]: +def fen_and_uci_for_blunders( + pgn_text: str, blunders: list[Blunder] +) -> list[tuple[str, str, str, Blunder]]: game = chess.pgn.read_game(io.StringIO(pgn_text)) if game is None: msg = "Failed to parse PGN from log" @@ -147,14 +152,17 @@ def fen_and_uci_for_blunders(pgn_text: str, blunders: list[Blunder]) -> list[tup continue else: continue - # Parse best suggestion SAN to UCI in the same position; if it fails, skip this blunder + # Parse best suggestion SAN to UCI in the same position; + # if it fails, skip this blunder try: best_move = board.parse_san(bl.best_suggestion_san) best_uci = best_move.uci() except Exception as e: msg = ( - f"Failed to parse best_suggestion SAN '{bl.best_suggestion_san}' at ply {bl.ply} side {bl.side} " - f"in position FEN: {fen_before}. Error: {e}" + f"Failed to parse best_suggestion SAN " + f"'{bl.best_suggestion_san}' at ply {bl.ply} " + f"side {bl.side} in position FEN: {fen_before}. " + f"Error: {e}" ) raise ValueError(msg) results.append((fen_before, move.uci(), best_uci, bl)) @@ -174,7 +182,9 @@ import chess import pytest # Ensure repo root is importable when running pytest directly -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +REPO_ROOT = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +) if REPO_ROOT not in sys.path: sys.path.insert(0, REPO_ROOT) @@ -184,7 +194,11 @@ BLUNDER_CASES = [ ] -@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) +@pytest.mark.parametrize( + 'fen,blunder_uci,label', + BLUNDER_CASES, + ids=[c[2] for c in BLUNDER_CASES], +) def test_engine_avoids_logged_blunder(fen, blunder_uci, label): board = chess.Board(fen) eng = RandomEngine(depth=4, max_time_sec=1.2) @@ -201,12 +215,17 @@ def test_engine_avoids_logged_blunder(fen, blunder_uci, label): move = eng.choose_move(board) assert move is not None, 'Engine returned no move' assert move in board.legal_moves, 'Engine move is illegal' - assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' + assert move.uci() != blunder_uci, ( + f'Engine repeated blunder {blunder_uci} at {label}. ' + f'Explanation: {explanation}' + ) """ ) -def append_cases_to_unified_test(unified_path: str, cases: list[tuple[str, str, str, Blunder]]) -> int: +def append_cases_to_unified_test( + unified_path: str, cases: list[tuple[str, str, str, Blunder]] +) -> int: """Append new cases to BLUNDER_CASES in the unified test file, skipping duplicates. Returns the number of cases actually appended. @@ -229,20 +248,30 @@ def append_cases_to_unified_test(unified_path: str, cases: list[tuple[str, str, for fen, uci, best_uci, bl in cases: key = (fen, uci) if key in existing: - # If a best move UCI is available, try to backfill or update it into the label + # If a best move UCI is available, try to backfill + # or update it into the label if best_uci: side = "W" if bl.side == "W" else "B" fen_re = re.escape(fen) uci_re = re.escape(uci) base_label = f"ply{bl.ply}_{side}_{uci}" # Pattern A: no best suffix yet - pattern_no_best = rf"\(\"{fen_re}\",\s*\"{uci_re}\",\s*\"({re.escape(base_label)})\"\)" - # Pattern B: existing best suffix (whatever it is) - replace it with the new best_uci - pattern_with_best = rf"\(\"{fen_re}\",\s*\"{uci_re}\",\s*\"({re.escape(base_label)}_best_[^\"]+)\"\)" + pattern_no_best = ( + rf"\(\"{fen_re}\",\s*\"{uci_re}\"," + rf"\s*\"({re.escape(base_label)})\"\)" + ) + # Pattern B: existing best suffix (whatever it is) + # replace it with the new best_uci + pattern_with_best = ( + rf"\(\"{fen_re}\",\s*\"{uci_re}\"," + rf"\s*\"({re.escape(base_label)}_best_[^\"]+)\"\)" + ) if re.search(pattern_no_best, content): content = re.sub( pattern_no_best, - lambda m: m.group(0).replace(m.group(1), f"{base_label}_best_{best_uci}"), + lambda m: m.group(0).replace( + m.group(1), f"{base_label}_best_{best_uci}" + ), content, count=1, ) @@ -250,14 +279,17 @@ def append_cases_to_unified_test(unified_path: str, cases: list[tuple[str, str, elif re.search(pattern_with_best, content): content = re.sub( pattern_with_best, - lambda m: m.group(0).replace(m.group(1), f"{base_label}_best_{best_uci}"), + lambda m: m.group(0).replace( + m.group(1), f"{base_label}_best_{best_uci}" + ), content, count=1, ) updated_existing += 1 continue label = f"ply{bl.ply}_{'W' if bl.side == 'W' else 'B'}_{uci}" - # Encode the best move UCI in the label so tests can extract it without changing tuple shape + # Encode the best move UCI in the label so tests can + # extract it without changing tuple shape label += f"_best_{best_uci}" lines.append(f' ("{fen}", "{uci}", "{label}"),\n') @@ -307,7 +339,10 @@ def _process_single_log(log_path: str) -> int: print(f"Error converting SAN to UCI in {os.path.basename(log_path)}: {e}") return 2 if not cases: - print(f"Failed to reconstruct any blunder positions from PGN: {os.path.basename(log_path)}") + print( + f"Failed to reconstruct any blunder positions " + f"from PGN: {os.path.basename(log_path)}" + ) return 1 base = os.path.basename(log_path) @@ -315,10 +350,15 @@ def _process_single_log(log_path: str) -> int: game_id = m.group(1) if m else os.path.splitext(base)[0] # Always append to the unified test file - unified = os.path.join(os.path.dirname(__file__), "..", "tests", "test_blunders_all.py") + unified = os.path.join( + os.path.dirname(__file__), "..", "tests", "test_blunders_all.py" + ) unified = os.path.abspath(unified) added = append_cases_to_unified_test(unified, cases) - print(f"Appended {added} new blunder checks to {os.path.relpath(unified)} (game {game_id}).") + print( + f"Appended {added} new blunder checks to " + f"{os.path.relpath(unified)} (game {game_id})." + ) return 0 @@ -346,7 +386,10 @@ def main(argv: list[str]) -> int: rc = _process_single_log(lp) if rc == 0: ok += 1 - print(f"Processed {len(logs)} logs from {past_dir}, succeeded: {ok}, failed: {len(logs) - ok}") + print( + f"Processed {len(logs)} logs from {past_dir}, " + f"succeeded: {ok}, failed: {len(logs) - ok}" + ) return 0 if ok > 0 else 1 # One argument: game id or file path diff --git a/PYTHON/randomJPG/generateJpeg.py b/PYTHON/randomJPG/generateJpeg.py index 8f8ac4d..a8a19b3 100644 --- a/PYTHON/randomJPG/generateJpeg.py +++ b/PYTHON/randomJPG/generateJpeg.py @@ -6,11 +6,14 @@ import random from PIL import Image -def generate_bloated_jpeg(size, color_list, block_size, output_path, quality, image_index, folder): +def generate_bloated_jpeg( + size, color_list, block_size, output_path, quality, image_index, folder +): """Generates a random JPEG image with given size, list of colors, and block size. Args: - size (int): Size of the image (both width and height, must be divisible by block_size). + size (int): Size of the image (both width and height, + must be divisible by block_size). color_list (list of str): List of colors in hex format. block_size (int): Size of the pixel blocks. output_path (str): Output path for the JPEG image. @@ -28,9 +31,12 @@ def generate_bloated_jpeg(size, color_list, block_size, output_path, quality, im 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] + rgb_colors = [ + tuple(int(color[i : i + 2], 16) for i in (1, 3, 5)) for color in color_list + ] - # Fill the image with block_size x block_size pixel squares of random colors from the 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): color = random.choice(rgb_colors) @@ -55,7 +61,9 @@ def generate_bloated_jpeg(size, color_list, block_size, output_path, quality, im if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Generate bloated JPEG images with random colors.") + parser = argparse.ArgumentParser( + description="Generate bloated JPEG images with random colors." + ) parser.add_argument( "-n", "--num_images", @@ -68,7 +76,10 @@ if __name__ == "__main__": "--size", type=int, default=1000, - help="Size of the images (must be 1000 or less and divisible by block size). Default is 1000.", + help=( + "Size of the images (must be 1000 or less " + "and divisible by block size). Default is 1000." + ), ) parser.add_argument( "-c", @@ -82,7 +93,10 @@ if __name__ == "__main__": "--block_size", type=int, default=4, - help="Size of the pixel blocks (must divide the image size evenly). Default is 4.", + help=( + "Size of the pixel blocks (must divide the " + "image size evenly). Default is 4." + ), ) parser.add_argument( "-o", diff --git a/PYTHON/randomize_numbers/random_digits.py b/PYTHON/randomize_numbers/random_digits.py index f94f835..3e98ed1 100644 --- a/PYTHON/randomize_numbers/random_digits.py +++ b/PYTHON/randomize_numbers/random_digits.py @@ -17,7 +17,8 @@ def randomize_numbers(numbers, min_percentage=1, max_percentage=20): def parse_input(input_string): - # Replace commas with dots and remove non-numeric characters except dots, commas, and digits + # Replace commas with dots and remove non-numeric characters + # except dots, commas, and digits cleaned_input = re.sub(r"[^\d.,\s]", "", input_string).replace(",", ".") # Split the cleaned input into individual numbers number_strings = cleaned_input.split() @@ -37,7 +38,10 @@ def parse_input(input_string): if __name__ == "__main__": if len(sys.argv) < 2: - print("Usage: python random_digits.py ... [min_percentage max_percentage]") + print( + "Usage: python random_digits.py ... " + "[min_percentage max_percentage]" + ) sys.exit(1) try: diff --git a/PYTHON/scapeWebsite/scrape_comics.py b/PYTHON/scapeWebsite/scrape_comics.py index 45cd363..3180fb1 100644 --- a/PYTHON/scapeWebsite/scrape_comics.py +++ b/PYTHON/scapeWebsite/scrape_comics.py @@ -8,7 +8,9 @@ from selenium.webdriver.common.by import By # Initialize argument parser to accept the website URL as an argument parser = argparse.ArgumentParser(description="Download images from a comic website.") -parser.add_argument("url", type=str, help="The URL of the website to start downloading images from") +parser.add_argument( + "url", type=str, help="The URL of the website to start downloading images from" +) args = parser.parse_args() # Initialize WebDriver (Use the appropriate driver for your browser) diff --git a/PYTHON/screen_locker/screen_lock.py b/PYTHON/screen_locker/screen_lock.py index c008855..d1b3390 100755 --- a/PYTHON/screen_locker/screen_lock.py +++ b/PYTHON/screen_locker/screen_lock.py @@ -24,7 +24,9 @@ class ScreenLocker: self.root = tk.Tk() self.root.title("Workout Locker" + (" [DEMO MODE]" if demo_mode else "")) self.demo_mode = demo_mode - self.lockout_time = 10 if demo_mode else 1800 # 10 seconds for demo, 30 minutes for production + self.lockout_time = ( + 10 if demo_mode else 1800 + ) # 10 seconds for demo, 30 minutes for production self.workout_data = {} # Get total screen dimensions across all monitors @@ -235,7 +237,9 @@ class ScreenLocker: self.pace_entry.pack(side="left", padx=10) # Timer countdown label - self.timer_label = tk.Label(self.container, text="", font=("Arial", 16), fg="#ffaa00", bg="#1a1a1a") + self.timer_label = tk.Label( + self.container, text="", font=("Arial", 16), fg="#ffaa00", bg="#1a1a1a" + ) self.timer_label.pack(pady=10) self.submit_btn = tk.Button( @@ -294,7 +298,10 @@ class ScreenLocker: tolerance = expected_pace * 0.15 # 15% tolerance if pace_diff > tolerance: - self.show_error(f"Pace doesn't match! Expected ~{expected_pace:.2f} min/km, got {pace:.2f}") + self.show_error( + f"Pace doesn't match! " + f"Expected ~{expected_pace:.2f} min/km, got {pace:.2f}" + ) return # Data looks good @@ -382,7 +389,9 @@ class ScreenLocker: self.total_weight_entry.pack(side="left", padx=10) # Timer countdown label - self.timer_label = tk.Label(self.container, text="", font=("Arial", 16), fg="#ffaa00", bg="#1a1a1a") + self.timer_label = tk.Label( + self.container, text="", font=("Arial", 16), fg="#ffaa00", bg="#1a1a1a" + ) self.timer_label.pack(pady=10) self.submit_btn = tk.Button( @@ -432,7 +441,9 @@ class ScreenLocker: # Check all lists have same length if not (len(exercises) == len(sets) == len(reps) == len(weights)): - self.show_error("Number of exercises, sets, reps, and weights must match") + self.show_error( + "Number of exercises, sets, reps, and weights must match" + ) return # Check for empty or lazy entries @@ -454,13 +465,16 @@ class ScreenLocker: return # Calculate expected total weight - expected_total = sum(sets[i] * reps[i] * weights[i] for i in range(len(exercises))) + expected_total = sum( + sets[i] * reps[i] * weights[i] for i in range(len(exercises)) + ) weight_diff = abs(total_weight - expected_total) tolerance = expected_total * 0.15 # 15% tolerance if weight_diff > tolerance: self.show_error( - f"Total weight doesn't match! Expected ~{expected_total:.1f} kg, got {total_weight:.1f}" + f"Total weight doesn't match! " + f"Expected ~{expected_total:.1f} kg, got {total_weight:.1f}" ) return @@ -475,7 +489,9 @@ class ScreenLocker: # Check if widgets still exist (user might have clicked back) try: if self.submit_unlock_time > 0: - self.timer_label.config(text=f"Submit available in {self.submit_unlock_time} seconds...") + self.timer_label.config( + text=f"Submit available in {self.submit_unlock_time} seconds..." + ) self.submit_unlock_time -= 1 self.root.after(1000, self.update_submit_timer) else: diff --git a/PYTHON/split/split_x_into_n_symmetrically.py b/PYTHON/split/split_x_into_n_symmetrically.py index 0c2e7d8..ff4147f 100644 --- a/PYTHON/split/split_x_into_n_symmetrically.py +++ b/PYTHON/split/split_x_into_n_symmetrically.py @@ -3,8 +3,9 @@ def calculate_symmetric_weights(N, middle_weight, factors=None): N: Number in which to split. middle_weight: The middle value for symmetry. - factors: If provided, controls the difference in weights (used for the `split_x_into_n_symmetrically` function). - Must have length N // 2 or N // 2 - 1 depending on N. + factors: If provided, controls the difference in weights (used for the + `split_x_into_n_symmetrically` function). + Must have length N // 2 or N // 2 - 1 depending on N. """ half_N = N // 2 weights_left = [middle_weight] diff --git a/PYTHON/stockfish_analysis/analyze_chess_game.py b/PYTHON/stockfish_analysis/analyze_chess_game.py index d9aaf91..617512f 100755 --- a/PYTHON/stockfish_analysis/analyze_chess_game.py +++ b/PYTHON/stockfish_analysis/analyze_chess_game.py @@ -40,7 +40,9 @@ try: import chess.pgn except Exception: # pragma: no cover print("Missing dependency. Please install python-chess:", file=sys.stderr) - print(" pip install -r PYTHON/stockfish_analysis/requirements.txt", file=sys.stderr) + print( + " pip install -r PYTHON/stockfish_analysis/requirements.txt", file=sys.stderr + ) raise @@ -80,11 +82,13 @@ def extract_pgn_text(raw: str) -> str | None: return None -def score_to_cp(score: chess.engine.PovScore, pov_white: bool) -> tuple[int | None, int | None]: +def score_to_cp( + score: chess.engine.PovScore, pov_white: bool +) -> tuple[int | None, int | None]: """Return tuple (cp, mate_in) from a PovScore for the given POV color. - If it's a mate score, cp will be None and mate_in will be +/-N (positive means mate for POV side). - If it's a cp score, mate_in will be None. + If it's a mate score, cp will be None and mate_in will be +/-N + (positive means mate for POV side). If it's a cp score, mate_in will be None. """ pov = chess.WHITE if pov_white else chess.BLACK s = score.pov(pov) @@ -198,7 +202,9 @@ def _auto_hash_mb(threads_wanted: int, engine_options) -> int: def main(): - ap = argparse.ArgumentParser(description="Analyze a chess game's moves with Stockfish and rate each move.") + ap = argparse.ArgumentParser( + description="Analyze a chess game's moves with Stockfish and rate each move." + ) ap.add_argument("file", help="Path to a PGN file or a log containing a PGN section") ap.add_argument( "--engine", @@ -242,7 +248,10 @@ def main(): ap.add_argument( "--last-move-only", action="store_true", - help="Analyze only the last move of the main line (reports its eval and the best move)", + help=( + "Analyze only the last move of the main line " + "(reports its eval and the best move)" + ), ) args = ap.parse_args() @@ -281,7 +290,9 @@ def main(): options = {} # Threads - wanted_threads = args.threads if args.threads is not None else (multiprocessing.cpu_count() or 1) + wanted_threads = ( + args.threads if args.threads is not None else (multiprocessing.cpu_count() or 1) + ) # Respect engine bounds if present if "Threads" in options: try: @@ -343,7 +354,9 @@ def main(): result = game.headers.get("Result", "*") print(f" {white} vs {black} Result: {result}") print() - print("Columns: ply side move played_eval best_eval loss class best_suggestion") + print( + "Columns: ply side move played_eval best_eval loss class best_suggestion" + ) # Brief performance summary (best-effort) try: thr_show = int(wanted_threads) @@ -351,14 +364,19 @@ def main(): thr_show = 1 try: hash_show = ( - int(engine.options.get("Hash").value) if hasattr(engine, "options") and engine.options.get("Hash") else None + int(engine.options.get("Hash").value) + if hasattr(engine, "options") and engine.options.get("Hash") + else None ) except Exception: hash_show = None if hash_show is not None: - print(f"Using engine options: Threads={thr_show}, Hash={hash_show} MB, MultiPV={effective_mpv}") + print( + f"Using engine options: Threads={thr_show}, " + f"Hash={hash_show} MB, MultiPV={effective_mpv}" + ) else: - print(f"Using engine options: Threads={thr_show}, MultiPV={effective_mpv}") + print(f"Using engine options: Threads={thr_show}, " f"MultiPV={effective_mpv}") ply = 1 try: @@ -377,10 +395,20 @@ def main(): # If this is the final move in the mainline, analyze it and stop. if not move_node.variations: # Analyse current position to get engine best move suggestion - info_root_raw = engine.analyse(board, limit=limit, multipv=effective_mpv) - info_root = info_root_raw[0] if isinstance(info_root_raw, list) else info_root_raw + info_root_raw = engine.analyse( + board, limit=limit, multipv=effective_mpv + ) + info_root = ( + info_root_raw[0] + if isinstance(info_root_raw, list) + else info_root_raw + ) best_move = None - if info_root is not None and "pv" in info_root and info_root["pv"]: + if ( + info_root is not None + and "pv" in info_root + and info_root["pv"] + ): best_move = info_root["pv"][0] if best_move is None: res = engine.play(board, limit) @@ -391,24 +419,42 @@ def main(): # Evaluate played move board_played = board.copy() board_played.push(move) - info_played_raw = engine.analyse(board_played, limit=limit, multipv=effective_mpv) - info_played = info_played_raw[0] if isinstance(info_played_raw, list) else info_played_raw + info_played_raw = engine.analyse( + board_played, limit=limit, multipv=effective_mpv + ) + info_played = ( + info_played_raw[0] + if isinstance(info_played_raw, list) + else info_played_raw + ) if info_played is None or "score" not in info_played: played_cp, played_mate = None, None else: - played_cp, played_mate = score_to_cp(info_played["score"], pov_white=mover_white) + played_cp, played_mate = score_to_cp( + info_played["score"], pov_white=mover_white + ) # Evaluate best move position (for mover POV) - best_san = board.san(best_move) if best_move is not None else "?" + best_san = ( + board.san(best_move) if best_move is not None else "?" + ) if best_move is not None: board_best = board.copy() board_best.push(best_move) - info_best_raw = engine.analyse(board_best, limit=limit, multipv=effective_mpv) - info_best = info_best_raw[0] if isinstance(info_best_raw, list) else info_best_raw + info_best_raw = engine.analyse( + board_best, limit=limit, multipv=effective_mpv + ) + info_best = ( + info_best_raw[0] + if isinstance(info_best_raw, list) + else info_best_raw + ) if info_best is None or "score" not in info_best: best_cp, best_mate = None, None else: - best_cp, best_mate = score_to_cp(info_best["score"], pov_white=mover_white) + best_cp, best_mate = score_to_cp( + info_best["score"], pov_white=mover_white + ) else: best_cp, best_mate = None, None @@ -441,9 +487,11 @@ def main(): side = "W" if mover_white else "B" print( - f"{ply:>3} {side} {san:<8} {fmt_eval(played_cp, played_mate):>10} " + f"{ply:>3} {side} {san:<8} " + f"{fmt_eval(played_cp, played_mate):>10} " f"{fmt_eval(best_cp, best_mate):>9} " - f"{(str(cp_loss) if cp_loss is not None else '—'):>5} {classification:<12} {best_san}" + f"{(str(cp_loss) if cp_loss is not None else '—'):>5} " + f"{classification:<12} {best_san}" ) break @@ -459,8 +507,14 @@ def main(): mover_white = board.turn # Analyse position to get engine best move suggestion - info_root_raw = engine.analyse(board, limit=limit, multipv=effective_mpv) - info_root = info_root_raw[0] if isinstance(info_root_raw, list) else info_root_raw + info_root_raw = engine.analyse( + board, limit=limit, multipv=effective_mpv + ) + info_root = ( + info_root_raw[0] + if isinstance(info_root_raw, list) + else info_root_raw + ) best_move = None if info_root is not None and "pv" in info_root and info_root["pv"]: best_move = info_root["pv"][0] @@ -473,24 +527,40 @@ def main(): san = board.san(move) board_played = board.copy() board_played.push(move) - info_played_raw = engine.analyse(board_played, limit=limit, multipv=effective_mpv) - info_played = info_played_raw[0] if isinstance(info_played_raw, list) else info_played_raw + info_played_raw = engine.analyse( + board_played, limit=limit, multipv=effective_mpv + ) + info_played = ( + info_played_raw[0] + if isinstance(info_played_raw, list) + else info_played_raw + ) if info_played is None or "score" not in info_played: played_cp, played_mate = None, None else: - played_cp, played_mate = score_to_cp(info_played["score"], pov_white=mover_white) + played_cp, played_mate = score_to_cp( + info_played["score"], pov_white=mover_white + ) # Evaluate best move position (for mover POV) best_san = board.san(best_move) if best_move is not None else "?" if best_move is not None: board_best = board.copy() board_best.push(best_move) - info_best_raw = engine.analyse(board_best, limit=limit, multipv=effective_mpv) - info_best = info_best_raw[0] if isinstance(info_best_raw, list) else info_best_raw + info_best_raw = engine.analyse( + board_best, limit=limit, multipv=effective_mpv + ) + info_best = ( + info_best_raw[0] + if isinstance(info_best_raw, list) + else info_best_raw + ) if info_best is None or "score" not in info_best: best_cp, best_mate = None, None else: - best_cp, best_mate = score_to_cp(info_best["score"], pov_white=mover_white) + best_cp, best_mate = score_to_cp( + info_best["score"], pov_white=mover_white + ) else: best_cp, best_mate = None, None @@ -502,7 +572,8 @@ def main(): if best_mate is not None and played_mate is not None: # Same sign -> compare speed if (best_mate > 0) and (played_mate > 0): - # Keeping a mate: equal speed Best; slower -> Inaccuracy; faster -> Best + # Keeping a mate: equal speed Best; + # slower -> Inaccuracy; faster -> Best if abs(played_mate) == abs(best_mate): classification = "Best" elif abs(played_mate) > abs(best_mate): @@ -510,7 +581,8 @@ def main(): else: classification = "Best" elif (best_mate < 0) and (played_mate < 0): - # Defending: equal delay Best; sooner mate -> Blunder; + # Defending: equal delay Best; + # sooner mate -> Blunder; # if played delays more -> Good if abs(played_mate) == abs(best_mate): classification = "Best" @@ -530,9 +602,11 @@ def main(): side = "W" if mover_white else "B" print( - f"{ply:>3} {side} {san:<8} {fmt_eval(played_cp, played_mate):>10} " + f"{ply:>3} {side} {san:<8} " + f"{fmt_eval(played_cp, played_mate):>10} " f"{fmt_eval(best_cp, best_mate):>9} " - f"{(str(cp_loss) if cp_loss is not None else '—'):>5} {classification:<12} {best_san}" + f"{(str(cp_loss) if cp_loss is not None else '—'):>5} " + f"{classification:<12} {best_san}" ) node = move_node diff --git a/PYTHON/tagDivider/tagDivider.py b/PYTHON/tagDivider/tagDivider.py index 5a63042..7564720 100644 --- a/PYTHON/tagDivider/tagDivider.py +++ b/PYTHON/tagDivider/tagDivider.py @@ -2,7 +2,9 @@ import os # for: os.getcwd; os.mkdir; os.listdir; from os import path # for: os.path.abspath import shutil # for: shutil.move -import cv2 # for: cv2.imread; cv2.namedWindow; cv2.imshow; cv2.waitKey; cv2.destroyAllWindows; cv2.IMREAD_COLOR +# for: cv2.imread; cv2.namedWindow; cv2.imshow; +# cv2.waitKey; cv2.destroyAllWindows; cv2.IMREAD_COLOR +import cv2 IMAGE_EXTENSION = ( ".bmp", @@ -34,16 +36,24 @@ RIGHT_FOLDER_CODE = 97 # Default 97 - 'a' firstFolderName = input("Enter first folder name: [a] ") secondFolderName = input("Enter second folder name: [d] ") -currentPath = os.path.abspath(os.getcwd()) # Stolen from: https://stackoverflow.com/q/3430372 +currentPath = os.path.abspath( + os.getcwd() +) # Stolen from: https://stackoverflow.com/q/3430372 os.chdir(currentPath) # Change working directory to the path where the python file is -if path.isdir(firstFolderName) != 1: # Check if folder already exists, if it does not make it +if ( + path.isdir(firstFolderName) != 1 +): # Check if folder already exists, if it does not make it os.mkdir(firstFolderName) if path.isdir(secondFolderName) != 1: os.mkdir(secondFolderName) -for filename in os.listdir(os.getcwd()): # Go through every file in the working directory - if (filename.lower()).endswith(IMAGE_EXTENSION): # If the file name ends with image extension +for filename in os.listdir( + os.getcwd() +): # Go through every file in the working directory + if (filename.lower()).endswith( + IMAGE_EXTENSION + ): # If the file name ends with image extension print(filename) image = cv2.imread(filename, cv2.IMREAD_COLOR) window_name = filename.split(".")[0] diff --git a/articles/test_server_api.py b/articles/test_server_api.py index 9cb0bd0..8c7ce6f 100644 --- a/articles/test_server_api.py +++ b/articles/test_server_api.py @@ -40,7 +40,9 @@ def test_crud_roundtrip(tmp_path): # wait briefly for server to be ready for _ in range(30): try: - with urllib.request.urlopen(base + "/api/articles", timeout=0.2) as resp: + with urllib.request.urlopen( + base + "/api/articles", timeout=0.2 + ) as resp: resp.read() break except Exception: @@ -73,7 +75,9 @@ def test_crud_roundtrip(tmp_path): assert got["title"] == "T1" # Update - code, body = _req(base + f"/api/articles/{art_id}", method="PUT", data={"title": "T2"}) + code, body = _req( + base + f"/api/articles/{art_id}", method="PUT", data={"title": "T2"} + ) assert code == 200 updated = json.loads(body) assert updated["title"] == "T2" diff --git a/poker-modifier-app/poker_modifier_app.py b/poker-modifier-app/poker_modifier_app.py index 5ea6733..fb43e81 100644 --- a/poker-modifier-app/poker_modifier_app.py +++ b/poker-modifier-app/poker_modifier_app.py @@ -9,23 +9,37 @@ class PokerModifierApp: # Hand Bonus Modifiers (Balatro-inspired) { "name": "Pair Bonus", - "description": "Any pocket pair: everyone else pays you 1 chip, even if you lose the hand.", + "description": ( + "Any pocket pair: everyone else pays you 1 chip, " + "even if you lose the hand." + ), }, { "name": "Flush Fever", - "description": "Make a flush: collect 1 chip from each other player (separate from main pot).", + "description": ( + "Make a flush: collect 1 chip from each other player " + "(separate from main pot)." + ), }, { "name": "Straight Shot", - "description": "Complete a straight: choose one player to pay you half the current pot size.", + "description": ( + "Complete a straight: choose one player " + "to pay you half the current pot size." + ), }, { "name": "Full House Party", - "description": "Make full house: everyone else pays 2 chips + takes 2 drinks.", + "description": ( + "Make full house: everyone else pays 2 chips " "+ takes 2 drinks." + ), }, { "name": "High Card Hero", - "description": "Win with just high card: collect your normal winnings + 1 chip from each player.", + "description": ( + "Win with just high card: collect your normal winnings " + "+ 1 chip from each player." + ), }, # Card Enhancement Modifiers { @@ -34,11 +48,16 @@ class PokerModifierApp: }, { "name": "Red Suit Boost", - "description": "Hearts and Diamonds are worth +1 rank (Jack becomes Queen, etc.)", + "description": ( + "Hearts and Diamonds are worth +1 rank " + "(Jack becomes Queen, etc.)" + ), }, { "name": "Black Magic", - "description": "Spades and Clubs can be used as any suit for straights/flushes.", + "description": ( + "Spades and Clubs can be used as any suit " "for straights/flushes." + ), }, { "name": "Lucky Sevens", @@ -46,30 +65,46 @@ class PokerModifierApp: }, { "name": "Steel Cards", - "description": "Random rank chosen: {steel_rank}. All {steel_rank}s beat everything this hand!", + "description": ( + "Random rank chosen: {steel_rank}. " + "All {steel_rank}s beat everything this hand!" + ), }, # Ante-Based Effects (Clear Money Source) { "name": "Bonus Pool", - "description": "Everyone puts 2 chips in bonus pool. First person to make any pair wins it all.", + "description": ( + "Everyone puts 2 chips in bonus pool. " + "First person to make any pair wins it all." + ), }, # Deck Manipulation (Balatro-style) { "name": "Deck Shuffle", - "description": "After dealing hole cards, shuffle deck and redeal all community cards.", + "description": ( + "After dealing hole cards, shuffle deck " + "and redeal all community cards." + ), }, { "name": "Extra Draw", - "description": "Deal each player a 3rd hole card. Discard one before the flop.", + "description": ( + "Deal each player a 3rd hole card. " "Discard one before the flop." + ), }, { "name": "Phantom Cards", - "description": "Deal 6 community cards, but randomly remove 1 before showdown.", + "description": ( + "Deal 6 community cards, " "but randomly remove 1 before showdown." + ), }, # Special Betting Rules (Realistic Economics) { "name": "Escalation", - "description": "Each raise must be at least 2x the previous raise (not just matching).", + "description": ( + "Each raise must be at least 2x the previous raise " + "(not just matching)." + ), }, # Position and Action Modifiers { @@ -78,16 +113,25 @@ class PokerModifierApp: }, { "name": "Call Penalty", - "description": "Anyone who only calls (never raises) pays 1 chip penalty to pot.", + "description": ( + "Anyone who only calls (never raises) " + "pays 1 chip penalty to pot." + ), }, # Information Warfare { "name": "Poker Face", - "description": "No talking, no expressions allowed. Pure silent poker this hand.", + "description": ( + "No talking, no expressions allowed. " + "Pure silent poker this hand." + ), }, { "name": "Truth or Consequences", - "description": "If asked 'good hand or bad hand?' you must answer truthfully or pay penalty.", + "description": ( + "If asked 'good hand or bad hand?' " + "you must answer truthfully or pay penalty." + ), }, { "name": "Open Book", @@ -96,11 +140,15 @@ class PokerModifierApp: # Drinking Game Integration { "name": "Liquid Courage", - "description": "Take a drink before betting to get chip bonus to all your bets.", + "description": ( + "Take a drink before betting " "to get chip bonus to all your bets." + ), }, { "name": "Last Call", - "description": "Everyone must finish their current drink before the river card.", + "description": ( + "Everyone must finish their current drink " "before the river card." + ), }, { "name": "Shot Clock", @@ -108,12 +156,17 @@ class PokerModifierApp: }, { "name": "Drink Tax", - "description": "Each red card in your final hand = one sip (reveal afret play) .", + "description": ( + "Each red card in your final hand = one sip " "(reveal after play)." + ), }, # Wild and Chaos Effects { "name": "Joker's Wild", - "description": "All Jacks become completely wild - any suit, any rank you choose.", + "description": ( + "All Jacks become completely wild - " + "any suit, any rank you choose." + ), }, { "name": "Suit Swap", @@ -126,7 +179,8 @@ class PokerModifierApp: { "name": "Time Warp", "description": ( - "Play the hand completely backwards: showdown first, " "then remove random cards from table!" + "Play the hand completely backwards: showdown first, " + "then remove random cards from table!" ), }, # Economic Effects (Clear Money Sources) @@ -140,7 +194,10 @@ class PokerModifierApp: }, { "name": "Charity Case", - "description": "Player with fewest chips get ther ente funded by richest player.", + "description": ( + "Player with fewest chips gets their ante " + "funded by richest player." + ), }, # Penalty-Based Modifiers (Clear Consequences) { @@ -153,50 +210,74 @@ class PokerModifierApp: }, { "name": "Speed Fine", - "description": "Take longer than 10 seconds to act = pay 1 chip to pot.", + "description": ( + "Take longer than 10 seconds to act " "= pay 1 chip to pot." + ), }, { "name": "Talk Tax", - "description": "Every word spoken during betting costs 1 chip to the pot.", + "description": ( + "Every word spoken during betting " "costs 1 chip to the pot." + ), }, # Skill Challenges (With Clear Rewards/Penalties) { "name": "Memory Challenge", "description": ( "Dealer names all community cards in order. " - "Success = collect 1 chip from each player. Fail = pay 1 chip to each." + "Success = collect 1 chip from each. " + "Fail = pay 1 chip to each." ), }, { "name": "Quick Draw", "description": ( - "Everyone pays 1 chip to quick-draw pot. " "First to correctly announce their hand wins the pot." + "Everyone pays 1 chip to quick-draw pot. " + "First to correctly announce their hand wins the pot." ), }, { "name": "Bluff Bonus", - "description": "Successfully bluff with 7-high or worse = collect 2 chips from each other player.", + "description": ( + "Successfully bluff with 7-high or worse " + "= collect 2 chips from each other player." + ), }, { "name": "Prediction Pool", - "description": "Everyone puts 1 chip in pool. Guess the river card exactly = win the pool.", + "description": ( + "Everyone puts 1 chip in pool. " + "Guess the river card exactly = win the pool." + ), }, # Partnership Modifiers { "name": "Buddy System", - "description": "Each player chooses a partner. Partners share fate - both win or both lose.", + "description": ( + "Each player chooses a partner. " + "Partners share fate - both win or both lose." + ), }, { "name": "Duo Power", - "description": "Partners can combine their hole cards - each player plays with 4 cards total.", + "description": ( + "Partners can combine their hole cards - " + "each player plays with 4 cards total." + ), }, { "name": "Shared Vision", - "description": "Partners can show each other one hole card before betting starts.", + "description": ( + "Partners can show each other one hole card " + "before betting starts." + ), }, { "name": "Tag Team", - "description": "Partners alternate who plays each betting round (pre-flop, flop, turn, river).", + "description": ( + "Partners alternate who plays each betting round " + "(pre-flop, flop, turn, river)." + ), }, { "name": "Power Couple", @@ -212,7 +293,9 @@ class PokerModifierApp: # Classic Endgame Modifiers { "name": "Final Boss", - "description": "This is the last hand. Winner takes all remaining chips.", + "description": ( + "This is the last hand. " "Winner takes all remaining chips." + ), }, { "name": "Sudden Death", @@ -220,38 +303,61 @@ class PokerModifierApp: }, { "name": "Comeback Kid", - "description": "Player with the worst hand can't lose chips this round (reveal at the end of round).", + "description": ( + "Player with the worst hand can't lose chips this round " + "(reveal at the end of round)." + ), }, { "name": "Double or Nothing", - "description": "Winner gets double payout, but everyone else pays double penalty.", + "description": ( + "Winner gets double payout, " + "but everyone else pays double penalty." + ), }, # High Stakes Endgame { "name": "All In Madness", - "description": "Everyone must go all-in. No calling, no folding allowed this hand.", + "description": ( + "Everyone must go all-in. " + "No calling, no folding allowed this hand." + ), }, { "name": "Chip Volcano", - "description": "Everyone puts half their remaining chips in the center. Winner takes the mountain.", + "description": ( + "Everyone puts half their remaining chips in the center. " + "Winner takes the mountain." + ), }, { "name": "Last Stand", - "description": "Player with fewest chips gets to act last in ALL betting rounds.", + "description": ( + "Player with fewest chips gets to act last " + "in ALL betting rounds." + ), }, # Dramatic Reversals { "name": "Underdog Victory", - "description": "Worst hand wins the pot instead of best hand this round.", + "description": ( + "Worst hand wins the pot " "instead of best hand this round." + ), }, # Winner Takes All Variants { "name": "Crown Jewels", - "description": "Winner of this hand becomes the 'King' - all other players pay tribute (2 chips each).", + "description": ( + "Winner of this hand becomes the 'King' - " + "all other players pay tribute (2 chips each)." + ), }, { "name": "Championship Belt", - "description": "Winner takes 75% of all chips on the table. Remaining 25% goes for the second best.", + "description": ( + "Winner takes 75% of all chips on the table. " + "Remaining 25% goes for the second best." + ), }, # Elimination Mechanics { @@ -260,55 +366,87 @@ class PokerModifierApp: }, { "name": "Survivor", - "description": "Only players who improve their hand from pre-flop to river survive to next round.", + "description": ( + "Only players who improve their hand from pre-flop to river " + "survive to next round." + ), }, # Time Pressure Endgame { "name": "Speed Round", - "description": "3 seconds to act or auto-fold. No exceptions, no delays.", + "description": ( + "3 seconds to act or auto-fold. " "No exceptions, no delays." + ), }, { "name": "Auction House", - "description": "Players bid chips to see each other's hole cards before betting.", + "description": ( + "Players bid chips to see each other's hole cards " + "before betting." + ), }, { "name": "Lightning Round", - "description": "Deal all 5 community cards at once. Betting happens after each card revealed.", + "description": ( + "Deal all 5 community cards at once. " + "Betting happens after each card revealed." + ), }, # Psychological Warfare { "name": "Confession Booth", - "description": "Each player must truthfully state their biggest bluff this session.", + "description": ( + "Each player must truthfully state " + "their biggest bluff this session." + ), }, { "name": "Truth Serum", - "description": "Everyone must honestly rate their hand 1-10 before any betting.", + "description": ( + "Everyone must honestly rate their hand 1-10 " "before any betting." + ), }, { "name": "Poker Face Off", - "description": "Staring contest: losers must reveal one hole card to the table.", + "description": ( + "Staring contest: losers must reveal " "one hole card to the table." + ), }, # Endgame Economics { "name": "Wealth Redistribution", - "description": "Before the hand, richest player gives 3 chips to poorest player.", + "description": ( + "Before the hand, richest player " + "gives 3 chips to poorest player." + ), }, { "name": "Emergency Fund", - "description": "All players with less than 5 chips get emergency funding from the pot.", + "description": ( + "All players with less than 5 chips " + "get emergency funding from the pot." + ), }, { "name": "Final Ante", - "description": "Everyone must put in their last 2 chips before seeing cards. No backing out.", + "description": ( + "Everyone must put in their last 2 chips " + "before seeing cards. No backing out." + ), }, # Apocalypse Modifiers { "name": "Nuclear Option", - "description": "Dealer burns the top 3 cards. Play with whatever's left in the deck.", + "description": ( + "Dealer burns the top 3 cards. " + "Play with whatever's left in the deck." + ), }, { "name": "Meteor Strike", - "description": "Remove all face cards from the deck for this hand only.", + "description": ( + "Remove all face cards from the deck " "for this hand only." + ), }, { "name": "Solar Flare", @@ -317,15 +455,22 @@ class PokerModifierApp: # Legacy Modifiers { "name": "Hall of Fame", - "description": "Winner's name gets written down as 'Champion of the Session'.", + "description": ( + "Winner's name gets written down " "as 'Champion of the Session'." + ), }, { "name": "Legendary Hand", - "description": "This hand will be retold as a story. Play like legends.", + "description": ( + "This hand will be retold as a story. " "Play like legends." + ), }, { "name": "Photo Finish", - "description": "Take a photo of the winning hand - it goes in the poker hall of fame.", + "description": ( + "Take a photo of the winning hand - " + "it goes in the poker hall of fame." + ), }, # Chaos Theory { @@ -337,17 +482,24 @@ class PokerModifierApp: }, { "name": "Time Paradox", - "description": "Play the hand twice with same cards. Best average result wins.", + "description": ( + "Play the hand twice with same cards. " "Best average result wins." + ), }, { "name": "Multiverse", - "description": "Deal 2 separate boards. Players choose which board to play after seeing both.", + "description": ( + "Deal 2 separate boards. Players choose " + "which board to play after seeing both." + ), }, ] # Remove endgame modifiers from regular modifier list endgame_modifier_names = [mod["name"] for mod in self.endgame_modifiers] - self.modifiers = [mod for mod in self.modifiers if mod["name"] not in endgame_modifier_names] + self.modifiers = [ + mod for mod in self.modifiers if mod["name"] not in endgame_modifier_names + ] # Game state tracking self.rounds_played = 0 @@ -507,7 +659,9 @@ class PokerModifierApp: self.length_label.pack(side=tk.RIGHT) # Result display frame - self.result_frame = tk.Frame(main_frame, bg="#2d2d2d", relief=tk.RIDGE, bd=3, height=150) + self.result_frame = tk.Frame( + main_frame, 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) @@ -541,7 +695,9 @@ class PokerModifierApp: command=self.start_round, cursor="hand2", ) - self.start_button.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=10, padx=(0, 5)) + self.start_button.pack( + side=tk.LEFT, fill=tk.X, expand=True, ipady=10, padx=(0, 5) + ) # Reset button self.reset_button = tk.Button( @@ -596,7 +752,9 @@ class PokerModifierApp: ) mods_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(3, 3)) - self.mods_label = tk.Label(mods_frame, text="0", font=("Arial", 20, "bold"), fg="#ffd700", bg="#1a6b4d") + self.mods_label = tk.Label( + mods_frame, text="0", font=("Arial", 20, "bold"), fg="#ffd700", bg="#1a6b4d" + ) self.mods_label.pack(pady=10) # Game phase indicator @@ -731,13 +889,20 @@ class PokerModifierApp: "Ace", ] steel_rank = random.choice(ranks) - selected_modifier["description"] = selected_modifier["description"].format(steel_rank=steel_rank) + selected_modifier["description"] = selected_modifier["description"].format( + steel_rank=steel_rank + ) # Update result frame styling for modifier - self.result_frame.config(bg=bg_color, highlightbackground="#ffd700", highlightthickness=2) + self.result_frame.config( + bg=bg_color, highlightbackground="#ffd700", highlightthickness=2 + ) # Update display with modifier info - modifier_text = f"{modifier_type} {selected_modifier['name']}\n\n{selected_modifier['description']}" + modifier_text = ( + f"{modifier_type} {selected_modifier['name']}\n\n" + f"{selected_modifier['description']}" + ) # Add endgame indicator if applicable if self.is_endgame(): @@ -747,12 +912,16 @@ class PokerModifierApp: else: modifier_text += "\n\n⚠️ FINAL ROUND!" - self.result_label.config(text=modifier_text, fg="#ffd700", bg=bg_color, font=("Arial", 14, "bold")) + self.result_label.config( + text=modifier_text, fg="#ffd700", bg=bg_color, font=("Arial", 14, "bold") + ) def show_no_modifier(self): """Show no modifier message.""" # Update result frame styling for no modifier - self.result_frame.config(bg="#2d2d2d", highlightbackground="#666666", highlightthickness=1) + self.result_frame.config( + bg="#2d2d2d", highlightbackground="#666666", highlightthickness=1 + ) # Update display self.result_label.config( @@ -774,7 +943,9 @@ class PokerModifierApp: self.phase_label.config(text="Early", fg="#4CAF50") # Reset result frame - self.result_frame.config(bg="#2d2d2d", highlightbackground="#666666", highlightthickness=1) + self.result_frame.config( + bg="#2d2d2d", highlightbackground="#666666", highlightthickness=1 + ) self.result_label.config( text="Click 'Start Round' to begin!", fg="#cccccc", @@ -794,7 +965,11 @@ class PokerModifierApp: def get_stats(self): """Get current statistics.""" - modifier_rate = 0 if self.rounds_played == 0 else (self.modifiers_applied / self.rounds_played) * 100 + modifier_rate = ( + 0 + if self.rounds_played == 0 + else (self.modifiers_applied / self.rounds_played) * 100 + ) rounds_remaining = max(0, self.total_game_rounds - self.rounds_played) return { diff --git a/pyproject.toml b/pyproject.toml index d321461..2c78c4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ requires-python = ">=3.10" # RUFF - Extremely fast Python linter and formatter (written in Rust) # ============================================================================ [tool.ruff] -line-length = 120 # Relaxed for scripts with long strings +line-length = 88 target-version = "py310" # Include all Python files include = ["*.py", "**/*.py"]