国际象棋游戏的面向对象设计


88

我试图了解如何以面向对象的方式进行设计和思考,并希望从社区中获得有关此主题的一些反馈。以下是我希望以面向对象的方式设计的国际象棋游戏的示例。这是一个非常广泛的设计,在此阶段,我的重点只是确定谁负责哪些消息以及对象之间如何交互以模拟游戏。请指出是否存在设计不良的因素(耦合度高,内聚力不良等)以及如何对其进行改进。

国际象棋游戏具有以下类别

  • 播放器
  • 广场
  • 国际象棋游戏

董事会由正方形组成,因此可以使董事会负责创建和管理Square对象。每块也都在一个正方形上,因此每块也都有对其所在正方形的引用。(这有意义吗?)。然后,每一块负责将自己从一个正方形移动到另一个正方形。Player类拥有对他拥有的所有作品的引用,并且还负责其创建(玩家应该创建Pieces吗?)。播放器有一个方法takeTurn,该方法又调用属于棋子类的movePiece方法,该方法将棋子的位置从其当前位置更改为另一个位置。现在,我对董事会班级究竟应负责什么感到困惑。我假设需要确定游戏的当前状态并知道游戏何时结束。但是当一块改变的时候 的位置板应该如何更新?它是否应该在存在的碎片上保持独立的正方形阵列,并且随着碎片的移动而更新?

同样,ChessGame最初创建棋盘和玩家对象,然后分别创建正方形和棋子并开始模拟。简而言之,这可能就是ChessGame中的代码的样子

Player p1 =new Player();
Player p2 = new Player();

Board b = new Board();

while(b.isGameOver())
{
  p1.takeTurn(); // calls movePiece on the Piece object
  p2.takeTurn();

}

我不清楚董事会的状态将如何更新。作品应该对董事会有参考吗?责任在哪里?谁拥有什么参考?请帮助我提供您的意见,并指出此设计中的问题。我故意不关注任何算法或游戏玩法的更多细节,因为我只对设计方面感兴趣。我希望这个社区能够提供宝贵的见解。


3
Nitpicky评论:takeTurn()如果p1的举动结束了游戏,则p2不应该呼叫。不太挑剔的评论:我觉得给玩家white和打电话更自然black
克里斯托弗·约翰逊

同意 但是,就像我说的那样,我对设计方面以及哪些对象应该负责哪些操作以及谁拥有哪些引用更感兴趣。
Sid

正如您在摘要中概述的那样,我确实很喜欢。在我的实现中,每个部分都具有完整位置的内部副本,因为它将在其自身的canMove()功能中使用它。完成移动后,所有其他部件都会更新其自己的内部副本。我知道这不是最佳方法,但是那时候学习C ++很有趣。后来,一个非象棋玩家的朋友告诉我,他将拥有classes每个方块而不是每个方块。那句话使我感到非常有趣。
eigenfield '18

Answers:


54

我实际上只是编写了棋盘,棋子,规则等的完整C#实现。这大致是我建模的方式(删除了实际实现,因为我不想从代码中获得所有乐趣):

public enum PieceType {
    None, Pawn, Knight, Bishop, Rook, Queen, King
}

public enum PieceColor {
    White, Black
}

public struct Piece {
    public PieceType Type { get; set; }
    public PieceColor Color { get; set; }
}

public struct Square {
    public int X { get; set; }
    public int Y { get; set; }

    public static implicit operator Square(string str) {
        // Parses strings like "a1" so you can write "a1" in code instead
        // of new Square(0, 0)
    }
}

public class Board {
    private Piece[,] board;

    public Piece this[Square square] { get; set; }

    public Board Clone() { ... }
}

public class Move {
    public Square From { get; }
    public Square To { get; }
    public Piece PieceMoved { get; }
    public Piece PieceCaptured { get; }
    public PieceType Promotion { get; }
    public string AlgebraicNotation { get; }
}

public class Game {
    public Board Board { get; }
    public IList<Move> Movelist { get; }
    public PieceType Turn { get; set; }
    public Square? DoublePawnPush { get; set; } // Used for tracking valid en passant captures
    public int Halfmoves { get; set; }

    public bool CanWhiteCastleA { get; set; }
    public bool CanWhiteCastleH { get; set; }
    public bool CanBlackCastleA { get; set; }
    public bool CanBlackCastleH { get; set; }
}

public interface IGameRules {
    // ....
}

基本思想是Game / Board / etc仅存储游戏状态。如果需要的话,您可以操纵他们例如建立一个职位。我有一个实现IGameRules接口的类,该类负责:

  • 确定哪些动作是有效的,包括掷骰子和传球。
  • 确定特定动作是否有效。
  • 确定玩家何时处于结算/对局/僵持状态。
  • 执行动作。

将规则从游戏/棋盘类中分离出来还意味着您可以相对轻松地实现变体。规则界面的所有方法都需要一个Game对象,可以检查对象以确定哪些移动有效。

请注意,我不会在上存储播放器信息Game。我有一个单独的类Table,负责存储游戏的元数据,例如谁在玩,游戏何时发生等。

编辑:请注意,此答案的目的并不是真正为您提供可以填写的模板代码-我的代码实际上在每个项目上存储了更多信息,更多方法等。目的是指导您朝着您想要达到的目标。


1
感谢您的详细答复。但是,我对设计有一些疑问。例如,为什么Move应该是一个类并不清楚。我唯一的重点是分配职责并以最简洁的方式确定类之间的交互。我想知道任何设计决定背后的“原因”。我不清楚您是如何得出自己所做的设计决策的,以及为什么它们是不错的选择。
Sid

Move是一类,这样就可以存储整个移动历史此举列表,符号和像什么片拍摄的辅助信息,什么样的棋子可能已经被提升为等
cdhowie

@cdhowie是在Game向对象的实现者实施IGameRules还是在对象外部实施规则?后者似乎是不适当的,因为游戏无法保护自己的状态,对吗?
plalx

1
这可能很愚蠢,但是在Game类中打开“ Turn”类不应该是PieceColor类型而不是PieceType类型吗?
丹尼斯·范·吉尔斯

1
@nikhil它们指示两个玩家仍然可以向哪个方向移动(朝向A和H文件)。这些值开始时为true。如果怀特的A菜鸟移动,则将CanWhiteCastleA设置为假,同样将H菜鸟设置为假。如果怀特国王移动了,那么两者都是错误的。和黑色相同的过程。
cdhowie

6

对于一个基本的国际象棋游戏,这是我的想法:

class GameBoard {
 IPiece config[8][8];  

 init {
  createAndPlacePieces("Black");
  createAndPlacePieces("White");
  setTurn("Black");

 }

 createAndPlacePieces(color) {
   //generate pieces using a factory method
   //for e.g. config[1][0] = PieceFactory("Pawn",color);
 }

 setTurn(color) {
   turn = color;
 }

 move(fromPt,toPt) {
  if(getPcAt(fromPt).color == turn) {
    toPtHasOppositeColorPiece = getPcAt(toPt) != null && getPcAt(toPt).color != turn;
    possiblePath = getPcAt(fromPt).generatePossiblePath(fromPt,toPt,toPtHasOppositeColorPiece);
   if(possiblePath != NULL) {
      traversePath();
      changeTurn();
   }
  }
 } 

}

Interface IPiece {
  function generatePossiblePath(fromPt,toPt,toPtHasEnemy);
}

class PawnPiece implements IPiece{
  function generatePossiblePath(fromPt,toPt,toPtHasEnemy) {
    return an array of points if such a path is possible
    else return null;
  }
}

class ElephantPiece implements IPiece {....}

0

我最近用PHP创建了一个国际象棋程序(网站单击此处源代码单击此处),并使它面向对象。这是我使用的类。

  • ChessRulebook(静态)-我把所有generate_legal_moves()代码都放在这里。该方法有一个董事会,该董事会轮到该董事会,并提供一些变量来设置输出的详细程度,并且它将生成该职位的所有合法举动。它返回ChessMoves的列表。
  • ChessMove-存储创建代数符号所需的所有内容,包括开始方块,结束方块,颜色,样片类型,捕获,检查,校验,提升样片类型和整个过程。可选的其他变量包括消除歧义(针对Rae4之类的举动),小腿和板。
  • ChessBoard-存储与国际象棋FEN相同的信息,包括表示正方形并存储ChessPieces的8x8数组,其轮流为目标方格,划定权利,半移动时钟和全移动时钟。
  • ChessPiece-存储棋子类型,颜色,正方形和棋子值(例如,典当= 1,骑士= 3,车子= 5,依此类推)
  • ChessSquare-将等级和文件存储为ints。

我目前正在尝试将此代码转换为国际象棋AI,因此必须快速。我已经generate_legal_moves()从1500ms到8ms优化了功能,并且仍在努力。我从中学到的教训是...

  • 默认情况下,不要在每个ChessMove中存储整个ChessBoard。仅在需要时才将板存放在移动中。
  • int尽可能使用原始类型。这就是为什么ChessSquare将rank和file存储为int,而不是还存储字母数字的原因string具有人类可读的国际象棋方形符号(例如“ a4”)。
  • 该程序在搜索移动树时会创建数以万计的ChessSquares。我可能会将该程序重构为不使用ChessSquares,这会提高速度。
  • 不要在您的类中计算任何不必要的变量。最初,在我的每个国际象棋棋盘中计算FEN确实是在破坏程序的速度。我必须用探查器找出来。

我知道这很老了,但希望对您有所帮助。祝好运!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.