mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 16:34:06 +01: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:
|
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("input_html", help="Path to input HTML file")
|
||||||
ap.add_argument(
|
ap.add_argument(
|
||||||
"output_txt",
|
"output_txt",
|
||||||
nargs="?",
|
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()
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
|||||||
@ -87,7 +87,9 @@ class KeyboardCoopGame:
|
|||||||
def load_dictionary(self):
|
def load_dictionary(self):
|
||||||
"""Load dictionary from words_dictionary.json file."""
|
"""Load dictionary from words_dictionary.json file."""
|
||||||
try:
|
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:
|
with open(dictionary_path, encoding="utf-8") as f:
|
||||||
dictionary_data = json.load(f)
|
dictionary_data = json.load(f)
|
||||||
# Convert to set for faster lookup (we only need the keys)
|
# Convert to set for faster lookup (we only need the keys)
|
||||||
@ -147,7 +149,10 @@ class KeyboardCoopGame:
|
|||||||
"good",
|
"good",
|
||||||
}
|
}
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print("Warning: Error reading words_dictionary.json, using fallback dictionary")
|
print(
|
||||||
|
"Warning: Error reading words_dictionary.json, "
|
||||||
|
"using fallback dictionary"
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"cat",
|
"cat",
|
||||||
"dog",
|
"dog",
|
||||||
@ -280,13 +285,19 @@ class KeyboardCoopGame:
|
|||||||
self.current_word += letter
|
self.current_word += letter
|
||||||
|
|
||||||
# Update available letters to include adjacent letters AND the same 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
|
adjacent_letters.add(letter) # Allow selecting the same letter again
|
||||||
self.available_letters = adjacent_letters
|
self.available_letters = adjacent_letters
|
||||||
|
|
||||||
# Switch player
|
# Switch player
|
||||||
self.current_player = 1 - self.current_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 no valid moves available, force word submission
|
||||||
if not self.available_letters:
|
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):
|
if len(self.current_word) >= 3 and self.is_valid_word(self.current_word):
|
||||||
points = self.calculate_score(len(self.current_word))
|
points = self.calculate_score(len(self.current_word))
|
||||||
self.score += points
|
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
|
# Randomize keyboard layout after scoring
|
||||||
self.generate_random_keyboard()
|
self.generate_random_keyboard()
|
||||||
@ -357,7 +371,9 @@ class KeyboardCoopGame:
|
|||||||
self.screen.blit(title, (30, 20))
|
self.screen.blit(title, (30, 20))
|
||||||
|
|
||||||
# Current word
|
# 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))
|
self.screen.blit(word_text, (30, 50))
|
||||||
|
|
||||||
# Score
|
# Score
|
||||||
@ -366,7 +382,9 @@ class KeyboardCoopGame:
|
|||||||
|
|
||||||
# Current player
|
# Current player
|
||||||
player_color = PLAYER_COLORS[self.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))
|
self.screen.blit(player_text, (30, 100))
|
||||||
|
|
||||||
# Message
|
# Message
|
||||||
|
|||||||
@ -39,7 +39,9 @@ class RandomEngine:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.engine_path = engine_path or default_path
|
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 = (
|
msg = (
|
||||||
f"C engine not found or not executable at '{self.engine_path}'. "
|
f"C engine not found or not executable at '{self.engine_path}'. "
|
||||||
"Build it first (make -C C/lichess_random_engine)."
|
"Build it first (make -C C/lichess_random_engine)."
|
||||||
@ -65,7 +67,9 @@ class RandomEngine:
|
|||||||
return (proc.stdout or "").strip()
|
return (proc.stdout or "").strip()
|
||||||
|
|
||||||
def choose_move(self, board: chess.Board) -> chess.Move:
|
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
|
return mv
|
||||||
|
|
||||||
def choose_move_with_explanation(
|
def choose_move_with_explanation(
|
||||||
@ -77,7 +81,8 @@ class RandomEngine:
|
|||||||
return None, "no_legal_moves"
|
return None, "no_legal_moves"
|
||||||
|
|
||||||
args = ["--fen", board.fen()] + [m.uci() for m in legal]
|
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.
|
# We deliberately avoid adding annotations here per request.
|
||||||
|
|
||||||
output = self._call_engine(args, timeout=max(0.1, time_budget_sec))
|
output = self._call_engine(args, timeout=max(0.1, time_budget_sec))
|
||||||
@ -103,7 +108,7 @@ class RandomEngine:
|
|||||||
*,
|
*,
|
||||||
time_budget_sec: float,
|
time_budget_sec: float,
|
||||||
) -> tuple[float, str, chess.Move | None, str]:
|
) -> 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)
|
Returns (candidate_score, candidate_expl, best_move, best_expl)
|
||||||
where explanations are concise JSON snippets from the engine. All logic is
|
where explanations are concise JSON snippets from the engine. All logic is
|
||||||
@ -113,7 +118,9 @@ class RandomEngine:
|
|||||||
if not legal:
|
if not legal:
|
||||||
return 0.0, "no_legal_moves", None, "no_best_move"
|
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))
|
out = self._call_engine(args, timeout=max(0.1, time_budget_sec))
|
||||||
|
|
||||||
# Try to parse the engine's JSON explanation
|
# 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.
|
"""Wrapper around session.request that logs every request/response.
|
||||||
|
|
||||||
- Logs start (method+URL) and end (status, elapsed).
|
- Logs start (method+URL) and end (status, elapsed).
|
||||||
@ -47,7 +49,10 @@ class LichessAPI:
|
|||||||
except Exception:
|
except Exception:
|
||||||
snippet = None
|
snippet = None
|
||||||
if snippet:
|
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:
|
else:
|
||||||
logging.warning(f"HTTP {method} {url} -> {status} in {elapsed:.2f}s")
|
logging.warning(f"HTTP {method} {url} -> {status} in {elapsed:.2f}s")
|
||||||
else:
|
else:
|
||||||
@ -63,7 +68,9 @@ class LichessAPI:
|
|||||||
try:
|
try:
|
||||||
# Use NDJSON Accept and no timeout for long-lived stream
|
# Use NDJSON Accept and no timeout for long-lived stream
|
||||||
headers = {"Accept": "application/x-ndjson"}
|
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()
|
r.raise_for_status()
|
||||||
backoff = 0.5 # reset on success
|
backoff = 0.5 # reset on success
|
||||||
for line in r.iter_lines(decode_unicode=True):
|
for line in r.iter_lines(decode_unicode=True):
|
||||||
@ -91,7 +98,9 @@ class LichessAPI:
|
|||||||
data = {"reason": reason}
|
data = {"reason": reason}
|
||||||
self._request("POST", url, data=data, timeout=30, raise_for_status=True)
|
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."""
|
"""Deprecated: use stream_game_events and parse initial state there."""
|
||||||
# Fallback to initial behavior for compatibility
|
# Fallback to initial behavior for compatibility
|
||||||
url = f"{LICHESS_API}/api/board/game/stream/{game_id}"
|
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}]")
|
logging.info(f"Starting game thread for {game_id} [bot v{bot_version}]")
|
||||||
board = chess.Board()
|
board = chess.Board()
|
||||||
color: str | None = my_color
|
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
|
last_handled_len = -1
|
||||||
# Prepare a per-game log file
|
# Prepare a per-game log file
|
||||||
game_log_path = os.path.join(os.getcwd(), f"lichess_bot_game_{game_id}.log")
|
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", "")
|
moves = state.get("moves", "")
|
||||||
status = state.get("status")
|
status = state.get("status")
|
||||||
# clocks are in milliseconds if present
|
# clocks are in milliseconds if present
|
||||||
my_ms = state.get("wtime") if color == "white" else state.get("btime")
|
my_ms = (
|
||||||
opp_ms = state.get("btime") if color == "white" else state.get("wtime")
|
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
|
inc_ms = state.get("winc") or state.get("binc") or 0
|
||||||
# Discover my color from gameFull
|
# Discover my color from gameFull
|
||||||
white_id = event["white"].get("id")
|
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
|
# Set site and date if available
|
||||||
try:
|
try:
|
||||||
# Lichess event may include 'createdAt' ms epoch
|
# 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:
|
if created_ms:
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
game_date_iso = datetime.datetime.utcfromtimestamp(int(created_ms) / 1000).strftime(
|
game_date_iso = datetime.datetime.utcfromtimestamp(
|
||||||
"%Y.%m.%d"
|
int(created_ms) / 1000
|
||||||
)
|
).strftime("%Y.%m.%d")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
site_url = f"https://lichess.org/{game_id}"
|
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 []
|
moves_list = moves.split() if moves else []
|
||||||
new_len = len(moves_list)
|
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:
|
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
|
continue
|
||||||
|
|
||||||
# Rebuild board from moves
|
# 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}")
|
logging.debug(f"Game {game_id}: could not apply move {m}")
|
||||||
|
|
||||||
if color is None:
|
if color is None:
|
||||||
logging.info(f"Game {game_id}: color unknown yet; waiting for gameFull")
|
logging.info(
|
||||||
# Do not mark this position handled on gameFull; wait for authoritative gameState
|
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":
|
if et == "gameState":
|
||||||
last_handled_len = new_len
|
last_handled_len = new_len
|
||||||
continue
|
continue
|
||||||
|
|
||||||
is_white_turn = board.turn
|
is_white_turn = board.turn
|
||||||
my_turn = (is_white_turn and color == "white") or ((not is_white_turn) and color == "black")
|
my_turn = (is_white_turn and color == "white") or (
|
||||||
logging.info(f"Game {game_id}: turn={'white' if is_white_turn else 'black'}, my_turn={my_turn}")
|
(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:
|
# Move policy:
|
||||||
# - Always move on 'gameState' (authoritative)
|
# - Always move on 'gameState' (authoritative)
|
||||||
# - Also allow moving on the initial 'gameFull' when there are
|
# - Also allow moving on the initial 'gameFull' when there are
|
||||||
# zero moves and it's our turn. This avoids stalling at game
|
# zero moves and it's our turn. This avoids stalling at game
|
||||||
# start when Lichess doesn't immediately send a 'gameState'
|
# start when Lichess doesn't immediately send a 'gameState'
|
||||||
# for 0 moves.
|
# 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:
|
if my_turn and allow_move:
|
||||||
# Compute a per-move time budget (seconds) based on remaining time
|
# Compute a per-move time budget (seconds) based on
|
||||||
# Heuristic: use min( max_time_sec, max(0.05, 0.6 * my_time_left/remaining_moves + inc) )
|
# 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]
|
# 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
|
time_left_sec = (my_ms or 0) / 1000.0
|
||||||
inc_sec = (inc_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
|
# Spend more time per move (requested): double the budget
|
||||||
budget *= 2.0
|
budget *= 2.0
|
||||||
# Keep within reasonable bounds
|
# Keep within reasonable bounds
|
||||||
budget = max(0.05, min(engine.max_time_sec, budget))
|
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:
|
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
|
break
|
||||||
try:
|
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:
|
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:
|
else:
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Game {game_id}: playing {move.uci()} "
|
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)"
|
f"inc={inc_sec:.2f}s)"
|
||||||
)
|
)
|
||||||
if game_log_path:
|
if game_log_path:
|
||||||
with open(game_log_path, "a") as lf:
|
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)
|
api.make_move(game_id, move)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"Game {game_id}: move {move.uci()} failed: {e}")
|
logging.warning(
|
||||||
# Mark this position as handled on authoritative gameState, or after we've
|
f"Game {game_id}: " f"move {move.uci()} failed: {e}"
|
||||||
# actually attempted a move (including the first move on gameFull len=0).
|
)
|
||||||
|
# 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):
|
if et == "gameState" or (my_turn and allow_move):
|
||||||
last_handled_len = new_len
|
last_handled_len = new_len
|
||||||
if status in {"mate", "resign", "stalemate", "timeout", "draw"}:
|
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
|
pass
|
||||||
with open(game_log_path, "a") as lf:
|
with open(game_log_path, "a") as lf:
|
||||||
lf.write("\nPGN:\n")
|
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(game.accept(exporter))
|
||||||
lf.write("\n")
|
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:
|
if game_log_path:
|
||||||
analysis_text: str | None = None
|
analysis_text: str | None = None
|
||||||
try:
|
try:
|
||||||
@ -223,7 +276,10 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No
|
|||||||
except Exception:
|
except Exception:
|
||||||
total_plies = 0
|
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
|
# Run analyzer unbuffered and stream output for progress
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
[sys.executable, "-u", analyze_script, game_log_path],
|
[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:
|
if m:
|
||||||
# Count as one analyzed ply
|
# Count as one analyzed ply
|
||||||
analyzed += 1
|
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:
|
if total_plies:
|
||||||
pct = analyzed / total_plies * 100.0
|
pct = analyzed / total_plies * 100.0
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Game {game_id}: analysis progress "
|
f"Game {game_id}: analysis progress "
|
||||||
f"{analyzed}/{total_plies} ({pct:.0f}%), left {left}"
|
f"{analyzed}/{total_plies} "
|
||||||
|
f"({pct:.0f}%), left {left}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.info(
|
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
|
# 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()
|
ret = proc.wait()
|
||||||
analysis_text = "".join(lines)
|
analysis_text = "".join(lines)
|
||||||
if ret != 0:
|
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:
|
if stderr_text:
|
||||||
analysis_text += "\n[stderr]\n" + stderr_text
|
analysis_text += "\n[stderr]\n" + stderr_text
|
||||||
logging.info(f"Game {game_id}: analysis complete")
|
logging.info(f"Game {game_id}: analysis complete")
|
||||||
else:
|
else:
|
||||||
logging.info(
|
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:
|
except Exception as e:
|
||||||
logging.debug(f"Game {game_id}: analysis run failed: {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:
|
if analysis_text:
|
||||||
try:
|
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()
|
content = f.read()
|
||||||
|
|
||||||
# Find the start of the 'PGN:' line
|
# Find the start of the 'PGN:' line
|
||||||
insert_idx = 0
|
insert_idx = 0
|
||||||
p = content.find("\nPGN:\n")
|
p = content.find("\nPGN:\n")
|
||||||
if p != -1:
|
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"):
|
elif content.startswith("PGN:\n"):
|
||||||
insert_idx = 0
|
insert_idx = 0
|
||||||
else:
|
else:
|
||||||
@ -294,20 +365,32 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No
|
|||||||
if game_date_iso:
|
if game_date_iso:
|
||||||
meta_lines.append(f"Date: {game_date_iso}")
|
meta_lines.append(f"Date: {game_date_iso}")
|
||||||
if white_name or black_name:
|
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:
|
if meta_lines:
|
||||||
meta_block = "\n".join(meta_lines) + "\n"
|
meta_block = "\n".join(meta_lines) + "\n"
|
||||||
else:
|
else:
|
||||||
meta_block = ""
|
meta_block = ""
|
||||||
|
|
||||||
analysis_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:
|
with open(game_log_path, "w", encoding="utf-8") as f:
|
||||||
f.write(new_content)
|
f.write(new_content)
|
||||||
except Exception as e:
|
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:
|
except Exception as e:
|
||||||
logging.debug(f"Game {game_id}: could not write PGN: {e}")
|
logging.debug(f"Game {game_id}: could not write PGN: {e}")
|
||||||
logging.info(f"Ending game thread for {game_id}")
|
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")
|
variant = challenge.get("variant", {}).get("key", "standard")
|
||||||
speed = challenge.get("speed")
|
speed = challenge.get("speed")
|
||||||
perf_ok = speed in {"bullet", "blitz", "rapid", "classical"}
|
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:
|
if variant == "standard" and perf_ok and not_corr:
|
||||||
logging.info(f"Accepting challenge {ch_id} ({speed})")
|
logging.info(f"Accepting challenge {ch_id} ({speed})")
|
||||||
api.accept_challenge(ch_id)
|
api.accept_challenge(ch_id)
|
||||||
else:
|
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)
|
api.decline_challenge(ch_id)
|
||||||
|
|
||||||
elif event.get("type") == "gameStart":
|
elif event.get("type") == "gameStart":
|
||||||
game_id = event["game"]["id"]
|
game_id = event["game"]["id"]
|
||||||
# Spin up a game thread
|
# Spin up a game thread
|
||||||
if game_id not in game_threads or not game_threads[game_id].is_alive():
|
if (
|
||||||
t = threading.Thread(target=handle_game, args=(game_id,), name=f"game-{game_id}")
|
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
|
t.daemon = True
|
||||||
game_threads[game_id] = t
|
game_threads[game_id] = t
|
||||||
t.start()
|
t.start()
|
||||||
@ -355,7 +449,9 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Run a minimal Lichess bot")
|
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(
|
parser.add_argument(
|
||||||
"--decline-correspondence",
|
"--decline-correspondence",
|
||||||
action="store_true",
|
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.
|
This lets us keep historical files in the repo without collecting them.
|
||||||
"""
|
"""
|
||||||
basename = collection_path.name
|
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(
|
@pytest.mark.parametrize(
|
||||||
("fen", "moves_str"),
|
("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):
|
def test_puzzle_engine_follow_solution(fen: str, moves_str: str):
|
||||||
board = chess.Board(fen)
|
board = chess.Board(fen)
|
||||||
eng = RandomEngine(max_time_sec=1.0)
|
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()
|
solution_moves = moves_str.split()
|
||||||
step = 0
|
step = 0
|
||||||
for uci in solution_moves:
|
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)
|
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}"
|
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:
|
if mv.uci() != uci:
|
||||||
# Ask the engine to analyze the correct move for debug
|
# Ask the engine to analyze the correct move for debug
|
||||||
score_cp, proposed_expl, best_mv, best_expl = eng.evaluate_proposed_move_with_suggestion(
|
score_cp, proposed_expl, best_mv, best_expl = (
|
||||||
board, uci, time_budget_sec=0.5
|
eng.evaluate_proposed_move_with_suggestion(
|
||||||
|
board, uci, time_budget_sec=0.5
|
||||||
|
)
|
||||||
)
|
)
|
||||||
details = [
|
details = [
|
||||||
f"Puzzle failed at step {step}.",
|
f"Puzzle failed at step {step}.",
|
||||||
|
|||||||
@ -22,7 +22,8 @@ Usage examples:
|
|||||||
python PYTHON/lichess_bot/tools/generate_blunder_tests.py OVmR29MI
|
python PYTHON/lichess_bot/tools/generate_blunder_tests.py OVmR29MI
|
||||||
|
|
||||||
# Process an explicit file path
|
# 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:
|
It will create files like:
|
||||||
PYTHON/lichess_bot/tests/test_blunders_<gameid>.py
|
PYTHON/lichess_bot/tests/test_blunders_<gameid>.py
|
||||||
@ -70,7 +71,8 @@ def parse_columns_for_blunders(text: str) -> list[Blunder]:
|
|||||||
continue
|
continue
|
||||||
# Split by 2+ spaces to get columns
|
# Split by 2+ spaces to get columns
|
||||||
parts = re.split(r"\s{2,}", ln.strip())
|
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:
|
if len(parts) < 8:
|
||||||
continue
|
continue
|
||||||
try:
|
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
|
# Require best suggestion to be provided; if it's missing, raise
|
||||||
if not best_suggestion_san:
|
if not best_suggestion_san:
|
||||||
msg = (
|
msg = (
|
||||||
f"Missing best_suggestion in Columns for blunder row: ply={ply} side={side} move={move_san}.\n"
|
f"Missing best_suggestion in Columns "
|
||||||
f"Raw line: '{ln.strip()}'"
|
f"for blunder row: ply={ply} side={side} "
|
||||||
|
f"move={move_san}.\nRaw line: '{ln.strip()}'"
|
||||||
)
|
)
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
blunders.append(
|
blunders.append(
|
||||||
@ -119,7 +122,9 @@ def san_list_from_game(game: chess.pgn.Game) -> list[str]:
|
|||||||
return san_moves
|
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))
|
game = chess.pgn.read_game(io.StringIO(pgn_text))
|
||||||
if game is None:
|
if game is None:
|
||||||
msg = "Failed to parse PGN from log"
|
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
|
continue
|
||||||
else:
|
else:
|
||||||
continue
|
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:
|
try:
|
||||||
best_move = board.parse_san(bl.best_suggestion_san)
|
best_move = board.parse_san(bl.best_suggestion_san)
|
||||||
best_uci = best_move.uci()
|
best_uci = best_move.uci()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = (
|
msg = (
|
||||||
f"Failed to parse best_suggestion SAN '{bl.best_suggestion_san}' at ply {bl.ply} side {bl.side} "
|
f"Failed to parse best_suggestion SAN "
|
||||||
f"in position FEN: {fen_before}. Error: {e}"
|
f"'{bl.best_suggestion_san}' at ply {bl.ply} "
|
||||||
|
f"side {bl.side} in position FEN: {fen_before}. "
|
||||||
|
f"Error: {e}"
|
||||||
)
|
)
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
results.append((fen_before, move.uci(), best_uci, bl))
|
results.append((fen_before, move.uci(), best_uci, bl))
|
||||||
@ -174,7 +182,9 @@ import chess
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
# Ensure repo root is importable when running pytest directly
|
# 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:
|
if REPO_ROOT not in sys.path:
|
||||||
sys.path.insert(0, REPO_ROOT)
|
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):
|
def test_engine_avoids_logged_blunder(fen, blunder_uci, label):
|
||||||
board = chess.Board(fen)
|
board = chess.Board(fen)
|
||||||
eng = RandomEngine(depth=4, max_time_sec=1.2)
|
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)
|
move = eng.choose_move(board)
|
||||||
assert move is not None, 'Engine returned no move'
|
assert move is not None, 'Engine returned no move'
|
||||||
assert move in board.legal_moves, 'Engine move is illegal'
|
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.
|
"""Append new cases to BLUNDER_CASES in the unified test file, skipping duplicates.
|
||||||
|
|
||||||
Returns the number of cases actually appended.
|
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:
|
for fen, uci, best_uci, bl in cases:
|
||||||
key = (fen, uci)
|
key = (fen, uci)
|
||||||
if key in existing:
|
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:
|
if best_uci:
|
||||||
side = "W" if bl.side == "W" else "B"
|
side = "W" if bl.side == "W" else "B"
|
||||||
fen_re = re.escape(fen)
|
fen_re = re.escape(fen)
|
||||||
uci_re = re.escape(uci)
|
uci_re = re.escape(uci)
|
||||||
base_label = f"ply{bl.ply}_{side}_{uci}"
|
base_label = f"ply{bl.ply}_{side}_{uci}"
|
||||||
# Pattern A: no best suffix yet
|
# Pattern A: no best suffix yet
|
||||||
pattern_no_best = rf"\(\"{fen_re}\",\s*\"{uci_re}\",\s*\"({re.escape(base_label)})\"\)"
|
pattern_no_best = (
|
||||||
# Pattern B: existing best suffix (whatever it is) - replace it with the new best_uci
|
rf"\(\"{fen_re}\",\s*\"{uci_re}\","
|
||||||
pattern_with_best = rf"\(\"{fen_re}\",\s*\"{uci_re}\",\s*\"({re.escape(base_label)}_best_[^\"]+)\"\)"
|
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):
|
if re.search(pattern_no_best, content):
|
||||||
content = re.sub(
|
content = re.sub(
|
||||||
pattern_no_best,
|
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,
|
content,
|
||||||
count=1,
|
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):
|
elif re.search(pattern_with_best, content):
|
||||||
content = re.sub(
|
content = re.sub(
|
||||||
pattern_with_best,
|
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,
|
content,
|
||||||
count=1,
|
count=1,
|
||||||
)
|
)
|
||||||
updated_existing += 1
|
updated_existing += 1
|
||||||
continue
|
continue
|
||||||
label = f"ply{bl.ply}_{'W' if bl.side == 'W' else 'B'}_{uci}"
|
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}"
|
label += f"_best_{best_uci}"
|
||||||
lines.append(f' ("{fen}", "{uci}", "{label}"),\n')
|
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}")
|
print(f"Error converting SAN to UCI in {os.path.basename(log_path)}: {e}")
|
||||||
return 2
|
return 2
|
||||||
if not cases:
|
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
|
return 1
|
||||||
|
|
||||||
base = os.path.basename(log_path)
|
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]
|
game_id = m.group(1) if m else os.path.splitext(base)[0]
|
||||||
|
|
||||||
# Always append to the unified test file
|
# 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)
|
unified = os.path.abspath(unified)
|
||||||
added = append_cases_to_unified_test(unified, cases)
|
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
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@ -346,7 +386,10 @@ def main(argv: list[str]) -> int:
|
|||||||
rc = _process_single_log(lp)
|
rc = _process_single_log(lp)
|
||||||
if rc == 0:
|
if rc == 0:
|
||||||
ok += 1
|
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
|
return 0 if ok > 0 else 1
|
||||||
|
|
||||||
# One argument: game id or file path
|
# One argument: game id or file path
|
||||||
|
|||||||
@ -6,11 +6,14 @@ import random
|
|||||||
from PIL import Image
|
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.
|
"""Generates a random JPEG image with given size, list of colors, and block size.
|
||||||
|
|
||||||
Args:
|
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.
|
color_list (list of str): List of colors in hex format.
|
||||||
block_size (int): Size of the pixel blocks.
|
block_size (int): Size of the pixel blocks.
|
||||||
output_path (str): Output path for the JPEG image.
|
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()
|
pixels = image.load()
|
||||||
|
|
||||||
# Convert hex colors to RGB
|
# 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 y in range(0, size, block_size):
|
||||||
for x in range(0, size, block_size):
|
for x in range(0, size, block_size):
|
||||||
color = random.choice(rgb_colors)
|
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__":
|
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(
|
parser.add_argument(
|
||||||
"-n",
|
"-n",
|
||||||
"--num_images",
|
"--num_images",
|
||||||
@ -68,7 +76,10 @@ if __name__ == "__main__":
|
|||||||
"--size",
|
"--size",
|
||||||
type=int,
|
type=int,
|
||||||
default=1000,
|
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(
|
parser.add_argument(
|
||||||
"-c",
|
"-c",
|
||||||
@ -82,7 +93,10 @@ if __name__ == "__main__":
|
|||||||
"--block_size",
|
"--block_size",
|
||||||
type=int,
|
type=int,
|
||||||
default=4,
|
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(
|
parser.add_argument(
|
||||||
"-o",
|
"-o",
|
||||||
|
|||||||
@ -17,7 +17,8 @@ def randomize_numbers(numbers, min_percentage=1, max_percentage=20):
|
|||||||
|
|
||||||
|
|
||||||
def parse_input(input_string):
|
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(",", ".")
|
cleaned_input = re.sub(r"[^\d.,\s]", "", input_string).replace(",", ".")
|
||||||
# Split the cleaned input into individual numbers
|
# Split the cleaned input into individual numbers
|
||||||
number_strings = cleaned_input.split()
|
number_strings = cleaned_input.split()
|
||||||
@ -37,7 +38,10 @@ def parse_input(input_string):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) < 2:
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -8,7 +8,9 @@ from selenium.webdriver.common.by import By
|
|||||||
|
|
||||||
# Initialize argument parser to accept the website URL as an argument
|
# Initialize argument parser to accept the website URL as an argument
|
||||||
parser = argparse.ArgumentParser(description="Download images from a comic website.")
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Initialize WebDriver (Use the appropriate driver for your browser)
|
# Initialize WebDriver (Use the appropriate driver for your browser)
|
||||||
|
|||||||
@ -24,7 +24,9 @@ class ScreenLocker:
|
|||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
self.root.title("Workout Locker" + (" [DEMO MODE]" if demo_mode else ""))
|
self.root.title("Workout Locker" + (" [DEMO MODE]" if demo_mode else ""))
|
||||||
self.demo_mode = demo_mode
|
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 = {}
|
self.workout_data = {}
|
||||||
|
|
||||||
# Get total screen dimensions across all monitors
|
# Get total screen dimensions across all monitors
|
||||||
@ -235,7 +237,9 @@ class ScreenLocker:
|
|||||||
self.pace_entry.pack(side="left", padx=10)
|
self.pace_entry.pack(side="left", padx=10)
|
||||||
|
|
||||||
# Timer countdown label
|
# 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.timer_label.pack(pady=10)
|
||||||
|
|
||||||
self.submit_btn = tk.Button(
|
self.submit_btn = tk.Button(
|
||||||
@ -294,7 +298,10 @@ class ScreenLocker:
|
|||||||
tolerance = expected_pace * 0.15 # 15% tolerance
|
tolerance = expected_pace * 0.15 # 15% tolerance
|
||||||
|
|
||||||
if pace_diff > 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
|
return
|
||||||
|
|
||||||
# Data looks good
|
# Data looks good
|
||||||
@ -382,7 +389,9 @@ class ScreenLocker:
|
|||||||
self.total_weight_entry.pack(side="left", padx=10)
|
self.total_weight_entry.pack(side="left", padx=10)
|
||||||
|
|
||||||
# Timer countdown label
|
# 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.timer_label.pack(pady=10)
|
||||||
|
|
||||||
self.submit_btn = tk.Button(
|
self.submit_btn = tk.Button(
|
||||||
@ -432,7 +441,9 @@ class ScreenLocker:
|
|||||||
|
|
||||||
# Check all lists have same length
|
# Check all lists have same length
|
||||||
if not (len(exercises) == len(sets) == len(reps) == len(weights)):
|
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
|
return
|
||||||
|
|
||||||
# Check for empty or lazy entries
|
# Check for empty or lazy entries
|
||||||
@ -454,13 +465,16 @@ class ScreenLocker:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Calculate expected total weight
|
# 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)
|
weight_diff = abs(total_weight - expected_total)
|
||||||
tolerance = expected_total * 0.15 # 15% tolerance
|
tolerance = expected_total * 0.15 # 15% tolerance
|
||||||
|
|
||||||
if weight_diff > tolerance:
|
if weight_diff > tolerance:
|
||||||
self.show_error(
|
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
|
return
|
||||||
|
|
||||||
@ -475,7 +489,9 @@ class ScreenLocker:
|
|||||||
# Check if widgets still exist (user might have clicked back)
|
# Check if widgets still exist (user might have clicked back)
|
||||||
try:
|
try:
|
||||||
if self.submit_unlock_time > 0:
|
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.submit_unlock_time -= 1
|
||||||
self.root.after(1000, self.update_submit_timer)
|
self.root.after(1000, self.update_submit_timer)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -3,8 +3,9 @@ def calculate_symmetric_weights(N, middle_weight, factors=None):
|
|||||||
|
|
||||||
N: Number in which to split.
|
N: Number in which to split.
|
||||||
middle_weight: The middle value for symmetry.
|
middle_weight: The middle value for symmetry.
|
||||||
factors: If provided, controls the difference in weights (used for the `split_x_into_n_symmetrically` function).
|
factors: If provided, controls the difference in weights (used for the
|
||||||
Must have length N // 2 or N // 2 - 1 depending on N.
|
`split_x_into_n_symmetrically` function).
|
||||||
|
Must have length N // 2 or N // 2 - 1 depending on N.
|
||||||
"""
|
"""
|
||||||
half_N = N // 2
|
half_N = N // 2
|
||||||
weights_left = [middle_weight]
|
weights_left = [middle_weight]
|
||||||
|
|||||||
@ -40,7 +40,9 @@ try:
|
|||||||
import chess.pgn
|
import chess.pgn
|
||||||
except Exception: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
print("Missing dependency. Please install python-chess:", file=sys.stderr)
|
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
|
raise
|
||||||
|
|
||||||
|
|
||||||
@ -80,11 +82,13 @@ def extract_pgn_text(raw: str) -> str | None:
|
|||||||
return 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.
|
"""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 mate score, cp will be None and mate_in will be +/-N
|
||||||
If it's a cp score, mate_in will be None.
|
(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
|
pov = chess.WHITE if pov_white else chess.BLACK
|
||||||
s = score.pov(pov)
|
s = score.pov(pov)
|
||||||
@ -198,7 +202,9 @@ def _auto_hash_mb(threads_wanted: int, engine_options) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
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("file", help="Path to a PGN file or a log containing a PGN section")
|
||||||
ap.add_argument(
|
ap.add_argument(
|
||||||
"--engine",
|
"--engine",
|
||||||
@ -242,7 +248,10 @@ def main():
|
|||||||
ap.add_argument(
|
ap.add_argument(
|
||||||
"--last-move-only",
|
"--last-move-only",
|
||||||
action="store_true",
|
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()
|
args = ap.parse_args()
|
||||||
|
|
||||||
@ -281,7 +290,9 @@ def main():
|
|||||||
options = {}
|
options = {}
|
||||||
|
|
||||||
# Threads
|
# 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
|
# Respect engine bounds if present
|
||||||
if "Threads" in options:
|
if "Threads" in options:
|
||||||
try:
|
try:
|
||||||
@ -343,7 +354,9 @@ def main():
|
|||||||
result = game.headers.get("Result", "*")
|
result = game.headers.get("Result", "*")
|
||||||
print(f" {white} vs {black} Result: {result}")
|
print(f" {white} vs {black} Result: {result}")
|
||||||
print()
|
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)
|
# Brief performance summary (best-effort)
|
||||||
try:
|
try:
|
||||||
thr_show = int(wanted_threads)
|
thr_show = int(wanted_threads)
|
||||||
@ -351,14 +364,19 @@ def main():
|
|||||||
thr_show = 1
|
thr_show = 1
|
||||||
try:
|
try:
|
||||||
hash_show = (
|
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:
|
except Exception:
|
||||||
hash_show = None
|
hash_show = None
|
||||||
if hash_show is not 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:
|
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
|
ply = 1
|
||||||
try:
|
try:
|
||||||
@ -377,10 +395,20 @@ def main():
|
|||||||
# If this is the final move in the mainline, analyze it and stop.
|
# If this is the final move in the mainline, analyze it and stop.
|
||||||
if not move_node.variations:
|
if not move_node.variations:
|
||||||
# Analyse current position to get engine best move suggestion
|
# Analyse current position to get engine best move suggestion
|
||||||
info_root_raw = engine.analyse(board, limit=limit, multipv=effective_mpv)
|
info_root_raw = engine.analyse(
|
||||||
info_root = info_root_raw[0] if isinstance(info_root_raw, list) else info_root_raw
|
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
|
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]
|
best_move = info_root["pv"][0]
|
||||||
if best_move is None:
|
if best_move is None:
|
||||||
res = engine.play(board, limit)
|
res = engine.play(board, limit)
|
||||||
@ -391,24 +419,42 @@ def main():
|
|||||||
# Evaluate played move
|
# Evaluate played move
|
||||||
board_played = board.copy()
|
board_played = board.copy()
|
||||||
board_played.push(move)
|
board_played.push(move)
|
||||||
info_played_raw = engine.analyse(board_played, limit=limit, multipv=effective_mpv)
|
info_played_raw = engine.analyse(
|
||||||
info_played = info_played_raw[0] if isinstance(info_played_raw, list) else info_played_raw
|
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:
|
if info_played is None or "score" not in info_played:
|
||||||
played_cp, played_mate = None, None
|
played_cp, played_mate = None, None
|
||||||
else:
|
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)
|
# 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:
|
if best_move is not None:
|
||||||
board_best = board.copy()
|
board_best = board.copy()
|
||||||
board_best.push(best_move)
|
board_best.push(best_move)
|
||||||
info_best_raw = engine.analyse(board_best, limit=limit, multipv=effective_mpv)
|
info_best_raw = engine.analyse(
|
||||||
info_best = info_best_raw[0] if isinstance(info_best_raw, list) else info_best_raw
|
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:
|
if info_best is None or "score" not in info_best:
|
||||||
best_cp, best_mate = None, None
|
best_cp, best_mate = None, None
|
||||||
else:
|
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:
|
else:
|
||||||
best_cp, best_mate = None, None
|
best_cp, best_mate = None, None
|
||||||
|
|
||||||
@ -441,9 +487,11 @@ def main():
|
|||||||
|
|
||||||
side = "W" if mover_white else "B"
|
side = "W" if mover_white else "B"
|
||||||
print(
|
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"{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
|
break
|
||||||
|
|
||||||
@ -459,8 +507,14 @@ def main():
|
|||||||
mover_white = board.turn
|
mover_white = board.turn
|
||||||
|
|
||||||
# Analyse position to get engine best move suggestion
|
# Analyse position to get engine best move suggestion
|
||||||
info_root_raw = engine.analyse(board, limit=limit, multipv=effective_mpv)
|
info_root_raw = engine.analyse(
|
||||||
info_root = info_root_raw[0] if isinstance(info_root_raw, list) else info_root_raw
|
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
|
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]
|
best_move = info_root["pv"][0]
|
||||||
@ -473,24 +527,40 @@ def main():
|
|||||||
san = board.san(move)
|
san = board.san(move)
|
||||||
board_played = board.copy()
|
board_played = board.copy()
|
||||||
board_played.push(move)
|
board_played.push(move)
|
||||||
info_played_raw = engine.analyse(board_played, limit=limit, multipv=effective_mpv)
|
info_played_raw = engine.analyse(
|
||||||
info_played = info_played_raw[0] if isinstance(info_played_raw, list) else info_played_raw
|
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:
|
if info_played is None or "score" not in info_played:
|
||||||
played_cp, played_mate = None, None
|
played_cp, played_mate = None, None
|
||||||
else:
|
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)
|
# 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:
|
if best_move is not None:
|
||||||
board_best = board.copy()
|
board_best = board.copy()
|
||||||
board_best.push(best_move)
|
board_best.push(best_move)
|
||||||
info_best_raw = engine.analyse(board_best, limit=limit, multipv=effective_mpv)
|
info_best_raw = engine.analyse(
|
||||||
info_best = info_best_raw[0] if isinstance(info_best_raw, list) else info_best_raw
|
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:
|
if info_best is None or "score" not in info_best:
|
||||||
best_cp, best_mate = None, None
|
best_cp, best_mate = None, None
|
||||||
else:
|
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:
|
else:
|
||||||
best_cp, best_mate = None, None
|
best_cp, best_mate = None, None
|
||||||
|
|
||||||
@ -502,7 +572,8 @@ def main():
|
|||||||
if best_mate is not None and played_mate is not None:
|
if best_mate is not None and played_mate is not None:
|
||||||
# Same sign -> compare speed
|
# Same sign -> compare speed
|
||||||
if (best_mate > 0) and (played_mate > 0):
|
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):
|
if abs(played_mate) == abs(best_mate):
|
||||||
classification = "Best"
|
classification = "Best"
|
||||||
elif abs(played_mate) > abs(best_mate):
|
elif abs(played_mate) > abs(best_mate):
|
||||||
@ -510,7 +581,8 @@ def main():
|
|||||||
else:
|
else:
|
||||||
classification = "Best"
|
classification = "Best"
|
||||||
elif (best_mate < 0) and (played_mate < 0):
|
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 played delays more -> Good
|
||||||
if abs(played_mate) == abs(best_mate):
|
if abs(played_mate) == abs(best_mate):
|
||||||
classification = "Best"
|
classification = "Best"
|
||||||
@ -530,9 +602,11 @@ def main():
|
|||||||
|
|
||||||
side = "W" if mover_white else "B"
|
side = "W" if mover_white else "B"
|
||||||
print(
|
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"{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
|
node = move_node
|
||||||
|
|||||||
@ -2,7 +2,9 @@ import os # for: os.getcwd; os.mkdir; os.listdir;
|
|||||||
from os import path # for: os.path.abspath
|
from os import path # for: os.path.abspath
|
||||||
import shutil # for: shutil.move
|
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 = (
|
IMAGE_EXTENSION = (
|
||||||
".bmp",
|
".bmp",
|
||||||
@ -34,16 +36,24 @@ RIGHT_FOLDER_CODE = 97 # Default 97 - 'a'
|
|||||||
firstFolderName = input("Enter first folder name: [a] ")
|
firstFolderName = input("Enter first folder name: [a] ")
|
||||||
secondFolderName = input("Enter second folder name: [d] ")
|
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
|
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)
|
os.mkdir(firstFolderName)
|
||||||
if path.isdir(secondFolderName) != 1:
|
if path.isdir(secondFolderName) != 1:
|
||||||
os.mkdir(secondFolderName)
|
os.mkdir(secondFolderName)
|
||||||
|
|
||||||
for filename in os.listdir(os.getcwd()): # Go through every file in the working directory
|
for filename in os.listdir(
|
||||||
if (filename.lower()).endswith(IMAGE_EXTENSION): # If the file name ends with image extension
|
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)
|
print(filename)
|
||||||
image = cv2.imread(filename, cv2.IMREAD_COLOR)
|
image = cv2.imread(filename, cv2.IMREAD_COLOR)
|
||||||
window_name = filename.split(".")[0]
|
window_name = filename.split(".")[0]
|
||||||
|
|||||||
@ -40,7 +40,9 @@ def test_crud_roundtrip(tmp_path):
|
|||||||
# wait briefly for server to be ready
|
# wait briefly for server to be ready
|
||||||
for _ in range(30):
|
for _ in range(30):
|
||||||
try:
|
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()
|
resp.read()
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -73,7 +75,9 @@ def test_crud_roundtrip(tmp_path):
|
|||||||
assert got["title"] == "T1"
|
assert got["title"] == "T1"
|
||||||
|
|
||||||
# Update
|
# 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
|
assert code == 200
|
||||||
updated = json.loads(body)
|
updated = json.loads(body)
|
||||||
assert updated["title"] == "T2"
|
assert updated["title"] == "T2"
|
||||||
|
|||||||
@ -9,23 +9,37 @@ class PokerModifierApp:
|
|||||||
# Hand Bonus Modifiers (Balatro-inspired)
|
# Hand Bonus Modifiers (Balatro-inspired)
|
||||||
{
|
{
|
||||||
"name": "Pair Bonus",
|
"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",
|
"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",
|
"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",
|
"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",
|
"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
|
# Card Enhancement Modifiers
|
||||||
{
|
{
|
||||||
@ -34,11 +48,16 @@ class PokerModifierApp:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Red Suit Boost",
|
"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",
|
"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",
|
"name": "Lucky Sevens",
|
||||||
@ -46,30 +65,46 @@ class PokerModifierApp:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Steel Cards",
|
"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)
|
# Ante-Based Effects (Clear Money Source)
|
||||||
{
|
{
|
||||||
"name": "Bonus Pool",
|
"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)
|
# Deck Manipulation (Balatro-style)
|
||||||
{
|
{
|
||||||
"name": "Deck Shuffle",
|
"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",
|
"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",
|
"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)
|
# Special Betting Rules (Realistic Economics)
|
||||||
{
|
{
|
||||||
"name": "Escalation",
|
"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
|
# Position and Action Modifiers
|
||||||
{
|
{
|
||||||
@ -78,16 +113,25 @@ class PokerModifierApp:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Call Penalty",
|
"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
|
# Information Warfare
|
||||||
{
|
{
|
||||||
"name": "Poker Face",
|
"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",
|
"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",
|
"name": "Open Book",
|
||||||
@ -96,11 +140,15 @@ class PokerModifierApp:
|
|||||||
# Drinking Game Integration
|
# Drinking Game Integration
|
||||||
{
|
{
|
||||||
"name": "Liquid Courage",
|
"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",
|
"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",
|
"name": "Shot Clock",
|
||||||
@ -108,12 +156,17 @@ class PokerModifierApp:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Drink Tax",
|
"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
|
# Wild and Chaos Effects
|
||||||
{
|
{
|
||||||
"name": "Joker's Wild",
|
"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",
|
"name": "Suit Swap",
|
||||||
@ -126,7 +179,8 @@ class PokerModifierApp:
|
|||||||
{
|
{
|
||||||
"name": "Time Warp",
|
"name": "Time Warp",
|
||||||
"description": (
|
"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)
|
# Economic Effects (Clear Money Sources)
|
||||||
@ -140,7 +194,10 @@ class PokerModifierApp:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Charity Case",
|
"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)
|
# Penalty-Based Modifiers (Clear Consequences)
|
||||||
{
|
{
|
||||||
@ -153,50 +210,74 @@ class PokerModifierApp:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Speed Fine",
|
"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",
|
"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)
|
# Skill Challenges (With Clear Rewards/Penalties)
|
||||||
{
|
{
|
||||||
"name": "Memory Challenge",
|
"name": "Memory Challenge",
|
||||||
"description": (
|
"description": (
|
||||||
"Dealer names all community cards in order. "
|
"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",
|
"name": "Quick Draw",
|
||||||
"description": (
|
"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",
|
"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",
|
"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
|
# Partnership Modifiers
|
||||||
{
|
{
|
||||||
"name": "Buddy System",
|
"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",
|
"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",
|
"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",
|
"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",
|
"name": "Power Couple",
|
||||||
@ -212,7 +293,9 @@ class PokerModifierApp:
|
|||||||
# Classic Endgame Modifiers
|
# Classic Endgame Modifiers
|
||||||
{
|
{
|
||||||
"name": "Final Boss",
|
"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",
|
"name": "Sudden Death",
|
||||||
@ -220,38 +303,61 @@ class PokerModifierApp:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Comeback Kid",
|
"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",
|
"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
|
# High Stakes Endgame
|
||||||
{
|
{
|
||||||
"name": "All In Madness",
|
"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",
|
"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",
|
"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
|
# Dramatic Reversals
|
||||||
{
|
{
|
||||||
"name": "Underdog Victory",
|
"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
|
# Winner Takes All Variants
|
||||||
{
|
{
|
||||||
"name": "Crown Jewels",
|
"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",
|
"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
|
# Elimination Mechanics
|
||||||
{
|
{
|
||||||
@ -260,55 +366,87 @@ class PokerModifierApp:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Survivor",
|
"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
|
# Time Pressure Endgame
|
||||||
{
|
{
|
||||||
"name": "Speed Round",
|
"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",
|
"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",
|
"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
|
# Psychological Warfare
|
||||||
{
|
{
|
||||||
"name": "Confession Booth",
|
"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",
|
"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",
|
"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
|
# Endgame Economics
|
||||||
{
|
{
|
||||||
"name": "Wealth Redistribution",
|
"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",
|
"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",
|
"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
|
# Apocalypse Modifiers
|
||||||
{
|
{
|
||||||
"name": "Nuclear Option",
|
"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",
|
"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",
|
"name": "Solar Flare",
|
||||||
@ -317,15 +455,22 @@ class PokerModifierApp:
|
|||||||
# Legacy Modifiers
|
# Legacy Modifiers
|
||||||
{
|
{
|
||||||
"name": "Hall of Fame",
|
"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",
|
"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",
|
"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
|
# Chaos Theory
|
||||||
{
|
{
|
||||||
@ -337,17 +482,24 @@ class PokerModifierApp:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Time Paradox",
|
"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",
|
"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
|
# Remove endgame modifiers from regular modifier list
|
||||||
endgame_modifier_names = [mod["name"] for mod in self.endgame_modifiers]
|
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
|
# Game state tracking
|
||||||
self.rounds_played = 0
|
self.rounds_played = 0
|
||||||
@ -507,7 +659,9 @@ class PokerModifierApp:
|
|||||||
self.length_label.pack(side=tk.RIGHT)
|
self.length_label.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
# Result display frame
|
# 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(fill=tk.BOTH, expand=True, pady=(0, 20), padx=10)
|
||||||
self.result_frame.pack_propagate(False)
|
self.result_frame.pack_propagate(False)
|
||||||
|
|
||||||
@ -541,7 +695,9 @@ class PokerModifierApp:
|
|||||||
command=self.start_round,
|
command=self.start_round,
|
||||||
cursor="hand2",
|
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
|
# Reset button
|
||||||
self.reset_button = tk.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))
|
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)
|
self.mods_label.pack(pady=10)
|
||||||
|
|
||||||
# Game phase indicator
|
# Game phase indicator
|
||||||
@ -731,13 +889,20 @@ class PokerModifierApp:
|
|||||||
"Ace",
|
"Ace",
|
||||||
]
|
]
|
||||||
steel_rank = random.choice(ranks)
|
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
|
# 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
|
# 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
|
# Add endgame indicator if applicable
|
||||||
if self.is_endgame():
|
if self.is_endgame():
|
||||||
@ -747,12 +912,16 @@ class PokerModifierApp:
|
|||||||
else:
|
else:
|
||||||
modifier_text += "\n\n⚠️ FINAL ROUND!"
|
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):
|
def show_no_modifier(self):
|
||||||
"""Show no modifier message."""
|
"""Show no modifier message."""
|
||||||
# Update result frame styling for no modifier
|
# 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
|
# Update display
|
||||||
self.result_label.config(
|
self.result_label.config(
|
||||||
@ -774,7 +943,9 @@ class PokerModifierApp:
|
|||||||
self.phase_label.config(text="Early", fg="#4CAF50")
|
self.phase_label.config(text="Early", fg="#4CAF50")
|
||||||
|
|
||||||
# Reset result frame
|
# 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(
|
self.result_label.config(
|
||||||
text="Click 'Start Round' to begin!",
|
text="Click 'Start Round' to begin!",
|
||||||
fg="#cccccc",
|
fg="#cccccc",
|
||||||
@ -794,7 +965,11 @@ class PokerModifierApp:
|
|||||||
|
|
||||||
def get_stats(self):
|
def get_stats(self):
|
||||||
"""Get current statistics."""
|
"""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)
|
rounds_remaining = max(0, self.total_game_rounds - self.rounds_played)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ requires-python = ">=3.10"
|
|||||||
# RUFF - Extremely fast Python linter and formatter (written in Rust)
|
# RUFF - Extremely fast Python linter and formatter (written in Rust)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 120 # Relaxed for scripts with long strings
|
line-length = 88
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
# Include all Python files
|
# Include all Python files
|
||||||
include = ["*.py", "**/*.py"]
|
include = ["*.py", "**/*.py"]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user