Об’єктно-орієнтований дизайн для гри в шахи [закрито]


88

Я намагаюся зрозуміти, як проектувати та мислити об’єктно-орієнтовано, і хочу отримати відгук від спільноти на цю тему. Далі наведено приклад гри в шахи, яку я хочу оформити в ОО. Це дуже широкий дизайн, і на цьому етапі я зосереджуюся лише на тому, щоб визначити, хто відповідає за які повідомлення та як об’єкти взаємодіють між собою, щоб імітувати гру. Будь ласка, вкажіть, чи є елементи поганої конструкції (висока муфта, погана згуртованість тощо) та способи їх покращення.

Гра в шахи має наступні класи

  • Дошка
  • Гравець
  • Шматок
  • Площа
  • ChessGame

Дошка складається з квадратів, і тому Дошка може бути покладена на відповідальність за створення та управління об’єктами квадрата. Кожен фрагмент також знаходиться на квадраті, тому кожен фрагмент також має посилання на квадрат, на якому він знаходиться. (Це має сенс?). Потім кожен фрагмент відповідає за переміщення себе з одного квадрата на інший. Клас гравця містить посилання на всі його володіння, а також відповідає за їх створення (Чи повинен гравець створювати штуки?). Гравець має метод takeTurn, який, у свою чергу, викликає метод movePiece, який належить класу фігури, який змінює розташування фігури з поточного місця в інше. Зараз мене бентежить, за що саме повинен відповідати клас Board. Я припустив, що це потрібно, щоб визначити поточний стан гри та знати, коли гра закінчиться. Але коли шматок його змінює s розташування, як слід оновити дошку? чи повинен він підтримувати окремий масив квадратів, на яких існують фрагменти, і які отримують оновлення під час переміщення фрагментів?

Крім того, ChessGame фактично створює об'єкти дошки та гравця, які, у свою чергу, створюють квадрати і фігури відповідно і запускають моделювання. Коротко кажучи, це може бути так, як може виглядати код у 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();

}

Мені незрозуміло, як буде оновлюватися стан ради. Чи повинен шматок мати посилання на дошку? Де повинна лежати відповідальність? Хто має які посилання? Будь ласка, допоможіть мені з вашими введеннями та вкажіть на проблеми у цій конструкції. Я навмисно не зосереджуюся на будь-яких алгоритмах чи подальших подробицях гри, оскільки мене цікавить лише аспект дизайну. Сподіваюсь, ця спільнота може надати цінну інформацію.


3
Коментар Nitpicky: p2 не повинен дзвонити, takeTurn()якщо хід p1 закінчує гру. Менш нікчемний коментар: Я вважаю більш природним зателефонувати гравцям whiteі black.
Крістофер Джонсон,

Домовились. Але, як я вже сказав, мене більше цікавлять аспекти дизайну та те, які Об'єкти повинні відповідати за які дії та хто має які посилання.
Сід

Мені сподобалось, як ви зазначили вище у своєму фрагменті. У моїй реалізації кожна частина має внутрішню копію повної позиції, оскільки вона використовуватиме її у своїй canMove()функції. А коли переїзд зроблений, усі інші фігури оновлюють власну внутрішню копію дошки. Я знаю, що це не оптимально, але тоді було цікаво вивчати C ++. Пізніше друг, який не є шахістом, сказав мені, що він буде брати classesза кожен квадрат замість кожної фігури. І цей коментар видався мені дуже цікавим.
eigenfield

Відповіді:


54

Я насправді щойно написав повну реалізацію C # шахової дошки, фігури, правила тощо. Ось приблизно, як я її змоделював (фактична реалізація вилучена, оскільки я не хочу вилучати все задоволення з вашого кодування):

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

Основна ідея полягає в тому, що Game / Board / etc просто зберігає стан гри. Ви можете маніпулювати ними, наприклад, встановити посаду, якщо це те, що ви хочете. У мене є клас, який реалізує мій інтерфейс IGameRules, який відповідає за:

  • Визначення допустимих ходів, включаючи рокіровку та ан-пасант.
  • Визначення, чи є конкретний хід дійсним.
  • Визначення часу, коли гравці перебувають у чеку / мат / патовій ситуації.
  • Виконання ходів.

Відокремлення правил від класів гри / дошки також означає, що ви можете відносно легко реалізовувати варіанти. Усі методи інтерфейсу правил беруть Gameоб'єкт, який вони можуть перевірити, щоб визначити, які ходи є допустимими.

Зверніть увагу, що я не зберігаю інформацію про програвач Game. У мене є окремий клас, Tableякий відповідає за зберігання метаданих гри, таких як хто грав, коли гра відбулася тощо.

EDIT: Зверніть увагу, що мета цієї відповіді насправді не в тому, щоб дати вам код шаблону, який ви можете заповнити - мій код насправді містить трохи більше інформації про кожен елемент, більше методів і т.д. мети, яку ви намагаєтесь досягти.


1
Дякую за детальну відповідь. Однак у мене є кілька питань щодо дизайну. Наприклад, не одразу очевидно, чому Move повинен бути класом. Моє єдине завдання - розподілити обов'язки та вирішити взаємодію між класами якнайчистішим способом. Я хочу знати, чому саме стоїть будь-яке дизайнерське рішення. Мені не ясно, як ви дійшли до дизайнерських рішень, які ви прийняли, і чому вони є правильним вибором.
Сід

Moveце клас, завдяки якому ви можете зберігати всю історію переміщень у списку переміщення, із позначеннями та допоміжною інформацією, наприклад, про те, яку фігуру було захоплено, до якого пішака могли
підвищити

@cdhowie Чи є Gameделегування реалізатору IGameRulesчи ви застосовуєте правила поза об'єктом? Останнє здається недоречним, оскільки гра не може захистити власну державу, ні?
plalx

1
Це може бути по-дурному, але чи не слід перетворювати клас гри на тип PieceColor замість PieceType?
Денніс ван Гілс

1
@nikhil Вони вказують, в якому напрямку обидва гравці все ще можуть закріпитися (до файлів A і H). Ці значення починаються з істинних. Якщо гравець білого A рухається, CanWhiteCastleA стає хибним, так само як і грань H. Якщо король білих рухається, обидва робляться помилковими. І той самий процес для чорного.
cdhowie

6

Ось моя ідея щодо досить базової гри в шахи:

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

Нещодавно я створив шахову програму в PHP ( веб-сайт клацніть тут , джерело натисніть тут ) і зробив її об’єктно-орієнтованою. Ось класи, якими я користувався.

  • ChessRulebook (статичний) - я помістив generate_legal_moves()сюди весь свій код. Цей метод отримує дошку, черга якої зараз, і деякі змінні для встановлення рівня деталізації результату, і він генерує всі легальні кроки для цієї позиції. Він повертає список ChessMoves.
  • ChessMove - зберігає все необхідне для створення алгебраїчних позначень , включаючи початковий квадрат, кінцевий квадрат, колір, тип фігури, захоплення, галочку, мат, тип фігури для просування та en passant. Додаткові додаткові змінні включають неоднозначність (для ходів, таких як Rae4), рокадію та борт.
  • ChessBoard - зберігає ту саму інформацію, що і шаховий FEN , включаючи масив 8x8, що представляє квадрати і зберігає ChessPieces, черга яких настала, цільовий квадрат з великим розміром, права на рокіровку, годинник на половину ходу та годинник fullmove.
  • ChessPiece - зберігає тип фігури, колір, квадрат та вартість фігури (наприклад, пішак = 1, лицар = 3, ладья = 5 тощо)
  • ChessSquare - зберігає рядовий, як ints.

В даний час я намагаюся перетворити цей код на шаховий ШІ, тому він повинен бути ШВИДКИМ. Я оптимізував generate_legal_moves()функцію з 1500 мс до 8 мс і досі працюю над нею. Уроки, які я дізнався з цього, це ...

  • Не зберігайте всю ChessBoard у кожному ChessMove за замовчуванням. Зберігайте дошку в русі лише тоді, коли це потрібно.
  • Використовуйте примітивні типи, наприклад, intколи це можливо. Ось чому ChessSquareзберігається рядовий номер int, замість того, щоб зберігати буквено-цифрові символи stringіз зручним для читання шаховим квадратним позначенням, таким як "a4".
  • Програма створює десятки тисяч ChessSquares при пошуку дерева переміщення. Можливо, я рефакторирую програму, щоб вона не використовувала ChessSquares, що повинно збільшити швидкість.
  • Не обчислюйте зайвих змінних у своїх класах. Спочатку обчислення FEN у кожній з моїх шахівниць дійсно вбивало швидкість програми. Мені довелося з’ясувати це за допомогою профілювача .

Я знаю, що це давно, але, сподіваюся, це комусь допоможе. Удачі!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.