Skip to content

Commit aaa3726

Browse files
added lichess_utils
added a lichess_utils file that is helpful for playing against your own player.
1 parent a901d93 commit aaa3726

1 file changed

Lines changed: 153 additions & 0 deletions

File tree

lichess_utils.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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

Comments
 (0)