Projekt zorientowany obiektowo do gry w szachy [zamknięte]

88

Próbuję się zorientować, jak projektować i myśleć w sposób zorientowany obiektowo i chcę uzyskać opinie społeczności na ten temat. Poniżej znajduje się przykład gry w szachy, którą chciałbym zaprojektować w sposób OO. To bardzo szeroki projekt i na tym etapie skupiam się tylko na zidentyfikowaniu, kto jest odpowiedzialny za jakie komunikaty i jak obiekty oddziałują na siebie, aby symulować grę. Proszę wskazać, czy są elementy złego projektu (wysokie sprzężenie, zła spójność itp.) I jak je poprawić.

Gra w szachy ma następujące klasy

  • Deska
  • Gracz
  • Kawałek
  • Plac
  • Gra w szachy

Tablica składa się z kwadratów, więc może ona odpowiadać za tworzenie obiektów Square i zarządzanie nimi. Każdy element również znajduje się na kwadracie, więc każdy element ma również odniesienie do kwadratu, na którym się znajduje. (Czy to ma sens?). Każdy pionek jest następnie odpowiedzialny za przemieszczanie się z jednego pola na drugie. Klasa gracza zawiera odniesienia do wszystkich elementów, które posiada, a także jest odpowiedzialna za ich tworzenie (czy gracz powinien tworzyć elementy?). Gracz ma metodę takeTurn, która z kolei wywołuje metodę movePiece należącą do klasy figury, która zmienia lokalizację figury z jej aktualnej lokalizacji na inną. Teraz nie wiem, za co dokładnie musi być odpowiedzialna klasa Board. Założyłem, że trzeba było określić aktualny stan gry i wiedzieć, kiedy gra się skończyła. Ale kiedy kawałek to zmienia s lokalizacja w jaki sposób należy aktualizować tablicę? czy powinien utrzymywać oddzielną tablicę kwadratów, na których istnieją figury i która jest aktualizowana wraz z ruchem pionków?

Ponadto ChessGame początkowo tworzy planszę i obiekty gracza, które z kolei tworzą odpowiednio kwadraty i figury i rozpoczynają symulację. Krótko mówiąc, może tak wyglądać kod w 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();

}

Nie jestem pewien, w jaki sposób będzie aktualizowany stan tablicy. Czy kawałek powinien mieć odniesienie do tablicy? Gdzie powinna leżeć odpowiedzialność? Kto posiada jakie referencje? Proszę, pomóż mi ze swoimi danymi wejściowymi i wskaż problemy w tym projekcie. Celowo nie skupiam się na żadnych algorytmach ani dalszych szczegółach rozgrywki, ponieważ interesuje mnie tylko aspekt projektowy. Mam nadzieję, że ta społeczność dostarczy cennych informacji.

Sid
źródło
3
Nitpicky komentarz: p2 nie powinien sprawdzać, takeTurn()jeśli ruch p1 kończy grę. Mniej złośliwy komentarz: bardziej naturalne jest dzwonienie do graczy whitei black.
Kristopher Johnson
Zgoda. Ale jak powiedziałem, bardziej interesują mnie aspekty projektowe i to, jakie obiekty powinny być odpowiedzialne za jakie działania i kto posiada jakie odniesienia.
Sid
Podobało mi się to, co opisałeś powyżej w swoim fragmencie. W mojej realizacji każdy element ma wewnętrzną kopię kompletnej pozycji, ponieważ będzie używał jej w swojej własnej canMove()funkcji. A kiedy ruch jest zakończony, wszystkie inne elementy aktualizują swoją wewnętrzną kopię planszy. Wiem, że to nie jest optymalne, ale w tamtym czasie nauka C ++ była interesująca. Później znajomy, który nie grał w szachy, powiedział mi, że będzie miał classesdla każdego kwadratu zamiast każdej figury. Ten komentarz wydał mi się bardzo interesujący.
eigenfield

Odpowiedzi:

54

Właściwie właśnie napisałem pełną implementację C # szachownicy, figur, reguł itp. Oto z grubsza sposób, w jaki to modelowałem (rzeczywista implementacja została usunięta, ponieważ nie chcę odbierać całej przyjemności z kodowania):

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

Podstawową ideą jest to, że gra / plansza / etc po prostu zapisuje stan gry. Możesz nimi manipulować, aby np. Ustawić stanowisko, jeśli tego chcesz. Mam klasę, która implementuje mój interfejs IGameRules, która jest odpowiedzialna za:

  • Określenie, które ruchy są prawidłowe, w tym roszada i przelot.
  • Ustalenie, czy dany ruch jest ważny.
  • Określanie, kiedy gracze są w szachu / mat / pat.
  • Wykonywanie ruchów.

Oddzielenie reguł od klas gry / planszy oznacza również, że możesz stosunkowo łatwo wdrażać warianty. Wszystkie metody interfejsu reguł pobierają Gameobiekt, który mogą sprawdzić, aby określić, które ruchy są prawidłowe.

Pamiętaj, że nie przechowuję informacji o graczach Game. Mam osobną klasę, Tablektóra jest odpowiedzialna za przechowywanie metadanych gry, takich jak kto grał, kiedy gra miała miejsce itp.

EDYCJA: Zwróć uwagę, że celem tej odpowiedzi nie jest tak naprawdę podanie kodu szablonu, który możesz wypełnić - mój kod faktycznie zawiera nieco więcej informacji przechowywanych w każdym elemencie, więcej metod itp. Celem jest poprowadzenie Cię do cel, który próbujesz osiągnąć.

cdhowie
źródło
1
Dziękuję za szczegółową odpowiedź. Mam jednak kilka pytań dotyczących projektu. Na przykład nie jest od razu oczywiste, dlaczego Move powinno być klasą. Moim jedynym celem jest przydzielanie obowiązków i decydowanie o interakcjach między zajęciami w możliwie najczystszy sposób. Chcę wiedzieć, „dlaczego” stoją za każdą decyzją projektową. Nie jestem pewien, w jaki sposób podjąłeś decyzje projektowe i dlaczego są to dobre wybory.
Sid
Moveto klasa, dzięki której możesz przechowywać całą historię ruchów na liście ruchów, z notacją i informacjami pomocniczymi, takimi jak to, która
figura została zbita
@cdhowie Czy Gamedelegowanie reguł do podmiotu wdrażającego IGameRuleslub egzekwujesz reguły poza obiektem? Ta ostatnia wydaje się nieodpowiednia, ponieważ gra nie może chronić własnego stanu, prawda?
plalx
1
To może być głupie, ale czy klasa Game nie powinna być typu PieceColor zamiast PieceType?
Dennis van Gils
1
@nikhil Wskazują kierunek, w którym obaj gracze mogą nadal robić roszady (w kierunku plików A i H). Te wartości na początku są prawdziwe. Jeśli biała wieża A poruszy się, CanWhiteCastleA jest fałszywa, podobnie jak wieża H. Jeśli król białych się poruszy, oba są fałszywe. To samo dotyczy czerni.
cdhowie
6

Oto mój pomysł na dość podstawową grę w szachy:

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
źródło
0

Niedawno stworzyłem program szachowy w PHP ( kliknij tutaj , źródło kliknij tutaj ) i uczyniłem go zorientowanym obiektowo. Oto klasy, z których korzystałem.

  • ChessRulebook (statyczny) - tutaj umieszczam cały mój generate_legal_moves()kod. Ta metoda otrzymuje planszę, której kolejka jest i pewne zmienne do ustawiania poziomu szczegółowości wyjścia i generuje wszystkie legalne ruchy dla tej pozycji. Zwraca listę ChessMoves.
  • ChessMove - przechowuje wszystko, co jest potrzebne do tworzenia notacji algebraicznej , w tym pole początkowe, pole końcowe, kolor, typ figury, zbicie , czek, mat, typ figury promocyjnej i pasujący. Opcjonalne dodatkowe zmienne obejmują ujednoznacznienie (dla ruchów takich jak Rae4), roszadę i planszę.
  • ChessBoard - przechowuje te same informacje, co Chess FEN , w tym tablicę 8x8 reprezentującą kwadraty i przechowującą ChessPieces, których jest kolej, pole docelowe w przelocie, prawa roszady, zegar półruchu i zegar pełnego ruchu.
  • ChessPiece - Przechowuje typ figury, kolor, kwadrat i wartość figury (na przykład pionek = 1, skoczek = 3, wieża = 5 itd.)
  • ChessSquare - Przechowuje ranking i plik, jak ints.

Obecnie próbuję przekształcić ten kod w szachową sztuczną inteligencję, więc musi być SZYBKI. Zoptymalizowałem generate_legal_moves()funkcję z 1500 ms do 8 ms i nadal nad nią pracuję. Lekcje, których się z tego nauczyłem, to ...

  • Nie przechowuj domyślnie całej tablicy ChessBoard w każdym ChessMove. Przechowuj planszę w ruchu tylko wtedy, gdy jest to potrzebne.
  • intJeśli to możliwe, używaj typów pierwotnych . Dlatego ChessSquarezapisuje rangę i plik jako int, a nie zapisuje również alfanumeryczny stringz czytelną dla człowieka notacją szachową, taką jak „a4”.
  • Podczas przeszukiwania drzewa ruchu program tworzy dziesiątki tysięcy ChessSquares. Prawdopodobnie zmienię program tak, aby nie korzystał z ChessSquares, co powinno przyspieszyć.
  • Nie obliczaj żadnych niepotrzebnych zmiennych na swoich zajęciach. Początkowo obliczanie FEN na każdym z moich ChessBoardów naprawdę zabijało szybkość programu. Musiałem się tego dowiedzieć z profilerem .

Wiem, że to stare, ale mam nadzieję, że komuś pomoże. Powodzenia!

RedDragonWebDesign
źródło