Design orientado a objetos para um jogo de xadrez [fechado]


88

Estou tentando ter uma ideia de como projetar e pensar de maneira orientada a objetos e gostaria de obter algum feedback da comunidade sobre este tópico. A seguir está um exemplo de um jogo de xadrez que desejo projetar de maneira OO. Este é um design muito amplo e meu foco neste estágio é apenas identificar quem é responsável por quais mensagens e como os objetos interagem entre si para simular o jogo. Indique se há elementos de design ruim (alto acoplamento, má coesão, etc.) e como melhorá-los.

O jogo de xadrez tem as seguintes classes

  • Borda
  • Jogador
  • Peça
  • Quadrado
  • Jogo de xadrez

O tabuleiro é composto de quadrados e, portanto, o tabuleiro pode ser responsabilizado por criar e gerenciar objetos quadrados. Cada peça também está em um quadrado, então cada peça também tem uma referência ao quadrado em que está. (Isso faz sentido?). Cada peça então é responsável por se mover de uma casa para outra. A classe do jogador contém referências a todas as peças que possui e também é responsável por sua criação (o jogador deve criar peças?). Player tem um método takeTurn que, por sua vez, chama um método movePiece que pertence à classe da peça que muda a localização da peça de sua localização atual para outra localização. Agora estou confuso sobre o que exatamente a classe Board deve ser responsável. Presumi que fosse necessário determinar o estado atual do jogo e saber quando o jogo acabou. Mas quando uma peça muda ' s localização como o conselho deve ser atualizado? deve manter uma matriz separada de quadrados em que existem peças e que recebe atualizações conforme as peças se movem?

Além disso, ChessGame cria inicialmente o tabuleiro e os objetos do jogador que, por sua vez, criam quadrados e peças, respectivamente, e iniciam a simulação. Resumidamente, pode ser assim que o código no ChessGame se parece

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();

}

Não estou certo de como o estado do conselho será atualizado. A peça deve ter uma referência ao tabuleiro? Onde deveria estar a responsabilidade? Quem detém quais referências? Por favor me ajude com suas entradas e aponte problemas neste projeto. Eu deliberadamente não estou focando em nenhum algoritmo ou detalhes adicionais do jogo, pois estou interessado apenas no aspecto do design. Espero que esta comunidade possa fornecer informações valiosas.


3
Comentário de Nitpicky: p2 não deve chamar takeTurn()se o movimento de p1 terminar o jogo. Comentário menos minucioso: Acho mais natural chamar os jogadores whitee black.
Kristopher Johnson

Acordado. Mas como eu disse, estou mais interessado nos aspectos de design e quais Objetos devem ser responsáveis ​​por quais ações e quem detém quais referências.
Sid

Eu gostei como você descreveu acima em seu snippet. Em minha implementação, cada peça tem uma cópia interna da posição completa porque ela a usará em sua própria canMove()função. E quando o movimento é concluído, todas as outras peças atualizam sua própria cópia interna do tabuleiro. Eu sei que não é o ideal, mas era interessante aprender C ++ naquela época. Mais tarde, um amigo não jogador de xadrez me disse que teria classespara cada casa em vez de cada peça. E esse comentário me pareceu muito interessante.
eigenfield

Respostas:


54

Na verdade, acabei de escrever uma implementação completa em C # de um tabuleiro de xadrez, peças, regras, etc. Aqui está mais ou menos como eu a modelei (a implementação real foi removida, pois não quero tirar toda a diversão de sua codificação):

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

A ideia básica é que o jogo / tabuleiro / etc simplesmente armazene o estado do jogo. Você pode manipulá-los para, por exemplo, definir uma posição, se for isso que você deseja. Eu tenho uma classe que implementa minha interface IGameRules que é responsável por:

  • Determinar quais movimentos são válidos, incluindo roque e en passant.
  • Determinar se um movimento específico é válido.
  • Determinar quando os jogadores estão em xeque / xeque / empate.
  • Executando movimentos.

Separar as regras das classes de jogo / tabuleiro também significa que você pode implementar variantes com relativa facilidade. Todos os métodos da interface de regras usam um Gameobjeto que eles podem inspecionar para determinar quais movimentos são válidos.

Observe que eu não armazeno informações do jogador no Game. Tenho uma classe separada Tableresponsável por armazenar metadados do jogo, como quem estava jogando, quando o jogo aconteceu, etc.

EDITAR: Observe que o objetivo desta resposta não é realmente fornecer a você um código de modelo que você possa preencher - meu código, na verdade, tem um pouco mais de informações armazenadas em cada item, mais métodos, etc. O objetivo é guiá-lo em direção ao objetivo que você está tentando alcançar.


1
Obrigado pela resposta detalhada. No entanto, tenho algumas dúvidas sobre o design. Por exemplo, não é imediatamente óbvio por que Move deve ser uma classe. Meu único foco é atribuir responsabilidades e decidir as interações entre as classes da maneira mais limpa possível. Quero saber o "porquê" de qualquer decisão de design. Não estou certo de como você chegou às decisões de design que tomou e por que elas são boas escolhas.
Sid

Moveé uma classe para que você possa armazenar todo o histórico de movimentos em uma lista de movimentos, com notação e informações auxiliares, como a peça que foi capturada, a que posição um peão pode ter sido promovido, etc.
cdhowie

@cdhowie A Gamedelegação a um implementador IGameRulesou você impõe regras fora do objeto? Este último parece inadequado, pois o jogo não pode proteger seu próprio estado, não?
plalx

1
Isso pode ser estúpido, mas Turn na classe Game não deveria ser do tipo PieceColor em vez de PieceType?
Dennis van Gils

1
@nikhil Eles indicam em qual direção os dois jogadores ainda podem fazer roque (em direção aos arquivos A e H). Esses valores começam verdadeiros. Se a torre A do branco se mover, CanWhiteCastleA se torna falso, e o mesmo ocorre com a torre H. Se o rei branco se mover, ambos serão falsos. E o mesmo processo para preto.
cdhowie de

6

Aqui está minha ideia, para um jogo de xadrez 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 {....}

0

Recentemente criei um programa de xadrez em PHP ( site clique aqui , fonte clique aqui ) e o tornei orientado a objetos. Aqui estão as aulas que usei.

  • ChessRulebook (estático) - coloquei todo o meu generate_legal_moves()código aqui. Esse método recebe um tabuleiro, de quem é a vez, e algumas variáveis ​​para definir o nível de detalhe da saída, e ele gera todos os movimentos legais para aquela posição. Ele retorna uma lista de ChessMoves.
  • ChessMove - Armazena tudo o que é necessário para criar notação algébrica , incluindo quadrado inicial, quadrado final, cor, tipo de peça, captura, verificação, xeque-mate, tipo de peça promocional e en passant. Variáveis ​​adicionais opcionais incluem desambiguação (para movimentos como Rae4), roque e tabuleiro.
  • ChessBoard - Armazena as mesmas informações de um Chess FEN , incluindo uma matriz 8x8 representando os quadrados e armazenando os ChessPieces, de quem é a vez, quadrado de destino en passant, direitos de roque, relógio de meio movimento e relógio de movimento completo.
  • Peça de xadrez - Armazena tipo de peça, cor, quadrado e valor da peça (por exemplo, peão = 1, cavalo = 3, torre = 5, etc.)
  • ChessSquare - armazena a classificação e o arquivo, como ints.

No momento, estou tentando transformar esse código em uma IA de xadrez, então ele precisa ser RÁPIDO. generate_legal_moves()Otimizei a função de 1500ms para 8ms e ainda estou trabalhando nisso. As lições que aprendi com isso são ...

  • Não armazene um ChessBoard inteiro em cada ChessMove por padrão. Apenas armazene o tabuleiro no movimento quando necessário.
  • Use tipos primitivos, como intquando possível. É por isso que ChessSquarearmazena rank and file como int, em vez de também armazenar um alfanumérico stringcom notação de quadrados de xadrez legível por humanos, como "a4".
  • O programa cria dezenas de milhares de ChessSquares ao pesquisar a árvore de movimentos. Provavelmente irei refatorar o programa para não usar ChessSquares, o que deve aumentar a velocidade.
  • Não calcule nenhuma variável desnecessária em suas classes. Originalmente, calcular o FEN em cada um dos meus ChessBoards estava realmente matando a velocidade do programa. Tive que descobrir isso com um profiler .

Eu sei que isso é antigo, mas espero que ajude alguém. Boa sorte!

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.