Conception orientée objet pour une partie d'échecs [fermé]

88

J'essaie d'avoir une idée de la façon de concevoir et de penser de manière orientée objet et je souhaite obtenir des commentaires de la communauté sur ce sujet. Ce qui suit est un exemple de jeu d'échecs que je souhaite concevoir de manière OO. Il s'agit d'une conception très large et mon objectif à ce stade est simplement d'identifier qui est responsable de quels messages et comment les objets interagissent pour simuler le jeu. Veuillez indiquer s'il y a des éléments de mauvaise conception (couplage élevé, mauvaise cohésion, etc.) et comment les améliorer.

Le jeu d'échecs a les classes suivantes

  • Planche
  • Joueur
  • Pièce
  • Carré
  • Jeu d'échecs

Le tableau est composé de carrés et le tableau peut donc être chargé de la création et de la gestion des objets Square. Chaque pièce est également sur un carré, donc chaque pièce a également une référence au carré sur lequel elle se trouve. (Est-ce que ça a du sens?). Chaque pièce est alors responsable de se déplacer d'une case à l'autre. La classe de joueur contient des références à toutes les pièces qu'il possède et est également responsable de leur création (le joueur doit-il créer des pièces?). Player a une méthode takeTurn qui à son tour appelle une méthode movePiece qui appartient à la classe de pièce qui change l'emplacement de la pièce de son emplacement actuel à un autre emplacement. Maintenant, je ne sais pas exactement de quoi la classe du Conseil doit être responsable. J'ai supposé qu'il était nécessaire de déterminer l'état actuel du jeu et de savoir quand le jeu est terminé. Mais quand un morceau le change ' s emplacement comment le tableau doit-il être mis à jour? devrait-il maintenir un tableau séparé de carrés sur lesquels les pièces existent et qui sont mises à jour au fur et à mesure que les pièces se déplacent?

De plus, ChessGame crée initialement le plateau et les objets du joueur qui à leur tour créent respectivement des carrés et des pièces et démarrent la simulation. En bref, cela pourrait ressembler au code de 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();

}

Je ne sais pas comment l'état du conseil d'administration sera mis à jour. La pièce doit-elle avoir une référence à la planche? Où devrait être la responsabilité? Qui détient quelles références? Veuillez m'aider avec vos entrées et signaler les problèmes dans cette conception. Je ne me concentre délibérément sur aucun algorithme ou sur d'autres détails du jeu car je ne m'intéresse qu'à l'aspect design. J'espère que cette communauté pourra fournir des informations précieuses.

Sid
la source
3
Commentaire de Nitpicky: p2 ne devrait pas appeler takeTurn()si le mouvement de p1 met fin à la partie. Commentaire moins pointilleux: je trouve plus naturel d'appeler les joueurs whiteet black.
Kristopher Johnson
D'accord. Mais comme je l'ai dit, je suis plus intéressé par les aspects de conception et par quels objets devraient être responsables de quelles actions et qui détient quelles références.
Sid
J'ai aimé ce que vous avez décrit ci-dessus dans votre extrait de code. Dans ma mise en œuvre, chaque pièce a une copie intérieure de la position complète car elle l'utilisera dans sa propre canMove()fonction. Et lorsque le déplacement est terminé, toutes les autres pièces mettent à jour leur propre copie interne du tableau. Je sais que ce n'est pas optimal, mais c'était intéressant à l'époque d'apprendre le C ++. Plus tard, un ami non joueur d'échecs m'a dit qu'il en aurait classespour chaque case au lieu de chaque pièce. Et ce commentaire m'a semblé très intéressant.
eigenfield

Réponses:

54

En fait, je viens d' écrire une implémentation C # complète d'un échiquier, de pièces, de règles, etc. Voici à peu près comment je l'ai modélisé (implémentation réelle supprimée car je ne veux pas prendre tout le plaisir de votre codage):

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 {
    // ....
}

L'idée de base est que Game / Board / etc stocke simplement l'état du jeu. Vous pouvez les manipuler pour créer une position par exemple, si c'est ce que vous voulez. J'ai une classe qui implémente mon interface IGameRules qui est responsable de:

  • Déterminer quels mouvements sont valides, y compris le roque et en passant.
  • Déterminer si un mouvement spécifique est valide.
  • Déterminer quand les joueurs sont en échec / échec et mat / dans l'impasse.
  • Exécuter des mouvements.

Séparer les règles des classes de jeu / plateau signifie également que vous pouvez implémenter des variantes relativement facilement. Toutes les méthodes de l'interface de règles prennent un Gameobjet qu'elles peuvent inspecter pour déterminer quels mouvements sont valides.

Notez que je ne stocke pas d'informations sur les joueurs Game. J'ai une classe distincte Tablequi est chargée de stocker les métadonnées du jeu telles que qui jouait, quand le jeu a eu lieu, etc.

EDIT: Notez que le but de cette réponse n'est pas vraiment de vous donner un code de modèle que vous pouvez remplir - mon code contient en fait un peu plus d'informations stockées sur chaque élément, plus de méthodes, etc. Le but est de vous guider vers le objectif que vous essayez d'atteindre.

cdhowie
la source
1
Merci pour la réponse détaillée. Cependant, j'ai quelques questions concernant la conception. Par exemple, il n'est pas immédiatement évident pourquoi Move devrait être une classe. Mon seul objectif est d'attribuer les responsabilités et de décider des interactions entre les classes de la manière la plus propre possible. Je veux savoir le «pourquoi» derrière toute décision de conception. Je ne sais pas comment vous en êtes arrivé aux décisions de conception que vous avez prises et pourquoi ce sont de bons choix.
Sid
Moveest une classe qui vous permet de stocker l'historique complet des mouvements dans une liste de
coups
@cdhowie Est-ce que la Gamedélégation à un réalisateur IGameRulesou vous appliquez des règles en dehors de l'objet? Ce dernier semble inapproprié puisque le jeu ne peut pas protéger son propre état non?
plalx
1
Cela peut être stupide, mais Turn in the Game ne devrait-il pas être de type PieceColor au lieu de PieceType?
Dennis van Gils
1
@nikhil Ils indiquent dans quelle direction les deux joueurs peuvent encore roquer (vers les fichiers A et H). Ces valeurs commencent vraies. Si la tour A des blancs se déplace, CanWhiteCastleA est rendu faux, et de même pour la tour H. Si le roi des blancs bouge, les deux sont rendus faux. Et le même processus pour le noir.
cdhowie
6

Voici mon idée, pour une partie d'échecs assez basique:

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 {....}
simplfuzz
la source
0

J'ai récemment créé un programme d'échecs en PHP ( site Web cliquez ici , source cliquez ici ) et je l'ai orienté objet. Voici les classes que j'ai utilisées.

  • ChessRulebook (statique) - J'ai mis tout mon generate_legal_moves()code ici. Cette méthode reçoit une carte, dont c'est le tour, et quelques variables pour définir le niveau de détail de la sortie, et elle génère tous les mouvements légaux pour cette position. Il renvoie une liste de ChessMoves.
  • ChessMove - Stocke tout ce dont vous avez besoin pour créer une notation algébrique , y compris le carré de départ, le carré de fin, la couleur, le type de pièce, la capture, le contrôle, l'échec et mat, le type de pièce de promotion et en passant. Les variables supplémentaires facultatives incluent l'homonymie (pour les mouvements comme Rae4), le roque et le conseil.
  • ChessBoard - Stocke les mêmes informations qu'un Chess FEN , y compris un tableau 8x8 représentant les carrés et stockant les ChessPieces, dont c'est le tour, en passant sur la case cible, les droits de roque, l'horloge de demi-mouvement et l'horloge de plein mouvement.
  • ChessPiece - Stocke le type de pièce, la couleur, le carré et la valeur de la pièce (par exemple, pion = 1, chevalier = 3, tour = 5, etc.)
  • ChessSquare - Stocke le rang et le fichier, comme ints.

J'essaie actuellement de transformer ce code en une IA d'échecs, il doit donc être RAPIDE. J'ai optimisé la generate_legal_moves()fonction de 1500 ms à 8 ms et je travaille toujours dessus. Les leçons que j'en ai tirées sont ...

  • Ne stockez pas un échiquier entier dans chaque ChessMove par défaut. Ne stockez la carte dans le mouvement que lorsque cela est nécessaire.
  • Utilisez des types primitifs comme intlorsque cela est possible. C'est pourquoi ChessSquarestocke le rang et le fichier en tant que int, plutôt que de stocker également un alphanumérique stringavec une notation carrée lisible par l'homme, telle que "a4".
  • Le programme crée des dizaines de milliers de ChessSquares lors de la recherche dans l'arbre de déplacement. Je vais probablement refactoriser le programme pour ne pas utiliser ChessSquares, ce qui devrait augmenter la vitesse.
  • Ne calculez pas de variables inutiles dans vos classes. À l'origine, calculer le FEN dans chacun de mes échecs tuait vraiment la vitesse du programme. J'ai dû le découvrir avec un profileur .

Je sais que c'est vieux, mais j'espère que cela aide quelqu'un. Bonne chance!

RedDragonWebDesign
la source