Entities/Board/BoardModels.cs

Models and parser for Lichess board-related data and events. Defines enums and DTOs for seek options, game info, players, clock/state, start/finish/full/chat events, and helpers to convert seek options to form data and parse JSON board events and move lists.

NetworkingFile Access
#nullable enable annotations

using System.Globalization;
using LichessNET.Entities.Enumerations;
using LichessNET.Extensions;

namespace LichessNET.Entities.Board;

public enum BoardColorPreference
{
    Random,
    White,
    Black
}

public enum BoardChatRoom
{
    Player,
    Spectator
}

public sealed class BoardSeekOptions
{
    public bool Rated { get; set; }
    public double TimeMinutes { get; set; } = 5;
    public int IncrementSeconds { get; set; }
    public int? DaysPerTurn { get; set; }
    public BoardColorPreference Color { get; set; } = BoardColorPreference.Random;
    public ChessVariant Variant { get; set; } = ChessVariant.Standard;
    public string RatingRange { get; set; }

    internal Dictionary<string, string> ToFormData()
    {
        Validate();

        var form = new Dictionary<string, string>
        {
            ["rated"] = Rated.ToString().ToLowerInvariant(),
            ["variant"] = Variant.ToApiName(),
            ["color"] = ToApiName(Color)
        };

        if (DaysPerTurn.HasValue)
        {
            form["days"] = DaysPerTurn.Value.ToString(CultureInfo.InvariantCulture);
        }
        else
        {
            form["time"] = TimeMinutes.ToString(CultureInfo.InvariantCulture);
            form["increment"] = IncrementSeconds.ToString(CultureInfo.InvariantCulture);
        }

        if (!string.IsNullOrWhiteSpace(RatingRange))
            form["ratingRange"] = RatingRange;

        return form;
    }

    private void Validate()
    {
        if (DaysPerTurn.HasValue)
        {
            if (DaysPerTurn.Value < 1 || DaysPerTurn.Value > 14)
                throw new ArgumentOutOfRangeException(nameof(DaysPerTurn), "Days per turn must be between 1 and 14.");

            return;
        }

        if (TimeMinutes <= 0 || TimeMinutes > 180)
            throw new ArgumentOutOfRangeException(nameof(TimeMinutes), "Clock time must be between 0 and 180 minutes.");

        if (IncrementSeconds < 0 || IncrementSeconds > 180)
            throw new ArgumentOutOfRangeException(nameof(IncrementSeconds), "Increment must be between 0 and 180 seconds.");
    }

    private static string ToApiName(BoardColorPreference color)
    {
        return color switch
        {
            BoardColorPreference.White => "white",
            BoardColorPreference.Black => "black",
            _ => "random"
        };
    }
}

public sealed class BoardGameInfo
{
    [JsonPropertyName("gameId")]
    public string GameId { get; set; }

    [JsonPropertyName("id")]
    public string Id { get; set; }

    [JsonPropertyName("fullId")]
    public string FullId { get; set; }

    [JsonPropertyName("color")]
    public string Color { get; set; }

    [JsonPropertyName("fen")]
    public string Fen { get; set; }

    [JsonPropertyName("hasMoved")]
    public bool? HasMoved { get; set; }

    [JsonPropertyName("isMyTurn")]
    public bool? IsMyTurn { get; set; }

    [JsonPropertyName("lastMove")]
    public string LastMove { get; set; }

    [JsonPropertyName("source")]
    public string Source { get; set; }

    [JsonPropertyName("status")]
    public string Status { get; set; }

    [JsonPropertyName("variant")]
    public string Variant { get; set; }

    [JsonPropertyName("speed")]
    public string Speed { get; set; }

    [JsonPropertyName("perf")]
    public string Perf { get; set; }

    [JsonPropertyName("rated")]
    public bool? Rated { get; set; }

    [JsonPropertyName("opponent")]
    public BoardPlayerInfo Opponent { get; set; }

    [JsonPropertyName("white")]
    public BoardPlayerInfo White { get; set; }

    [JsonPropertyName("black")]
    public BoardPlayerInfo Black { get; set; }

    [JsonPropertyName("state")]
    public BoardGameState State { get; set; }

    [JsonExtensionData]
    public Dictionary<string, JsonElement> Extra { get; set; }

    public string BestGameId => !string.IsNullOrWhiteSpace(GameId) ? GameId : Id;
}

public sealed class BoardPlayerInfo
{
    [JsonPropertyName("id")]
    public string Id { get; set; }

    [JsonPropertyName("name")]
    public string Name { get; set; }

    [JsonPropertyName("username")]
    public string Username { get; set; }

    [JsonPropertyName("title")]
    public string Title { get; set; }

    [JsonPropertyName("rating")]
    public int? Rating { get; set; }

    [JsonPropertyName("provisional")]
    public bool? Provisional { get; set; }

    [JsonExtensionData]
    public Dictionary<string, JsonElement> Extra { get; set; }
}

public sealed class BoardClockState
{
    public int? WhiteTimeMilliseconds { get; set; }
    public int? BlackTimeMilliseconds { get; set; }
    public int? WhiteIncrementMilliseconds { get; set; }
    public int? BlackIncrementMilliseconds { get; set; }

    public TimeSpan? WhiteTime => WhiteTimeMilliseconds.HasValue
        ? TimeSpan.FromMilliseconds(WhiteTimeMilliseconds.Value)
        : null;

    public TimeSpan? BlackTime => BlackTimeMilliseconds.HasValue
        ? TimeSpan.FromMilliseconds(BlackTimeMilliseconds.Value)
        : null;

    public static BoardClockState FromState(BoardGameState state)
    {
        return new BoardClockState
        {
            WhiteTimeMilliseconds = state?.WhiteTimeMilliseconds,
            BlackTimeMilliseconds = state?.BlackTimeMilliseconds,
            WhiteIncrementMilliseconds = state?.WhiteIncrementMilliseconds,
            BlackIncrementMilliseconds = state?.BlackIncrementMilliseconds
        };
    }
}

public sealed class BoardGameState
{
    [JsonPropertyName("type")]
    public string Type { get; set; }

    [JsonPropertyName("moves")]
    public string Moves { get; set; }

    [JsonPropertyName("wtime")]
    public int? WhiteTimeMilliseconds { get; set; }

    [JsonPropertyName("btime")]
    public int? BlackTimeMilliseconds { get; set; }

    [JsonPropertyName("winc")]
    public int? WhiteIncrementMilliseconds { get; set; }

    [JsonPropertyName("binc")]
    public int? BlackIncrementMilliseconds { get; set; }

    [JsonPropertyName("status")]
    public string Status { get; set; }

    [JsonPropertyName("winner")]
    public string Winner { get; set; }

    [JsonPropertyName("wdraw")]
    public bool? WhiteOfferingDraw { get; set; }

    [JsonPropertyName("bdraw")]
    public bool? BlackOfferingDraw { get; set; }

    [JsonExtensionData]
    public Dictionary<string, JsonElement> Extra { get; set; }

    public BoardClockState Clock => BoardClockState.FromState(this);

    public bool IsFinished => !string.IsNullOrWhiteSpace(Status)
                              && !Status.Equals("started", StringComparison.OrdinalIgnoreCase)
                              && !Status.Equals("created", StringComparison.OrdinalIgnoreCase);

    public IReadOnlyList<string> MoveList => BoardEventParser.SplitMoves(Moves);
}

public sealed class BoardGameStartEvent
{
    [JsonPropertyName("type")]
    public string Type { get; set; }

    [JsonPropertyName("game")]
    public BoardGameInfo Game { get; set; }
}

public sealed class BoardGameFinishEvent
{
    [JsonPropertyName("type")]
    public string Type { get; set; }

    [JsonPropertyName("game")]
    public BoardGameInfo Game { get; set; }
}

public sealed class BoardGameFullEvent
{
    [JsonPropertyName("type")]
    public string Type { get; set; }

    [JsonPropertyName("id")]
    public string Id { get; set; }

    [JsonPropertyName("initialFen")]
    public string InitialFen { get; set; }

    [JsonPropertyName("rated")]
    public bool? Rated { get; set; }

    [JsonPropertyName("variant")]
    public string Variant { get; set; }

    [JsonPropertyName("speed")]
    public string Speed { get; set; }

    [JsonPropertyName("perf")]
    public string Perf { get; set; }

    [JsonPropertyName("createdAt")]
    public long? CreatedAt { get; set; }

    [JsonPropertyName("white")]
    public BoardPlayerInfo White { get; set; }

    [JsonPropertyName("black")]
    public BoardPlayerInfo Black { get; set; }

    [JsonPropertyName("state")]
    public BoardGameState State { get; set; }

    [JsonExtensionData]
    public Dictionary<string, JsonElement> Extra { get; set; }
}

public sealed class BoardChatLineEvent
{
    [JsonPropertyName("type")]
    public string Type { get; set; }

    [JsonPropertyName("room")]
    public string Room { get; set; }

    [JsonPropertyName("username")]
    public string Username { get; set; }

    [JsonPropertyName("text")]
    public string Text { get; set; }
}

public static class BoardEventParser
{
    public static string GetEventType(JsonElement element)
    {
        return element.TryGetProperty("type", out var type)
            ? type.GetString()
            : string.Empty;
    }

    public static BoardGameStartEvent ParseGameStart(JsonElement element)
    {
        return element.Deserialize<BoardGameStartEvent>(LichessJson.Options);
    }

    public static BoardGameFinishEvent ParseGameFinish(JsonElement element)
    {
        return element.Deserialize<BoardGameFinishEvent>(LichessJson.Options);
    }

    public static BoardGameFullEvent ParseGameFull(JsonElement element)
    {
        return element.Deserialize<BoardGameFullEvent>(LichessJson.Options);
    }

    public static BoardGameState ParseGameState(JsonElement element)
    {
        return element.Deserialize<BoardGameState>(LichessJson.Options);
    }

    public static BoardChatLineEvent ParseChatLine(JsonElement element)
    {
        return element.Deserialize<BoardChatLineEvent>(LichessJson.Options);
    }

    public static IReadOnlyList<string> SplitMoves(string moves)
    {
        if (string.IsNullOrWhiteSpace(moves))
            return Array.Empty<string>();

        return moves.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
    }
}