# Bot API Play on Lichess as a bot with engine assistance. Only works with Bot accounts. **Namespace:** `LichessSharp.Api.Contracts` **Access:** `client.Bot` **Required Scope:** `bot:play` > **Warning:** Bot accounts are permanently different from regular accounts. Once upgraded, you cannot play rated games manually. --- ## Quick Start: Minimal Working Bot ```csharp using LichessSharp; using LichessSharp.Api.Contracts; var token = Environment.GetEnvironmentVariable("LICHESS_BOT_TOKEN") ?? throw new InvalidOperationException("Set LICHESS_BOT_TOKEN"); using var client = new LichessClient(token); using var cts = new CancellationTokenSource(); Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; // Main event loop - listen for challenges and game starts await foreach (var evt in client.Bot.StreamEventsAsync(cts.Token)) { switch (evt.Type) { case "challenge": // Accept all challenges await client.Challenges.AcceptAsync(evt.Challenge!.Id); break; case "gameStart": // Start playing the game in a background task _ = PlayGameAsync(client, evt.Game!.GameId!, cts.Token); break; } } async Task PlayGameAsync(ILichessClient client, string gameId, CancellationToken ct) { string? myColor = null; await foreach (var evt in client.Bot.StreamGameAsync(gameId, ct)) { switch (evt) { case BotGameFullEvent full: // Determine our color var me = await client.Account.GetProfileAsync(ct); myColor = full.White?.Id == me.Id.ToLower() ? "white" : "black"; // Make first move if we're white if (myColor == "white") await client.Bot.MakeMoveAsync(gameId, "e2e4", ct); break; case BotGameStateEvent state: if (state.Status != "started") break; // Count moves to determine whose turn var moveCount = string.IsNullOrEmpty(state.Moves) ? 0 : state.Moves.Split(' ').Length; var isWhiteTurn = moveCount % 2 == 0; var isMyTurn = (myColor == "white") == isWhiteTurn; if (isMyTurn) { // Your engine logic here - this just plays a random legal move var move = GetBestMove(state.Moves); await client.Bot.MakeMoveAsync(gameId, move, ct); } break; case BotOpponentGoneEvent gone when gone.ClaimWinInSeconds > 0: await Task.Delay(TimeSpan.FromSeconds(gone.ClaimWinInSeconds.Value + 1), ct); await client.Bot.ClaimVictoryAsync(gameId, ct); break; } } } ``` --- ## Streaming Events ### StreamEventsAsync Stream incoming events for your bot account (game starts, challenges, etc.). ```csharp IAsyncEnumerable StreamEventsAsync( CancellationToken cancellationToken = default) ``` **Returns:** Stream of `BotAccountEvent` with these event types: | Type | Description | Properties Available | |------|-------------|---------------------| | `gameStart` | A game has started | `Game` - game info including `GameId` | | `gameFinish` | A game has finished | `Game` - game info including winner | | `challenge` | Incoming challenge | `Challenge` - challenger info, time control | | `challengeCanceled` | Challenge was canceled | `Challenge` | | `challengeDeclined` | Challenge was declined | `Challenge` | **Example:** ```csharp using var client = new LichessClient(token); using var cts = new CancellationTokenSource(); await foreach (var evt in client.Bot.StreamEventsAsync(cts.Token)) { Console.WriteLine($"Event: {evt.Type}"); switch (evt.Type) { case "gameStart": Console.WriteLine($"Game started: {evt.Game?.GameId}"); Console.WriteLine($"Opponent: {evt.Game?.Opponent?.Username}"); Console.WriteLine($"My turn: {evt.Game?.IsMyTurn}"); break; case "challenge": Console.WriteLine($"Challenge from: {evt.Challenge?.Challenger?.Name}"); Console.WriteLine($"Time control: {evt.Challenge?.TimeControl?.Show}"); // Accept or decline... break; case "gameFinish": Console.WriteLine($"Game ended: {evt.Game?.Status?.Name}"); break; } } ``` --- ### StreamGameAsync Stream the state of a game being played. **This is the core method for playing games.** ```csharp IAsyncEnumerable StreamGameAsync( string gameId, CancellationToken cancellationToken = default) ``` **Parameters:** | Name | Type | Description | |------|------|-------------| | gameId | string | The game ID (8 characters) | | cancellationToken | CancellationToken | Cancellation token | **Returns:** Stream of polymorphic `BotGameEvent` objects. Use pattern matching to handle each type: | Type | Class | When | Key Properties | |------|-------|------|----------------| | `gameFull` | `BotGameFullEvent` | First event - full game state | `White`, `Black`, `InitialFen`, `State`, `Clock` | | `gameState` | `BotGameStateEvent` | After each move | `Moves`, `WhiteTime`, `BlackTime`, `Status` | | `chatLine` | `BotChatLineEvent` | Chat message | `Username`, `Text`, `Room` | | `opponentGone` | `BotOpponentGoneEvent` | Opponent disconnected | `Gone`, `ClaimWinInSeconds` | **Example - Pattern Matching (Recommended):** ```csharp await foreach (var evt in client.Bot.StreamGameAsync(gameId, cancellationToken)) { switch (evt) { case BotGameFullEvent full: // Game just started - initialize your engine Console.WriteLine($"Game ID: {full.Id}"); Console.WriteLine($"White: {full.White?.Name} ({full.White?.Rating})"); Console.WriteLine($"Black: {full.Black?.Name} ({full.Black?.Rating})"); Console.WriteLine($"Time Control: {full.Clock?.Initial / 60000}+{full.Clock?.Increment / 1000}"); Console.WriteLine($"Initial FEN: {full.InitialFen ?? "startpos"}"); // The State property contains the current game state if (full.State != null) { Console.WriteLine($"Moves so far: {full.State.Moves}"); } break; case BotGameStateEvent state: // A move was made - analyze and respond Console.WriteLine($"Moves: {state.Moves}"); Console.WriteLine($"White time: {state.WhiteTime}ms"); Console.WriteLine($"Black time: {state.BlackTime}ms"); Console.WriteLine($"Status: {state.Status}"); // Check for draw offers if (state.WhiteOfferingDraw == true) Console.WriteLine("White is offering a draw"); if (state.BlackOfferingDraw == true) Console.WriteLine("Black is offering a draw"); // Check if game ended if (state.Status != "started") { Console.WriteLine($"Game over! Winner: {state.Winner ?? "draw"}"); } break; case BotChatLineEvent chat: Console.WriteLine($"[{chat.Room}] {chat.Username}: {chat.Text}"); break; case BotOpponentGoneEvent gone: Console.WriteLine($"Opponent gone: {gone.Gone}"); if (gone.ClaimWinInSeconds.HasValue && gone.ClaimWinInSeconds > 0) { Console.WriteLine($"Can claim victory in {gone.ClaimWinInSeconds} seconds"); } break; } } ``` **Example - Filtering by Type:** ```csharp // Only process game state updates await foreach (var state in client.Bot.StreamGameAsync(gameId) .OfType()) { ProcessMove(state.Moves); } ``` --- ## Making Moves ### MakeMoveAsync Make a move in a game. ```csharp Task MakeMoveAsync( string gameId, string move, bool? offeringDraw = null, CancellationToken cancellationToken = default) ``` **Parameters:** | Name | Type | Description | |------|------|-------------| | gameId | string | The game ID | | move | string | Move in UCI format (e.g., "e2e4", "e7e8q" for promotion) | | offeringDraw | bool? | `true` to offer/accept draw, `false` to decline, `null` to ignore | | cancellationToken | CancellationToken | Cancellation token | **Returns:** `true` if successful. **Example:** ```csharp // Simple move await client.Bot.MakeMoveAsync(gameId, "e2e4"); // Pawn promotion to queen await client.Bot.MakeMoveAsync(gameId, "e7e8q"); // Make move and offer draw await client.Bot.MakeMoveAsync(gameId, "d4d5", offeringDraw: true); // Make move and decline draw offer await client.Bot.MakeMoveAsync(gameId, "c2c4", offeringDraw: false); ``` --- ## Chat ### WriteChatAsync Write a message in the game chat. ```csharp Task WriteChatAsync( string gameId, ChatRoom room, string text, CancellationToken cancellationToken = default) ``` **Parameters:** | Name | Type | Description | |------|------|-------------| | gameId | string | The game ID | | room | ChatRoom | `ChatRoom.Player` or `ChatRoom.Spectator` | | text | string | Message text | **Example:** ```csharp // Say hi to opponent await client.Bot.WriteChatAsync(gameId, ChatRoom.Player, "Good luck, have fun!"); // Message spectators await client.Bot.WriteChatAsync(gameId, ChatRoom.Spectator, "I'm thinking about Nf3..."); ``` ### GetChatAsync Get all chat messages from a game. ```csharp Task> GetChatAsync( string gameId, CancellationToken cancellationToken = default) ``` **Example:** ```csharp var messages = await client.Bot.GetChatAsync(gameId); foreach (var msg in messages) { Console.WriteLine($"[{msg.Room}] {msg.User}: {msg.Text}"); } ``` --- ## Game Control ### AbortAsync Abort a game (only possible before both players have moved). ```csharp Task AbortAsync(string gameId, CancellationToken cancellationToken = default) ``` **Example:** ```csharp await client.Bot.AbortAsync(gameId); ``` ### ResignAsync Resign a game. ```csharp Task ResignAsync(string gameId, CancellationToken cancellationToken = default) ``` **Example:** ```csharp // Give up await client.Bot.ResignAsync(gameId); ``` ### HandleDrawAsync Offer, accept, or decline a draw. ```csharp Task HandleDrawAsync( string gameId, bool accept, CancellationToken cancellationToken = default) ``` **Example:** ```csharp // Offer or accept a draw await client.Bot.HandleDrawAsync(gameId, accept: true); // Decline a draw offer await client.Bot.HandleDrawAsync(gameId, accept: false); ``` ### HandleTakebackAsync Accept or decline a takeback proposal. ```csharp Task HandleTakebackAsync( string gameId, bool accept, CancellationToken cancellationToken = default) ``` **Example:** ```csharp // Accept takeback await client.Bot.HandleTakebackAsync(gameId, accept: true); // Decline takeback await client.Bot.HandleTakebackAsync(gameId, accept: false); ``` ### ClaimDrawAsync Claim a draw by 50-move rule or threefold repetition. ```csharp Task ClaimDrawAsync(string gameId, CancellationToken cancellationToken = default) ``` **Example:** ```csharp // Claim draw when position repeats 3 times or 50 moves without capture/pawn move await client.Bot.ClaimDrawAsync(gameId); ``` ### ClaimVictoryAsync Claim victory when opponent has left the game. ```csharp Task ClaimVictoryAsync(string gameId, CancellationToken cancellationToken = default) ``` **Example:** ```csharp // When you receive BotOpponentGoneEvent with ClaimWinInSeconds > 0 await foreach (var evt in client.Bot.StreamGameAsync(gameId)) { if (evt is BotOpponentGoneEvent { ClaimWinInSeconds: > 0 } gone) { // Wait for the timeout, then claim await Task.Delay(TimeSpan.FromSeconds(gone.ClaimWinInSeconds.Value + 1)); await client.Bot.ClaimVictoryAsync(gameId); } } ``` --- ## Account Management ### UpgradeAccountAsync Upgrade a regular Lichess account to a Bot account. ```csharp Task UpgradeAccountAsync(CancellationToken cancellationToken = default) ``` > **Warning:** This is irreversible! The account must have played zero games. **Example:** ```csharp // Create a new account on lichess.org first, then: using var client = new LichessClient(newAccountToken); await client.Bot.UpgradeAccountAsync(); Console.WriteLine("Account is now a bot!"); ``` ### GetOnlineBotsAsync Get a list of online bots. ```csharp IAsyncEnumerable GetOnlineBotsAsync( int? count = null, CancellationToken cancellationToken = default) ``` **Parameters:** | Name | Type | Description | |------|------|-------------| | count | int? | Max bots to fetch (1-300, default 50) | **Example:** ```csharp // Get 10 online bots to potentially challenge await foreach (var bot in client.Bot.GetOnlineBotsAsync(count: 10)) { Console.WriteLine($"{bot.Username} - Blitz: {bot.Perfs?.Blitz?.Rating}"); if (bot.Perfs?.Blitz?.Rating < 2000) { // Challenge this bot await client.Challenges.CreateAsync(bot.Username, new ChallengeOptions { Clock = new ClockOptions { Limit = 180, Increment = 2 } }); } } ``` --- ## Event Types Reference ### BotAccountEvent Event from `StreamEventsAsync()`. | Property | Type | Description | |----------|------|-------------| | Type | string | Event type: `gameStart`, `gameFinish`, `challenge`, `challengeCanceled`, `challengeDeclined` | | Game | BotAccountGameInfo? | Game information (for game events) | | Challenge | ChallengeJson? | Challenge information (for challenge events) | ### BotGameFullEvent First event from `StreamGameAsync()`, contains complete game information. | Property | Type | Description | |----------|------|-------------| | Id | string? | Game ID | | Variant | BotVariant? | Variant info (standard, chess960, etc.) | | Clock | BotClock? | Clock settings (initial, increment) | | Speed | string? | Game speed (bullet, blitz, rapid, classical) | | Rated | bool | Whether the game is rated | | White | BotPlayer? | White player info | | Black | BotPlayer? | Black player info | | InitialFen | string? | Starting position (null = standard) | | State | BotGameStateEvent? | Current game state | ### BotGameStateEvent Move update from `StreamGameAsync()`. | Property | Type | Description | |----------|------|-------------| | Moves | string? | All moves in UCI format, space-separated | | WhiteTime | long? | White's remaining time (ms) | | BlackTime | long? | Black's remaining time (ms) | | WhiteIncrement | int? | White's increment (ms) | | BlackIncrement | int? | Black's increment (ms) | | Status | string? | Game status (`started`, `mate`, `resign`, `draw`, etc.) | | Winner | string? | Winner color if game over (`white`, `black`, or null) | | WhiteOfferingDraw | bool? | White is offering a draw | | BlackOfferingDraw | bool? | Black is offering a draw | ### BotChatLineEvent Chat message from `StreamGameAsync()`. | Property | Type | Description | |----------|------|-------------| | Room | string? | Chat room: `player` or `spectator` | | Username | string? | Sender's username | | Text | string? | Message text | ### BotOpponentGoneEvent Opponent disconnection from `StreamGameAsync()`. | Property | Type | Description | |----------|------|-------------| | Gone | bool | Whether opponent is currently disconnected | | ClaimWinInSeconds | int? | Seconds until you can claim victory (-1 if not applicable) | --- ## See Also - [Board API](Board.md) - Similar API for human players with external boards - [Challenges API](Challenges.md) - Creating and accepting challenges - [Streaming Guide](../guides/Streaming-Events.md) - Patterns for handling streaming events