Перевод с английского с адаптацией
Автор оригинала:
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 диаграмму.
Затем сформировав связи и отношения, строить классы. Такой подход к проектированию был бы более наглядным и написание кода отнимало бы меньше времени.
Далее в тексте: Прим. - примечание автора перевода.
Основными классами будут:
Клетка (класс Spot): Клетка представляет из себя один блок сетки и дополнительные элемент.
Фигура (класс 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)
Terranz
10.04.2022 00:25-4В 2022 в моде использовать lombok, а не писать геттеры, сеттеры и конструкторы
alexdoublesmile
10.04.2022 08:13+3Ну, если речь о дизайне кода, то в "хорошем" дизайне максимальная иммутабельность и минимальное кол-во условных ветвлений (чуть больше любви к полиморфизму и параметризации) все-таки получше смотрится, чем бездумное клепание сеттеров ко всему состоянию и бесконечно увеличивающееся спагетти из if-else функционала, имхо.
SadOcean
10.04.2022 22:39Теоретически такие правила, как проверка возможности хода, можно сделать без наследования, просто на структуре данных.
Но даже без этого можно абстрагировать и вынести именно абстракцию для проверки ходов, а не делать фигуры наследованием.Ar20L80 Автор
10.04.2022 23:46Благодарю. Можно. Но тогда его публикация называлась бы по другому. Здесь для чего нужно продумать хорошую структуру с наследованием? Для дальнейшей поддержки кода проекта. Я сам пока еще думаю над структурой. Использовать абстрактный класс для фигур и интерфейсы. Реализовывать методы интерфейсов. В классах хранить состояние фигуры и ценность фигуры. Совет по использованию максимальной иммунабельности, тоже очень хороший.
SadOcean
11.04.2022 00:28Это не противоречит принципам ООП. Главное ведь - объекты и связи между ними.
Можно сделать ООП через чистую композицию или компонентны.
Наследование реализации вообще не является обязательным, и как многие считают, может быть вредным.
Я, правда, так не считаю, но согласен с аргументами, что глубокие иерархии хрупки, и, следовательно, не стоит ими злоупотреблять. Лучше использовать интерфейсы, либо правило "abstract or final" - то есть делать иерархии максимум из 2-х уровней, в одном - абстрактный универсальный класс с общими требованиями, а от него наследуется класс реализации.Здесь именно такой случай, реализация фигур через наследование неплоха, просто возможны другие способы - например, оставить одну фигуру, а через интерфейс в нее внедрять правила ходов. Просто другой способ композиции.
Throwable
11.04.2022 16:07+2В данном случае ООП как никогда плохо подходит для описания предметной области, тем более в таком наивном исполнении. Для реализации шахматной логики нам нужно всего 3 сущности:
Position: текущее состояние игры. Содержит поклеточное фото доски с фигурами плюс специальные флаги типа взятие на проходе, рокировки, 50-ходов, 3-кратное повторение, etc. В шахматах для этого даже есть специальная нотация: FEN. Для удобства Position может выдавать всю сопутствующую информацию по каждой клетке: фигура + цвет -- не надо для этого создавать никакого Spot.
Move: ход игрока -- "откуда", "куда" и опционально превращение пешки в фигуру.
Game (партия): хранит начальную позицию + список всех ходов.
Далее необходим stateless-контроллер, имплементирующий логику игры. Причем это не фигура "должна знать как ей ходить и куда", а именно внешнаяя сущность, отвещающая за правила для всех фигур в зависимости от текущей позиции. По сути это функция, вычисляющая новую позицию от сделанного хода: Position1 = Apply(Position0, Move). В обязанности входит проверка валидности хода и вычисление завершения игры.
Иногда также полезно делать вспомогательный класс Engine, который содержит логику по генерации ходов, маскок взятия, etc...
Dlougach
Наивно полагать, что состояние игры в шахматах описывается только текущей позицией. А как же такие интересные правила как взятие на проходе, рокировка, и ничья после N-кратного повторения позиции?
Ar20L80 Автор
Вы совершенно правы. Но сейчас, в данный момент, я работаю только над переводом статьи. Затем, возможно, я займусь ревью кода. Видите ли, чтобы игру доделать, потребуется достаточно много времени. Самого алгоритма игры и описания правил здесь нет. Правила будут в отдельных классах. А в классах описания самих фигур потребуется добавить еще "стоимость фигуры". Данная работа только перевод. И я не могу за пять минут, в условиях крайней нехватки времени всё сделать одновременно.
Я одобряю автора. Он дал пример хорошего задания студентам. Здесь нет готового решения.
sovaz1997
Эти параметры также могут относится к текущей позиции, почему нет? Та же FEN-нотация однозначно отображает позицию