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?

6
  • 1
    Just because we regard something as an object in real life, it does not mean it has to be an object in OO.
    – Theraot
    Nov 9 '20 at 5:11
  • 1
    @Theraot true, and just because something is an object in OO does not mean it is an object in real life. At least, I've never seen a living hashset in the wild. Nov 9 '20 at 5:48
  • 1
    I would like to point out that object-oriented designs aren't all that popular among game developers at the moment. Yes, object-oriented patterns still see some use in game development, but the architectural pattern currently favored by many game developers for their core mechanics is the Entity - Component - System architecture.
    – Philipp
    Nov 9 '20 at 10:48
  • I think of the game pieces as data. The pieces are just information. Their ability to move is based on the state of the game (both the current board and the moves prior). Nov 10 '20 at 18:09
  • As you code up an engine, you might want to use a Piece object to determine its moves and that's okay too. I'd consider passing in the board/history rather than having it reach out to a singleton. Another note: it's easier to manage if there's less state hidden/split up throughout the code. Nov 10 '20 at 18:13
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.

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.

3
  • 1
    "That's just the pawn and chess is a fairly simple game." Other than maybe castling kings, you've picked the singlemost complex piece there is. It's not "just" the pawn. While a pawn is often considered the most inferior piece, its movement logic is decidedly more complex than any other piece's movement logic, so I feel like you're misrepresenting how complex the rest of the pieces' logic would be. Even for the most complex piece, you were able to fully describe it in 3/4 rules (depending whether en passant is allowed, which is not a given for all games), so it's clearly not insurmountable.
    – Flater
    Nov 9 '20 at 11:43
  • castling, promotion, blocking pieces, checks these games are defined by the exceptions to the rules. If I chose the most difficult piece, you have chosen the most simple piece and not thought through the difficulties
    – Ewan
    Nov 9 '20 at 11:48
  • "you have chosen the most simple piece and not thought through the difficulties" says the guy whose answer boils down to "you have set of rules which consider the entire history of Actions in the game to evaluate possible Actions" without elaborating on any implementation details or how to even begin to structure these rules.
    – Flater
    Nov 9 '20 at 11:57
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.

5
  • I think this is the most important concept here. Even in OO, there are always many solutions to the problem. Some will simplify your ability to think about the solution, others will complicate it. Nov 10 '20 at 17:53
  • I would expand this idea a bit to "think of it as a board" and a series of moves (or transitions between board states). The series of moves is required for some rules. Nov 10 '20 at 18:00
  • @bitsoflogic In chess? Nope. two kinds of pawn: Pawn yet to move. Moved Pawn. Two kinds of Rook: Can Castle, Can't Castle. The rules then do not require any history. Checkers doesn't require it either. Hard pressed to think of a board game that does.
    – Kain0_0
    Nov 10 '20 at 21:55
  • You're just caching the state of things based on the series of moves, which is fine. It's just another way of storing history. For En Passant, it's only valid on the move after a pawn moved two squares. You'd have to expand what you're caching for pawns or just store the series of moves. My point was simply that the current board state isn't enough to go on. You need to consider what and when moves happened as well. Nov 12 '20 at 17:43
  • 1
    To be clear, I'm agreeing with your answer. I think the "series of moves" is one way of achieving tracking "the rules about how one board can be changed into another board". Nov 12 '20 at 17:47
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) 
           && (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.

14
  • unfortunately even this "potential moves" list fails because you have conditional moves such as castling, en passant etc. your better just having a map of them rather than using a piece object
    – Ewan
    Nov 9 '20 at 11:45
  • @Ewan: I didn't discuss every implementation detail. But I did explicitly discuss that the board can further validate the moveset based on board conditions. In such a case, the piece itself could return the (assumed to be valid) special case moves, and have the board validate them further. Or you could add a secondary piece of logic (for each special case move) to work this out. The finer details didn't really seem appropriate for a question which seems rooted on issues with the basics of OOP.
    – Flater
    Nov 9 '20 at 11:47
  • @Ewan: Regardless of how you implement it, whether your way or mine, the valid moves have to be implemented somehow. The total complexity of the move rules themselves doesn't change based on your or my implementation. The code's structure is a matter of categorization rather than feature addition, since the rules of chess have already been rigorously defined.
    – Flater
    Nov 9 '20 at 11:49
  • unfortunately the board cant evaluate the rule conditions, you need a history of moves.
    – Ewan
    Nov 9 '20 at 11:49
  • my point is your implementation breaks down immediately on considering some "potential moves" are conditional and some are not. you need a PotentialMove object which encapsulates the square and the conditions
    – Ewan
    Nov 9 '20 at 11:50

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.