mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 20:54:00 +01:00
feat: log failures in connecting to lichess
This commit is contained in:
parent
08dfd062b7
commit
8716b7213c
@ -1 +1 @@
|
|||||||
4
|
10
|
||||||
@ -43,6 +43,20 @@ class RandomEngine:
|
|||||||
("e2e4", "c7c5"): ["g1f3", "d2d4", "c2c3", "b1c3"],
|
("e2e4", "c7c5"): ["g1f3", "d2d4", "c2c3", "b1c3"],
|
||||||
("d2d4", "d7d5"): ["c2c4", "g1f3", "e2e3"],
|
("d2d4", "d7d5"): ["c2c4", "g1f3", "e2e3"],
|
||||||
("d2d4", "g8f6"): ["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]:
|
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)
|
early = self._is_early_game(board)
|
||||||
piece = board.piece_at(m.from_square)
|
piece = board.piece_at(m.from_square)
|
||||||
if piece:
|
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):
|
if board.is_castling(m):
|
||||||
s += 650
|
s += 650
|
||||||
if piece.piece_type in (chess.KNIGHT, chess.BISHOP):
|
if piece.piece_type in (chess.KNIGHT, chess.BISHOP):
|
||||||
@ -304,11 +342,20 @@ class RandomEngine:
|
|||||||
from_file = chess.square_file(m.from_square)
|
from_file = chess.square_file(m.from_square)
|
||||||
from_rank = chess.square_rank(m.from_square)
|
from_rank = chess.square_rank(m.from_square)
|
||||||
to_rank = chess.square_rank(m.to_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 from_file == 5:
|
||||||
if piece.color == chess.WHITE and from_rank == 1 and to_rank == 2:
|
if piece.color == chess.WHITE and from_rank == 1 and to_rank == 2:
|
||||||
s -= 140
|
s -= 140
|
||||||
if piece.color == chess.BLACK and from_rank == 6 and to_rank == 5:
|
if piece.color == chess.BLACK and from_rank == 6 and to_rank == 5:
|
||||||
s -= 140
|
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):
|
if chess.square_file(m.to_square) in (3, 4):
|
||||||
s += 50
|
s += 50
|
||||||
return s
|
return s
|
||||||
@ -371,6 +418,22 @@ class RandomEngine:
|
|||||||
if bk_sq not in (chess.E8, chess.G8, chess.C8):
|
if bk_sq not in (chess.E8, chess.G8, chess.C8):
|
||||||
safety += 40
|
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)
|
# Piece-square tendencies (small)
|
||||||
pst = self._pst_score(board)
|
pst = self._pst_score(board)
|
||||||
|
|
||||||
@ -378,7 +441,7 @@ class RandomEngine:
|
|||||||
hanging_pen = self._hanging_pieces_penalty(board)
|
hanging_pen = self._hanging_pieces_penalty(board)
|
||||||
|
|
||||||
# Aggregate white-centric score then convert to side-to-move via negamax
|
# 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
|
return white_score if board.turn == chess.WHITE else -white_score
|
||||||
|
|
||||||
def _opening_book_move(self, board: chess.Board) -> Optional[chess.Move]:
|
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)"
|
"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]:
|
def stream_events(self) -> Generator[Dict, None, None]:
|
||||||
url = f"{LICHESS_API}/api/stream/event"
|
url = f"{LICHESS_API}/api/stream/event"
|
||||||
with self.session.get(url, stream=True, timeout=60) as r:
|
backoff = 0.5
|
||||||
r.raise_for_status()
|
while True:
|
||||||
for line in r.iter_lines(decode_unicode=True):
|
try:
|
||||||
if not line:
|
# 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
|
continue
|
||||||
try:
|
raise
|
||||||
yield json.loads(line)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
logging.debug(f"Skipping non-JSON line: {line}")
|
|
||||||
|
|
||||||
def accept_challenge(self, challenge_id: str) -> None:
|
def accept_challenge(self, challenge_id: str) -> None:
|
||||||
url = f"{LICHESS_API}/api/challenge/{challenge_id}/accept"
|
url = f"{LICHESS_API}/api/challenge/{challenge_id}/accept"
|
||||||
r = self.session.post(url, timeout=30)
|
self._request("POST", url, timeout=30, raise_for_status=True)
|
||||||
r.raise_for_status()
|
|
||||||
|
|
||||||
def decline_challenge(self, challenge_id: str, reason: str = "generic") -> None:
|
def decline_challenge(self, challenge_id: str, reason: str = "generic") -> None:
|
||||||
url = f"{LICHESS_API}/api/challenge/{challenge_id}/decline"
|
url = f"{LICHESS_API}/api/challenge/{challenge_id}/decline"
|
||||||
data = {"reason": reason}
|
data = {"reason": reason}
|
||||||
r = self.session.post(url, data=data, timeout=30)
|
self._request("POST", url, data=data, timeout=30, raise_for_status=True)
|
||||||
r.raise_for_status()
|
|
||||||
|
|
||||||
def join_game_stream(self, game_id: str, my_color: Optional[str]) -> Tuple[chess.Board, str]:
|
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."""
|
"""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}"
|
url = f"{LICHESS_API}/api/board/game/stream/{game_id}"
|
||||||
board = chess.Board()
|
board = chess.Board()
|
||||||
color = my_color or "white"
|
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()
|
r.raise_for_status()
|
||||||
for line in r.iter_lines(decode_unicode=True):
|
for line in r.iter_lines(decode_unicode=True):
|
||||||
if not line:
|
if not line:
|
||||||
@ -80,7 +127,8 @@ class LichessAPI:
|
|||||||
|
|
||||||
def stream_game_events(self, game_id: str) -> Generator[Dict, None, None]:
|
def stream_game_events(self, game_id: str) -> Generator[Dict, None, None]:
|
||||||
url = f"{LICHESS_API}/api/board/game/stream/{game_id}"
|
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()
|
r.raise_for_status()
|
||||||
for line in r.iter_lines(decode_unicode=True):
|
for line in r.iter_lines(decode_unicode=True):
|
||||||
if not line:
|
if not line:
|
||||||
@ -92,10 +140,15 @@ class LichessAPI:
|
|||||||
|
|
||||||
def make_move(self, game_id: str, move: chess.Move) -> None:
|
def make_move(self, game_id: str, move: chess.Move) -> None:
|
||||||
url = f"{LICHESS_API}/api/board/game/{game_id}/move/{move.uci()}"
|
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:
|
if r.status_code == 429:
|
||||||
|
logging.warning(f"HTTP POST {url} -> 429; retrying once after 0.5s")
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
r = self.session.post(url, timeout=30)
|
r = self._request("POST", url, timeout=30)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
def get_game_state(self, game_id: str) -> Optional[Dict]:
|
def get_game_state(self, game_id: str) -> Optional[Dict]:
|
||||||
@ -104,7 +157,7 @@ class LichessAPI:
|
|||||||
|
|
||||||
def get_my_user_id(self) -> Optional[str]:
|
def get_my_user_id(self) -> Optional[str]:
|
||||||
url = f"{LICHESS_API}/api/account"
|
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:
|
if r.status_code == 200:
|
||||||
return r.json().get("id")
|
return r.json().get("id")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -59,6 +59,9 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No
|
|||||||
black_name: Optional[str] = None
|
black_name: Optional[str] = None
|
||||||
site_url: Optional[str] = None
|
site_url: Optional[str] = None
|
||||||
try:
|
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):
|
for event in api.stream_game_events(game_id):
|
||||||
et = event.get("type")
|
et = event.get("type")
|
||||||
if et in ("gameFull", "gameState"):
|
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:
|
elif me == black_id:
|
||||||
color = "black"
|
color = "black"
|
||||||
logging.info(f"Game {game_id}: joined as {color} (gameFull)")
|
logging.info(f"Game {game_id}: joined as {color} (gameFull)")
|
||||||
|
seen_game_full = True
|
||||||
else:
|
else:
|
||||||
moves = event.get("moves", "")
|
moves = event.get("moves", "")
|
||||||
status = event.get("status")
|
status = event.get("status")
|
||||||
@ -132,7 +136,9 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No
|
|||||||
logging.info(
|
logging.info(
|
||||||
f"Game {game_id}: turn={'white' if is_white_turn else 'black'}, my_turn={my_turn}"
|
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
|
# 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) )
|
# 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]
|
||||||
@ -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)")
|
logging.info(f"Game {game_id}: no legal moves (game likely over)")
|
||||||
break
|
break
|
||||||
try:
|
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)")
|
# Double-check legality just before sending to avoid 400s when state changed.
|
||||||
if game_log_path:
|
if move not in board.legal_moves:
|
||||||
with open(game_log_path, "a") as lf:
|
logging.info(f"Game {game_id}: selected move no longer legal; skipping send")
|
||||||
lf.write(f"ply {last_handled_len+1}: {move.uci()}\n{reason}\n\n")
|
else:
|
||||||
api.make_move(game_id, move)
|
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:
|
except Exception as e:
|
||||||
logging.warning(f"Game {game_id}: move {move.uci()} failed: {e}")
|
logging.warning(f"Game {game_id}: move {move.uci()} failed: {e}")
|
||||||
# Mark this position as handled (whether or not we moved)
|
# 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("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)")
|
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
|
# 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)")
|
ap.add_argument("--depth", type=int, default=None, help="Fixed depth per evaluation (overrides --time)")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user