4
\$\begingroup\$

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
}
\$\endgroup\$
7
  • \$\begingroup\$ I have restructured the code a little. Since the horizontal and diagonal moves will not be influenced by black or white I have moved them outside the loop. The code is already easier to read, but it still doesn't feel entirely right. \$\endgroup\$ Commented Nov 19, 2013 at 22:25
  • \$\begingroup\$ Color is a selfdefined enum ;) \$\endgroup\$ Commented Nov 19, 2013 at 23:24
  • \$\begingroup\$ I'll get back to this post, but for now I'll say that having the concept of color in your validation is rather annoying. And are you using System.Drawing.Color or a custom enum? (Ok good!) \$\endgroup\$ Commented Nov 19, 2013 at 23:27
  • \$\begingroup\$ Could you include the listings for your custom enums? Thanks! \$\endgroup\$ Commented Nov 20, 2013 at 0:15
  • 1
    \$\begingroup\$ @retailcoder: Yeah, I've simply added the new version of my validator to the end. I figured I could overwrite the tests because all that changed was a little formatting (which is still reflected in Michael's post since he copied the original). \$\endgroup\$ Commented Nov 20, 2013 at 0:22

2 Answers 2

5
\$\begingroup\$

There are 33 if statements in your validation, all highly nested. There are lots of ways you could deal with that; what immediately comes to mind are helper functions. Don't be afraid to use helper functions, especially if you can give them a good, descriptive name.

For instance, you could replace this:

        if (piece.Color == Color.White) {
          // lots of stuff
        } else if (piece.Color == Color.Black) {
          // lots of other stuff
        }

with this:

        if (piece.Color == Color.White) {
          moveWhite();
        } else {
          moveBlack();
        }

and keep doing that for subdecisions.

Looking at moveWhite() and moveBlack(), it seems like we're duplicating effort; move(Color) would be better. This would help prevent problems where there's a bug in one branch but not in the other.

After looking at some of the nested ifs, it looks like a decision table might help, if not solve the whole problem. The idea is to make a (possibly multidimensional) array, and index into it to find the solution. So you could do something like decisionTable[White][Vertical][Forwards] and get back a function (or an object with a method named .decide()) that you could give start, end, and move, and it would tell you whether it validates.

Some nitpicks:

In the definition of a pawn, you have an amount, which is an integer. That seems like it will be problematic when it comes to pieces which can cross the board in a single move. You also have both a direction and an orientation for the move, which seems duplicative. Either get rid of one, or find a better name for one, so that it's clear why we need both.

Making forwards for white different from forwards for black seems odd. Why not make forwards a single direction that applies differently to each color? Conceptually, pawns move forwards whether they're white or black.

Orientation.BothForwardsAndDownwards is a bit wordy and not quite clear.

This:

            Assert.AreEqual(ValidationResult.Forbidden,
                                                validator.Validate(chessboard,
                                                                    location,
                                                                    new Location {
                                                                        X = location.X,
                                                                        Y = location.Piece.Color == Color.White ? location.Y + 3 : location.Y - 3,
                                                                        Piece = null
                                                                    }
                                ));

seems like it could be better formatted like this:

            Assert.AreEqual(ValidationResult.Forbidden,
                            validator.Validate(chessboard,
                                               location,
                                               new Location {
                                                 X = location.X,
                                                 Y = location.Piece.Color == Color.White 
                                                     ? location.Y + 3 
                                                     : location.Y - 3,
                                                 Piece = null
                                               })
                          );

I'm not entirely sure that's an idiomatic C# way of formatting it, but it doesn't run off the page as much.

\$\endgroup\$
5
  • \$\begingroup\$ I'm rather hesitant to extract everything into different methods because this would rarely add any additional value. Most of the checks are 2 lines of code, including the if statement so the gain would be minimal. In fact it would be cumbersome because I'd have to check every method if I wanted to advance trough the tree to check for bugs. I just fixed a bug with horizontal movement (rooks now work, yay!) and I must say that finding a fix went fluently. \$\endgroup\$ Commented Nov 19, 2013 at 23:50
  • \$\begingroup\$ The decision table looks very interesting though, I'll have to look into that. How is this typically implemented? Or is it more a mathematical concept? Could you clarify how the int amount causes issues? An amount of 2 should go two to the side and two to the front/back, there is an easy correlation. Good point about direction/orientation, I'll look into revising this. Forwards for white/black differ in the addition or subtraction of the amount. I have formatted the code by putting the new location in a separate variable. I appreciate your feedback! \$\endgroup\$ Commented Nov 19, 2013 at 23:55
  • 1
    \$\begingroup\$ If you can give 2 lines of code a clear, simple name, it would still be worth extracting them to a helper function. I was thinking more of extracting larger chunks of nested stuff, though, especially if they're repeated. If you go with helper functions, the idea is to extract bits that make sense as a standalone unit, so to verify stuff works, you don't go down the tree looking for bugs, you look at individual functions, separately verify them, and then verify that the logic as a whole (using the functions) works. \$\endgroup\$ Commented Nov 20, 2013 at 1:36
  • \$\begingroup\$ Decision tables are a generic idea (and an old one). It's usually implemented as an array of some kind, with the indices being what you want to decide based on, and the entries being what the decision is. I suggested having a bit of code in the entries, because I was assuming that there would be a lot of values for end.X, start.Y, move.Amount and so forth; if that's not the case (or things can be changed so that's not the case) then the table could just hold True or False. \$\endgroup\$ Commented Nov 20, 2013 at 1:56
  • \$\begingroup\$ I was thinking amount would cause issues with rooks and knights, but looking at some other comments, it looks like you got around any possible issues. \$\endgroup\$ Commented Nov 20, 2013 at 1:57
4
\$\begingroup\$

Let's recap what we have here:

  • We have a Pawn which is a Piece that has a Color and a Worth, that knows what its possible Moves are.
    • I don't see Name nor Color defined as properties of your Pawn, so I'm assuming they're defined in the Piece class. If that's the case, you should pass them to the base constructor and make them readonly - it's not like a Pawn ever turns into a Queen right? (no, it shouldn't - if a Pawn gets to the end of the board you should spawn a new Queen and kill the Pawn).
    • If Worth relates to how many points a piece is worth, I think Value or even ScoreValue could be a better name.
    • The way you have it, it looks like every move can be 1 or 2 steps forward, which is wrong. Your Pawn is missing some bool _hasPlayedFirstMove so that GetPossibleMoves() can only return the 1-step forward move whenever that flag is true.
  • We have a Move which has a Direction and an Amount.

    • Such a small class could affort to be a struct.
    • I wonder how the Knight : Piece will move... as a matter of fact you seem to have saved the best for last, right? It doesn't look like your code can actually support that piece very well, unless you added a couple more values to your Direction enum - consider using these values:

      • Direction.Forward
      • Direction.Backward
      • Direction.Left
      • Direction.Right
      • Direction.ForwardLeft
      • Direction.ForwardRight
      • Direction.BackwardLeft
      • Direction.BackwardRight
      • Direction.KnightForwardLeft
      • Direction.KnightForwardRight
      • Direction.KnightLeftForward
      • Direction.KnightLeftBackward
      • Direction.KnightRightForward
      • Direction.KnightRightBackward
      • Direction.KnightBackwardLeft
      • Direction.KnightBackwardRight
  • Finally we have an AllowedMoveValidator with a Validate method that takes the game board and a start and end locations.

    • I find it's a bit conceptually awkward that a Location has a Piece. I would have thought a Piece has a Location, which means I would have taken the board along with a Piece and a target Location for parameters.
    • As I hinted in the comments, I don't think the color is relevant here. The easy-and-not-so-efficient way out would be to literally flip the board so that Forward mean the same thing whether you're playing a black or a white piece.

Now to address the main issue here, which is the atrocious nesting of conditionals (seriously: foreach/if/if/if/if), I think it's worth noting that you're always branching on enum values. This looks like a job for a Dictionary<Direction, Action>, where Action points to a method that does one thing: move a piece in a very specific direction.

\$\endgroup\$
6
  • \$\begingroup\$ Some great remarks! The Worth and Name are defined in the constructor of the subclass and uses a protected set; in the base class. I definitely overlooked pawns only being able to make two moves when they're at their start position, thanks! The knight will not use this method of move validation. Instead, I will create a separate validator that will hardcodedly check for the up-to-8 spots a knight can go to. It's only one piece and I don't expect the chess rules to change anytime soon. \$\endgroup\$ Commented Nov 20, 2013 at 1:10
  • \$\begingroup\$ I was under serious doubt with my approach of Location -> Piece or Piece -> Location. At first I wanted to do it the way you describe, but changed afterwards for a different reason. This reason became obsolete after I went with a different validation approach (the validators I have now). It's a matter of perspective: either you say that a piece has a location or you say that a board contains locations where pieces might be on. The way it is now works and I don't think I will gain anything by changing the perspective. Your last suggestion is spot on. Once everything works, I will refactor it. \$\endgroup\$ Commented Nov 20, 2013 at 1:14
  • \$\begingroup\$ (RE: first comment) Right, but then you'll duplicate lots of code doing that - moving the knight is easy once you know you want it to go KnightForwardLeft: move it Forward x1 and then ForwardLeft x1. If you have methods that do one thing this should be a piece of cake :) \$\endgroup\$ Commented Nov 20, 2013 at 1:15
  • 1
    \$\begingroup\$ Regarding the Directions, why not let a Direction be two integers: deltaX, deltaY? Having 16 possible values for a Direction doesn't seem clean to me. \$\endgroup\$ Commented Nov 20, 2013 at 13:08
  • \$\begingroup\$ @SimonAndréForsberg the thought occurred to me, but that means a complete redesign of Move.. and I thought it'd be good enough to start with directions that support a Knight. \$\endgroup\$ Commented Nov 20, 2013 at 13:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.