Diseño orientado a objetos para una partida de ajedrez [cerrado]

88

Estoy tratando de tener una idea de cómo diseñar y pensar de una manera orientada a objetos y quiero obtener comentarios de la comunidad sobre este tema. El siguiente es un ejemplo de un juego de ajedrez que deseo diseñar de manera orientada a objetos. Este es un diseño muy amplio y mi enfoque en esta etapa es solo identificar quién es responsable de qué mensajes y cómo los objetos interactúan entre sí para simular el juego. Indique si hay elementos de mal diseño (alto acoplamiento, mala cohesión, etc.) y cómo mejorarlos.

El juego de ajedrez tiene las siguientes clases

  • Tablero
  • Jugador
  • Pieza
  • Cuadrado
  • Ajedrez

El tablero está formado por cuadrados, por lo que se puede hacer responsable de crear y administrar objetos cuadrados. Cada pieza también está en un cuadrado, por lo que cada pieza también tiene una referencia al cuadrado en el que se encuentra. (¿Esto tiene sentido?). Entonces, cada pieza es responsable de moverse de una casilla a otra. La clase de jugador contiene referencias a todas las piezas que posee y también es responsable de su creación (¿Debería el jugador crear piezas?). El jugador tiene un método takeTurn que a su vez llama a un método movePiece que pertenece a la clase de pieza que cambia la ubicación de la pieza de su ubicación actual a otra ubicación. Ahora estoy confundido sobre de qué debe ser responsable exactamente la clase de la Junta. Supuse que era necesario para determinar el estado actual del juego y saber cuándo termina el juego. Pero cuando una pieza lo cambia ' s ubicación ¿cómo se debe actualizar el tablero? ¿Debería mantener una matriz separada de cuadrados en los que existen piezas y que se actualiza a medida que las piezas se mueven?

Además, ChessGame crea inicialmente el tablero y los objetos del jugador que, a su vez, crean cuadrados y piezas respectivamente y comienzan la simulación. Brevemente, este podría ser el aspecto del código en 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();

}

No tengo claro cómo se actualizará el estado de la placa. ¿Debe la pieza tener una referencia al tablero? ¿Dónde debería estar la responsabilidad? ¿Quién tiene qué referencias? Ayúdenme con sus aportes y señalen problemas en este diseño. Deliberadamente no me estoy enfocando en ningún algoritmo o más detalles del juego, ya que solo estoy interesado en el aspecto del diseño. Espero que esta comunidad pueda brindar información valiosa.

Sid
fuente
3
Comentario quisquilloso: p2 no debería takeTurn()pagar si el movimiento de p1 termina el juego. Comentario menos quisquilloso: me parece más natural llamar a los jugadores whitey black.
Kristopher Johnson
Convenido. Pero como dije, me interesan más los aspectos de diseño y qué Objetos deberían ser responsables de qué acciones y quién tiene qué referencias.
Sid
Me gustó como lo describió anteriormente en su fragmento. En mi implementación, cada pieza tiene una copia interna de la posición completa porque la usará en su propia canMove()función. Y cuando se realiza el movimiento, todas las demás piezas actualizan su propia copia interna del tablero. Sé que no es óptimo, pero fue interesante en ese momento aprender C ++. Más tarde, un amigo que no era ajedrecista me dijo que tendría classespara cada casilla en lugar de cada pieza. Y ese comentario me pareció muy interesante.
eigenfield

Respuestas:

54

De hecho, acabo de escribir una implementación completa en C # de un tablero de ajedrez, piezas, reglas, etc. Así es como lo modelé (la implementación real se eliminó ya que no quiero quitarle toda la diversión a tu codificación):

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

La idea básica es que Juego / Tablero / etc. simplemente almacena el estado del juego. Puede manipularlos para, por ejemplo, establecer una posición, si eso es lo que desea. Tengo una clase que implementa mi interfaz IGameRules que es responsable de:

  • Determinar qué movimientos son válidos, incluido el enroque y al paso.
  • Determinar si un movimiento específico es válido.
  • Determinar cuándo los jugadores están en jaque / jaque mate / estancamiento.
  • Ejecutando movimientos.

Separar las reglas de las clases de juego / tablero también significa que puede implementar variantes con relativa facilidad. Todos los métodos de la interfaz de reglas toman un Gameobjeto que pueden inspeccionar para determinar qué movimientos son válidos.

Tenga en cuenta que no almaceno información del jugador en Game. Tengo una clase separada Tableque se encarga de almacenar los metadatos del juego, como quién estaba jugando, cuándo tuvo lugar el juego, etc.

EDITAR: Tenga en cuenta que el propósito de esta respuesta no es realmente brindarle un código de plantilla que pueda completar; mi código en realidad tiene un poco más de información almacenada en cada elemento, más métodos, etc. El propósito es guiarlo hacia el objetivo que estás intentando alcanzar.

cdhowie
fuente
1
Gracias por la respuesta detallada. Sin embargo, tengo algunas preguntas sobre el diseño. Por ejemplo, no es inmediatamente obvio por qué Move debería ser una clase. Mi único objetivo es asignar responsabilidades y decidir las interacciones entre las clases de la manera más limpia posible. Quiero saber el "por qué" detrás de cualquier decisión de diseño. No tengo claro cómo llegó a las decisiones de diseño que tomó y por qué son buenas opciones.
Sid
Movees una clase de modo que usted puede almacenar todo el historial de movimientos en una lista de movimientos, con la notación y la información auxiliar, como lo fue capturado pieza, lo que es un peón podría haber sido ascendido a, etc.
cdhowie
@cdhowie ¿Se Gamedelega en un implementador IGameRuleso se hacen cumplir reglas fuera del objeto? Esto último parece inapropiado ya que el juego no puede proteger su propio estado, ¿no?
plalx
1
Esto puede ser estúpido, pero ¿no debería ser la clase Turn in the Game del tipo PieceColor en lugar de PieceType?
Dennis van Gils
1
@nikhil Indican en qué dirección ambos jugadores pueden enrocar (hacia las filas A y H). Estos valores comienzan siendo verdaderos. Si la torre A de las blancas se mueve, CanWhiteCastleA se convierte en falso, y lo mismo ocurre con la torre H. Si el rey blanco se mueve, ambos se vuelven falsos. Y el mismo proceso para el negro.
cdhowie
6

Aquí está mi idea, para un juego de ajedrez bastante básico:

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
fuente
0

Recientemente creé un programa de ajedrez en PHP ( sitio web haga clic aquí , fuente haga clic aquí ) y lo hice orientado a objetos. Aquí están las clases que utilicé.

  • ChessRulebook (estático): puse todo mi generate_legal_moves()código aquí. A ese método se le da un tablero, a quién le toca el turno, y algunas variables para establecer el nivel de detalle de la salida, y genera todos los movimientos legales para ese puesto. Devuelve una lista de ChessMoves.
  • ChessMove: almacena todo lo necesario para crear notación algebraica , incluido el cuadrado inicial, el cuadrado final, el color, el tipo de pieza, la captura, el cheque, el jaque mate, el tipo de pieza de promoción y al paso. Las variables adicionales opcionales incluyen desambiguación (para movimientos como Tae4), enroque y tablero.
  • Tablero de ajedrez: almacena la misma información que un FEN de ajedrez , incluida una matriz de 8x8 que representa las casillas y almacena las piezas de ajedrez, de quién es el turno, casilla de destino al paso, derechos de enroque, reloj de medio movimiento y reloj de movimiento completo.
  • ChessPiece: almacena el tipo de pieza, el color, la casilla y el valor de la pieza (por ejemplo, peón = 1, caballo = 3, torre = 5, etc.)
  • ChessSquare: almacena el rango y el archivo, como ints.

Actualmente estoy tratando de convertir este código en una IA de ajedrez, por lo que debe ser RÁPIDO. generate_legal_moves()Optimicé la función de 1500 ms a 8 ms y todavía estoy trabajando en ella. Las lecciones que aprendí de eso son ...

  • No guarde un ChessBoard completo en cada ChessMove por defecto. Solo guarde la tabla en movimiento cuando sea necesario.
  • Utilice tipos primitivos como intcuando sea posible. Es por eso que ChessSquarealmacena el rango y el archivo como int, en lugar de almacenar también un alfanumérico stringcon notación de cuadrados de ajedrez legible por humanos, como "a4".
  • El programa crea decenas de miles de ChessSquares al buscar en el árbol de movimientos. Probablemente refactorice el programa para que no use ChessSquares, lo que debería dar un impulso de velocidad.
  • No calcule ninguna variable innecesaria en sus clases. Originalmente, calcular el FEN en cada uno de mis ChessBoards realmente estaba matando la velocidad del programa. Tuve que averiguar esto con un perfilador .

Sé que esto es viejo, pero espero que ayude a alguien. ¡Buena suerte!

RojoDragonWebDiseño
fuente