greghope667 / square-off-remote

GNU General Public License v3.0
1 stars 2 forks source link

[Todo] Add experimental stream of ongoing / live games from broadcasts to Square Off GKS using Lichess API #6

Closed eqikkwkp25-cyber closed 6 months ago

eqikkwkp25-cyber commented 7 months ago

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.

eqikkwkp25-cyber commented 7 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.

eqikkwkp25-cyber commented 7 months ago

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
greghope667 commented 7 months ago

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?

eqikkwkp25-cyber commented 6 months ago

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 :-)

eqikkwkp25-cyber commented 6 months ago

See https://github.com/greghope667/square-off-remote/pull/8