Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement complex move types #12

Closed
wants to merge 42 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1ad5d3b
Add projects for tests and benchmarking
Goju-Ryu Oct 26, 2021
3ba148e
Added examples of use for unittesting and benchmarking
Goju-Ryu Oct 26, 2021
b378e31
Merge pull request #1 from Goju-Ryu/setup-testptrojects
Goju-Ryu Oct 26, 2021
be64bbf
Add an implementation of minMax using servaral dummy methods
Goju-Ryu Oct 27, 2021
3e3cdb6
Refactor functions used by MinMax into interfaces for easy testing
Goju-Ryu Oct 28, 2021
769219e
Implement test for MoveSelector.BestMove
Goju-Ryu Oct 28, 2021
41423c6
Implement an iterative version of BestMove
Goju-Ryu Oct 28, 2021
eecf492
Fix error cause by the wrong Assert method being used
Goju-Ryu Oct 28, 2021
b523b4f
Make BestMoveIterative leave the BestMoves array in a valid state whe…
Goju-Ryu Oct 28, 2021
d00b877
Add benchmark of the BestMove method
Goju-Ryu Oct 29, 2021
4d89807
Add benchmark of BestMoveIterative
Goju-Ryu Oct 29, 2021
5f2efc7
Merge pull request #2 from Goju-Ryu/experiment-minMaxAlgorithm
Goju-Ryu Nov 1, 2021
726f16e
Add preliminary state implementations
GrobaxGrappleBlast Nov 9, 2021
7702e88
Add preliminary state implementations
GrobaxGrappleBlast Nov 9, 2021
12d71c4
Fix rename of Namespace I missed earlier
Goju-Ryu Nov 9, 2021
70e82fe
Rename files to fix capitalization
Goju-Ryu Nov 9, 2021
e74cfcd
Refactor each class/struct/interface into its own file in an appropri…
Goju-Ryu Nov 10, 2021
29ff6b6
Add a preliminary Piece implementation that uses a Flag eneum
Goju-Ryu Nov 11, 2021
87a1b97
Add Board implementation with limited number of helper functions
Goju-Ryu Nov 15, 2021
4916f7b
Replace the Flags enum based piece implementation with a custom struct
Goju-Ryu Nov 15, 2021
a4f53df
creating Moves Generator
GrobaxGrappleBlast Nov 16, 2021
5f0cf93
implementing Moves for horse
GrobaxGrappleBlast Nov 16, 2021
4476603
Merge pull request #4 from Goju-Ryu/setup-projectStructure
GrobaxGrappleBlast Nov 16, 2021
0b2bf13
Implement basic Board Methods
Goju-Ryu Nov 16, 2021
c1ee4f6
Implement some GameState methods
Goju-Ryu Nov 16, 2021
aa2bd6d
Replace Interface string types Move types
Goju-Ryu Nov 16, 2021
9d4496d
Remove old piece implementation
Goju-Ryu Nov 16, 2021
e1d4762
commit before branch change
GrobaxGrappleBlast Nov 16, 2021
7892bc1
Merge pull request #5 from Goju-Ryu/implement-data-types
GrobaxGrappleBlast Nov 16, 2021
f7582d9
Resolve merg. NOT COMPILEABLE
Goju-Ryu Nov 16, 2021
b425789
MoveCalculator now returns an Empty list, for Connectivity purposes, …
GrobaxGrappleBlast Nov 16, 2021
f43d74f
Fix error that prevented compilation by pumping the .net version
Goju-Ryu Nov 17, 2021
0132889
Add Equals method to Piece
Goju-Ryu Nov 17, 2021
9fe654a
Add position to the pieces
Goju-Ryu Nov 17, 2021
e000717
Implement piece lists for white and black
Goju-Ryu Nov 17, 2021
c1d571d
Fix erronious array length check in the Board(Piece[]) constructor)
Goju-Ryu Nov 18, 2021
ae68975
Add constructor Piece(int flags)
Goju-Ryu Nov 18, 2021
83cfdb8
Fix bug where Board would expose a mutable array
Goju-Ryu Nov 19, 2021
0719d6d
Add direction offsets for the two sides as public property of Board
Goju-Ryu Nov 19, 2021
7c68baa
Add properties to move that lets it represent a wider range of move t…
Goju-Ryu Nov 19, 2021
c1f9583
Add a MoveType to move and use that instead of is special
Goju-Ryu Nov 19, 2021
5d60fe3
Fix the ApplyMove method in GameState
Goju-Ryu Nov 19, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ChessAI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
3 changes: 2 additions & 1 deletion ChessAI/ChessAI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

</Project>
124 changes: 124 additions & 0 deletions ChessAI/DataClasses/Board.cs
Original file line number Diff line number Diff line change
@@ -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<T>

/// <summary>
/// A constructor taking an array of pieces representing the board. Note that the array length must
/// be consistent with an 0x88 board.
/// </summary>
/// <param name="fields">an array representing the board</param>
/// <exception cref="ArgumentException">thrown if the given array is not of the right length</exception>
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;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="pieceList">A list of pieces with valid positions</param>
public Board(List<Piece> 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<Piece> 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,
};

}
92 changes: 92 additions & 0 deletions ChessAI/DataClasses/GameState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using static ChessAI.DataClasses.Piece;

namespace ChessAI.DataClasses
{
/**
* <summary>
* A struct that keeps track of all relevant information about a game
* </summary>
*/
public readonly struct GameState
{
/// <summary>
/// An easy to use constructor taking only a board.
/// </summary>
/// <param name="board">The board that should be represented by this state</param>
/// <remarks>
/// This implementation is slow as it has to construct a PieceList itself.
/// Use other constructors in time critical sections of code.
/// </remarks>
public GameState(Board board)
{
State = board;

Span<Piece> tempBoardFields = stackalloc Piece[State.Fields.Length];
State.Fields.CopyTo(tempBoardFields);

List<Piece> tempWhitePieceList = new List<Piece>();
List<Piece> tempBlackPieceList = new List<Piece>();

// 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;


/**
* <summary>A dummy method representing some logic to calculate the applying a move to the state</summary>
* <param name="move">The move that should be applied to a state</param>
* <returns>A new <see cref="GameState"/> with the move applied</returns>
*/
public GameState ApplyMove(Move move)
{
Span<Piece> 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;
}
}
}
141 changes: 141 additions & 0 deletions ChessAI/DataClasses/Move.cs
Original file line number Diff line number Diff line change
@@ -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;


/// <summary>
/// a simple constructor taking the start and end position.
/// </summary>
/// <param name="startPos">starting position</param>
/// <param name="endPos">destination</param>
/// <remarks>
/// 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 <see cref="CreateSimpleMove"/> 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.
/// </remarks>
[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;
}

/// <summary>
/// A constructor that takes all the required parameters to define a move
/// </summary>
/// <param name="startPos"></param>
/// <param name="endPos"></param>
/// <param name="isSpecialMove"></param>
/// <param name="movePiece"></param>
/// <param name="targetPiece"></param>
private Move(byte startPos, byte endPos, MoveType moveType, Piece movePiece, Piece targetPiece)
{
StartPos = startPos;
EndPos = endPos;
MoveType = moveType;
MovePiece = movePiece;
TargetPiece = targetPiece;
}

/// <summary>
/// A factory method that creates a simple move where no special rules are involved.
/// </summary>
/// <param name="startPos">The position from which the movePiece originates</param>
/// <param name="endPos">The destination of the movePiece</param>
/// <param name="state">The state of the game at the time of the move</param>
/// <returns>A new move from startPos to endPos</returns>
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;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="castlePosition">The position of the rook that is involved in the castling</param>
/// <param name="state">The state of the game at the time of the move</param>
/// <returns>
/// A new castling move that represents the king taking the castlePosition and the
/// occupying rook being moved in the appropriate direction
/// </returns>
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;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="startPosition">The starting position of the pawn</param>
/// <param name="endPos">
/// The destination of the pawn. note that this may also occur through an attack
/// which opens the possibility of three different end positions
/// </param>
/// <param name="promotionPiece">The piece the pawn should be promoted to</param>
/// <param name="state">The state of the game at the time of the move</param>
/// <returns>
/// A new pawn promotion move that represents a pawn moving to the 8th rank, being promote to the promotionPiece
/// </returns>
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
}
}
Loading