рис 1.
рис 1.

Перевод с английского с адаптацией

Автор оригинала:
shashipk11
@shashipk11
https://auth.geeksforgeeks.org/user/shashipk11/articles

Ссылки на оригинал:
https://www.geeksforgeeks.org/design-a-chess-game/
https://massivetechinterview.blogspot.com/2015/07/design-chess-game-using-oo-principles.html

Уровень сложности: Сложный
Последнее обновление: 30 Сент., 2020

Постановка задачи:
Проблема состоит в том, чтобы разработать шахматную игру с использованием объектно-ориентированных принципов.

На вопрос: Adobe, Amazon, Microsoft и т. д.

Решение:
Эти виды вопросов задаются на интервью, чтобы судить о навыке объектно ориентированного дизайна кандидата. Итак, прежде всего, мы должны подумать о классах.

Примечание автора перевода:
Хорошо было бы определиться со структурой кода и построить UML диаграмму.
Затем сформировав связи и отношения, строить классы. Такой подход к проектированию был бы более наглядным и написание кода отнимало бы меньше времени.

Далее в тексте: Прим. - примечание автора перевода.

Основными классами будут:

  1. Клетка (класс Spot): Клетка представляет из себя один блок сетки и дополнительные элемент.

  2. Фигура (класс Piece) - базовый блок системы.
    Каждый объект класса фигура, а точнее объект наследника класса Piece, будет расположен на клетке. Фигура (класс Piece) будет абстрактным классом. Классы наследники ( пешка (класс Pawn), король (класс King), ферзь (класс Queen), ладья (класс Rook), конь (класс Knight), слон или офицер(класс Bishop) ) реализуют абстрактные методы.

Примечание автора перевода: слово - "Bishop" может в данном случае быть переведено как офицер. Имеются в виду шахматные фигуры, и "Епископом" шахматную фигуру не называют. Слово "Епископ", употребляемое как значение шахматной фигуры встречалось довольно редко, в очень древних трактатах по шахматам.

  • Доска (класс Board): набор клеток 8х8.

  • Игрок (класс Player): представляет одного из участников игры.

  • Движение (класс Move): Движение(ход) представляет ход в игре, содержит стартовую и конечную клетку. Движение также будет содержать трэк игрока, который делает ходы.

  • Игра (класс Game): Этот класс контролирует поток игры.
    Он содержит путь и отслеживает путь всех ходов игры, которые сделал игрок, а также финальный результат игры.

Давайте взглянем на детали. Код ясный и говорит сам за себя. Вы можете взглянуть на свойства/переменные и методы различных классов.

Ячейка (класс Spot): Для представления ячейки(клетки) на шахматной доске:

public class Spot {
    private Piece piece;
    private int x;
    private int y;
  
    public Spot(int x, int y, Piece piece)
    {
        this.setPiece(piece);
        this.setX(x);
        this.setY(y);
    }
  

    public Piece getPiece() // метод возвращает объект фигуру
    {
        return this.piece;
    }
  
    public void setPiece(Piece p)
    {
        this.piece = p;
    }
  
    public int getX()
    {
        return this.x;
    }
  
    public void setX(int x)
    {
        this.x = x;
    }
  
    public int getY()
    {
        return this.y;
    }
  
    public void setY(int y)
    {
        this.y = y;
    }
}

Фигура (класс Piece): абстрактный класс для представления общей функциональности всех шахматных фигур:

public abstract class Piece {
  
    private boolean killed = false;
    private boolean white = false;
  
    public Piece(boolean white)
    {
        this.setWhite(white);
    }
  
    public boolean isWhite()
    {
        return this.white;
    }
  
    public void setWhite(boolean white)
    {
        this.white = white;
    }
  
    public boolean isKilled()
    {
        return this.killed;
    }
  
    public void setKilled(boolean killed)
    {
        this.killed = killed;
    }
  
    public abstract boolean canMove(Board board, 
                                 Spot start, Spot end);
}

Конь (класс Knight): представляет шахматную фигуру - конь:

public class Knight extends Piece {
    public Knight(boolean white)
    {
        super(white);
    }
  
    @Override
    public boolean canMove(Board board, Spot start, 
                                            Spot end)
    {
        // we can't move the piece to a spot that has
        // a piece of the same colour
        if (end.getPiece().isWhite() == this.isWhite()) {
            return false;
        }
  
        int x = Math.abs(start.getX() - end.getX());
        int y = Math.abs(start.getY() - end.getY());
        return x * y == 2;
    }
}

Подобным образом, так же, мы можем создать классы для других фигур, таких как пешка, конь, слон, ладья, ферзь, король.

Доска (класс Board): для представления шахматной доски:

public class Board {
    Spot[][] boxes;
  
    public Board()
    {
        this.resetBoard();
    }
  
    public Spot getBox(int x, int y)
    {
  
        if (x < 0 || x > 7 || y < 0 || y > 7) {
            throw new Exception("Index out of bound");
        }
  
        return boxes[x][y];
    }
  
    public void resetBoard()
    {
        // initialize white pieces
        boxes[0][0] = new Spot(0, 0, new Rook(true));
        boxes[0][1] = new Spot(0, 1, new Knight(true));
        boxes[0][2] = new Spot(0, 2, new Bishop(true));
        //...
        boxes[1][0] = new Spot(1, 0, new Pawn(true));
        boxes[1][1] = new Spot(1, 1, new Pawn(true));
        //...
  
        // initialize black pieces
        boxes[7][0] = new Spot(7, 0, new Rook(false));
        boxes[7][1] = new Spot(7, 1, new Knight(false));
        boxes[7][2] = new Spot(7, 2, new Bishop(false));
        //...
        boxes[6][0] = new Spot(6, 0, new Pawn(false));
        boxes[6][1] = new Spot(6, 1, new Pawn(false));
        //...
  
        // initialize remaining boxes without any piece
        for (int i = 2; i < 6; i++) {
            for (int j = 0; j < 8; j++) {
                boxes[i][j] = new Spot(i, j, null);
            }
        }
    }
}

Прим.

public Spot getBox(int x, int y) throws Exception {

    if (x < 0 || x > 7 || y < 0 || y > 7) {
        throw new Exception("Index out of bound");
    }

Игрок (класс Player): Абстрактный класс игрок. Он может представлять из себя человека или компьютер.

Прим. В том смысле, что игра может быть: Человек-Человек, Компьютер-Компьютер,
Человек-Компьютер.

Игрок (класс Player):

public abstract class Player {
    public boolean whiteSide;
    public boolean humanPlayer;
  
    public boolean isWhiteSide()
    {
        return this.whiteSide;
    }
    public boolean isHumanPlayer()
    {
        return this.humanPlayer;
    }
}

Класс: HumanPlayer:

public class HumanPlayer extends Player {
  
    public HumanPlayer(boolean whiteSide)
    {
        this.whiteSide = whiteSide;
        this.humanPlayer = true;
    }
}

Класс: ComputerPlayer:

public class ComputerPlayer extends Player {
  
    public ComputerPlayer(boolean whiteSide)
    {
        this.whiteSide = whiteSide;
        this.humanPlayer = false;
    }
}

Движение - ход (класс Move): Для представления перемещения (хода):

public class Move {
    private Player player;
    private Spot start;
    private Spot end;
    private Piece pieceMoved;
    private Piece pieceKilled;
    private boolean castlingMove = false;
  
    public Move(Player player, Spot start, Spot end)
    {
        this.player = player;
        this.start = start;
        this.end = end;
        this.pieceMoved = start.getPiece();
    }
  
    public boolean isCastlingMove()
    {
        return this.castlingMove;
    }
  
    public void setCastlingMove(boolean castlingMove)
    {
        this.castlingMove = castlingMove;
    }
}

Перечисление: Статус игры :

public enum GameStatus {
    ACTIVE,
    BLACK_WIN,
    WHITE_WIN,
    FORFEIT,
    STALEMATE,
    RESIGNATION
}

Игра (класс Game): для представления игры в шахматы:

public class Game {
    private Player[] players;
    private Board board;
    private Player currentTurn;
    private GameStatus status;
    private List<Move> movesPlayed;
  
    private void initialize(Player p1, Player p2)
    {
        players[0] = p1;
        players[1] = p2;
  
        board.resetBoard();
  
        if (p1.isWhiteSide()) {
            this.currentTurn = p1;
        }
        else {
            this.currentTurn = p2;
        }
  
        movesPlayed.clear();
    }
  
    public boolean isEnd()
    {
        return this.getStatus() != GameStatus.ACTIVE;
    }
  
    public boolean getStatus()
    {
        return this.status;
    }
  
    public void setStatus(GameStatus status)
    {
        this.status = status;
    }
  
    public boolean playerMove(Player player, int startX, 
                                int startY, int endX, int endY)
    {
        Spot startBox = board.getBox(startX, startY);
        Spot endBox = board.getBox(startY, endY);
        Move move = new Move(player, startBox, endBox);
        return this.makeMove(move, player);
    }
  
    private boolean makeMove(Move move, Player player)
    {
        Piece sourcePiece = move.getStart().getPiece();
        if (sourcePiece == null) {
            return false;
        }
  
        // valid player
        if (player != currentTurn) {
            return false;
        }
  
        if (sourcePiece.isWhite() != player.isWhiteSide()) {
            return false;
        }
  
        // valid move?
        if (!sourcePiece.canMove(board, move.getStart(), 
                                            move.getEnd())) {
            return false;
        }
  
        // kill?
        Piece destPiece = move.getStart().getPiece();
        if (destPiece != null) {
            destPiece.setKilled(true);
            move.setPieceKilled(destPiece);
        }
  
        // castling?
        if (sourcePiece != null && sourcePiece instanceof King
            && sourcePiece.isCastlingMove()) {
            move.setCastlingMove(true);
        }
  
        // store the move
        movesPlayed.add(move);
  
        // move piece from the stat box to end box
        move.getEnd().setPiece(move.getStart().getPiece());
        move.getStart.setPiece(null);
  
        if (destPiece != null && destPiece instanceof King) {
            if (player.isWhiteSide()) {
                this.setStatus(GameStatus.WHITE_WIN);
            }
            else {
                this.setStatus(GameStatus.BLACK_WIN);
            }
        }
  
        // set the current turn to the other player
        if (this.currentTurn == players[0]) {
            this.currentTurn = players[1];
        }
        else {
            this.currentTurn = players[0];
        }
  
        return true;
    }
}

Спасибо за внимание. Будет время и желание - сделаю ревью кода.
В данном тексте я оставил код как есть, как он написан в оригинале.

Примечание переводчика: Автор показал свое решение. Вы можете для себя самостоятельно что-то улучшить. Подумать, какие недочеты имеются в коде. И как-бы вы выполнили такое задание.

Продолжение следует.

Комментарии (12)


  1. Dlougach
    09.04.2022 23:41
    +3

    Наивно полагать, что состояние игры в шахматах описывается только текущей позицией. А как же такие интересные правила как взятие на проходе, рокировка, и ничья после N-кратного повторения позиции?


    1. Ar20L80 Автор
      10.04.2022 11:04

      Вы совершенно правы. Но сейчас, в данный момент, я работаю только над переводом статьи. Затем, возможно, я займусь ревью кода. Видите ли, чтобы игру доделать, потребуется достаточно много времени. Самого алгоритма игры и описания правил здесь нет. Правила будут в отдельных классах. А в классах описания самих фигур потребуется добавить еще "стоимость фигуры". Данная работа только перевод. И я не могу за пять минут, в условиях крайней нехватки времени всё сделать одновременно.

      Я одобряю автора. Он дал пример хорошего задания студентам. Здесь нет готового решения.


    1. sovaz1997
      10.04.2022 15:47
      +1

      Эти параметры также могут относится к текущей позиции, почему нет? Та же FEN-нотация однозначно отображает позицию


  1. Blacklynx
    10.04.2022 00:02
    +3

    Knight это конь, а не король.


  1. Terranz
    10.04.2022 00:25
    -4

    В 2022 в моде использовать lombok, а не писать геттеры, сеттеры и конструкторы


  1. alexdoublesmile
    10.04.2022 08:13
    +3

    Ну, если речь о дизайне кода, то в "хорошем" дизайне максимальная иммутабельность и минимальное кол-во условных ветвлений (чуть больше любви к полиморфизму и параметризации) все-таки получше смотрится, чем бездумное клепание сеттеров ко всему состоянию и бесконечно увеличивающееся спагетти из if-else функционала, имхо.



  1. Balticman
    10.04.2022 13:10

    на русском следующие фигуры - пешка, конь, слон, ладья, ферзь, король


  1. SadOcean
    10.04.2022 22:39

    Теоретически такие правила, как проверка возможности хода, можно сделать без наследования, просто на структуре данных.
    Но даже без этого можно абстрагировать и вынести именно абстракцию для проверки ходов, а не делать фигуры наследованием.


    1. Ar20L80 Автор
      10.04.2022 23:46

      Благодарю. Можно. Но тогда его публикация называлась бы по другому. Здесь для чего нужно продумать хорошую структуру с наследованием? Для дальнейшей поддержки кода проекта. Я сам пока еще думаю над структурой. Использовать абстрактный класс для фигур и интерфейсы. Реализовывать методы интерфейсов. В классах хранить состояние фигуры и ценность фигуры. Совет по использованию максимальной иммунабельности, тоже очень хороший.


      1. SadOcean
        11.04.2022 00:28

        Это не противоречит принципам ООП. Главное ведь - объекты и связи между ними.
        Можно сделать ООП через чистую композицию или компонентны.
        Наследование реализации вообще не является обязательным, и как многие считают, может быть вредным.
        Я, правда, так не считаю, но согласен с аргументами, что глубокие иерархии хрупки, и, следовательно, не стоит ими злоупотреблять. Лучше использовать интерфейсы, либо правило "abstract or final" - то есть делать иерархии максимум из 2-х уровней, в одном - абстрактный универсальный класс с общими требованиями, а от него наследуется класс реализации.

        Здесь именно такой случай, реализация фигур через наследование неплоха, просто возможны другие способы - например, оставить одну фигуру, а через интерфейс в нее внедрять правила ходов. Просто другой способ композиции.


  1. Throwable
    11.04.2022 16:07
    +2

    В данном случае ООП как никогда плохо подходит для описания предметной области, тем более в таком наивном исполнении. Для реализации шахматной логики нам нужно всего 3 сущности:

    • Position: текущее состояние игры. Содержит поклеточное фото доски с фигурами плюс специальные флаги типа взятие на проходе, рокировки, 50-ходов, 3-кратное повторение, etc. В шахматах для этого даже есть специальная нотация: FEN. Для удобства Position может выдавать всю сопутствующую информацию по каждой клетке: фигура + цвет -- не надо для этого создавать никакого Spot.

    • Move: ход игрока -- "откуда", "куда" и опционально превращение пешки в фигуру.

    • Game (партия): хранит начальную позицию + список всех ходов.

    Далее необходим stateless-контроллер, имплементирующий логику игры. Причем это не фигура "должна знать как ей ходить и куда", а именно внешнаяя сущность, отвещающая за правила для всех фигур в зависимости от текущей позиции. По сути это функция, вычисляющая новую позицию от сделанного хода: Position1 = Apply(Position0, Move). В обязанности входит проверка валидности хода и вычисление завершения игры.

    Иногда также полезно делать вспомогательный класс Engine, который содержит логику по генерации ходов, маскок взятия, etc...