3

In an OOP design, suppose I have some board type game that has pieces, such as chess, checkers.

In you opinion, what properties should pieces of that game board have?

imo, I feel like the pieces should only contain its respective rules, e.g. it can move like this, and it can do this. The issue I see with this is that, it would require a lot of data passing via arguments, A piece can only know how it is supposed to move if it knows its current location, and maybe location of other pieces etc. This list can become extensive the more dependent a piece is to the state of a game, this is what makes me nervous.

Assuming, I have proper singleton access of the board, the alternative is to let all pieces have access to the entire board instance, and also store data about its state with respect to the game (like its location). This way No arguments need to be passed. is this bad to do?

4 Answers4

7

This way No arguments need to be passed. is this bad to do?

We pass arguments for a reason. They show dependencies explicitly. They make code more readable. They make change easy. Global access is rarely a better alternative.

A piece can only know how it is supposed to move if it knows its current location, and maybe location of other pieces

Who said a piece needs to know all that? What makes a queen a queen isn't where she is or the other pieces on the board.

What the queen needs to know is that she's a queen and her color. Everything else changes so just let something else tell her that other stuff.

moveList.addAll( board[x, y].legalMoves(x, y, board, turn) );

Done this way the pieces are immutable. This code doesn't know which piece it's looking at. But it tells the piece what it needs to know about the changing world right now to prune it's move list down to what's legal now.

This follows a principle that says: separate what changes from what stays the same. Yes it means passing stuff but so what? It keeps the pieces clean and immutable. You can model the game with 2 objects of each kind of piece, one for each color. And just reuse their references when you need more than one piece of each kind. You can pass them their color when you construct them.

You'll also want a null object piece type to represent blank squares. One that produces no legal moves and that anyone can capture. Well, except for pawns moving diagonally.

You can use piece types to handle the stranger rules of chess. You can do castling with a CastleKing and CastleRook that leave a moved King and Rook behind after they move. En passant can be solved if Pawns leave a PawnTail behind them when they double move. When captured by a Pawn a PawnTail removes the pawn in front of it. Just remember to remove all your PawnTails before you start your turn.

The goal here is to model the state of the game with the board. But this only works up to a point. Spotting that a move offers a draw due to repeated position absolutely requires more than board state. It requires knowledge of board history. A HashMap may prove useful.

Done this way it's all very OOP and polymorphic. But most importantly it creates a place in the code that doesn't care what piece it's talking to.

In case you're interested, I've talked about chess before.

candied_orange
  • 119,268
4

In terms of game rules? None. In terms of display/UI/Storage whatever is useful.

It's generally a mistake to try and encode game rules as objects. They tend not to follow any structure which is amenable to generic patterns.

For Example, you may think that in chess all pawns can move forward 1 square. But immediately you encounter an exception,

  • black and white pieces have a different definition of forward.

OK you solve that, but then

  • pawns can take diagonally when there is piece in one of those squares.

OK, we can maybe pass in the positions of other pieces and work out of any occupy those squares. Maybe we need that for taking in the general case anyway.

  • pawns can move 2 squares... on their first move

OK so now we need to keep track of whether its the first move or not.

  • pawns can take en passant if the piece they are taking is a pawn, that moved its first move, and moved two squares, and its in the correct relative position

OK so now we need to know the move history of the piece its trying to take.

That's just the pawn and chess is a fairly simple game.

With these types of games you are best of considering them in game theory terms, ie you have Players, who take Actions, after each Action the active player may or may not change, you have set of rules which consider the entire history of Actions in the game to evaluate possible Actions and if the game has ended etc.

For example chess games are recorded as the sequence of moves

  1. e4 e5
  2. Nf3 Nc6
  3. Bb5 a6

On each turn a players possible actions break down in to moving a piece, offering a draw, resigning, accepting a draw etc or maybe no possible action, which would indicate the end of the game

moving a piece further breaks down into the list of pieces. Each possible move is a function of the previous moves, which defines the state of the game.

You can see that an Action can't be encapsulated in a piece object because non move actions like "Resign" exist, you can see that piece moves cant be encapsulated in a piece object because history dependent moves like castling exist.

Ewan
  • 83,178
2

You do need to keep track of the pieces, but perhaps you are focusing on the wrong object.

Try thinking of it as a board.

  • The pieces are just markers on the board for human convenience. They represent a given state at a given location on the board.
  • The rules are about how one board can be changed into another board.

You can still expose "pieces" as individual objects, but these are flyweights like an iterator is a bookmark into a container.

Kain0_0
  • 16,561
1

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:

enter image description here

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:

enter image description here

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) 
       &amp;&amp; (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.

Flater
  • 58,824