【问题标题】:Best way to store chess piece locations to easily lookup a location given a piece or a piece given a location using object oriented design principles存储棋子位置的最佳方法,以便使用面向对象的设计原则轻松查找给定棋子的位置或给定位置的棋子
【发布时间】:2022-01-03 09:51:33
【问题描述】:

我目前正在研究简化国际象棋游戏的面向对象表示。其中大部分都很简单,但我有点不确定的一个设计决定是棋盘和棋子之间的关系。 底线是我试图找出表示棋盘/棋子关系的最佳方式,以便我可以轻松有效地查询“给定一个位置,这里有一个棋子”和“给定一个棋子,什么现场是不是”?

目前,Piece 类存储它们自己的位置(使用 Spot 类)和对 Board 类的引用。棋盘类将棋盘表示为 Dictionary简化的游戏只有几块棋子,因此感觉没有必要像我见过的其他示例一样存储 N x M 点的大部分空数组 em>) 来跟踪板上的哪些点上有哪些棋子。这很有用,对我来说似乎很直观,因为当试图移动并询问“这个地方有人吗?”时,这件作品可以使用对棋盘的参考。如果在给定地点有任何东西,它可以返回。同样,为了移动这块,我需要知道它当前在哪里,并将位置存储在块本身上似乎是一种很好的 OO 方法。

我遇到一些问题/麻烦的部分是如何使这些值保持同步这样,如果我调用(公共方法) Board.MovePiece() 或(私人二传手) Piece .CurrentPosition = new spot() Piece.CurrentPosition 和 Board's Dictionary 都已正确更新。尽可能保持所有内容的私密性,同时确保调用棋盘或棋子上的方法,同时保持其他类同步,即使不是不可能,也是非常棘手的。如果 C# 有像 C++ 这样的朋友类,我可以让 Board 成为 Piece 的朋友,然后他们可以设置彼此的私有 var 没问题(我知道 internal 可以改进事情,但我认为它不会阻止其他代码项目从理论上调用它不应该的东西)。现在我对此的解决方案是在 piece.CurrentPosition 属性上的自定义设置器,这样每次更改它的调用都会导致对公共董事会成员的正确调用(这绝对有效),但我觉得我可以做得更好。最大的风险是板子方法是公开的,可以在片段类之外调用,因此不会更新片段位置,但我不喜欢代码当前的冗余/轻微复杂性。

下面是我的代码的简化视图:

public class Board : IBoard
    {
        private uint _width;
        private uint _height;
        private Dictionary<Spot, Piece> _pieceLocations;

        public Board(uint width, uint height)
        {
            _width = width;
            _height = height;

            _pieceLocations = new Dictionary<Spot, Piece>();
        }

        // heavily used by piece when determining legal moves, what pieces and free spaces are in range etc.
        public Piece CheckSpot(Spot spot)
        {
            Piece piece;
            if (_pieceLocations.TryGetValue(transformSpot(spot), out piece))
            {
                return piece;
            }
            return null;
        }
 
        /// remove instead of null to potentially optimize space a bit
        public bool RemovePiece(Spot spot)
        {
            if (spot != null)
            {
                return _pieceLocations.Remove(transformSpot(spot));
            }
            return false;
        }

        /// This function will simply attempt to just move the piece to the specified destination.
        /// It's up to the caller to make sure the move is a valid chess move, etc
        public Spot MovePiece(Spot destination, Piece pieceToBeMoved)
        {
            // remove piece from current position
            // note the need to have current position here
            RemovePiece(pieceToBeMoved.CurrentPosition);

            // attempt to place at and return new position
            return PlacePiece(destination, pieceToBeMoved);
        }

        /// Simply places piece at specified destination if not occupied by another piece
        private Spot PlacePiece(Spot destination, Piece pieceToPlace)
        {
            var transformedDestination = transformSpot(destination);

            //business logic to check for stuff like a piece already at destination

            _pieceLocations.Add(transformedDestination, pieceToPlace);

            return transformedDestination;
        }

注意,transformSpot 只是确保坐标没有超出范围,并在需要时“将它们环绕”以在板尺寸内。

public abstract class Piece : IPiece
    {
        protected IBoard _board;
        public ColorType Color { get; protected set; }
        private Spot _currentPosition;

        public Piece(ColorType color, Spot currentPosition, IBoard board)
        {
            _board = board;
            Color = color;
            CurrentPosition = currentPosition ?? throw new ArgumentNullException(nameof(currentPosition), "When creating a piece you must specify a location");
        }

        public Spot CurrentPosition { 
            get { return _currentPosition; } 

            // I wanted to make sure that the piece's current position was always in sync with the board.
            // Calling <Board> functinos here seemed like a reasonable approach.
            protected set {

                // if position is being set to null remove from board
                if (value == null)
                {
                    _board.RemovePiece(_currentPosition);
                    _currentPosition = null;
                }
                else
                {
                    _currentPosition = _board.MovePiece(value, this);
                }
            }
        }

        public void Move()
        { 
            // note that I now need the current position to figure out where I can go etc.
            
            // insert logic to determine where we can move using chess rules etc.
            var destination = new Spot(x,y);

            // if spot is occupied remove piece
            var occupyingPiece = _board.CheckSpot(destination);
            if (occupyingPiece != null)
            {
                occupyingPiece.RemovePiece();
            }

            // call custom setter which in turn updates board to move piece from current spot to destination
            CurrentPosition = destination;
        }

        public void RemovePiece()
        {
            // call custom setter which in turn updates board to remove piece
            CurrentPosition = null;
        }

还有一些超级粗略的主驱动伪代码

            List<Piece> whitePieces = generateWhitePieces();
            List<Piece> blackPieces = generateBlackPieces();

            while (gameActive)
            {

                //somehow determine which piece to move
                var pieceToMove = whitePieces.PickPiece();

                // could pass and store CurrentPosition at this level but if it's not stored on the piece or easily 
                // accessible from the board but feels slightly icky
                pieceToMove.Move();

                pieceToMove = blackPieces.PickPiece();

                pieceToMove.Move();
                // etc.

            }

再一次,除了一些可能不需要的复杂性之外,主要缺陷似乎是 Board.MovePiece() 需要公开以便 Piece 调用它,但这意味着任何人也可以调用 MovePiece() 并在板上移动某些东西而无需更新这件作品。

我考虑过的可能解决方案:

我完全想多了,这是一个相当合理的方法。

  • 拥有自己的位置感觉不错,只是希望我有更好的访问修饰符来保护敏感值但允许朋友更改它们(我知道内部但似乎并没有完全解决我的问题)李>

我可以删除 currentPos 并且:

  • 让 board 类维护一个 Dictionary

这感觉就像只是简单地转移问题并让董事会以不同的顺序维护两个基本相同信息的字典感觉很愚蠢,但至少它将数据紧密耦合在一起并允许董事会成为一切的“权威” .倾向于这一点,但感觉这可以通过正确的数据结构以某种方式进行优化

  • 让主驱动程序/游戏类维护一个包含棋子及其当前位置的字典/元组列表,并让 Piece.Move() 传回棋盘告诉它们它们现在所在位置的信息。

再一次,这感觉就像我们在转移问题,但更是如此。另一方面,我可以看出游戏想要跟踪棋子的理由,但这仍然感觉很反 OO,我可能更愿意将其保留给董事会,如上所述。

理论上也可以

  • 只需公开当前位置设置器,以便 Board 可以直接更改它,并在 Board 函数和 Piece.CurrentPosition 设置器中添加一些复杂的检查,以确保是否更改了另一个。

感觉比我认为的解决方案更糟糕,并且增加了更多的复杂性/风险。如果不是因为这个问题,CurrentPosition 应该有一个私有的 setter。

无论如何,非常感谢任何反馈/见解!

编辑:为了清楚起见,我假设片断类在它如何移动时拥有它的逻辑,并使用板的状态来确定它可以移动到哪些空间。像 Piece.DetermineMove() 这样的东西。具体来说,这件作品需要知道它在哪里,以及它可以移动到的空间上有哪些碎片,威胁它的碎片等等。

如果这是一个糟糕的设计,我会全力以赴,但我很难相信 Piece 类不应该在 OOP 设计中拥有自己的运动逻辑。

【问题讨论】:

  • 首先,我会问自己是否真的需要额外的冗余/复杂性,或者简单(线性)搜索是否足以满足两个可能的查询之一。
  • 这是一个非常有效的问题,但我认为这并不是一个很好的可扩展/长期设计决策。为了清楚起见,您是在建议我抓住 Dictionary 并遍历条目直到找到一个片段?我明白你的意思,但我试图展示良好的设计实践,这感觉很幼稚。
  • 是的,这是我的第一个想法。如果只有很少的部分,这将很难被更复杂的算法所击败。
  • 假设我忽略被翻倍的空间并添加倒置 Dictionary 你认为维护第二个字典所需的额外添加/删除步骤仍然会使 O (N) 每次在列表中搜索块位置的复杂性?这感觉就像它进入了实现细节和编译器优化的杂草,但我的直觉说,与仅维护第二个字典相比,任何大于 2-3 块的东西都会开始拖累。
  • 也就是说,我猜想降低复杂性和空间使用是有价值的(尽管我们再次假设字典包含很少的条目)。在纸上,虽然检索从时间 O(1) 和空间 O(n) 与两个 dicts 到时间 O(n) 和仍然空间 O(n) 如果我们只搜索一个。也许时钟时间有利于搜索方法,但在纸面上,我不相信我们在简单性和更好的所有权之外得到了太多。

标签: c# dictionary oop design-patterns chess


【解决方案1】:

关于这一点的意见会很丰富,而且真的没有绝对正确的答案。每个都有优点和缺点,其中一些你可能直到开发的很晚才发现。话虽如此,这是我的。 :)

您真正想做的是确保在您的应用程序中进行任何操作只有一种方法,并且圣经数据只存储在一个地方应用(除了宗教内涵,圣经意思是“真实”)。

您可以从逻辑对象(在您的域中)开始,并尝试将这一方法放入有意义的对象中。如果它没有意义,那么您可能需要一个不同的 [心理] 模型。

你从BoardPieceSpot 开始,到目前为止我喜欢这个。由于我们想将一块位置的数据保存在一个地方,我认为我们希望在板级做到这一点,因为它会使移动更容易(一块不必“上”到板上再“下”到另一块上)。

这意味着Piece 不会有Spot 属性,只有类型(骑士、主教等)和颜色类型(白/黑)。棋盘上有以Spot 作为键的棋子的字典(这是一个很好的键 - 一个正方形上只能有一个棋子)。这使得很多逻辑更容易。移动一段意味着将其从字典中取出并放回目的地:

// Move attempt method might look like this:

Piece thisPiece = ... ;
Spot old = (1, 1); // origin .. 
Spot new = (2, 2); // .. destination

// do some validation
if (pieces.Contains(new) && pieces[new].color == thisPiece.color)
    throw new SpotOccupiedByFriendlyPieceAndThisIsntACastleException(); 

// capture a piece
if (pieces.Contains(new) && pieces[new].color != thisPiece.color)
{
    pieces.Remove(new);
}
pieces.Remove(old); // move the old piece from "old"...
pieces.Add(new, thisPiece); //... to "new"

通过将所有棋子放在棋盘级别的Dictionary&lt;Spot, Piece&gt; 中,只有一个集合保存棋子到点的关系(棋盘拥有它),棋盘可以执行它需要执行的所有逻辑。

【讨论】:

  • 我同意您的建议,即我保留实际移动棋子的逻辑以及它们在棋盘类本身中的棋盘位置的表示,但这并不能解决我遇到的基本问题:如何棋盘类是否可以仅使用 Dictionary 有效地确定给定棋子的位置?在我的模型中,假设我保留了所有用于确定在 Piece.DecideWhereToMove() 函数中进行什么移动的逻辑。该功能需要向董事会提出两个不同的问题:“X、Y 空间上有什么?”和“我在哪里?”
  • 我可以通过现有的 Board.CheckSpot(Spot spot) 用 Dictionary 轻松回答第一个问题,如上图所示。但是,如果我将所有内容都保留在棋盘类中,我还需要 Board.GetPieceLocation(Piece) 之类的东西,它会返回棋子所在的位置。我遇到的想法包括第二个字典,但它是倒置的(所以 Dictionary ),但这在双倍存储和必须维护两个字典方面有一些缺点。另一个建议是对现有字典进行线性迭代,直到我们盲目地找到我们正在寻找的部分,但这感觉很幼稚。
  • 在我上面的模型中,你永远不需要“为给定的一块找到一个位置”,而是“为一个给定的位置找到一个位置”。这件作品不应该知道移动到哪里,或者对棋盘状态有任何了解。该数据“高于其薪酬等级”。董事会应该执行这项工作。
  • 在两个 dicts 中复制数据(通常)真的很糟糕——你会遇到一类不同步的错误。我的意思是,在国际象棋中,我们永远不需要在抽象中得到一个棋子(即,我们从不说“我的国王在什么位置”),我们只从一个位置开始得到棋子(“什么是在 (1,1) 上?")。让我换一种说法——当你问“棋盘类如何有效地确定给定棋子的位置?”时,我的回答是“棋盘永远不需要”。没有一个点就不存在碎片,我们从一个点开始每个操作(移动到、捕获、评估到从)。
  • 你是不是建议棋子不应该拥有自己的移动逻辑(棋子可以这样移动,国王可以那样移动,等等)?如果我理解正确,那么棋盘不仅拥有棋盘的状态,而且还封装了每种棋子类型的所有运动能力,这对于本质上是 OOP 的东西来说是错误的。也许如果你举一个例子说明一块如何调用板方法来移动它,我会更好地理解。具体来说,我想了解 Spot old 和 Spot new 来自哪里/如果不是逐块传递的话。
猜你喜欢
  • 1970-01-01
  • 2022-01-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多