5

I am writing a library to allow two people to play chess for a toy project. For what is worth, it is in C++.

I have deemed useful to have a class named Move, which represents a move which needs to be annotated in the history of the game. Objects of this class would provide the same information as an annotated move in a traditional sheet: whether a piece has been captured, the origin and destination square of the piece being moved, whether the move comes with an offer to draw, whether there is check or checkmate... Part of the reason I want to store this information is to make life easier to applications using this library in the future, so that they need to calculate almost nothing (they can animate a move by seeing which piece moved from where to where, they don't need to verify if a move resulted in check themselves but can simply ask, etc.).

However, moves in chess not always involve moving a single piece from one square to another:

  • Promoting a pawn involves moving the pawn and specifying into which piece it is going to convert.
  • Castling implies moving two pieces of the same color: the king and a rook.
  • Resigning does not involve moving any pieces.
  • Accepting a draw does not involve moving any pieces.

Hence, I believe the class Move is starting to smell a bit. I want to keep a list of all moves as they would be annotated in a game "over the board" (that includes draw offers, resignations and accepting draws). For that, it makes sense that all annotable moves are of the class Move.

Option 1

On the one hand, having all possible moves be of type Move (no specialization) achieves that. On the other, doing that would mean that there are potentially a bunch of methods which do not make sense for certain objects, and the number of potentially invalid states skyrockets. For example, if a move represents the acceptance of a draw, it seems iffy to be able to ask which piece moved where; if a move is a resignation, it seems iffy to be able to ask to which piece the moving pawn was promoted to (there is no moving pawn!), etc.

Option 2

An alternative I can think of would mean creating an abstract class Move from which all different moves would specialize (PawnPromotionMove, RegularMove, ResignationMove, AcceptDrawMove). Move would only contain the methods which are truly common to all kinds of moves: toString(), which would return the string representation of the move (in algebraic notation); and typeMove(), which would allow the user to know the underlying type of the object. The Move object could then be downcasted to its actual type, which would offer only the relevant methods.

The drawback of this is that it would require downcasting and breaks the abstraction.

Question

This really looks like a case of "If it looks like a duck and quacks like a duck but it needs batteries, you probably have the wrong abstraction.", but I can't figure a good way out of this. Are there any reasonable ways outside of these two?

10
  • I guess what you need to think about is what the users are going to use the Move class for, and how exactly are they going to do it. E.g. if they are going to create the concrete subclasses themselves, then there's no need for downcasting (the users already know the concrete type); your framework can still use them through the abstract interface. If your framework doesn't use these classes in an abstract way, then there's no need to have them all be a part of the same hierarchy. Jun 20 '21 at 17:16
  • For Option 1, if you ask which piece moved where for a type of "move" that does not involve moving a piece, you could return a value to represent "none" or "no move made". Same for the pawn promotion - if there was not a promotion you can return "none". Jun 20 '21 at 18:08
  • ^Oh, yeah, I wanted to say something about that too; you've said "moves in chess not always involve moving a single piece from one square to another, [and there are moves that do] not involve moving any pieces". If the chess community accepts the term, and gives a domain-specific meaning to it, why not adopt it for your library? Assume that your library's users will either be familiar with chess terminology, or willing to lean it, since they are programming a chess-related application. Jun 20 '21 at 18:38
  • 1
    The other good thing about a Move class is the undo/redo list is easy. Jun 20 '21 at 21:35
  • @FilipMilovanović I was thinking about the use a user might give to the class... I wanted to make the class a versatile as possible to cover all possible uses (when the user is given a move, it could use it to animate a move, or to get the move's string representation; when the user creates a move, it wants to express something that they want to happen in the game). Probably the right solution is not to have a Move class which can do everything, but instead make sure I give the user enough tools to do everything with minimal effort, potentially through different classes. Jun 21 '21 at 11:15
2

The inheritance hierarchy from option 2 looks like a good start for me. However, I don't see any compelling reason why this must lead to any downcasting. Instead, one could provide virtual methods in the class Move like

  • vector<PairOfSquare> getFromToSquares() : delivers one element for a regular move, zero for "resignation" or "accept draw", and two for castling. Alternatively, one could use a more "behavioural" implementation and provide a method void drawPieces(Board &board) in the move class, which draws either zero, one or two pieces.

  • PieceType convert(): returns the converted type for pawn promotion, and the current piece type for any other type. Alternatively, make the conversion part of drawPieces.

  • bool resign(): returns true for a "resignment move", and false for every other type of move.

(there will probably be more of these methods, but I think you got the idea).

Moreover, don't forget the reason behind avoiding downcasts. They become problematic when the abstract Move class lives in one black-box library, with several functions based on moves, and the different specializations are mostly detached from it, maybe in another lib, and the list of specializations might be extended in the future.

In this case, however, the rules of chess are fixed, if you don't plan to turn this into a game of experimental chess rule variations. Hence the number of derived clases will be pretty finite and not change after the chess rules are implemented completely. I would also expect the Move class to stay in the hands of the same maintainers as its derivations. In such a situation downcasting is usually acceptable, for example, when it is required to deal with some exceptional cases in the the rules.

1

If it looks like a duck and quacks like a duck but it needs batteries, you probably have the wrong abstraction

Haha! Lets start from there :)

The Moves you describe are not moves, but Actions (with an action implying zero, one or two moves - in the case of castling).

Your players can take one Action at a time, an Action needs to be recorded in the gameplay, and an Action can translate to zero, one or more Moves (depending on how you see castling and promoting a piece).

In such a case, an action would belong to a player (be emitted by a player) and a move would belong to the board (and be received by the board).

In terms of DDD, an action and a move are in different domains.

Your move examples would translate to:

class Action
{
public:
    virtual vector<unique_ptr<Move>> moves() = 0;
};

class PawnPromotion final: public Action
{
public:
    vector<unique_ptr<Move>> moves() override;
};

The Move object could then be downcasted to its actual type, which would offer only the relevant methods.

That's a sign you have a problem. If you are working new functionality over design that you cannot (feasibly) change, then downcasting is an option. If you include downcasting in your original design, then the design is wrong.

1
  • Very insightful, thanks! I agree downcasting is a code smell, that's why I wasn't too fond of that solution. I ended up doing something similar (splitting the conceptual class in different classes), take my upvote :) Jul 1 '21 at 18:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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