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?