5

I frequently come across this problem when developing games and simulations. As an example, I'm currently writing an implementation of chess. I'm using it as a vehicle to experiment with entity-component systems and responsibility chains, inspired by Brian Bucklew's talk on Roguelike architecture (https://www.youtube.com/watch?v=U03XXzcThGU). Ultimately I'd like to be able to express each behavioural component of Chess as a small, encapsulated component of a chess piece and swap these out at a whim to quickly create pieces with new behaviours.

So I have a Game which has a Board, and the Board may have a number of Pieces each of which may have a number of Components. I don't have a notion of what type a Piece is, rather I encode their behaviour through their Components; you can send a "IsOwnedByPlayerOne" message to determine which side the Piece is on, for example:

IComponent.cs:

public interface IComponent
{
    void Handle(ref Message message);
}

Piece.cs:

public bool IsOwnedByPlayerOne()
{
    var isOwnedByPlayerOneMessage = new Message(MessageType.IsOwnedByPlayerOne);
    Handle(ref isOwnedByPlayerOneMessage);

    return isOwnedByPlayerOneMessage.GetParameter<bool>(MessageParameter.IsOwnedByPlayerOne);
}

The issue I've come to is this: a Component is very much a low-level type, but they need awareness of the world around them which can only be achieved through access to higher-level types. For example, say I send a "IsLegalMove" message to a Rook Piece, asking whether it can legally move from a1 to a6. It would need to check whether there are any other Pieces between a1 and a6, which requires access to the Board. But the Board itself contains the Piece which in turn contains the Component, so conceptually the design is getting a bit loopy.

I could give either Piece or Component access to Board as a field. Or I could add Board as a parameter to IComponent``'s Handle() method. I don't feel great about either of these solutions but can't put my finger on why. Which one would be better aligned with quality software principles? Or are there others you'd recommend?

3

inspired by Brian Bucklew's talk on Roguelike architecture

What Bucklew has done is take all the parameters and methods that we normally enshrine in an interface (thus defining the mini language that client objects use to communicate with their service objects) and he's shoved them all into an event. So now the only thing in the interfaces between objects is the one method that accepts events.

enter image description here

Yet here you are with IsOwnedByPlayerOne(). That's not an event. But you don't have to follow his method.

Ultimately I'd like to be able to express each behavioural component of Chess as a small, encapsulated component of a chess piece and swap these out at a whim to quickly create pieces with new behaviours.

You can do this even in the old school OOP languages, without doing construction in xml or writing your own class loader. Rather then reach for inheritance every time, instead learn to prefer composition and delegation.

Let me tell you how I made my queen in my chess tournament. She was just a piece like any other. But when it came time to generate her moves she couldn't be bothered with it. She just asked the rook and the bishop what they could do if they were in her place.

I didn't do this by inheriting the Rook or Bishop classes. I composed my queen with them. They were injected into her when she was born. Her knowledge of them was controlled. She didn't even know which was which. She just knew that she had a team of pieces that could decide her move list. She delegated the work to them. All she did was turn their lists into one list.

This meant the Elephant and Hawk Seirawan chess pieces could be added to the game just by injecting the knight.

The issue I've come to is this: a Component is very much a low-level type, but they need awareness of the world around them which can only be achieved through access to higher-level types.

This is a different issue. Conceptually it's a bit hard. It reminds me of relativity. Let me ask you this: do you know where you are? How do you know? If I drugged you and took you and your bed and floated you out into a lake how would you know where you are? Something has to tell you that you're floating in a lake.

That's how I solved this problem. I told the pieces where they were. That way they don't have to remember and they don't have to ask.

To display a piece:

board[rank, file].display(rank, file);

To generate a pieces legal moves:

moves.addAll( board[rank, file].listMoves(board, rank, file) );

This follows a principle that goes by a few names but I like to call it "Tell, don't ask".

The design allowed the using code to deal with pieces polymorphically (the good part of OOP) and functionally. Which meant it could just loop the board, which was a simple 2D array of piece references and some game state. Needed to add a null object piece that displayed nothing, offered no moves, that anyone could capture, and that didn't block movement.

For example, say I send a "IsLegalMove" message to a Rook Piece, asking whether it can legally move from a1 to a6. It would need to check whether there are any other Pieces between a1 and a6, which requires access to the Board. But the Board itself contains the Piece which in turn contains the Component, so conceptually the design is getting a bit loopy.

Don't send "IsLegalMove" messages. Send

newMoveList = board("a2").filterMoves(board, rooksMoveList)

And let a2 prune the illegal moves for you with it's amazing ability to know whether or not it is friend, foe, or blank. You will do this not only for the a2 square but all the squares between a1 and a6. You do this for every move the rook could make. In addition to making the programming easy this ensures a rook surrounded with friendly pieces or board edges isn't eating up time pointlessly.

I could give either Piece or Component access to Board as a field. Or I could add Board as a parameter to IComponent``'s Handle() method. I don't feel great about either of these solutions but can't put my finger on why. Which one would be better aligned with quality software principles? Or are there others you'd recommend?

Feeling good about an approach only comes with time. Doesn't mean it's right. Just that you've gotten comfortable with it. After decades at this I'm very comfortable minimizing what knows about what. If a piece doesn't have to know where it is when I'm not using it, why make it remember? Let the board do that.

  • Thanks so much for this answer, it's really comprehensive. Regarding your criticism of Bucklew's architecture, I agree in part- as I re-watched it recently I noticed that some of his examples could plainly be achieved with the composite pattern. But I think there may be value in it- with the example of IsOwnedByPlayerOne, say I want to introduce a new ability that changes a piece's alignment but only if some contrived circumstances are met. Using this pattern I can add this behaviour without modifying the original method at all- I think of it as Open/Closed Principle taken to the extreme. – Jansky May 29 '18 at 18:57
  • You are correct. What he's done is a lot like Spring and many other DI containers. He's moved object construction into a new language (his xml). This does have a benefit. It formalizes construction and sets a definite pattern for it. If your team needs that to keep them from making a mess then fine. Or you could teach them Pure DI and composition. Can you trust your team to use the language or do they need safeties added to it? – candied_orange May 29 '18 at 19:12
3

say I send a "IsLegalMove" message to a Rook Piece, asking whether it can legally move from a1 to a6

Since a piece cannot know on its own if a move is legal, I would recommend against this design. A piece (knowing about its type, color and position) may know the rules where it can be moved from its current position on an empty board, so you can give it a virtual method GetListOfPotentialMoves to return the list of legal moves on an empty board (the only parameters this method requires are the width and height of the board). It should not just return the move as a final coordinate, but also the intermediate coordinates for each move (the ones which must be empty for a legal move).

The Board object, however, can generate the list of legal moves, by calling Piece.GetListOfPotentialMoves and filtering the results according to the rules (that the intermediate coordinates must be empty and the destination coordinate must be either empty or contain a piece of a different color).

Note that in this design, the piece does not have to know the Board, and the Board does not have to know which individual piece type it deals with.

  • Thanks for the reply. The issue I see with this implementation is that the piece still has to make some assumptions about the environment. Most likely it's on a standard 8x8 chess board, but it might not be. So if you express a Rook's movement as "I can go 8 tiles upwards" and the board is bigger than that, it may not work. Though, perhaps it could respond with a generic function that takes the board size and piece position and returns a number of possible moves? – Jansky May 26 '18 at 14:17
  • 1
    @Jansky: as I already wrote, the GetListOfPotentialMoves will need the board size as a parameter, which can be passed from the caller, without making the piece depending on the whole board data structure. The piece will probably know its position already, so this is not required as a parameter. The only thing the piece needs to know is that its environment is some kind of rectangular or square two-dimensional board. Trying to generalize even more (maybe 3D boards, maybe hexagonal) is possible, but will probably lead to overengineering. – Doc Brown May 26 '18 at 16:04
2

I am not sure if this is good design, but I have built a very similar 'learning' implementation.

My solution was to pass the board as a param to IsLegalMove, and the Piece then calls the Board to have it checked for being a legal move. The combination of the two have the necessary knowledge; for example, a Rook knows it can go along rows and columns (first check, in the Rook class which is derived from Piece), and then the Board knows (second check level) that pieces cannot move outside the board, cannot jump over other pieces, and can only take opponent's pieces.

I have extended that so the same game engine can handle many games; for example Camelot, Halma, and Shogi - what changes are only the rules the board applies to what is allowed (so I derive game-specific boards from the general board class).

I think this is called Dependency Injection (because you are 'injecting' into the Piece the necessary object (Board) to handle its dependency on its surroundings), but I am not sure if this case qualifies for the DC term.

0

In general, you need to specify both the WorldState and the Entity you want to handle in order to decide what to do.

Because the Entity will not act without prompting by someone knowing the proper GameState, adding a pointer to the latter is wasteful, in addition to inhibiting simple copying.

You should not save the Entity to act on in the GameState either, as it changes depending on the active method, and that's what method-parameters are for.

Whether you call a method switching on the component-type, or call a virtual member-function of the component, is very much your decision.

  • Thanks for the reply. >You should not save the Entity to act on in the GameState either, as it changes depending on the active method, and that's what method-parameters are for. To check that I parsed this correctly: Are you suggesting that the Piece shouldn't belong to the Board, and instead should be stored separately and made aware of it's own position? – Jansky May 26 '18 at 14:27
  • No, all the entities, meaning all their components, are saved in the gamestate. But that gamestate should not save which one to act on just now. – Deduplicator May 26 '18 at 14:29
0

If there are a Game, Board and Piece, then:

  • A Board has Fields, which has the Pieces.
  • Pieces have attributes (color, shape, size, can jump over pieces)
  • Pieces can verify by means of deltas (horizontal, vertical, diagonal) if a move is valid.
  • the Game has Rules of the Game
  • the Game asks/checks (executes) the Rules if a Move by a Piece is valid

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.