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:
PiecePlacement
: case class PiecePlacement(piece:Piece, row:Int, col:Int, board:Board) {...}
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游戏引擎
下一篇: 棋盘和棋子之间相互依赖