I assume we're talking chess here, at least for the sake of example.
Objects tend to look inwardly. They know about themselves and their own personal abilities. They are unaware of the world around them. It is in fact the other way around: the world around them that is aware of them. In chess programming terms, the Board
knows the Piece
, the Piece
doesn't know the Board
.
Anything that requires knowledge between individual pieces, or between a piece and its environment, is clearly not limited to a given piece's personal scope, and therefore is likely to be orchestrated by its parent (Board
in this case).
A piece can only know how it is supposed to move if it knows its current location,
That's not true at all. I think you're getting confused here by tying relative movement and board coordinates together as an inseparable whole. They both factor into calculating a valid move set, but they are separate steps that can be undertaken by separate actors.
A piece is perfectly capable of knowing how it can move relative to its current position. Simple example:
This diagram explain the transformations (moves) that a chess piece can make, but it does not rely on any board coordinates. It explains the movement relative to the starting position.
You could develop a system where the pieces only communicate using relative coordinates. I.e. the piece tells the board "I can move two cells to the left and one down", rather than "I can move to C4". This wouldn't require any input at all, this knowledge is perfectly encapsulated by the piece's internal logic.
The board, after having received these relative coordinates, could then translate them into actual board coordinates. The board knows that the piece is currently on E5, so it can calculate that "two cells to the left and one down" means C4.
However, at least for boardgames with a restrictive set of tiles like chess, it's usually easier and clearer if you always use your board coordinates, and don't implement this relative coordinate system.
If you wanted to be able to express those target cells using coordinates, I would need to supply you with the coordinates from the starting position. If I told you that the knight was on E5, you could figure out that the target cells are C4, C6, D3, D7, F3, F7, G4, G6.
This means that we already have a good idea on how to implement this logic:
public class Piece
{
public abstract IEnumerable<Cell> GetMoves(Cell startingPosition);
}
public class Knight : Piece
{
public override IEnumerable<Cell> GetMoves(Cell startingPosition)
{
yield return startingPosition.Left(2).Down(1);
yield return startingPosition.Left(2).Up(1);
// and so on...
}
}
Note that:
Cell
is an abstraction that I didn't bother to explain. You can assume that this denotes a particular coordinate on the board.
- Each piece type (knight, bishop, ...) should derive from
Piece
and override GetMoves
. This makes it easier for you to write logic that can handle any type of piece.
- I used a very specific syntax to alter the cell coordinates. This is just one of many possible ways. I picked this one because it's very easy to read and comprehend, but how you handle it in your codebase is up to you.
- How you handle cells that fall outside of the game board is something I've glossed over here, as it's not the focus of my answer. I would personally contain that logic in
Cell
itself, but depending on the boardgame you're playing this may be better encapsulated in the Board
or in another way.
and maybe location of other pieces etc.
I refer back to the diagram:
You're right that if there were e.g. a white pawn located on one of the listed target cells, that this move would not be valid.
But I also refer back to my initial paragraph: an object only knows itself. Therefore, if a piece tells you its valid moves, it doesn't yet account for other pieces on the board. That is a secondary calculation you have to do on top of it.
Think of it this way:
- The piece tells you where it can theoretically move to, as far as the piece is concerned.
- The board is able to further validate if a move is valid. This includes:
- Checking if the target cells is available
- Checking whether vacating the start position would put the player in check
- Confirming whether castling is still valid (which also requires looking at the game history, as a king that has been in check in the past can never castle again)
- ...
You'd expect something along the lines of:
public class Board
{
public void MovePiece(Piece piece, Cell targetCell)
{
var currentCell = GetPosition(piece);
var availableMoves = piece.GetMoves(currentCell);
if(availableMoves.Contains(targetCell)
&& (IsEmpty(targetCell) || ContainsOpponentPiece(targetCell)))
{
// perform move
}
else
{
// throw invalid move exception
}
}
}
I've glossed over some of the details, because I wanted to highlight the main point here.
The board is the one calling the shots. The piece itself only says where it could theoretically move to. It is then the board's responsibility to further check those potential destination cells to see if there is anything about the game's state that would invalidate it as a valid destination.
Assuming, I have proper singleton access of the board
As a basic example: what if your chess software allows players to play multiple games concurrently? Then you have more than one board. There are many more reasons to not do this, but this is a very straightforward reason.
Secondly, you don't really need a singleton here. What you're trying to achieve sounds more like static access, which is an even worse idea in an otherwise OOP scenario.
This way no arguments need to be passed. is this bad to do?
What you're saying here is "OOP is hard, should I just work around it?". You tagged this as object oriented, so I would assume that you want to be using actually object oriented code, right? Well, statics are the opposite of object oriented code.
I can't claim that it's impossible to write software using statics, but it's highly undersirable for many reasons. The entire purpose of OOP is to not be static. It brings with it many benefits, but it does come at the cost of needing to pass data between instances.
Rather than avoid the thing you're not good at, I suggest you use it; and you will learn to use it well. Don't avoid it just because you don't fully get it right now.