Design orientato agli oggetti per una partita a scacchi [chiuso]


88

Sto cercando di avere un'idea di come progettare e pensare in modo orientato agli oggetti e desidero ottenere un feedback dalla comunità su questo argomento. Quello che segue è un esempio di una partita a scacchi che desidero progettare in modo OO. Questo è un progetto molto ampio e il mio obiettivo in questa fase è solo quello di identificare chi è responsabile di quali messaggi e come gli oggetti interagiscono tra loro per simulare il gioco. Si prega di indicare se ci sono elementi di cattivo design (accoppiamento alto, cattiva coesione, ecc.) E come migliorarli.

Il gioco degli scacchi ha le seguenti classi

  • Tavola
  • Giocatore
  • Pezzo
  • Piazza
  • Gioco di scacchi

Il Board è composto da quadrati, quindi Board può essere responsabile della creazione e della gestione degli oggetti Square. Ogni pezzo è anche su un quadrato, quindi ogni pezzo ha anche un riferimento al quadrato in cui si trova. (Ha senso?). Ogni pezzo è quindi responsabile di spostarsi da una casella all'altra. La classe del giocatore contiene riferimenti a tutti i pezzi che possiede ed è anche responsabile della loro creazione (il giocatore dovrebbe creare i pezzi?). Il giocatore ha un metodo takeTurn che a sua volta chiama un metodo movePiece che appartiene alla classe del pezzo che cambia la posizione del pezzo dalla sua posizione corrente a un'altra posizione. Ora sono confuso su ciò di cui deve essere responsabile esattamente la classe Board. Ho pensato che fosse necessario per determinare lo stato attuale del gioco e sapere quando il gioco è finito. Ma quando un pezzo lo cambia ' s ubicazione come dovrebbe essere aggiornata la scheda? dovrebbe mantenere una matrice separata di quadrati su cui esistono i pezzi e che riceve aggiornamenti man mano che i pezzi si muovono?

Inoltre, ChessGame crea inizialmente il tabellone e gli oggetti giocatore che a loro volta creano rispettivamente quadrati e pezzi e iniziano la simulazione. In breve, questo potrebbe essere l'aspetto del codice in 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();

}

Non sono chiaro come verrà aggiornato lo stato del consiglio. Il pezzo dovrebbe avere un riferimento alla tavola? Dove dovrebbe essere la responsabilità? Chi detiene quali riferimenti? Per favore aiutami con i tuoi input e sottolinea i problemi in questo progetto. Non mi sto concentrando deliberatamente su alcun algoritmo o ulteriori dettagli del gioco poiché sono interessato solo all'aspetto del design. Spero che questa community possa fornire informazioni preziose.


3
Commento nitido: p2 non dovrebbe chiamare takeTurn()se la mossa di p1 termina il gioco. Commento meno nitido: trovo più naturale chiamare i giocatori whitee black.
Kristopher Johnson,

Concordato. Ma come ho detto, sono più interessato agli aspetti del design e quali oggetti dovrebbero essere responsabili di quali azioni e chi detiene quali riferimenti.
Sid

Mi è piaciuto come hai delineato sopra nel tuo frammento. Nella mia implementazione, ogni pezzo ha una copia interna della posizione completa perché la utilizzerà nella propria canMove()funzione. E quando la mossa è terminata, tutti gli altri pezzi aggiornano la propria copia interna del tabellone. So che non è ottimale, ma a quel tempo era interessante imparare il C ++. In seguito, un amico non giocatore di scacchi mi disse che avrebbe avuto classesper ogni quadrato invece che per ogni pezzo. E quel commento mi è sembrato molto interessante.
eigenfield

Risposte:


54

Io in realtà appena scritto una piena attuazione C # di una scacchiera, i pezzi, regole, ecc Ecco più o meno come ho modellato esso (effettiva attuazione rimosso dato che non voglio prendere tutto il divertimento del vostro codifica):

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'idea di base è che Game / Board / etc memorizzi semplicemente lo stato del gioco. Puoi manipolarli, ad esempio, per impostare una posizione, se è quello che vuoi. Ho una classe che implementa la mia interfaccia IGameRules responsabile di:

  • Determinare quali mosse sono valide, incluso l'arrocco e l'en passant.
  • Determinare se una mossa specifica è valida.
  • Determinare quando i giocatori sono sotto controllo / scacco matto / stallo.
  • Esecuzione di mosse.

Separare le regole dalle classi di gioco / da tavolo significa anche che puoi implementare le varianti in modo relativamente semplice. Tutti i metodi dell'interfaccia delle regole accettano un Gameoggetto che possono ispezionare per determinare quali mosse sono valide.

Nota che non memorizzo le informazioni sul giocatore su Game. Ho una classe separata Tableche è responsabile della memorizzazione dei metadati del gioco come chi stava giocando, quando si è svolto il gioco, ecc.

MODIFICA: nota che lo scopo di questa risposta non è proprio quello di darti il ​​codice del modello che puoi compilare - il mio codice ha effettivamente un po 'più di informazioni memorizzate su ogni elemento, più metodi, ecc. Lo scopo è guidarti verso il obiettivo che stai cercando di raggiungere.


1
Grazie per la risposta dettagliata. Tuttavia, ho alcune domande riguardanti il ​​design. Ad esempio, non è immediatamente ovvio perché Move dovrebbe essere una classe. Il mio unico obiettivo è assegnare responsabilità e decidere le interazioni tra le classi nel modo più pulito possibile. Voglio sapere il "perché" dietro qualsiasi decisione di progettazione. Non mi è chiaro come sei arrivato alle decisioni di progettazione che hai preso e perché sono buone scelte.
Sid

Moveè una classe in modo che tu possa memorizzare l'intera cronologia delle mosse in un elenco di mosse, con notazioni e informazioni ausiliarie come quale pezzo è stato catturato, a cosa potrebbe essere stato promosso un pedone, ecc.
cdhowie

@cdhowie È la Gamedelega a un implementatore IGameRuleso l'applicazione di regole al di fuori dell'oggetto? Quest'ultimo sembra inappropriato dal momento che il gioco non può proteggere il proprio stato no?
plalx

1
Potrebbe essere stupido, ma la classe Turn in the Game non dovrebbe essere di tipo PieceColor invece di PieceType?
Dennis van Gils

1
@nikhil Indicano in quale direzione entrambi i giocatori possono ancora arroccare (verso i file A e H). Questi valori iniziano veri. Se la torre A del bianco si muove, CanWhiteCastleA viene resa falsa, e lo stesso vale per la torre H. Se il re del bianco si muove, entrambi vengono resi falsi. E lo stesso processo per il nero.
cdhowie

6

Ecco la mia idea, per una partita a scacchi abbastanza semplice:

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 ho creato un programma di scacchi in PHP ( sito web clicca qui , fonte clicca qui ) e l'ho reso orientato agli oggetti. Ecco le classi che ho usato.

  • ChessRulebook (statico) - Ho inserito tutto il mio generate_legal_moves()codice qui. A quel metodo viene data una scacchiera, di cui è il turno, e alcune variabili per impostare il livello di dettaglio dell'output, e genera tutte le mosse legali per quella posizione. Restituisce un elenco di ChessMoves.
  • ChessMove - Memorizza tutto il necessario per creare notazioni algebriche , inclusi quadrato iniziale, quadrato finale, colore, tipo di pezzo, cattura, controllo, scacco matto, tipo di pezzo di promozione ed en passant. Le variabili aggiuntive opzionali includono disambiguazione (per mosse come Rae4), arrocco e tavola.
  • ChessBoard - Memorizza le stesse informazioni di un FEN di scacchi , incluso un array 8x8 che rappresenta le piazze e la memorizzazione dei pezzi degli scacchi, di cui è il turno, quadrato bersaglio en passant, diritti di arrocco, orologio a metà movimento e orologio a movimento completo.
  • ChessPiece - Memorizza il tipo di pezzo, il colore, il quadrato e il valore del pezzo (ad esempio, pedone = 1, cavaliere = 3, torre = 5, ecc.)
  • ChessSquare - Memorizza la classifica e il file, come ints.

Attualmente sto cercando di trasformare questo codice in un'IA di scacchi, quindi deve essere VELOCE. Ho ottimizzato la generate_legal_moves()funzione da 1500 ms a 8 ms e ci sto ancora lavorando. Le lezioni che ho imparato da questo sono ...

  • Per impostazione predefinita, non memorizzare un'intera scacchiera in ogni ChessMove. Conserva la tavola in movimento solo quando necessario.
  • Usa tipi primitivi come intquando possibile. Questo è il motivo per cui ChessSquarearchivia rango e archivio come int, invece di memorizzare anche un alfanumerico stringcon notazione quadrata di scacchi leggibile dall'uomo come "a4".
  • Il programma crea decine di migliaia di ChessSquares durante la ricerca nell'albero delle mosse. Probabilmente rifatterò il programma per non usare ChessSquares, che dovrebbe aumentare la velocità.
  • Non calcolare variabili non necessarie nelle tue classi. In origine, calcolare il FEN in ciascuna delle mie scacchiere stava davvero uccidendo la velocità del programma. Ho dovuto scoprirlo con un profiler .

So che questo è vecchio, ma spero che aiuti qualcuno. In bocca al lupo!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.