mutual dependency between chessboard and its pieces

There have been many similar questions on mutual dependencies, but each leaves me unsure about my own design.

I am writing a chess program to learn Scala. The close relationship between a board and its pieces makes me wonder if a Piece object should contain a reference to the board it belongs to. This was my approach when I wrote a chess program in Java.

However, this means a board is not fully defined until it has its pieces, and vice-versa. This codependency is no problem if the Board instance is a variable you can add pieces to as you build the board, but that goes against immutability.

Related:

The approach here would seem to suggest defining all the rules of piece movement in the board object: https://gamedev.stackexchange.com/questions/43681/how-to-avoid-circular-dependencies-between-player-and-world

The highest voted answer here has a similar suggestion: Two objects with dependencies for each other. Is that bad?

The selected answer for the above link is different -- it moves the mutual dependency from the class definitions to interfaces. I don't understand why that is better.

The design here mirrors my current approach : https://sourcemaking.com/refactoring/change-bidirectional-association-to-unidirectional

My Code:

abstract class Piece(val side: Side.Value, val row: Int, val col: Int){
  val piece_type: PieceType.Value //isInstanceOf() could accomplish the same
  def possible_moves(board: Board): List[Move]
}

class Board (val pieces: Array[Array[Piece]]){
  def this(){
    this(DefaultBoard.setup) //An object which builds the starting board
  } 
}

Having to pass as a parameter the board to which a piece belongs would work, but it feels wrong.

Thanks in advance!


I took some liberty to redesign your classes.

The first thing I noticed: Your Piece isn't really a piece. Say there is a white bishop on the top left field. If I move it to the top right field, does it become a different piece? - Obviously not. Therefore, the position of a piece on the board is not part of its identity.

So I would refactor the class Piece to this:

trait Piece {
  def side:Side.Value
  def piece_type:PieceType.Value
}

(I used a trait instead of an abstract class here so that I leave it open for the implementor how to implement those two methods.)

The information that was lost in the process should be put in a different type:

case class PiecePlacement(piece:Piece, row:Int, col:Int) {
  def possible_moves(board:Board):Seq[Move] = ??? // Why enfore a list here?
}

Now we can define a board like this:

case class Board(pieces:IndexedSeq[IndexedSeq[Piece]] = DefaultBoard.setup)

(Notice how I replaced the auxiliary constructor with a default parameter value, and also used the immutable IndexedSeq instead of the mutable Array .)

If you now would like to also have a dependency between the placement of the piece and the board, you can do it like this:

  • Add the board to PiecePlacement :
  • case class PiecePlacement(piece:Piece, row:Int, col:Int, board:Board) {...}

  • Make the board create the placement instances:
  • case class Board(...) { def place(piece:Piece, row:Int, col:Int):(Board,PiecePlacement) = ??? }

    Note that the return value of place returns not only the new PiecePlacement instance, but also the new Board instance, because we want to work with immutable instances.

    Now, if you look at this, the question should be raised why place even returns the PiecePlacement . What benefit would the caller ever have of it? It's pretty much just board-internal information. Therefore, you would probably refactor the place method such that it returns only the new Board . Then, you could consequently go without the Placement type altogether and thereby eliminate the mutual dependency.

    Another thing that you might want to notice is that the place method cannot be implemented. The returned Board must be a new instance, but the returned PiecePlacement must contain the new Board instance. Since the new Board instance also contains the PiecePlacement instance, it can never be created in a completely immutable way.

    So I would really follow the advice of @JörgWMittag and get rid of the mutual references. Start defining traits for your boards and pieces, and include only the absolute minimum of necessary information. For example:

    trait Board {
      def at(row:Int, col:Int):Option[Piece]
      def withPieceAt(piece:Piece, row:Int, col:Int):Board
      def withoutPieceAt(row:Int, col:Int):Board
    }
    
    sealed trait Move
    case class Movement(startRow:Int, startCol:Int, endRow:Int, endCol:Int) extends Move
    case class Capture(startRow:Int, startCol:Int, endRow:Int, endCol:Int) extends Move
    
    sealed trait PieceType {
      def possibleMoves(board:Board, row:Int, col:Int):Seq[Move]
    }
    object Pawn extends PieceType {...}
    object Bishop extends PieceType {...}
    
    sealed trait Piece {
      def side:Side.Value
      def pieceType:PieceType
    }
    case class WhitePiece(pieceType:PieceType) {
      def side:Side.White
    }
    case class BlackPiece(pieceType:PieceType) {
      def side:Side.Black
    }
    

    Now you can start writing code that uses those traits to reason about potential moves etc. Also, you can write the classes that implement those traits. You can start off with a straightforward implementation, and then optimize as necessary.

    For example: Every board position has only 13 possible states. One per chess piece type, times two for the two sides, plus one for the empty state. Those states are very enumerable, so you might optimize by enumerating them.

    Another potential optimization: Since a board position requires only 4 bits for modelling it, one whole row of the board fits into an Int variable when encoded. Therefore, the whole board state can be represented as just eight Int s, or even as four Long s. This optimization would trade off performance (bit shifting) in favor of memory usage. So this optimization would be better for an algorithm that generates a huge number of Board instances and gets in danger of getting an OutOfMemoryError .

    By modelling the board and the pieces as traits rather than classes, you can easily exchange the implementations, play around with them, and see which implementation works best for you use case - without having to change a bit of the algorithms that make use of the boards and pieces.

    Bottom line: Introduce things like methods and variables only when needed. Every line of code that you don't write is a line of code that cannot contain a bug. Don't worry about mutual dependencies between objects when they are not strictly necessary. And in the case of a chess board model, they definitely are not necessary.

    Focus on simplicity first. Every single method, class and parameter should justify its existence.

    For example, in the model that I proposed, there is always two parameters for a position: row and col . Since there are only 64 possible positions, I would argue to make a Position type. It can even be an AnyVal type that would be compiled as an Int , for example. Then, you wouldn't need the nested structure to store the board. You can just store 64 board placement information objects, that's it.

    Introduce only the minimum necessary, and extend when needed. In the extreme case, start with empty traits and add methods only when you really cannot go on without them. While you're at it, write unit tests for every single method. That way, you should get to a good, clean and reusable solution. The key of reusability is: Avoid features as much as you can. Every single feature that you introduce restricts versatility. Put in just what is strictly required.

    链接地址: http://www.djcxy.com/p/51044.html

    上一篇: 用于平铺的Java 2D游戏引擎

    下一篇: 棋盘和棋子之间相互依赖