mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 14:43:01 +02:00
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:
parent
d760aab07d
commit
5d4ce33dcd
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
)
|
||||
|
||||
@ -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}.",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user