|
| 1 | +import os |
| 2 | +import requests |
| 3 | +import json |
| 4 | +import time |
| 5 | +import random |
| 6 | +import chess |
| 7 | +#import custom_move_picker # this is your engine |
| 8 | + |
| 9 | +TOKEN = os.environ["lichess_token"] |
| 10 | +H = { |
| 11 | + "Authorization": f"Bearer {TOKEN}", |
| 12 | + "Accept": "application/x-ndjson", |
| 13 | +} |
| 14 | + |
| 15 | +# Smoke Test for |
| 16 | +def smoke_test_token(): |
| 17 | + global TOKEN |
| 18 | + H = {"Authorization": f"Bearer {TOKEN}"} |
| 19 | + me = requests.get("https://lichess.org/api/account", headers=H).json() |
| 20 | + print("Logged in as:", me["username"]) |
| 21 | + |
| 22 | + |
| 23 | +def ndjson_lines(resp): |
| 24 | + for line in resp.iter_lines(decode_unicode=True): |
| 25 | + if line: |
| 26 | + yield json.loads(line) |
| 27 | + |
| 28 | + |
| 29 | +def accept_challenge(ch_id): |
| 30 | + r = requests.post(f"https://lichess.org/api/challenge/{ch_id}/accept", headers=H) |
| 31 | + print("Accept challenge", ch_id, r.status_code, r.text[:200]) |
| 32 | + |
| 33 | + |
| 34 | +def decline_challenge(ch_id, reason="generic"): |
| 35 | + r = requests.post(f"https://lichess.org/api/challenge/{ch_id}/decline", |
| 36 | + headers=H, data={"reason": reason}) |
| 37 | + print("Decline challenge", ch_id, r.status_code, r.text[:200]) |
| 38 | + |
| 39 | + |
| 40 | +def play_move(game_id, uci): |
| 41 | + r = requests.post(f"https://lichess.org/api/bot/game/{game_id}/move/{uci}", headers=H) |
| 42 | + print("Move", game_id, uci, r.status_code, r.text[:200]) |
| 43 | + |
| 44 | + |
| 45 | +def resign(game_id): |
| 46 | + requests.post(f"https://lichess.org/api/bot/game/{game_id}/resign", headers=H) |
| 47 | + |
| 48 | + |
| 49 | +def challenge_user(username: str, |
| 50 | + rated: bool = False, |
| 51 | + clock_limit: int = 180, |
| 52 | + clock_increment: int = 0, |
| 53 | + color: str = "random", |
| 54 | + variant: str = "standard"): |
| 55 | + url = f"https://lichess.org/api/challenge/{username}" |
| 56 | + data = { |
| 57 | + "rated": str(rated).lower(), |
| 58 | + "clock.limit": str(clock_limit), |
| 59 | + "clock.increment": str(clock_increment), |
| 60 | + "color": color, |
| 61 | + "variant": variant, |
| 62 | + } |
| 63 | + r = requests.post(url, headers=H, data=data, timeout=30) |
| 64 | + r.raise_for_status() |
| 65 | + return r.json() |
| 66 | + |
| 67 | + |
| 68 | +def stream_game(game_id): |
| 69 | + """Stream a single game, maintain a board, and play random moves on our turns.""" |
| 70 | + url = f"https://lichess.org/api/bot/game/stream/{game_id}" |
| 71 | + with requests.get(url, headers=H, stream=True, timeout=90) as resp: |
| 72 | + resp.raise_for_status() |
| 73 | + board = chess.Board() |
| 74 | + my_color = None # True for white, False for black |
| 75 | + for msg in ndjson_lines(resp): |
| 76 | + t = msg.get("type") |
| 77 | + print("MSG TYPE:", t, "| RAW:", |
| 78 | + msg if t != "gameState" else {"type": t, "ply": len(msg.get("moves", "").split())}) |
| 79 | + |
| 80 | + if t == "gameFull": |
| 81 | + # Determine our color |
| 82 | + white_id = msg["white"]["id"] |
| 83 | + black_id = msg["black"]["id"] |
| 84 | + me = requests.get("https://lichess.org/api/account", headers={"Authorization": f"Bearer {TOKEN}"}).json()["id"] |
| 85 | + my_color = (white_id == me) |
| 86 | + # Apply existing moves |
| 87 | + for m in msg.get("state", {}).get("moves", "").split(): |
| 88 | + board.push(chess.Move.from_uci(m)) |
| 89 | + our_turn = (board.turn is True and my_color) or (board.turn is False and my_color is False) |
| 90 | + if our_turn and not board.is_game_over(): |
| 91 | + move = custom_move_picker.pick_move(board) |
| 92 | + play_move(game_id, move.uci()) |
| 93 | + elif t == "gameState": |
| 94 | + # Sync new moves |
| 95 | + moves = msg.get("moves", "").split() |
| 96 | + # Rebuild board from scratch to be safe (small & robust) |
| 97 | + board = chess.Board() |
| 98 | + for m in moves: |
| 99 | + board.push(chess.Move.from_uci(m)) |
| 100 | + |
| 101 | + # Is it our turn? |
| 102 | + our_turn = (board.turn is True and my_color) or (board.turn is False and my_color is False) |
| 103 | + if our_turn and not board.is_game_over(): |
| 104 | + legal = list(board.legal_moves) |
| 105 | + if not legal: |
| 106 | + continue |
| 107 | + |
| 108 | + move = random.choice(legal) # random choice |
| 109 | + # move = custom_move_picker.pick_move(board) |
| 110 | + play_move(game_id, move.uci()) |
| 111 | + elif t == "chatLine": |
| 112 | + # Optional: react to chat |
| 113 | + pass |
| 114 | + elif t == "opponentGone": |
| 115 | + # Opponent disconnected or flagged — do nothing |
| 116 | + pass |
| 117 | + |
| 118 | +def main(): |
| 119 | + # Stream account-level events forever |
| 120 | + url = "https://lichess.org/api/stream/event" |
| 121 | + while True: |
| 122 | + try: |
| 123 | + with requests.get(url, headers=H, stream=True, timeout=90) as resp: |
| 124 | + resp.raise_for_status() |
| 125 | + for event in ndjson_lines(resp): |
| 126 | + etype = event.get("type") |
| 127 | + if etype == "challenge": |
| 128 | + ch = event["challenge"] |
| 129 | + variant = ch["variant"]["key"] |
| 130 | + # Example policy: only accept standard/bullet/blitz/rapid; decline correspondence/variants |
| 131 | + acceptable = variant in {"standard"} and ch["speed"] in {"bullet", "blitz", "rapid"} |
| 132 | + if acceptable: |
| 133 | + accept_challenge(ch["id"]) |
| 134 | + else: |
| 135 | + decline_challenge(ch["id"], reason="variant") |
| 136 | + elif etype == "gameStart": |
| 137 | + gid = event["game"]["id"] |
| 138 | + print("Game start:", gid) |
| 139 | + stream_game(gid) |
| 140 | + elif etype == "gameFinish": |
| 141 | + print("Game finished:", event["game"]["id"]) |
| 142 | + except requests.exceptions.RequestException as e: |
| 143 | + print("Stream error, retrying in 3s:", e) |
| 144 | + time.sleep(3) |
| 145 | + |
| 146 | + |
| 147 | +if __name__ == "__main__": |
| 148 | + # Quick capability check: bot mode must be enabled on the account you’re using |
| 149 | + me = requests.get("https://lichess.org/api/account", headers={"Authorization": f"Bearer {TOKEN}"}).json() |
| 150 | + if not me.get("title") == "BOT": |
| 151 | + print("⚠Your account isn’t in BOT mode. Enable it at: https://lichess.org/account/oauth/bot") |
| 152 | + #challenge_user("OtherBotName", rated=False, clock_limit=180, clock_increment=2) |
| 153 | + main() |
0 commit comments