mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-06 18:23:15 +02:00
feat: log failures in connecting to lichess
This commit is contained in:
parent
cc0bd572be
commit
59e6f807b7
@ -1 +1 @@
|
||||
4
|
||||
10
|
||||
@ -43,6 +43,20 @@ class RandomEngine:
|
||||
("e2e4", "c7c5"): ["g1f3", "d2d4", "c2c3", "b1c3"],
|
||||
("d2d4", "d7d5"): ["c2c4", "g1f3", "e2e3"],
|
||||
("d2d4", "g8f6"): ["c2c4", "g1f3", "e2e3"],
|
||||
|
||||
# --- More specific continuations to steer sensible early play ---
|
||||
# 1.e4 e5 2.Nf3 (Black to move)
|
||||
("e2e4", "e7e5", "g1f3"): ["b8c6", "g8f6", "f8c5", "d7d6"],
|
||||
# Italian: 1.e4 e5 2.Nf3 Nc6 3.Bc4 (Black to move)
|
||||
("e2e4", "e7e5", "g1f3", "b8c6", "f1c4"): ["g8f6", "f8c5", "d7d6"],
|
||||
# Ruy Lopez: 1.e4 e5 2.Nf3 Nc6 3.Bb5 (Black to move)
|
||||
("e2e4", "e7e5", "g1f3", "b8c6", "f1b5"): ["a7a6", "g8f6", "f8c5", "d7d6"],
|
||||
# Scotch: 1.e4 e5 2.Nf3 Nc6 3.d4 (Black to move)
|
||||
("e2e4", "e7e5", "g1f3", "b8c6", "d2d4"): ["e5d4", "g8f6"],
|
||||
# Queen's Gambit: 1.d4 d5 2.c4 (Black to move)
|
||||
("d2d4", "d7d5", "c2c4"): ["e7e6", "c7c6", "d5c4"],
|
||||
# English: 1.c4 e5 2.Nc3 (Black to move)
|
||||
("c2c4", "e7e5", "b1c3"): ["g8f6", "b8c6"],
|
||||
}
|
||||
|
||||
def choose_move(self, board: chess.Board, time_budget_sec: Optional[float] = None) -> Optional[chess.Move]:
|
||||
@ -275,6 +289,30 @@ class RandomEngine:
|
||||
early = self._is_early_game(board)
|
||||
piece = board.piece_at(m.from_square)
|
||||
if piece:
|
||||
# Discourage premature queen adventures in the opening
|
||||
if piece.piece_type == chess.QUEEN and early:
|
||||
victim = board.piece_at(m.to_square)
|
||||
# Penalize queen pawn-grabs on edge pawns (a2/b2/g2/h2 or a7/b7/g7/h7)
|
||||
poison_targets_white = {chess.A7, chess.B7, chess.G7, chess.H7}
|
||||
poison_targets_black = {chess.A2, chess.B2, chess.G2, chess.H2}
|
||||
is_poison_target = (
|
||||
(piece.color == chess.WHITE and m.to_square in poison_targets_white)
|
||||
or (piece.color == chess.BLACK and m.to_square in poison_targets_black)
|
||||
)
|
||||
if is_cap and victim and victim.piece_type == chess.PAWN and is_poison_target:
|
||||
# If destination is heavily attacked, apply a large penalty
|
||||
attackers_op = len(board.attackers(not piece.color, m.to_square))
|
||||
defenders_me = len(board.attackers(piece.color, m.to_square))
|
||||
if attackers_op >= max(1, defenders_me):
|
||||
s -= 500
|
||||
else:
|
||||
s -= 250
|
||||
# General small penalty for non-check queen moves before minor development
|
||||
if not is_cap:
|
||||
if self._most_minors_undeveloped(board, piece.color):
|
||||
s -= 160
|
||||
else:
|
||||
s -= 60
|
||||
if board.is_castling(m):
|
||||
s += 650
|
||||
if piece.piece_type in (chess.KNIGHT, chess.BISHOP):
|
||||
@ -304,11 +342,20 @@ class RandomEngine:
|
||||
from_file = chess.square_file(m.from_square)
|
||||
from_rank = chess.square_rank(m.from_square)
|
||||
to_rank = chess.square_rank(m.to_square)
|
||||
# Discourage early f-pawn push and also random wing pawn thrusts like a/b/g/h
|
||||
if from_file == 5:
|
||||
if piece.color == chess.WHITE and from_rank == 1 and to_rank == 2:
|
||||
s -= 140
|
||||
if piece.color == chess.BLACK and from_rank == 6 and to_rank == 5:
|
||||
s -= 140
|
||||
if from_file in (0, 1, 6, 7) and ((piece.color == chess.WHITE and from_rank == 1 and to_rank == 2) or (piece.color == chess.BLACK and from_rank == 6 and to_rank == 5)):
|
||||
s -= 60
|
||||
# Discourage early c-pawn push to c4/c5 if we already advanced the e-pawn (prevents e5+c5 blunder-y structures)
|
||||
if from_file == 2:
|
||||
e_pawn_sq = chess.E2 if piece.color == chess.WHITE else chess.E7
|
||||
e_advanced = board.piece_at(e_pawn_sq) is None
|
||||
if e_advanced and ((piece.color == chess.WHITE and from_rank == 1 and to_rank == 3) or (piece.color == chess.BLACK and from_rank == 6 and to_rank == 4)):
|
||||
s -= 80
|
||||
if chess.square_file(m.to_square) in (3, 4):
|
||||
s += 50
|
||||
return s
|
||||
@ -371,6 +418,22 @@ class RandomEngine:
|
||||
if bk_sq not in (chess.E8, chess.G8, chess.C8):
|
||||
safety += 40
|
||||
|
||||
# Early queen raid penalty: queen deep in opponent camp in the opening
|
||||
queen_raid_pen = 0
|
||||
if self._is_early_game(board):
|
||||
q_w = board.pieces(chess.QUEEN, chess.WHITE)
|
||||
q_b = board.pieces(chess.QUEEN, chess.BLACK)
|
||||
if q_w:
|
||||
qsq = next(iter(q_w))
|
||||
# White queen on rank 7/8 is often risky early
|
||||
if chess.square_rank(qsq) >= 6:
|
||||
queen_raid_pen -= 30
|
||||
if q_b:
|
||||
qsq = next(iter(q_b))
|
||||
# Black queen on rank 1/2 is often risky early
|
||||
if chess.square_rank(qsq) <= 1:
|
||||
queen_raid_pen += 30
|
||||
|
||||
# Piece-square tendencies (small)
|
||||
pst = self._pst_score(board)
|
||||
|
||||
@ -378,7 +441,7 @@ class RandomEngine:
|
||||
hanging_pen = self._hanging_pieces_penalty(board)
|
||||
|
||||
# Aggregate white-centric score then convert to side-to-move via negamax
|
||||
white_score = material - dp_pen + mobility_term + center_score + rook_file_bonus + safety + pst - hanging_pen
|
||||
white_score = material - dp_pen + mobility_term + center_score + rook_file_bonus + safety + queen_raid_pen + pst - hanging_pen
|
||||
return white_score if board.turn == chess.WHITE else -white_score
|
||||
|
||||
def _opening_book_move(self, board: chess.Board) -> Optional[chess.Move]:
|
||||
|
||||
@ -20,28 +20,74 @@ class LichessAPI:
|
||||
"User-Agent": "minimal-lichess-bot/0.1 (+https://lichess.org)"
|
||||
})
|
||||
|
||||
def _request(self, method: str, url: str, *, raise_for_status: bool = False, **kwargs) -> requests.Response:
|
||||
"""Wrapper around session.request that logs every request/response.
|
||||
|
||||
- Logs start (method+URL) and end (status, elapsed).
|
||||
- On 4xx/5xx, logs a warning with a small snippet of the response body.
|
||||
- Optionally raises for status.
|
||||
"""
|
||||
t0 = time.monotonic()
|
||||
logging.info(f"HTTP {method} {url} -> sending")
|
||||
try:
|
||||
r = self.session.request(method, url, **kwargs)
|
||||
except Exception as e:
|
||||
logging.error(f"HTTP {method} {url} -> exception: {e}")
|
||||
raise
|
||||
elapsed = time.monotonic() - t0
|
||||
status = r.status_code
|
||||
if status >= 400:
|
||||
# Log a brief error body snippet if available
|
||||
snippet = None
|
||||
try:
|
||||
text = r.text or ""
|
||||
snippet = text[:200].replace("\n", " ")
|
||||
except Exception:
|
||||
snippet = None
|
||||
if snippet:
|
||||
logging.warning(f"HTTP {method} {url} -> {status} in {elapsed:.2f}s body='{snippet}'")
|
||||
else:
|
||||
logging.warning(f"HTTP {method} {url} -> {status} in {elapsed:.2f}s")
|
||||
else:
|
||||
logging.info(f"HTTP {method} {url} -> {status} in {elapsed:.2f}s")
|
||||
if raise_for_status:
|
||||
r.raise_for_status()
|
||||
return r
|
||||
|
||||
def stream_events(self) -> Generator[Dict, None, None]:
|
||||
url = f"{LICHESS_API}/api/stream/event"
|
||||
with self.session.get(url, stream=True, timeout=60) as r:
|
||||
r.raise_for_status()
|
||||
for line in r.iter_lines(decode_unicode=True):
|
||||
if not line:
|
||||
backoff = 0.5
|
||||
while True:
|
||||
try:
|
||||
# Use NDJSON Accept and no timeout for long-lived stream
|
||||
headers = {"Accept": "application/x-ndjson"}
|
||||
with self._request("GET", url, headers=headers, stream=True, timeout=None) as r:
|
||||
r.raise_for_status()
|
||||
backoff = 0.5 # reset on success
|
||||
for line in r.iter_lines(decode_unicode=True):
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
yield json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
logging.debug(f"Skipping non-JSON line: {line}")
|
||||
except requests.HTTPError as e:
|
||||
status = getattr(e.response, "status_code", None)
|
||||
if status == 429:
|
||||
logging.warning("Event stream hit 429; backing off")
|
||||
time.sleep(backoff)
|
||||
backoff = min(8.0, backoff * 2)
|
||||
continue
|
||||
try:
|
||||
yield json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
logging.debug(f"Skipping non-JSON line: {line}")
|
||||
raise
|
||||
|
||||
def accept_challenge(self, challenge_id: str) -> None:
|
||||
url = f"{LICHESS_API}/api/challenge/{challenge_id}/accept"
|
||||
r = self.session.post(url, timeout=30)
|
||||
r.raise_for_status()
|
||||
self._request("POST", url, timeout=30, raise_for_status=True)
|
||||
|
||||
def decline_challenge(self, challenge_id: str, reason: str = "generic") -> None:
|
||||
url = f"{LICHESS_API}/api/challenge/{challenge_id}/decline"
|
||||
data = {"reason": reason}
|
||||
r = self.session.post(url, data=data, timeout=30)
|
||||
r.raise_for_status()
|
||||
self._request("POST", url, data=data, timeout=30, raise_for_status=True)
|
||||
|
||||
def join_game_stream(self, game_id: str, my_color: Optional[str]) -> Tuple[chess.Board, str]:
|
||||
"""Deprecated: use stream_game_events and parse initial state there."""
|
||||
@ -49,7 +95,8 @@ class LichessAPI:
|
||||
url = f"{LICHESS_API}/api/board/game/stream/{game_id}"
|
||||
board = chess.Board()
|
||||
color = my_color or "white"
|
||||
with self.session.get(url, stream=True, timeout=60) as r:
|
||||
headers = {"Accept": "application/x-ndjson"}
|
||||
with self._request("GET", url, headers=headers, stream=True, timeout=None) as r:
|
||||
r.raise_for_status()
|
||||
for line in r.iter_lines(decode_unicode=True):
|
||||
if not line:
|
||||
@ -80,7 +127,8 @@ class LichessAPI:
|
||||
|
||||
def stream_game_events(self, game_id: str) -> Generator[Dict, None, None]:
|
||||
url = f"{LICHESS_API}/api/board/game/stream/{game_id}"
|
||||
with self.session.get(url, stream=True, timeout=60) as r:
|
||||
headers = {"Accept": "application/x-ndjson"}
|
||||
with self._request("GET", url, headers=headers, stream=True, timeout=None) as r:
|
||||
r.raise_for_status()
|
||||
for line in r.iter_lines(decode_unicode=True):
|
||||
if not line:
|
||||
@ -92,10 +140,15 @@ class LichessAPI:
|
||||
|
||||
def make_move(self, game_id: str, move: chess.Move) -> None:
|
||||
url = f"{LICHESS_API}/api/board/game/{game_id}/move/{move.uci()}"
|
||||
r = self.session.post(url, timeout=30)
|
||||
r = self._request("POST", url, timeout=30)
|
||||
if r.status_code in (400, 409):
|
||||
# Likely not our turn or move already played; do not retry to avoid spam
|
||||
r.raise_for_status()
|
||||
return
|
||||
if r.status_code == 429:
|
||||
logging.warning(f"HTTP POST {url} -> 429; retrying once after 0.5s")
|
||||
time.sleep(0.5)
|
||||
r = self.session.post(url, timeout=30)
|
||||
r = self._request("POST", url, timeout=30)
|
||||
r.raise_for_status()
|
||||
|
||||
def get_game_state(self, game_id: str) -> Optional[Dict]:
|
||||
@ -104,7 +157,7 @@ class LichessAPI:
|
||||
|
||||
def get_my_user_id(self) -> Optional[str]:
|
||||
url = f"{LICHESS_API}/api/account"
|
||||
r = self.session.get(url, timeout=30)
|
||||
r = self._request("GET", url, timeout=30)
|
||||
if r.status_code == 200:
|
||||
return r.json().get("id")
|
||||
return None
|
||||
|
||||
@ -59,6 +59,9 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No
|
||||
black_name: Optional[str] = None
|
||||
site_url: Optional[str] = None
|
||||
try:
|
||||
# Only send moves on authoritative gameState events to avoid race
|
||||
# conditions right after gameFull arrives.
|
||||
seen_game_full = False
|
||||
for event in api.stream_game_events(game_id):
|
||||
et = event.get("type")
|
||||
if et in ("gameFull", "gameState"):
|
||||
@ -92,6 +95,7 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No
|
||||
elif me == black_id:
|
||||
color = "black"
|
||||
logging.info(f"Game {game_id}: joined as {color} (gameFull)")
|
||||
seen_game_full = True
|
||||
else:
|
||||
moves = event.get("moves", "")
|
||||
status = event.get("status")
|
||||
@ -132,7 +136,9 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No
|
||||
logging.info(
|
||||
f"Game {game_id}: turn={'white' if is_white_turn else 'black'}, my_turn={my_turn}"
|
||||
)
|
||||
if my_turn:
|
||||
# Only move on 'gameState' events; skip making a move on initial 'gameFull'
|
||||
allow_move = (et == "gameState")
|
||||
if my_turn and allow_move:
|
||||
# Compute a per-move time budget (seconds) based on remaining time
|
||||
# Heuristic: use min( max_time_sec, max(0.05, 0.6 * my_time_left/remaining_moves + inc) )
|
||||
# Estimate remaining moves as 30 - ply/2 bounded to [10, 60]
|
||||
@ -149,11 +155,15 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No
|
||||
logging.info(f"Game {game_id}: no legal moves (game likely over)")
|
||||
break
|
||||
try:
|
||||
logging.info(f"Game {game_id}: playing {move.uci()} (budget={budget:.2f}s, my_time_left={time_left_sec:.1f}s, inc={inc_sec:.2f}s)")
|
||||
if game_log_path:
|
||||
with open(game_log_path, "a") as lf:
|
||||
lf.write(f"ply {last_handled_len+1}: {move.uci()}\n{reason}\n\n")
|
||||
api.make_move(game_id, move)
|
||||
# Double-check legality just before sending to avoid 400s when state changed.
|
||||
if move not in board.legal_moves:
|
||||
logging.info(f"Game {game_id}: selected move no longer legal; skipping send")
|
||||
else:
|
||||
logging.info(f"Game {game_id}: playing {move.uci()} (budget={budget:.2f}s, my_time_left={time_left_sec:.1f}s, inc={inc_sec:.2f}s)")
|
||||
if game_log_path:
|
||||
with open(game_log_path, "a") as lf:
|
||||
lf.write(f"ply {last_handled_len+1}: {move.uci()}\n{reason}\n\n")
|
||||
api.make_move(game_id, move)
|
||||
except Exception as e:
|
||||
logging.warning(f"Game {game_id}: move {move.uci()} failed: {e}")
|
||||
# Mark this position as handled (whether or not we moved)
|
||||
|
||||
@ -122,7 +122,7 @@ def main():
|
||||
ap.add_argument("file", help="Path to a PGN file or a log containing a PGN section")
|
||||
ap.add_argument("--engine", default="stockfish", help="Path to stockfish executable (default: stockfish)")
|
||||
# Exactly one of time or depth may be provided; default to time
|
||||
ap.add_argument("--time", type=float, default=0.2, help="Analysis time per evaluation in seconds (default: 0.2)")
|
||||
ap.add_argument("--time", type=float, default=4, help="Analysis time per evaluation in seconds (default: 0.2)")
|
||||
ap.add_argument("--depth", type=int, default=None, help="Fixed depth per evaluation (overrides --time)")
|
||||
args = ap.parse_args()
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user