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
This commit is contained in:
Krzysztof kuhy Rudnicki 2025-11-30 14:25:35 +01:00
parent d760aab07d
commit 5d4ce33dcd
18 changed files with 712 additions and 226 deletions

View File

@ -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 <input_basename>_links.txt in the same directory)",
help=(
"Path to output text file "
"(defaults to <input_basename>_links.txt in the same directory)"
),
)
args = ap.parse_args()

View File

@ -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

View File

@ -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

View File

@ -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}"

View File

@ -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",

View File

@ -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"
)

View File

@ -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}.",

View File

@ -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_<gameid>.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

View File

@ -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",

View File

@ -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 <number1> <number2> ... [min_percentage max_percentage]")
print(
"Usage: python random_digits.py <number1> <number2> ... "
"[min_percentage max_percentage]"
)
sys.exit(1)
try:

View File

@ -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)

View File

@ -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:

View File

@ -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]

View File

@ -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

View File

@ -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]

View File

@ -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"

View File

@ -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 {

View File

@ -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"]