diff --git a/ChessAI.sln b/ChessAI.sln index 798df40..25c6b03 100644 --- a/ChessAI.sln +++ b/ChessAI.sln @@ -2,6 +2,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChessAI", "ChessAI\ChessAI.csproj", "{D75AC44F-AC92-418E-8B9B-6911AE1FE830}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{0AB26848-CC9A-4CB1-AA2D-439A45A2DE66}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChessBenchMarks", "ChessBenchMarks\ChessBenchMarks.csproj", "{3E43ADE7-9DE7-4D19-BA25-890FD9A41D0D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +16,13 @@ Global {D75AC44F-AC92-418E-8B9B-6911AE1FE830}.Debug|Any CPU.Build.0 = Debug|Any CPU {D75AC44F-AC92-418E-8B9B-6911AE1FE830}.Release|Any CPU.ActiveCfg = Release|Any CPU {D75AC44F-AC92-418E-8B9B-6911AE1FE830}.Release|Any CPU.Build.0 = Release|Any CPU + {0AB26848-CC9A-4CB1-AA2D-439A45A2DE66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0AB26848-CC9A-4CB1-AA2D-439A45A2DE66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0AB26848-CC9A-4CB1-AA2D-439A45A2DE66}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0AB26848-CC9A-4CB1-AA2D-439A45A2DE66}.Release|Any CPU.Build.0 = Release|Any CPU + {3E43ADE7-9DE7-4D19-BA25-890FD9A41D0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E43ADE7-9DE7-4D19-BA25-890FD9A41D0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E43ADE7-9DE7-4D19-BA25-890FD9A41D0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E43ADE7-9DE7-4D19-BA25-890FD9A41D0D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/ChessAI/ChessAI.csproj b/ChessAI/ChessAI.csproj index 2c0dacc..0d3c58a 100644 --- a/ChessAI/ChessAI.csproj +++ b/ChessAI/ChessAI.csproj @@ -2,7 +2,8 @@ Exe - netcoreapp3.1 + net5.0 + true diff --git a/ChessAI/DataClasses/Board.cs b/ChessAI/DataClasses/Board.cs new file mode 100644 index 0000000..48448c4 --- /dev/null +++ b/ChessAI/DataClasses/Board.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; + +namespace ChessAI.DataClasses +{ + public readonly struct Board + { + private const byte Width = 0x10; + private const byte Height = 0x8; + private static readonly sbyte[] Directions = { + +0x10, // 1 up + -0x10, // 1 down + +0x01, // 1 right + -0x01, // 1 left + }; + + public static sbyte WhiteDirection(Direction direction) => Directions[(byte)direction]; + public static sbyte BlackDirection(Direction direction) => (sbyte) -(Directions[(byte)direction]); + + private readonly Piece[] _fields; //TODO consider replacing this array with a stack allocated ReadOnlySpan + + /// + /// A constructor taking an array of pieces representing the board. Note that the array length must + /// be consistent with an 0x88 board. + /// + /// an array representing the board + /// thrown if the given array is not of the right length + public Board(Piece[] fields) + { + var expectedSize = Width * Height; + if (fields.Length != expectedSize) + { + throw new ArgumentException( + "fields must be an array of length 0x" + expectedSize.ToString("X") + " / 0d" + expectedSize + + " but the provided array had length 0x" + fields.Length.ToString("X") + " / 0d" + fields.Length + ); + } + + _fields = fields; + } + + /// + /// A Constructor that takes a list of fields. It is assumed that all pieces have valid positions set. + /// If one or more pieces has an invalid position it can lead to unexpected behavior. + /// + /// A list of pieces with valid positions + public Board(List pieceList) + { + var fields = new Piece[Width * Height]; + foreach (var piece in pieceList) + { + fields[piece.Position] = piece; + } + + _fields = fields; + } + + public Piece this[int i] => Fields[i]; + + public ReadOnlySpan Fields => _fields.AsSpan(); + + public bool IsFieldOccupied(byte position) + { + return Fields[position] == Piece.Empty; + } + + public static bool IsIndexValid(byte index) + { + //Checks that only bits used for counting from 0-7 is used for each digit in the hex representation of the index + return (index & 0b1000_1000) == 0; + } + + public static string IndexToString(byte index) + { + if (!IsIndexValid(index)) throw new ArgumentException("Argument must be a valid index"); + + return (index & 0xF0) switch + { + 0x00 => "A" + (index & 0xF), + 0x10 => "B" + (index & 0xF), + 0x20 => "C" + (index & 0xF), + 0x30 => "D" + (index & 0xF), + 0x40 => "E" + (index & 0xF), + 0x50 => "F" + (index & 0xF), + 0x60 => "G" + (index & 0xF), + 0x70 => "H" + (index & 0xF), + _ => throw new ArgumentException("Argument must be a valid index") + }; + } + + public static byte StringToIndex(string fieldName) + { + if (fieldName.Length != 2 + || !"ABCDEFGHabcdefgh".Contains(fieldName[0]) + || !"12345678".Contains(fieldName[1])) + { + throw new ArgumentException("Argument must be a valid field name"); + } + + var lastDigit = Byte.Parse(fieldName.Substring(1)); + return (fieldName[0]) switch + { + 'A' => (byte)(0x00 + lastDigit), + 'B' => (byte)(0x10 + lastDigit), + 'C' => (byte)(0x20 + lastDigit), + 'D' => (byte)(0x30 + lastDigit), + 'E' => (byte)(0x40 + lastDigit), + 'F' => (byte)(0x50 + lastDigit), + 'G' => (byte)(0x60 + lastDigit), + 'H' => (byte)(0x70 + lastDigit), + _ => throw new ArgumentException("Argument must be a valid index") + }; + } + } + + public enum Direction : byte + { + Up = 0, + Down = 1, + Left = 2, + Right = 3, + }; + +} \ No newline at end of file diff --git a/ChessAI/DataClasses/GameState.cs b/ChessAI/DataClasses/GameState.cs new file mode 100644 index 0000000..f4e9b33 --- /dev/null +++ b/ChessAI/DataClasses/GameState.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using static ChessAI.DataClasses.Piece; + +namespace ChessAI.DataClasses +{ + /** + * + * A struct that keeps track of all relevant information about a game + * + */ + public readonly struct GameState + { + /// + /// An easy to use constructor taking only a board. + /// + /// The board that should be represented by this state + /// + /// This implementation is slow as it has to construct a PieceList itself. + /// Use other constructors in time critical sections of code. + /// + public GameState(Board board) + { + State = board; + + Span tempBoardFields = stackalloc Piece[State.Fields.Length]; + State.Fields.CopyTo(tempBoardFields); + + List tempWhitePieceList = new List(); + List tempBlackPieceList = new List(); + + // Fill PieceLists + for (byte index = 0; index < 0x88; index++) + { + // if the index is invalid we have exceeded the boundaries of the board. + // The body of the loop is skipped until the next valid index is encountered. + if (!Board.IsIndexValid(index)) continue; + + var currentPiece = board[index]; + if (currentPiece.PieceType != Empty) + { + + // If the found piece doesn't have the right position then set it + if (currentPiece.Position != index) + { + currentPiece = new Piece(currentPiece.Content, index); + tempBoardFields[index] = currentPiece; + } + + if ((currentPiece.PieceFlags & White) == White) + { + tempWhitePieceList.Add(currentPiece); + } + else + { + tempBlackPieceList.Add(currentPiece); + } + } + } + + State = new Board(tempBoardFields.ToArray()); + WhitePieces = tempWhitePieceList.ToArray(); + BlackPieces = tempBlackPieceList.ToArray(); + } + + public readonly Board State; + public readonly Piece[] WhitePieces; + public readonly Piece[] BlackPieces; + + + /** + * A dummy method representing some logic to calculate the applying a move to the state + * The move that should be applied to a state + * A new with the move applied + */ + public GameState ApplyMove(Move move) + { + Span newFields = stackalloc Piece[State.Fields.Length]; + State.Fields.CopyTo(newFields); + + var movedPiece = newFields[move.StartPos]; + newFields[move.StartPos] = new Piece(Empty); + var capturedPiece = newFields[move.EndPos]; + newFields[move.EndPos] = movedPiece; + + //TODO use another constructor to make this implementation faster (perhaps make one that takes a span) + var newBoard = new Board(newFields.ToArray()); + var newState = new GameState(newBoard); + return newState; + } + } +} \ No newline at end of file diff --git a/ChessAI/DataClasses/Move.cs b/ChessAI/DataClasses/Move.cs new file mode 100644 index 0000000..1e573ac --- /dev/null +++ b/ChessAI/DataClasses/Move.cs @@ -0,0 +1,141 @@ +using System; +using static ChessAI.DataClasses.MoveType; +using static ChessAI.DataClasses.Piece; + +namespace ChessAI.DataClasses +{ + public readonly struct Move + { + public readonly byte StartPos; + public readonly byte EndPos; + public readonly MoveType MoveType; + public readonly Piece MovePiece; + public readonly Piece TargetPiece; + + + /// + /// a simple constructor taking the start and end position. + /// + /// starting position + /// destination + /// + /// This constructor has only been left here for compatibility concerns and should not be used. + /// When encountered it should be replaced by a factory method call. + /// The Factory is the recommended alternative. It makes sure to set all the + /// required fields in the correct way only given start and end position and a state. + /// + [ObsoleteAttribute("When encountered it should be replaced by a factory method call." + + "The CreateSimpleMove Factory is the recommended alternative.", false)] + public Move(byte startPos, byte endPos) + { + StartPos = startPos; + EndPos = endPos; + + MoveType = Ordinary; + MovePiece = new Piece(Empty); + TargetPiece = MovePiece; + } + + /// + /// A constructor that takes all the required parameters to define a move + /// + /// + /// + /// + /// + /// + private Move(byte startPos, byte endPos, MoveType moveType, Piece movePiece, Piece targetPiece) + { + StartPos = startPos; + EndPos = endPos; + MoveType = moveType; + MovePiece = movePiece; + TargetPiece = targetPiece; + } + + /// + /// A factory method that creates a simple move where no special rules are involved. + /// + /// The position from which the movePiece originates + /// The destination of the movePiece + /// The state of the game at the time of the move + /// A new move from startPos to endPos + public static Move CreateSimpleMove(byte startPos, byte endPos, GameState state) + { + var movePiece = state.State[startPos]; + var targetPiece = state.State[endPos]; + var move = new Move(startPos, endPos, Ordinary, movePiece, targetPiece); + return move; + } + + /// + /// A factory method that creates a castling move. + /// This method performs no checks on the legality of the move, not even if the pieces required are + /// actually there. This method should therefore only be called after the legality has been determined. + /// + /// The position of the rook that is involved in the castling + /// The state of the game at the time of the move + /// + /// A new castling move that represents the king taking the castlePosition and the + /// occupying rook being moved in the appropriate direction + /// + public static Move CreateCastleMove(byte castlePosition, GameState state) + { + var targetPiece = state.State[castlePosition]; + + // Dependant on the board. Should this be a static member of board? + byte whiteKingIndex = 0x04; + byte blackKingIndex = 0x73; + + var kingIndex = (targetPiece.PieceFlags & White) == White ? whiteKingIndex : blackKingIndex; + var movePiece = state.State[kingIndex]; + + var move = new Move(kingIndex, castlePosition, Castling, movePiece, targetPiece); + return move; + } + + /// + /// A factory method that creates a pwn promotion move. + /// This method performs no checks on the legality of the move, not even if the pieces required are + /// actually there. This method should therefore only be called after the legality has been determined. + /// + /// The starting position of the pawn + /// + /// The destination of the pawn. note that this may also occur through an attack + /// which opens the possibility of three different end positions + /// + /// The piece the pawn should be promoted to + /// The state of the game at the time of the move + /// + /// A new pawn promotion move that represents a pawn moving to the 8th rank, being promote to the promotionPiece + /// + public static Move CreatePawnPromotionMove(byte startPosition, byte endPos, Piece promotionPiece, + GameState state) + { + var movePiece = state.State[startPosition]; + var promotionType = promotionPiece.PieceType switch + { + Queen => PromotionQueen, + Knight => PromotionKnight, + Bishop => PromotionBishop, + Rook => PromotionRook, + _ => throw new ArgumentOutOfRangeException( + $"The piece type {promotionPiece.PieceType} is not an option for promotion" + ) + }; + + var move = new Move(startPosition, endPos, promotionType, movePiece, promotionPiece); + return move; + } + } + + public enum MoveType : byte + { + Ordinary, + Castling, + PromotionQueen, + PromotionRook, + PromotionBishop, + PromotionKnight + } +} \ No newline at end of file diff --git a/ChessAI/DataClasses/Piece.cs b/ChessAI/DataClasses/Piece.cs new file mode 100644 index 0000000..e8939b9 --- /dev/null +++ b/ChessAI/DataClasses/Piece.cs @@ -0,0 +1,151 @@ +using System; +using System.Text; + +namespace ChessAI.DataClasses +{ + public readonly struct Piece : IEquatable + { + /// + /// _piece stores information describing a single piece possibly on a board. + /// The byte representing the piece is conceptually split into two parts; one describing the pieces color + /// and type, and another describing additional information relating to the state it is in. + /// The piece type is represented by the three least significant bits as consecutive numbers starting at 1. + /// The color is represented by the next bit where a 1 is white and 0 is black. + /// + /// below is a description of the layout of _piece starting from the most significant bit + /// towards the least significant on + /// + /// unused: 2 bits + /// challenges: 1 bit + /// is challenged: 1 bit + /// is white: 1 bit + /// Piece type: 3 bits + /// + /// + private readonly byte _piece; + + public byte Content => _piece; + public byte PieceFlags => (byte)(_piece & 0b1111_1000); + public byte PieceType => (byte)(_piece & PieceMask); + + /// + /// The position of the piece as an index on an 0x88 board. + /// If a position doesn't make sense or is unknown for this piece an invalid index is set instead. + /// + public byte Position { get; } + + public Piece(byte flags) + { + _piece = flags; + Position = 0xAA; // Outside valid indexes as the piece has been given no + } + + public Piece(int flags) + { + _piece = (byte)flags; + Position = 0xAA; // Outside valid indexes as the piece has been given no + } + + public Piece(byte flags, byte position) + { + _piece = flags; + Position = position; + } + + public Piece(int flags, byte position) + { + Position = position; + _piece = (byte)flags; + } + //######################################// + // Constants for easy use of this type // + //######################################// + + public const byte Empty = 0; + public const byte PieceMask = 0b0111; + + // Piece definitions + public const byte Pawn = 0b0001; + public const byte Rook = 0b0010; + public const byte Knight = 0b0011; + public const byte Bishop = 0b0100; + public const byte Queen = 0b0101; + public const byte King = 0b0110; + + // Flags + public const byte White = 0b1000; + public const byte Black = 0; + + + public override string ToString() + { + var builder = new StringBuilder(); + + builder.Append((PieceFlags & White) == White ? "White" : "Black"); + builder.Append(" "); + builder.Append( + (PieceType) switch + { + 0 => "None", + 1 => "Pawn", + 2 => "Rook", + 3 => "Knight", + 4 => "Bishop", + 5 => "Queen", + 6 => "King", + _ => "Invalid" + } + ); + + //Insert appends for other flags here as they are decided on + + return builder.ToString(); + } + + public bool Equals(Piece other) + { + return _piece == other._piece; + } + + public override bool Equals(object obj) + { + return obj is Piece other && Equals(other); + } + + public override int GetHashCode() + { + return _piece.GetHashCode(); + } + + //######################################// + // Operator overloads // + //######################################// + + // bitwise And + public static Piece operator &(Piece a, Piece b) => new Piece((byte)(a._piece & b._piece)); + public static Piece operator &(Piece a, byte b) => new Piece((byte)(a._piece & b)); + public static Piece operator &(byte a, Piece b) => new Piece((byte)(a & b._piece)); + + // bitwise Or + public static Piece operator |(Piece a, Piece b) => new Piece((byte)(a._piece | b._piece)); + public static Piece operator |(Piece a, byte b) => new Piece((byte)(a._piece | b)); + public static Piece operator |(byte a, Piece b) => new Piece((byte)(a | b._piece)); + + // bitwise XOr + public static Piece operator ^(Piece a, Piece b) => new Piece((byte)(a._piece ^ b._piece)); + public static Piece operator ^(Piece a, byte b) => new Piece((byte)(a._piece ^ b)); + public static Piece operator ^(byte a, Piece b) => new Piece((byte)(a ^ b._piece)); + + // Equality + public static bool operator ==(Piece a, Piece b) => a._piece == b._piece; + public static bool operator ==(byte a, Piece b) => a == b._piece; + public static bool operator ==(Piece a, byte b) => a._piece == b; + public static bool operator !=(Piece a, Piece b) => a._piece != b._piece; + public static bool operator !=(byte a, Piece b) => a != b._piece; + public static bool operator !=(Piece a, byte b) => a._piece != b; + public static bool operator ==(int a, Piece b) => a == b._piece; + public static bool operator ==(Piece a, int b) => a._piece == b; + public static bool operator !=(int a, Piece b) => a != b._piece; + public static bool operator !=(Piece a, int b) => a._piece != b; + } +} \ No newline at end of file diff --git a/ChessAI/GameController.cs b/ChessAI/GameController.cs new file mode 100644 index 0000000..2d761a7 --- /dev/null +++ b/ChessAI/GameController.cs @@ -0,0 +1,9 @@ +using ChessAI.DataClasses; +using ChessAI.IO; +namespace ChessAI +{ + public class GameController + { + + } +} \ No newline at end of file diff --git a/ChessAI/IO/IO.cs b/ChessAI/IO/IO.cs new file mode 100644 index 0000000..3c22414 --- /dev/null +++ b/ChessAI/IO/IO.cs @@ -0,0 +1,7 @@ +namespace ChessAI.IO +{ + public class IO + { + + } +} \ No newline at end of file diff --git a/ChessAI/MoveSelection/MoveGeneration/IMoveCalculator.cs b/ChessAI/MoveSelection/MoveGeneration/IMoveCalculator.cs new file mode 100644 index 0000000..872d52d --- /dev/null +++ b/ChessAI/MoveSelection/MoveGeneration/IMoveCalculator.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using ChessAI.DataClasses; + +namespace ChessAI.MoveSelection.MoveGeneration +{ + public interface IMoveCalculator + { + /** + * A dummy method representing some logic to calculate all moves from a given state + * The state for which possible moves should be calculated + * + * A boolean flag that tells which side's moves should be generated. true -> white and false -> black + * + * A of all legal moves for the given player + */ + List CalculatePossibleMoves(GameState state, bool calculateForWhite); + } +} \ No newline at end of file diff --git a/ChessAI/MoveSelection/MoveGeneration/MoveCalculator.cs b/ChessAI/MoveSelection/MoveGeneration/MoveCalculator.cs new file mode 100644 index 0000000..a84b7a1 --- /dev/null +++ b/ChessAI/MoveSelection/MoveGeneration/MoveCalculator.cs @@ -0,0 +1,157 @@ +using System.Collections.Generic; +using ChessAI.DataClasses; + +namespace ChessAI.MoveSelection.MoveGeneration { + + public enum DirectionIndex { + Up = 0, Down = 1, Left = 2, Right = 3, + UpLeft = 4, DownRight=5, UpRight=7, DownLeft=8 + }; + + + // Moves Implementation + public class MoveGenerator : IMoveCalculator{ + + //public enum DirectionIndex { + //Up = 0, Down = 1, Left = 2, Right = 3, + //UpLeft = 4, DownRight=5, UpRight=7, DownLeft=8}; + + // this is made to fit this Direction Index. + + public static readonly sbyte[] X88Dirs = { + 0x10, // 1 up 0 x + -0x10, // 1 down 0 x + 0x01, // 0 x 1 right + -0x01, // 0 x 1 left + 0x0e, // 1 up 1 left + -0x11, // 1 down 1 right + 0x11, // 1 up 1 right + -0x0e // 1 down 1 left + }; + + private bool king , queen ; + public List CalculatePossibleMoves(GameState state, bool calculateForWhite){ + return new List(); + } + + public List calcMovesForPiece(Piece piece , byte position ) { + + king = ( piece == Piece.King ); + queen = (piece == Piece.Queen); + + List moves = new List(); + if( king || queen || piece == Piece.Rook ){ + // i add king to this mehtod, because it is the only piece that has a limit of 1 distance, since "king" is a bool, i pass it as "depthIs1" + moves.AddRange( genLineMoves(position, king) ); + } + + if( king || queen || piece == Piece.Bishop ){ + // COPY OF PREV COMMENT ::: i add king to this mehtod, because it is the only piece that has a limit of 1 distance, since "king" is a bool, i pass it as "depthIs1" + moves.AddRange( genDiagMoves(position, king) ); + } + + return null; + } + + + bool moreMoves; + byte dirs; + byte tempPos; + + private List genLineMoves(byte position, bool depthIs1 ){ + List moves = new List(); + moreMoves = true; + for(dirs = 0 ; dirs < 4 ; dirs++) { + + tempPos = position; + + while(moreMoves){ + // this adds the offset + tempPos = (byte)(tempPos + X88Dirs[ dirs ]); + + if(false) // IS OUT OF BOUNDS OF BOARD + break; + + if(false) // is Blocked + break; + + // createMove and Add to list + moves.Add( new Move( position, tempPos ) ) ; + + + // for all directions. First 4, up down left right. + if(depthIs1) + break; + } + } + + return null; + } + private List genDiagMoves(byte position, bool depthIs1 ){ + List moves = new List(); + moreMoves = true; + for(dirs = 4 ; dirs < 8 ; dirs++) { + + tempPos = position; + + while(moreMoves) { + // this adds the offset + tempPos = (byte)(tempPos + X88Dirs[ dirs ]); + + if(false) // IS OUT OF BOUNDS OF BOARD + break; + + if(false) // is blocked by self ?? + break; + + // createMove and Add to list + moves.Add(new Move(position , tempPos)); + + + // for all directions. First 4, up down left right. + if(depthIs1) + break; + } + } + return moves; + } + + // source https://learn.inside.dtu.dk/d2l/le/content/80615/viewContent/284028/View + sbyte[] horseMoves = { + 0x21, // two up one right + 0x1F, // two up one left + 0x12, // one up two right + 0x0E, // one up two left + -0x21, // two down one left + -0x1F, // two down one right + -0x12, // one down two left + -0x0E + }; + private List genHorseMoves(byte position , bool depthIs1) { + List moves = new List(); + for(dirs = 0 ; dirs < horseMoves.Length ; dirs++) { + // this adds the offset + tempPos = (byte)(tempPos + horseMoves[ dirs ]); + + if(false) { // IS OUT OF BOUNDS OF BOARD + + continue; + } + + if(false) { // is blocked by self ?? + + continue; + } + + + // createMove and Add to list + moves.Add(new Move(position , tempPos)); + } + return moves; + } + } + + + + +} diff --git a/ChessAI/MoveSelection/MoveSelector.cs b/ChessAI/MoveSelection/MoveSelector.cs new file mode 100644 index 0000000..847e68d --- /dev/null +++ b/ChessAI/MoveSelection/MoveSelector.cs @@ -0,0 +1,164 @@ +using System; +using System.Threading.Tasks; +using ChessAI.DataClasses; +using ChessAI.MoveSelection.MoveGeneration; +using ChessAI.MoveSelection.StateAnalysis; + +namespace ChessAI.MoveSelection +{ + /** + * + * A Class that contains all the logic for finding the best move. + * + */ + public class MoveSelector + { + private readonly bool _isWhite; + private Move[] _tempBestMoves; + + private readonly IStateAnalyser _stateAnalyser; + private readonly IMoveAnalyser _moveAnalyser; + private readonly IMoveCalculator _moveCalculator; + + public Move[] BestMoves { get; private set; } + + public MoveSelector(bool playerIsWhite, IStateAnalyser stateAnalyser, IMoveAnalyser moveAnalyser, + IMoveCalculator moveCalculator, int initialMoveArraySize = 6) + { + _isWhite = playerIsWhite; + BestMoves = new Move[initialMoveArraySize]; + _tempBestMoves = new Move[initialMoveArraySize]; + _stateAnalyser = stateAnalyser; + _moveAnalyser = moveAnalyser; + _moveCalculator = moveCalculator; + } + + /** + * + * The method to use if you want to perform a fixed depth search for the best move. + * + * The depth to which the algorithm will search + * The current state of the game + */ + public Move BestMove(GameState state, int depth) + { + //TODO reduce amount of allocations and copies of arrays + if (BestMoves.Length < depth) + { + var newArray = new Move[depth]; + BestMoves.CopyTo(newArray, 0); + BestMoves = newArray; + } + + // To avoid _bestMoves and _tempBestMoves referring to the same array, + // _tempBestMoves have to be reassigned every call. + _tempBestMoves = new Move[depth]; + + + MinMax(depth, 0, true, state); + BestMoves = _tempBestMoves; + + return BestMoves[0]; + } + + /** + * + * The method to use if you want to perform a search for the best move with a time constraint. + * + * The current state of the game + * + * The time constraint of the search. + * When this limit is hit the function will return and drop any potential searches it is attempting. + * + * + * The depth to which the algorithm will search if it can do so within the allotted time span. + * If there is no max depth, set this argument to -1; + * + */ + public Move BestMoveIterative(GameState state, TimeSpan timeLimit, int maxDepth = -1) + { + var task = Task.Run(() => + { + for (int depth = 1; depth <= maxDepth; depth++) + { + BestMove(state, depth); + } + }); + + task.Wait(timeLimit); + + return BestMoves[0]; + } + + /** + * + * The implementation of the minMax algorithm using alpha pruning that we can use to search for the best move + * + * The desired depth to which it should search + * The depth of the current node + * A value deciding which role the node takes on; maximiser or minimiser + * The state of the game as it would look all moves leading to this node were taken + * The value storing the maximiser's current best value + * The value storing the minimiser's current best value + * The evaluation value of the best outcome + */ + private int MinMax(int searchDepth, int currentDepth, bool isMaximizer, in GameState state, + int alpha = int.MinValue, int beta = int.MaxValue) + { + if (searchDepth <= currentDepth) + { + return _stateAnalyser.StaticAnalysis(state); + } + + // Generate moves, sort them and remove the previous best move to avoid + // it being used in other branches than the best + var moves = _moveCalculator.CalculatePossibleMoves(state, _isWhite == isMaximizer); + _moveAnalyser.SortMovesByBest(state, moves, BestMoves[currentDepth]); + + if (isMaximizer) + { + foreach (var move in moves) + { + var child = state.ApplyMove(move); + var value = MinMax(searchDepth, currentDepth + 1, false, child, alpha, beta); + + if (value > alpha) + { + alpha = value; + + if (alpha >= beta) + { + return alpha; + } + + _tempBestMoves[currentDepth] = move; + } + } + + return alpha; + } + else + { + foreach (var move in moves) + { + var child = state.ApplyMove(move); + var value = MinMax(searchDepth, currentDepth + 1, true, child, alpha, beta); + + if (value < beta) + { + beta = value; + + if (alpha >= beta) + { + return beta; + } + + _tempBestMoves[currentDepth] = move; + } + } + + return beta; + } + } + } +} \ No newline at end of file diff --git a/ChessAI/MoveSelection/StateAnalysis/IMoveAnalyser.cs b/ChessAI/MoveSelection/StateAnalysis/IMoveAnalyser.cs new file mode 100644 index 0000000..701b4c1 --- /dev/null +++ b/ChessAI/MoveSelection/StateAnalysis/IMoveAnalyser.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using ChessAI.DataClasses; + +namespace ChessAI.MoveSelection.StateAnalysis +{ + public interface IMoveAnalyser + { + /** + * + * A method representing some logic to calculate the value of a move. + * This method should not be used to actually calculate the value of a potential state, as that is what the + * is for, but should give a rough estimate of how likely it is to be a good + * move to make. + * + * The current state before the move is applied + * The move that would be applied to the current state + * + * A numeric value that represents how good the move is likely to be. A positive value is in + * favor of this engine while a negative one is in favor of its opponent no matter their color + * + */ + int MoveAnalysis(GameState state, Move move); + + /** + * + * A dummy method representing some logic to sort the list of moves according to some simple + * fast logistic to increase the chance of greater cutoffs. + * Note that it sorts the list in place and therefore won't return anything. + * + * The current state before the move is applied + * A list of possible moves + * + * The best move at this point in an earlier run. + * This should always be the first move in the sorted list as it has a very + * high likelihood of being the best again + * + */ + void SortMovesByBest(GameState state, List moves, Move previousBest); + } +} \ No newline at end of file diff --git a/ChessAI/MoveSelection/StateAnalysis/IStateAnalyser.cs b/ChessAI/MoveSelection/StateAnalysis/IStateAnalyser.cs new file mode 100644 index 0000000..f4ec01e --- /dev/null +++ b/ChessAI/MoveSelection/StateAnalysis/IStateAnalyser.cs @@ -0,0 +1,17 @@ +using ChessAI.DataClasses; + +namespace ChessAI.MoveSelection.StateAnalysis +{ + public interface IStateAnalyser + { + /** + * A method representing some logic to calculate the value of the state + * The state that should be Analyses + * + * A numeric value that represents how good a state is. A positive value is in favor of this engine + * while a negative one is in favor of its opponent no matter their color + * + */ + int StaticAnalysis(GameState state /*todo may need an argument for who the engine is*/); + } +} \ No newline at end of file diff --git a/ChessAI/Program.cs b/ChessAI/Program.cs index 6ae5e68..6b15ab6 100644 --- a/ChessAI/Program.cs +++ b/ChessAI/Program.cs @@ -1,12 +1,9 @@ -using System; +using ChessAI.DataClasses; -namespace ChessAI -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello World!"); - } - } +namespace ChessAI{ + class Program { + static void Main(string[ ] args) { + var board = new Board(); + } + } } diff --git a/ChessBenchMarks/ChessBenchMarks.csproj b/ChessBenchMarks/ChessBenchMarks.csproj new file mode 100644 index 0000000..be66ede --- /dev/null +++ b/ChessBenchMarks/ChessBenchMarks.csproj @@ -0,0 +1,19 @@ + + + + Exe + netcoreapp5.0 + BenchMarks + true + + + + + + + + + + + + diff --git a/ChessBenchMarks/MoveSelectorBenchmark.cs b/ChessBenchMarks/MoveSelectorBenchmark.cs new file mode 100644 index 0000000..25b7a1c --- /dev/null +++ b/ChessBenchMarks/MoveSelectorBenchmark.cs @@ -0,0 +1,50 @@ +using System; +using BenchmarkDotNet.Attributes; +using ChessAI.DataClasses; +using ChessAI.MoveSelection; +using ChessAI.MoveSelection.MoveGeneration; +using ChessAI.MoveSelection.StateAnalysis; +using NUnit.Framework; +using UnitTests; + +namespace BenchMarks +{ + [MemoryDiagnoser] + public class MoveSelectorBenchmark + { + [Params(1, 2, 3, 4, 6)] //todo try deeper depths when they can be auto generated + public int Depth { get; set; } + [Params(0, 6, 12)] + public int InitialPathArraySize { get; set; } + + public MoveSelector MoveSelector; + + private IMoveAnalyser _moveAnalyser; + private IMoveCalculator _moveCalculator; + private IStateAnalyser _stateAnalyser; + + public MoveSelectorBenchmark() + { + //TODO switch out the interfaces with actual implementations once they are ready + var moveAndStateProvider = new MoveCalculatorStateAnalyserStub(); + _moveAnalyser = new MoveAnalyserStub(); + _moveCalculator = moveAndStateProvider; + _stateAnalyser = moveAndStateProvider; + } + + [GlobalSetup] + public void Setup() + { + MoveSelector = + new MoveSelector(true, _stateAnalyser, _moveAnalyser, _moveCalculator, InitialPathArraySize); + } + + [Benchmark] + public Move BestMove() => MoveSelector.BestMove(new GameState(), Depth); + + [Benchmark] + public Move BestMoveIterative() => + MoveSelector.BestMoveIterative(new GameState(), TimeSpan.FromSeconds(30), Depth); + + } +} \ No newline at end of file diff --git a/ChessBenchMarks/Program.cs b/ChessBenchMarks/Program.cs new file mode 100644 index 0000000..9eb849d --- /dev/null +++ b/ChessBenchMarks/Program.cs @@ -0,0 +1,14 @@ +using System; +using BenchmarkDotNet.Running; + +namespace BenchMarks +{ + class Program + { + static void Main(string[] args) + { + var summary = + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + } + } +} \ No newline at end of file diff --git a/Move.cs b/Move.cs new file mode 100644 index 0000000..67d3378 --- /dev/null +++ b/Move.cs @@ -0,0 +1,13 @@ +public struct Move{ + public byte from, to , pieceIndex; + public bool hasCaptured; + + public Move(byte from, byte to, byte pieceIndex, bool hasCaptured = false){ + this.from = from; + this.to = to; + this.pieceIndex = pieceIndex; + this.hasCaptured = hasCaptured; + } + + public Move(){} +} \ No newline at end of file diff --git a/MoveGen.cs b/MoveGen.cs new file mode 100644 index 0000000..7bc3b00 --- /dev/null +++ b/MoveGen.cs @@ -0,0 +1,9 @@ +public struct MoveGen{ + public list generateMoves( byte pieceType ){ + + } + + private list generateLineMoves(){ + + } +} \ No newline at end of file diff --git a/UML.png b/UML.png new file mode 100644 index 0000000..f31844f Binary files /dev/null and b/UML.png differ diff --git a/UML.uxf b/UML.uxf new file mode 100644 index 0000000..ea79ff2 --- /dev/null +++ b/UML.uxf @@ -0,0 +1,229 @@ + + + 9 + + UMLClass + + 540 + 351 + 117 + 90 + + Move +-- +from : byte +to : byte +hasCaptured:bool +pieceIndex:byte + + + + UMLClass + + 333 + 252 + 144 + 45 + + MoveGen + + + + UMLClass + + 288 + 351 + 117 + 45 + + State + + + + UMLClass + + 288 + 414 + 117 + 45 + + piece + + + + UMLClass + + 144 + 351 + 117 + 45 + + Board + + + + UMLClass + + 0 + 243 + 117 + 54 + + IMPCLOADR + + + + UMLClass + + 207 + 180 + 117 + 45 + + stateAnalyser + + + + UMLClass + + 360 + 180 + 117 + 45 + + MoveSelector + + + + UMLClass + + 576 + 252 + 117 + 45 + + MoveSorter + + + + Relation + + 603 + 288 + 27 + 81 + + lt=- + 10.0;10.0;10.0;70.0 + + + Relation + + 468 + 270 + 117 + 99 + + lt=- + 10.0;10.0;110.0;10.0;110.0;90.0 + + + Relation + + 396 + 360 + 162 + 27 + + lt=- + 160.0;10.0;10.0;10.0 + + + Relation + + 351 + 387 + 27 + 45 + + lt=- + 10.0;10.0;10.0;30.0 + + + Relation + + 252 + 369 + 54 + 27 + + lt=- + 40.0;10.0;10.0;10.0 + + + Relation + + 405 + 216 + 27 + 54 + + lt=- + 10.0;40.0;10.0;10.0 + + + Relation + + 468 + 189 + 162 + 81 + + lt=- + 10.0;10.0;160.0;10.0;160.0;70.0 + + + Relation + + 351 + 288 + 27 + 81 + + lt=- + 10.0;10.0;10.0;70.0 + + + Relation + + 315 + 189 + 63 + 27 + + lt=- + 50.0;10.0;10.0;10.0 + + + Relation + + 108 + 270 + 225 + 99 + + lt=- + 10.0;10.0;230.0;10.0;230.0;90.0 + + + Relation + + 108 + 216 + 171 + 63 + + lt=- + 10.0;50.0;170.0;50.0;170.0;10.0 + + diff --git a/UnitTests/DataClasses/BoardTests.cs b/UnitTests/DataClasses/BoardTests.cs new file mode 100644 index 0000000..b6d809e --- /dev/null +++ b/UnitTests/DataClasses/BoardTests.cs @@ -0,0 +1,29 @@ +using ChessAI.DataClasses; +using NUnit.Framework; + +namespace UnitTests.DataClasses +{ + public class BoardTests + { + [Test] + public void IsIndexValidTest() + { + for (byte i = 0; i < 0x10; i++) + { + for (byte j = 0; j < 0x10; j++) + { + + var index = (byte) (( i * 0x10 ) + j); // converts i and j to a combined index that should be correct + if (i < 8 && j < 8) + { + Assert.IsTrue(Board.IsIndexValid(index)); + } + else + { + Assert.IsFalse(Board.IsIndexValid(index)); + } + } + } + } + } +} \ No newline at end of file diff --git a/UnitTests/DataClasses/GameStateTests.cs b/UnitTests/DataClasses/GameStateTests.cs new file mode 100644 index 0000000..35ea39e --- /dev/null +++ b/UnitTests/DataClasses/GameStateTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using ChessAI.DataClasses; +using static ChessAI.DataClasses.Piece; +using NUnit.Framework; + + +namespace UnitTests.DataClasses +{ + public class GameStateTests + { + private readonly Piece _rook = new Piece(White | Rook, 0x00); + private readonly Piece _pawn = new Piece(White | Pawn, 0x02); + private readonly Piece _queen = new Piece(Black | Queen, 0x20); + + [Test] + public void GameStateConstructorTest() + { + var pieces = new[] { _rook, _pawn, _queen}.ToList(); + + var fields = new Piece[0x80]; + fields[_rook.Position] = _rook; + fields[_pawn.Position] = _pawn; + fields[_queen.Position] = _queen; + + var boardFromPieces = new Board(pieces); + var boardFromFields = new Board(fields); + + var stateFromPieces = new GameState(boardFromPieces); + var stateFromFields = new GameState(boardFromFields); + + AssertStatesDeepEqual(stateFromFields, stateFromPieces); + + Assert.AreEqual(1, stateFromFields.BlackPieces.Length); + Assert.Contains(_queen, stateFromFields.BlackPieces); + Assert.Contains(_queen, stateFromPieces.BlackPieces); + + Assert.AreEqual(2, stateFromPieces.WhitePieces.Length); + Assert.Contains(_rook, stateFromFields.WhitePieces); + Assert.Contains(_rook, stateFromPieces.WhitePieces); + Assert.Contains(_pawn, stateFromFields.WhitePieces); + Assert.Contains(_pawn, stateFromPieces.WhitePieces); + } + + [Test] + public void ApplyMoveTest() + { + var state0 = new GameState(new Board(new[] { _rook, _pawn, _queen }.ToList())); + + var move1 = Move.CreateSimpleMove(0x20, 0x22, state0); + var state1 = state0.ApplyMove(move1); + + var move2 = Move.CreateSimpleMove(move1.EndPos, move1.StartPos, state1); + var state2 = state1.ApplyMove(move2); + + AssertStatesDeepEqual(state0, state2); + + var move3 = Move.CreateSimpleMove(move2.EndPos, _rook.Position, state2); + var state3 = state2.ApplyMove(move3); + + Assert.AreEqual(1, state3.WhitePieces.Length); + Assert.Contains(_pawn, state3.WhitePieces); + } + + + private void AssertStatesDeepEqual(GameState state1, GameState state2) + { + //Test piece lists + Assert.AreEqual(state1.BlackPieces.Length, state2.BlackPieces.Length); + Assert.AreEqual(state1.WhitePieces.Length, state2.WhitePieces.Length); + + //Test that the same board was constructed + Assert.IsTrue(state1.State.Fields.SequenceEqual(state2.State.Fields)); + } + } +} \ No newline at end of file diff --git a/UnitTests/DataClasses/PieceTests.cs b/UnitTests/DataClasses/PieceTests.cs new file mode 100644 index 0000000..791d02d --- /dev/null +++ b/UnitTests/DataClasses/PieceTests.cs @@ -0,0 +1,42 @@ +using ChessAI.DataClasses; +using NUnit.Framework; +using static ChessAI.DataClasses.Piece; + +namespace UnitTests.DataClasses +{ + public class PieceTests + { + [Test] + public void CreatePiece() + { + Piece piece = new Piece( White | Bishop ); + + var expected = 0b1000 | Bishop; + Assert.IsTrue(expected == piece); + } + + [Test] + public void PieceToString() + { + var black = "Black"; + var white = "White"; + var pieces = new[] + { + "None", + "Pawn", + "Rook", + "Knight", + "Bishop", + "Queen", + "King", + }; + + + for (byte i = 1; i < 6; i++) + { + Assert.AreEqual(white + " " + pieces[i], new Piece(White | i).ToString()); + Assert.AreEqual(black + " " + pieces[i], new Piece(Black | i).ToString()); + } + } + } +} \ No newline at end of file diff --git a/UnitTests/MinMaxTests.cs b/UnitTests/MinMaxTests.cs new file mode 100644 index 0000000..bf3c5c8 --- /dev/null +++ b/UnitTests/MinMaxTests.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ChessAI; +using ChessAI.DataClasses; +using ChessAI.MoveSelection; +using ChessAI.MoveSelection.MoveGeneration; +using ChessAI.MoveSelection.StateAnalysis; +using NUnit.Framework; + +namespace UnitTests +{ + public class MinMaxTests + { + [Test] + public void BestMoveTest() + { + var moveAndState = new MoveCalculatorStateAnalyserStub(); + var moveSelector = + new MoveSelector(true, moveAndState, new MoveAnalyserStub(), moveAndState); + + Assert.AreEqual("b", moveSelector.BestMove(new GameState(), 3 )); + Assert.AreEqual(new[] { "b", "g", "s" }, moveSelector.BestMoves); + } + + [Test] + public void BestMoveIterativeTest() + { + var moveAndState = new MoveCalculatorStateAnalyserStub(); + var moveSelector = + new MoveSelector(true, moveAndState, new MoveAnalyserStub(), moveAndState); + + Assert.AreEqual( + "b", + moveSelector.BestMoveIterative(new GameState(), TimeSpan.FromSeconds(2), 3) + ); + + var expectedPath = new[] { "b", "g", "s" }; + for (int i = 0; i < expectedPath.Length; ++i) + { + Assert.AreEqual(expectedPath[i], moveSelector.BestMoves[i]); + } + } + } + + public class MoveAnalyserStub : IMoveAnalyser + { + public int MoveAnalysis(GameState state, Move move) + { + return 0; + } + + public void SortMovesByBest(GameState state, List moves, Move previousBest) + { + moves.Sort((s, s1) => + { + if (s.Equals(previousBest)) + { + return 1; + } + else if (s1.Equals(previousBest)) + { + return -1; + } + + return (MoveAnalysis(state, s) - MoveAnalysis(state, s1)) % 1; + } + ); + } + } + + public class MoveCalculatorStateAnalyserStub : IMoveCalculator, IStateAnalyser + { + private Dictionary _tree; + + public MoveCalculatorStateAnalyserStub() + { + // _tree = new Dictionary() + // { + // { new Move(), (5, new[] { "a", "b", "c" }) }, + // { "a", (4, new[] { "d", "e", "f" }) }, + // { "d", (4, new[] { "l" }) }, + // { "l", (4, Array.Empty()) }, + // { "e", (6, new[] { "m", "n", "o" }) }, + // { "m", (6, Array.Empty()) }, + // { "n", (2, Array.Empty()) }, + // { "o", (6, Array.Empty()) }, + // { "f", (9, new[] { "p", "q" }) }, + // { "p", (3, Array.Empty()) }, + // { "q", (9, Array.Empty()) }, + // { "b", (5, new[] { "g", "h" }) }, + // { "g", (5, new[] { "s", "t" }) }, + // { "s", (5, Array.Empty()) }, + // { "t", (2, Array.Empty()) }, + // { "h", (7, new[] { "u", "v" }) }, + // { "u", (7, Array.Empty()) }, + // { "v", (3, Array.Empty()) }, + // { "c", (1, new[] { "i", "j", "k" }) }, + // { "i", (1, new[] { "w" }) }, + // { "w", (1, Array.Empty()) }, + // { "j", (7, new[] { "x", "y" }) }, + // { "x", (7, Array.Empty()) }, + // { "y", (2, Array.Empty()) }, + // { "k", (6, new[] { "z", "aa", "ab" }) }, + // { "z", (4, Array.Empty()) }, + // { "aa", (6, Array.Empty()) }, + // { "ab", (3, Array.Empty()) } + // }; + // TODO re implement stub + } + + public MoveCalculatorStateAnalyserStub(Dictionary nodeMap) + { + _tree = nodeMap; + } + + public List CalculatePossibleMoves(GameState state, bool calculateForWhite) + { + return new List(); //_tree[state.State].Item2.ToList(); + } + + public int StaticAnalysis(GameState state) + { + return 0; //_tree[state.State].Item1; + } + } +} \ No newline at end of file diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj new file mode 100644 index 0000000..abb57b8 --- /dev/null +++ b/UnitTests/UnitTests.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp5.0 + + false + + + + + + + + + + + + + +