Usage

Use berserk by creating an API client:

>>> import berserk
>>> client = berserk.Client()

Authenticating

By default the client does not perform any authentication. However many of the endpoints are not open. To use a form of authentication, just pass the appropriate requests.Session-like object:

  • using an API token: berserk.TokenSession

  • using oauth: requests_oauthlib.Oauth2Session

Note

Some endpoints require specific Oauth2 permissions.

Using an API token

If you have a personal API token, you can simply use the TokenSession provided. For example, assuming you have written your token to './lichess.token':

>>> with open('./lichess.token') as f:
...     token = f.read()
...
>>> session = berserk.TokenSession(token)
>>> client = berserk.Client(session)

Using Oauth2

Some of the endpoints require OAuth2 authentication. Although outside the scope of this documentation, you can use requests_oauthlib.Oauth2Session for this.

>>> from requests_oauthlib import OAuth2Session
>>> session = OAuth2Session(...)
>>> client = berserk.Client(session)

Accounts

Information and Preferences

>>> client.account.get()
{'blocking': False,
 'count': {...},
 'createdAt': datetime.datetime(2018, 5, 16, 8, 9, 18, 187000),
 'followable': True,
 'following': False,
 'followsYou': False,
 'id': 'rhgrant10',
 'nbFollowers': 1,
 'nbFollowing': 1,
 'online': True,
 'perfs': {...},
 'playTime': {...},
 'seenAt': datetime.datetime(2018, 12, 9, 10, 28, 30, 221000),
 'url': 'https://lichess.org/@/rhgrant10',
 'username': 'rhgrant10'}

>>> client.account.get_email()
'rhgrant10@gmail.com'

>>> client.account.get_preferences()
{'animation': 2,
 'autoQueen': 1,
 ...
 'transp': False,
 'zen': 0}}

Kid Mode

Using Oauth2, you can set the kid mode.

>>> client.account.set_kid_mode(True)  # enable
True
>>> client.account.set_kid_mode(False)  # disable
True

Note that the set_kid_mode method returns an indicator of success and not the current or previous status.

>>> def show_kid_mode():
...     is_enabled = client.account.get_kid_mode()
...     print('enabled' if is_enabled else 'disabled')
...
>>> show_kid_mode()
disabled

>>> # try to enable, but the request fails
>>> client.account.set_kid_mode(True)
False
>>> show_kid_mode()
disabled

>>> # try again, this time it succeeds
>>> client.account.set_kid_mode(True)
True
>>> show_kid_mode()
enabled

Bot Account Upgrade

If this is a new account that has not yet played a game, and if you have the required OAuth2 permission, you can upgrade the account to a bot account:

>>> client.account.upgrade_to_bot()

Read more below about how to use bot functionality.

Users and Teams

Realtime Statuses

Get realtime information about one or more players:

>>> players = ['Sasageyo', 'Voinikonis_Nikita', 'Zugzwangerz', 'DOES-NOT-EXIST']
>>> client.users.get_realtime_statuses(*players)
[{'id': 'sasageyo',
'name': 'Sasageyo',
'title': 'IM',
'online': True,
'playing': True},
{'id': 'voinikonis_nikita',
'name': 'Voinikonis_Nikita',
'title': 'FM',
'online': True,
'playing': True},
{'id': 'zugzwangerz', 'name': 'Zugzwangerz'}]

Top 10 Lists

>>> top10 = client.users.get_all_top_10()
>>> list(top10)
['bullet',
 'blitz',
 'rapid',
 'classical',
 'ultraBullet',
 'crazyhouse',
 'chess960',
 'kingOfTheHill',
 'threeCheck',
 'antichess',
 'atomic',
 'horde',
 'racingKings']
>>> top10['horde'][0]
{'id': 'ingrid-vengeance',
 'perfs': {'horde': {'progress': 22, 'rating': 2443}},
 'username': 'Ingrid-Vengeance'}

Leaderboards

>>> client.users.get_leaderboard('horde', count=11)[-1]
{'id': 'philippesaner',
 'perfs': {'horde': {'progress': 10, 'rating': 2230}},
 'username': 'PhilippeSaner'}

Public Data

>>> client.users.get_public_data('PhilippeSaner')
{'completionRate': 87,
 'count': {...},
 'createdAt': datetime.datetime(2017, 1, 9, 16, 14, 31, 140000),
 'id': 'philippesaner',
 'nbFollowers': 40,
 'nbFollowing': 13,
 'online': False,
 'perfs': {...},
 'playTime': {'total': 1505020, 'tv': 1038007},
 'profile': {'country': 'CA', 'location': 'Ottawa'},
 'seenAt': datetime.datetime(2018, 12, 9, 10, 26, 28, 22000),
 'url': 'https://lichess.org/@/PhilippeSaner',
 'username': 'PhilippeSaner'}

Activity Feeds

>>> feed = client.users.get_activity_feed('PhilippeSaner')
>>> feed[0]
{'games': {'horde': {'draw': 0,
   'loss': 1,
   'rp': {'after': 2230, 'before': 2198},
   'win': 12}},
 'interval': {'end': datetime.datetime(2018, 12, 9, 16, 0),
  'start': datetime.datetime(2018, 12, 8, 16, 0)},
 'tournaments': {'best': [{'nbGames': 1,
    'rank': 6,
    'rankPercent': 33,
    'score': 2,
    'tournament': {'id': '9zm2uIdP', 'name': 'Daily Horde Arena'}}],
  'nb': 1}}

Team Members

>>> client.users.get_by_team('coders')
<map at 0x107c1acc0>
>>> members = list(_)
>>> len(members)
228

Live Streamers

>>> client.users.get_live_streamers()
[{'id': 'chesspatzerwal', 'name': 'ChesspatzerWAL', 'patron': True},
 {'id': 'ayrtontwigg', 'name': 'AyrtonTwigg', 'playing': True},
 {'id': 'fanatikchess', 'name': 'FanatikChess', 'patron': True},
 {'id': 'jwizzy74', 'name': 'Jwizzy74', 'patron': True, 'playing': True},
 {'id': 'devjamesb', 'name': 'DevJamesB', 'playing': True},
 {'id': 'kafka4x', 'name': 'Kafka4x', 'playing': True},
 {'id': 'sparklehorse', 'name': 'Sparklehorse', 'patron': True, 'title': 'IM'},
 {'id': 'ivarcode', 'name': 'ivarcode', 'playing': True},
 {'id': 'pepellou', 'name': 'pepellou', 'patron': True, 'playing': True},
 {'id': 'videogamepianist', 'name': 'VideoGamePianist', 'playing': True}]

Exporting Games

By Player

Finished games can be exported and current games can be listed. Let’s take a look at the most recent 300 games played by “LeelaChess” on Dec. 8th, 2018:

>>> start = berserk.utils.to_millis(datetime(2018, 12, 8))
>>> end = berserk.utils.to_millis(datetime(2018, 12, 9))
>>> client.games.export_by_player('LeelaChess', since=start, until=end,
                                  max=300))
<generator object Games.export_by_player at 0x10c24b048>
>>> games = list(_)
>>> games[0]['createdAt']
datetime.datetime(2018, 12, 9, 22, 54, 24, 195000, tzinfo=datetime.timezone.utc)
>>> games[-1]['createdAt']
datetime.datetime(2018, 12, 8, 9, 11, 42, 229000, tzinfo=datetime.timezone.utc)

Wow, they play a lot of chess :)

By ID

You can export games too using their IDs. Let’s export the last game LeelaChess played that day:

>>> game_id = games[0]['id']
>>> client.games.export(game_id)
{'analysis': [...],
 'clock': {'increment': 8, 'initial': 300, 'totalTime': 620},
 'createdAt': datetime.datetime(2018, 12, 9, 22, 54, 24, 195000, tzinfo=datetime.timezone.utc),
 'id': 'WatQhhbJ',
 'lastMoveAt': datetime.datetime(2018, 12, 9, 23, 5, 59, 396000, tzinfo=datetime.timezone.utc),
 'moves': ...
 'opening': {'eco': 'D38',
  'name': "Queen's Gambit Declined: Ragozin Defense",
  'ply': 8},
 'perf': 'rapid',
 'players': {'black': {'analysis': {'acpl': 44,
    'blunder': 1,
    'inaccuracy': 4,
    'mistake': 2},
   'rating': 1333,
   'ratingDiff': 0,
   'user': {'id': 'fsoto', 'name': 'fsoto'}},
  'white': {'analysis': {'acpl': 11,
    'blunder': 0,
    'inaccuracy': 2,
    'mistake': 0},
   'provisional': True,
   'rating': 2490,
   'ratingDiff': 0,
   'user': {'id': 'leelachess', 'name': 'LeelaChess', 'title': 'BOT'}}},
 'rated': True,
 'speed': 'rapid',
 'status': 'mate',
 'variant': 'standard',
 'winner': 'white'}

PGN vs JSON

Of course sometimes PGN format is desirable. Just pass as_pgn=True to any of the export methods:

>>> pgn = client.games.export(game_id, as_pgn=True)
>>> print(pgn)
[Event "Rated Rapid game"]
[Site "https://lichess.org/WatQhhbJ"]
[Date "2018.12.09"]
[Round "-"]
[White "LeelaChess"]
[Black "fsoto"]
[Result "1-0"]
[UTCDate "2018.12.09"]
[UTCTime "22:54:24"]
[WhiteElo "2490"]
[BlackElo "1333"]
[WhiteRatingDiff "+0"]
[BlackRatingDiff "+0"]
[WhiteTitle "BOT"]
[Variant "Standard"]
[TimeControl "300+8"]
[ECO "D38"]
[Opening "Queen's Gambit Declined: Ragozin Defense"]
[Termination "Normal"]

1. d4 { [%eval 0.08] [%clk 0:05:00] } 1... d5 ...

TV Channels

>>> channels = client.games.get_tv_channels()
>>> list(channels)
['Bot',
 'Blitz',
 'Racing Kings',
 'UltraBullet',
 'Bullet',
 'Classical',
 'Three-check',
 'Antichess',
 'Computer',
 'Horde',
 'Rapid',
 'Atomic',
 'Crazyhouse',
 'Chess960',
 'King of the Hill',
 'Top Rated']
>>> channels['King of the Hill']
{'gameId': 'YPL6tP2K',
 'rating': 1554,
 'user': {'id': 'linischoki', 'name': 'linischoki'}}

Working with tournaments

You have to specify the clock time, increment, and minutes, but creating a new tournament is easy:

>>> client.tournaments.create_arena(clock_time=10, clock_increment=3, minutes=180)
{'berserkable': True,
 'clock': {'increment': 3, 'limit': 600},
 'createdBy': 'rhgrant10',
 'duels': [],
 'fullName': "O'Kelly Arena",
 'greatPlayer': {'name': "O'Kelly",
  'url': "https://wikipedia.org/wiki/Alb%C3%A9ric_O'Kelly_de_Galway"},
 'id': '3uwyXjiC',
 'minutes': 180,
 'nbPlayers': 0,
 'perf': {'icon': '#', 'name': 'Rapid'},
 'quote': {'author': 'Bent Larsen',
  'text': 'I often play a move I know how to refute.'},
 'secondsToStart': 300,
 'standing': {'page': 1, 'players': []},
 'startsAt': '2018-12-10T00:32:12.116Z',
 'system': 'arena',
 'variant': 'standard',
 'verdicts': {'accepted': True, 'list': []}}

You can also create Swiss tournaments easily, specifying the team id, clock time, clock increment, and number of rounds.

>>> client.tournaments.create_swiss(teamid_="coders", clock_limit=10,
                                    clock_increment=0, nbRounds=5)
{'rated': true,
 'clock': {'increment': 0, 'limit': 600},
 'createdBy': "zccze",
 'greatPlayer': {'name': "Wang',
   'url':'https://wikipedia.org/wiki/Wang_Hao_(chess_player)' },
  'id': '3uwyXjiC'
  'name': 'Wang',
  'nbOngoing': 0,
  'nbPlayers': 0,
  'nbRounds': 5,
  'nextRound': { 'at': '2021-05-18T12:23:18.233-06:00', 'in': 600},
  'quote': {'author': 'Bent Larsen',
    'text': 'I often play a move I know how to refute.'},
  'round': 0,
  'startsAt': '2021-05-18T12:23:18.233-06:00',
  'status': 'created',
  'variant': 'standard'
  }

Additionally you can see tournaments that have recently finished, are in progress, and are about to start:

>>> tournaments = client.tournaments.get()
>>> list(tournaments)
['created', 'started', 'finished']
>>> len(tournaments['created'])
19
>>> tournaments['created'][0]
{'clock': {'increment': 0, 'limit': 300},
 'createdBy': 'bashkimneziri',
 'finishesAt': datetime.datetime(2018, 12, 24, 0, 21, 2, 179000, tzinfo=datetime.timezone.utc),
 'fullName': 'GM Arena',
 'id': 'COnVgmKH',
 'minutes': 45,
 'nbPlayers': 1,
 'perf': {'icon': ')', 'key': 'blitz', 'name': 'Blitz', 'position': 1},
 'rated': True,
 'secondsToStart': 160,
 'startsAt': datetime.datetime(2018, 12, 23, 23, 36, 2, 179000, tzinfo=datetime.timezone.utc),
 'status': 10,
 'system': 'arena',
 'variant': {'key': 'standard', 'name': 'Standard', 'short': 'Std'},
 'winner': None}

Being a bot

Warning

These commands only work using bot accounts. Make sure you have converted the account with which you authenticate into a bot account first. See above for details.

Bots stream game information and react by calling various endpoints. There are two streams of information:

  1. incoming events

  2. state of a particular game

In general, a bot will listen to the stream of incoming events, determine which challenges to accept, and once accepted, listen to the stream of game states and respond with the best moves in an attempt to win as many games as possible. You can create a bot that looses intentionally if that makes you happy, but regardless you will need to listen to both streams of information.

The typical pattern is to have one main thread that listens to the event stream and spawns new threads when accepting challenges. Each challenge thread then listens to the stream of state for that particular game and plays it to completion.

Responding to challenges

Here the goal is to respond to challenges and spawn workers to play those accepted. Here’s a bit of sample code that hits the highlights:

>>> is_polite = True
>>> for event in client.bots.stream_incoming_events():
...     if event['type'] == 'challenge':
...         if should_accept(event):
...             client.bots.accept_challenge(event['id'])
...         elif is_polite:
...             client.bots.decline_challenge(event['id'])
...     elif event['type'] == 'gameStart':
...         game = Game(event['id'])
...         game.start()
...

Playing a game

Having accepted a challenge and recieved the gameStart event for it, the main job here is to listen and react to the stream of the game state:

>>> class Game(threading.Thread):
...     def __init__(self, client, game_id, **kwargs):
...         super().__init__(**kwargs)
...         self.game_id = game_id
...         self.client = client
...         self.stream = client.bots.stream_game_state(game_id)
...         self.current_state = next(self.stream)
...
...     def run(self):
...         for event in self.stream:
...             if event['type'] == 'gameState':
...                 self.handle_state_change(event)
...             elif event['type'] == 'chatLine':
...                 self.handle_chat_line(event)
...
...     def handle_state_change(self, game_state):
...         pass
...
...     def handle_chat_line(self, chat_line):
...         pass
...

Obviously the code above is just to communicate the gist of what is required. But once you have your framework for reacting to changes in game state, there are a variety of actions you can take:

>>> client.bots.make_move(game_id, 'e2e4')
True
>>> client.bots.abort_game(game_id)
True
>>> client.bots.resign_game(game_id)
True
>>> client.bots.post_message(game_id, 'Prepare to loose')
True