I'm creating a chess engine and I'm currently in the stage of adding validation. I'm starting off with the major validationrule: checking if a piece can move to the given target destination.
I have setup a system that allows me to very fluently define how a piece can move and validation is done on these rules.
An example for a Pawn
:
public class Pawn : Piece {
public Pawn(Color color)
: base(color) {
Name = ChessPiece.Pawn;
Worth = 1;
}
public override List<Move> GetPossibleMoves() {
return new List<Move>
{
new Move
{
Amount = 1,
Direction = Direction.Forwards,
},
new Move
{
Amount = 2,
Direction = Direction.Forwards,
}
};
}
}
Or a Rook
:
public override List<Move> GetPossibleMoves() {
return new List<Move>
{
new Move
{
Amount = 7,
Direction = Direction.Vertical,
},
new Move
{
Amount = 7,
Direction = Direction.Horizontal,
},
};
}
My Tests to check the code:
[TestMethod]
public void AllowedPawnMoveValidatorWithValidInput() {
var chessboard = _testCaseProvider.GetChessboardForPawnMoves();
var validator = new AllowedMoveValidator();
// Test if a few pawns can move 1 spot ahead
foreach (var location in chessboard.ToList().Where(x => x.Piece is Pawn)) {
var endLocation = new Location {
X = location.X,
Y = location.Piece.Color == Color.White ? location.Y + 1 : location.Y - 1,
Piece = null
};
Assert.AreEqual(ValidationResult.Allowed, validator.Validate(chessboard, location, endLocation), "Move pawn 1 spot ahead. X = " + location.X + "; Y = " + location.Y);
}
// Test if a few pawns can move 2 spots ahead
foreach (var location in chessboard.ToList().Where(x => x.Piece is Pawn)) {
var endLocation = new Location {
X = location.X,
Y = location.Piece.Color == Color.White ? location.Y + 2 : location.Y - 2,
Piece = null
};
Assert.AreEqual(ValidationResult.Allowed, validator.Validate(chessboard, location, endLocation), "Move pawn 2 spots ahead. X = " + location.X + "; Y = " + location.Y);
}
}
[TestMethod]
public void AllowedPawnMoveValidatorWithInValidInput() {
var chessboard = _testCaseProvider.GetChessboardForPawnMoves();
var validator = new AllowedMoveValidator();
// Test if a few pawns can move 1 spot ahead
foreach (var location in chessboard.ToList().Where(x => x.Piece is Pawn)) {
// Too far ahead
var endLocation = new Location {
X = location.X,
Y = location.Piece.Color == Color.White ? location.Y + 3 : location.Y - 3,
Piece = null
};
Assert.AreEqual(ValidationResult.Forbidden, validator.Validate(chessboard, location, endLocation), "Move pawn 1 spot ahead. X = " + location.X + "; Y = " + location.Y);
// Changes column
endLocation = new Location {
X = location.X + 2,
Y = location.Piece.Color == Color.White ? location.Y + 1 : location.Y - 1,
Piece = null
};
Assert.AreEqual(ValidationResult.Forbidden, validator.Validate(chessboard, location, endLocation), "Move pawn 2 spots ahead. X = " + location.X + "; Y = " + location.Y);
}
}
[TestMethod]
public void AllowedRookMoveValidatorWithValidInput() {
var chessboard = _testCaseProvider.GetChessboardForRookMoves();
var validator = new AllowedMoveValidator();
foreach (var location in chessboard.ToList().Where(x => x.Piece is Rook)) {
// Horizontal movement
var endLocation = new Location {
X = location.X >= 5 ? 2 : 8,
Y = location.Y,
Piece = null
};
Assert.AreEqual(ValidationResult.Allowed, validator.Validate(chessboard, location, endLocation), "Move rook horizontally. X = " + location.X + "; Y = " + location.Y);
// Vertical movement
endLocation = new Location {
X = location.X,
Y = location.Y >= 5 ? 1 : 7,
Piece = null
};
Assert.AreEqual(ValidationResult.Allowed, validator.Validate(chessboard, location, endLocation), "Move rook vertically. X = " + location.X + "; Y = " + location.Y);
}
}
The actual validation:
public class AllowedMoveValidator : IMoveValidator {
public ValidationResult Validate(HashSet<Location> board, Location start, Location end) {
var piece = start.Piece;
var possibleMoves = piece.GetPossibleMoves();
foreach (var move in possibleMoves) {
if (piece.Color == Color.White) {
if (move.Direction == Direction.Vertical) {
if (move.Orientation == Orientation.Forwards) {
if ((end.Y <= start.Y + move.Amount) && end.X == start.X) {
return ValidationResult.Allowed;
}
} else if (move.Orientation == Orientation.Downwards) {
if ((end.Y >= start.Y - move.Amount) && end.X == start.X) {
return ValidationResult.Allowed;
}
} else if (move.Orientation == Orientation.BothForwardsAndDownwards) {
if (((end.Y >= start.Y - move.Amount) || (end.Y <= start.Y + move.Amount)) && end.X == start.X) {
return ValidationResult.Allowed;
}
} // End Vertical Direction
}
} else if (piece.Color == Color.Black) {
if (move.Direction == Direction.Vertical) {
if (move.Orientation == Orientation.Forwards) {
if ((end.Y >= start.Y - move.Amount) && end.X == start.X) {
return ValidationResult.Allowed;
}
} else if (move.Orientation == Orientation.Downwards) {
if ((end.Y <= start.Y + move.Amount) && end.X == start.X) {
return ValidationResult.Allowed;
}
} else if (move.Orientation == Orientation.BothForwardsAndDownwards) {
if (((end.Y >= start.Y - move.Amount) || (end.Y <= start.Y + move.Amount)) && end.X == start.X) {
return ValidationResult.Allowed;
}
} // End Vertical Direction
}
}
if (move.Direction == Direction.Horizontal) {
if (move.Orientation == Orientation.Sideways) {
if ((end.X < start.X) && end.Y == start.Y) {
if (end.X <= start.X - move.Amount) {
return ValidationResult.Allowed;
}
} else if ((end.X > start.X) && end.Y == start.Y) {
if (end.X <= start.X + move.Amount) {
return ValidationResult.Allowed;
}
}
} // End Horizontal Direction
}
if (move.Direction == Direction.Diagonal) {
if (move.Orientation == Orientation.BothForwardsAndDownwards) {
// TODO
}
}
} // End loop
return ValidationResult.Forbidden;
}
}
How would you advice I rewrite the validation? The way it is right now works (I have only tested it with pawns but I expect it to work for any other piece as well). Diagonally hasn't been implemented yet. There is a lot of duplication of the validation code because
White -> Forwards
is the same as
Black -> Backwards
But this would require for every move to check if it's black or white which doesn't look like a big improvement.
Suggestions?
Thanks to the wonderful suggestion by Michael Shaw that one of enums is in fact obsolete, I have been able to slim down the code tremendeously. Below you will find the current code I'm working with (all tests pass). I believe it has been simplified a lot, it's definitely easier to work with.
I will also swap my earlier versions of the test with a better formatted one. It takes a lot of unnecessary space, you can always look it up in the post history.
public class AllowedMoveValidator : IMoveValidator {
public ValidationResult Validate(HashSet<Location> board, Location start, Location end) {
var piece = start.Piece;
var possibleMoves = piece.GetPossibleMoves();
foreach (var move in possibleMoves) {
// Vertical moves - Rooks, Queens, Kings
if (move.Direction == Direction.Vertical) {
if (((end.Y >= start.Y - move.Amount) || (end.Y <= start.Y + move.Amount)) && end.X == start.X) {
return ValidationResult.Allowed;
}
}
// Forward moves - Pawns
if (move.Direction == Direction.Forwards) {
if (end.X == start.X) {
if (piece.Color == Color.White) {
if (end.Y <= start.Y + move.Amount) {
return ValidationResult.Allowed;
}
} else if (piece.Color == Color.Black) {
if (end.Y >= start.Y - move.Amount) {
return ValidationResult.Allowed;
}
}
}
}
// Horizontal moves - Rooks, Queens, Kings
if (move.Direction == Direction.Horizontal) {
if (end.Y == start.Y) {
if (end.X < start.X) {
if (end.X >= start.X - move.Amount) {
return ValidationResult.Allowed;
}
} else if (end.X > start.X) {
if (end.X <= start.X + move.Amount) {
return ValidationResult.Allowed;
}
}
}
}
// Diagonal moves - Bishops
if (move.Direction == Direction.Diagonal) {
// TODO
}
}
return ValidationResult.Forbidden;
}
}
Current Move
definition and other enums:
public enum Direction {
Horizontal,
Vertical,
Forwards,
Diagonal
}
public class Move {
public Direction Direction { get; set; }
public int Amount { get; set; }
}
public enum Color {
Black,
White
}
Color
is a selfdefined enum ;) \$\endgroup\$System.Drawing.Color
or a custom enum? (Ok good!) \$\endgroup\$