Closed eqikkwkp25-cyber closed 6 months ago
Update: I have a prototype now.
The bad: Need to test it more as the GKS seems to be disconnected after long periods of inactivity.
The ugly: API is suboptimal in my opinion. There is an endpoint for the pgn of a game but one would need to poll it regularly. Using the broadcast rounds endpoint one gets a stream of all games played during a round and unfortunately there is no obvious unique key for filtering a single game anymore, i used the name of the white player for instance...
The good: Some Lichess broadcasts / tournaments like the ongoing Tata Steel Masters seem to be in sync with Youtube live streams. Nice.
Below the code and the exception which occurs on a regular basis
# Experimental stream of games from broadcasts to Square Off GKS
# using Lichess API
import os
import io
import time
from simple_term_menu import TerminalMenu
import chess
import chess.pgn
import berserk
import credentials
import player
from squareoff import SquareOff
# time in seconds between moves
sleeptime = 0
session = berserk.TokenSession(credentials.LICHESS_BOARD_ACCESS_TOKEN)
client = berserk.Client(session=session)
def main():
bc_offical_list = [b for b in client.broadcasts.get_official(nb=40)]
bc_menu_title = "Lichess Broadcasts"
bc_menu_items = ["[" + b["tour"]["id"] + "] " + " -- "
+ b["tour"]["name"] + " -- "
+ b["tour"]["description"]
for b in bc_offical_list]
bc_menu_exit = False
bc_menu = TerminalMenu(
title=bc_menu_title,
menu_entries=bc_menu_items,
)
os.system("clear")
while not bc_menu_exit:
bc_sel = bc_menu.show()
if bc_sel == None:
bc_menu_exit = True
else:
rounds_menu_title = "Broadcast Rounds"
rounds_list = [r for r in bc_offical_list[bc_sel]["rounds"]]
rounds_menu_items = ["[" + r["id"] + "]" + " -- " + r["name"] + " -- "
+ r["tour"]["description"] for r in rounds_list]
rounds_menu_back = False
rounds_menu = TerminalMenu(
title=rounds_menu_title,
menu_entries=rounds_menu_items,
)
while not rounds_menu_back:
os.system("clear")
round_sel = rounds_menu.show()
if round_sel == None:
rounds_menu_back = True
else:
pgns = client.broadcasts.get_round_pgns(
broadcast_round_id=rounds_list[round_sel]["id"])
games_list = []
for g in pgns:
pgn = io.StringIO(g)
game = chess.pgn.read_game(pgn)
games_list.append(game)
games_menu_title = "Round Games"
games_menu_items = ["[" + g.headers["Result"] + " -- "
+ g.headers["White"] + " vs "
+ g.headers["Black"] + "]" for g in games_list]
games_menu_back = False
games_menu = TerminalMenu(
title=games_menu_title,
menu_entries=games_menu_items,
)
while not games_menu_back:
os.system("clear")
game_sel = games_menu.show()
if game_sel == None:
games_menu_back = True
else:
print("Start streaming ... " + games_menu_items[game_sel])
so = SquareOff()
try:
so.start_game()
board = chess.Board()
white_player = games_list[game_sel].headers["White"]
r_id = rounds_list[round_sel]["id"]
stream = client.broadcasts.stream_round(r_id)
moves = []
for g in stream:
game = chess.pgn.read_game(io.StringIO(g))
if game.headers["White"] == white_player:
moves_new = [m for m in game.mainline_moves()][len(moves):]
moves = moves + moves_new
print(moves_new)
for move in moves_new:
board.push(move)
so.make_move(move)
time.sleep(sleeptime)
if game.headers["Result"] in ["1/2-1/2", "1-0", "0-1"]:
stream.close()
#time.sleep(4)
finally:
so.disconnect()
print("End")
rounds_menu_back = False
bc_menu_back = False
if __name__ == "__main__":
main()
Output
Searching for device...
Found device, connecting...
Connected
Connected, battery = 2-100
*
[Move.from_uci('e2e4'), Move.from_uci('c7c5'), Move.from_uci('g1f3'), Move.from_uci('d7d6'), Move.from_uci('d2d4'), Move.from_uci('c5d4'), Move.from_uci('f3d4'), Move.from_uci('g8f6'), Move.from_uci('b1c3'), Move.from_uci('b8c6'), Move.from_uci('c1g5'), Move.from_uci('e7e6'), Move.from_uci('d1d2'), Move.from_uci('a7a6'), Move.from_uci('e1c1'), Move.from_uci('c8d7'), Move.from_uci('c1b1'), Move.from_uci('f8e7'), Move.from_uci('f2f3'), Move.from_uci('c6d4'), Move.from_uci('d2d4'), Move.from_uci('b7b5'), Move.from_uci('d4d2'), Move.from_uci('d8c7'), Move.from_uci('f1d3'), Move.from_uci('b5b4'), Move.from_uci('c3e2'), Move.from_uci('a6a5'), Move.from_uci('g2g4'), Move.from_uci('a5a4'), Move.from_uci('h2h4')]
[Move.from_uci('e8g8')]
[Move.from_uci('e4e5')]
Device was disconnected
[Move.from_uci('d6e5')]
Disconnecting...
Traceback (most recent call last):
File "/home/user/devel/squareoff/square-off-remote/lichess_broadcast.py", line 96, in main
so.make_move(move)
File "/home/user/devel/squareoff/square-off-remote/squareoff.py", line 45, in make_move
self._board.transmit(move.uci()[:4])
File "/home/user/devel/squareoff/square-off-remote/communicator.py", line 80, in transmit
asyncio.run_coroutine_threadsafe(self._tx_queue.put(packet), self._loop).result()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib64/python3.11/asyncio/tasks.py", line 936, in run_coroutine_threadsafe
loop.call_soon_threadsafe(callback)
File "/usr/lib64/python3.11/asyncio/base_events.py", line 806, in call_soon_threadsafe
self._check_closed()
File "/usr/lib64/python3.11/asyncio/base_events.py", line 519, in _check_closed
raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/user/devel/squareoff/square-off-remote/lichess_broadcast.py", line 108, in <module>
main()
File "/home/user/devel/squareoff/square-off-remote/lichess_broadcast.py", line 102, in main
so.disconnect()
File "/home/user/devel/squareoff/square-off-remote/squareoff.py", line 79, in disconnect
self._board.stop()
File "/home/user/devel/squareoff/square-off-remote/communicator.py", line 69, in stop
asyncio.run_coroutine_threadsafe(self._tx_queue.put(None), self._loop).result()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib64/python3.11/asyncio/tasks.py", line 936, in run_coroutine_threadsafe
loop.call_soon_threadsafe(callback)
File "/usr/lib64/python3.11/asyncio/base_events.py", line 806, in call_soon_threadsafe
self._check_closed()
File "/usr/lib64/python3.11/asyncio/base_events.py", line 519, in _check_closed
raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
sys:1: RuntimeWarning: coroutine 'Queue.put' was never awaited
I've been trying this out. The good news is that your code works fine. It's an issue with my communicator.py
really. If we get a disconnect the event loop stops, which causes all further interaction (e.g. calling so.make_move()
or so.disconnect()
) to crash. I should make this a specific, catch-able error and make so.disconnect()
always safe to call.
The bad part is that the underlying trigger here is regular device disconnections. Don't know what's causing these - there could be some bluetooth timeout triggering somewhere? Or maybe your device is sleeping and that cuts the comms. More investigation is needed.
Ultimately we need some way of re-establishing comms/resuming games. I'll try to see if there's any commands we can use for this. I wonder how the official app handles disconnections, it must have some fallback to continue games?
Thanks for your feedback. My current guess is that its a self-caused issue due to the distance between my PC and the GKS (its just about 3 meters). I put more or less
while True:
print(datetime.now())
#print("Battery" + so.battery())
time.sleep(60)
to game.py and got disconnections, in some cases after 15 minutes or so. Using the code on a Raspberry Pi sitting next to the GKS gives me a stable Bluetooth connection and seems to work pretty well with Lichess live broadcasts.
PR added.
Next steps: Get things done on a Raspberry Pi + TFT display and a simple terminal menu. I am working on a new hardware setup :-)
Finished games from broadcasts to Square Off GKS using Lichess API are already possible, ongoing / live games would be next. Need to have a deeper look into Lichess API.