Как написать шахматы на java

Код на Github.

картинки нет, но вы держитесь

Предыстория: когда-то давно я реализовывал шахматы в рамках лабораторной работы в университете.
Целью была практика работы с сетевыми взаимодействиями через сокеты Unix, а также изучение ООП, так как реализация была на C++.
Играли через терминал shell, а вместо фигур были буквы. Но все правила были реализованы строго.
Сейчас я хочу повторить то же, но уже на Java. Постараюсь соблюдать процесс, которому бы я следовал, если бы это был не искусственный пример,
а более-менее полноценная коммерческая разработка.

Пост будет длинным и довольно подробным, но весь объём кода в него не поместится, поэтому предлагаю сразу скачать проект с github и открыть его в IDE.
Здесь же буду приводить наиболее важные и показательные отрывки кода, основной уклон делая на ход рассуждений и принципы принятия решений.

Уточнение требований. Общий дизайн приложения.

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

Таковы в общих чертах бизнес требования. Если все согласны, то аналитики, команда тестирования и менеджер могут быть свободны или остаться, если им интересно,
а разговор переходит к технической части. Вебсокеты! Html5 Unreal Engine! Kotlin! Kafka! Kubernetes! Смелые и осторожные, рациональные и безумные, традиционные и новаторские — всевозможные идеи,
словно лозунги, доносились из разных сторон комнаты. Верите? Конечно, нет. Делаем на стандартном стеке, ничего лишнего, ничего нового. Слушали, постановили:

  • Frontend — html + jquery
  • Backend — Java 11, Spring Boot (web), Junit
  • Никакой сессии или cookie. Закрыл вкладку в браузере — вышел из игры.
  • Вебсокеты это слишком, привычный REST подойдет
  • На стороне клиента не будет никакой логики. Какой фигурой и на какое поле можно походить сообщает сервер.
    При этом не должно быть пауз и подгрузок при выборе фигуры.
  • У каждого игрока будет свой персональный токен, который будет передаваться с сервера при начале игры.
    Этот токен будет необходим при каждом ходе и будет передаваться в заголовке запроса.
  • Работу над сервером и клиентом начинаем одновременно

Сервер (Back-end)

Задача хоть и понятная, но достаточно объёмная. Ясно, что правила так или иначе мы реализуем. Конь ходит буквой Гэ, если при этом не выйдет за границы доски. И на этом поле нет другой фигуры того же цвета.
И если после хода не открывается король. Рокировка возможна, если ни одно из полей, которые пересекает король, не находится под боем и если король и ладья в этой партии ещё не ходили.
Взятие на проходе возможно только на следующий ход, после этого право теряется. Когда пешка достигает последней линии, она повышается до одной из четырёх фигур.
Короче говоря, работа кропотливая и количество условий и проверок огромно. Наша цель — написать код с минимальным количеством ошибок, хорошей читаемостью и структурой.

Вторая важная часть — жизненный цикл игры. Где хранить состояние, как обрабатывать начало игры, ожидание хода соперника, параллельные игровые сессии?
Архитектор сказал, что нужно использовать какой-то «Long polling».
Пожалуй, придётся и с многопоточностью поработать.

А что с самим интерфейсом, то есть с форматом данных? Фигуры, цвета, ходы… Фронтенд команда просит побыстрее предоставить первую версию сервиса,
чтобы им было с чем работать и не сооружать моки (mocks).
С этого тогда и начнём, а в процессе глубже вникнем в задачу и наметим план дальнейших действий.

Попробуем спроектировать сервис, предоставляющий данные о состоянии партии.
Понятно, что информация, которую мы должны передавать включает в себя положение всех фигур на доске.
Также важно, на какое поле можно переместить каждую фигуру на своём ходу, ведь мы решили,
что клиент будет «тонким», то есть без логики. Нужно указать, чей сейчас ход, последний ход соперника.
Важно, продолжается ли партия и объявлен ли шах.

Кроме того, какие данные мы включаем в ответ, важен также и формат.
Плохой формат необоснованно увеличит размер сообщения и усложнит разработку клиентской части.
Также может снизиться эффективность, но для доски размером 8*8 это несущественно, поэтому сделаем акцент на удобстве использования и понятности нашего сервиса.

Все классы, являющиеся телом ответа (@ResponseBody, эта аннотация включена в @RestController), я собрал в отдельном пакете api.model и добавил к их именам постфикс «Dto».
Я пользуюсь следующей конвенцией имён — бизнес сущности не имеют никакой специфики. К классам JPA добавляется окончание «Entity», а для REST интерфейса — Dto.
Преобразовывать классы разных уровней приходится вручную.
На одном из проектов, с которым я работал, все три случая обрабатывались одним классом и мне это показалось очень неудобным и хрупким подходом.
В таком классе по умолчанию случайный геттер сразу попадает в JSON, а любое поле Hibernate пытается записать а базу. Когда с этим неизбежно возникает проблема,
приходится расставлять повсюду всевозможные @JsonIgnore, transient и так далее. Кроме того, аннотации разных библиотек смешиваются вместе, а любые изменения нужно проверять на всех уровнях.

Но вернёмся к шахматам. Я решил передавать только фигуры, присутствующие на доске. Пустые поля клиент должен определить сам методом исключения.
Каждый раз передаётся полное состояние, а не только разница с предыдущим ходом. Позиции фигур будут строкой из двух символов («e2», «f1»)
— это не очень удобно в коде сервера, зато очень кратко, естественно и понятно всем. Забегая вперед, скажу, что в самом движке позиция будет представлена классом из двух целых чисел от 1 до 8.
Для типов фигур и цвета лучше всего подойдут перечисления (Enum), рассчитываем, что jackson по умолчанию успешно осуществит сериализацию и десериализацию enum в строку.
(Для сравнения — JPA по умолчанию превращает enum в число и нужно указывать аннотацию @Enumerated(EnumType.STRING).

Пока что создадим один сервис, с информацией о состоянии игры до первого хода. Вот, что получилось у меня. Это уже финальный результат,
но я действительно начал с этого сервиса и изменения в формате данных минимальны.
Из существенного — в первой версии я смоделировал позицию отдельной сущностью, но потом заменил
на просто строку, чтобы Javascript код, работающий с этими данными был проще.

GameStateDto — корневой класс. В нём список всех фигур, цвет текущего игрока, последний ход оппонента.
Потом были добавлены ещё несколько вспомогательных полей для обработки окончания партии, но сейчас они не важны.

    public class GameStateDto {
        private List<PieceDto> pieces;
        private ColorDto currentPlayer;
        private MoveDto lastOpponentMove;
        ...
    }
    

PieceDto — одна фигура. Тип, цвет и поле. Интересно, что среди картинок фигур был вариант где один конь смотрит вправо, а другой — влево.
В этом случае пришлось бы отличать одного от другого и хранить идентификатор. Но я решил, что для шахмат этого противоестественно, ведь теоретически в партии можно сделать до 10 коней для каждого игрока.

    public class PieceDto {
        private String position;
        private ColorDto color;
        private PieceTypeDto pieceType;

        private List<String> validMoves;
        ...
    }
    

Свойство validMoves — куда можно переместить фигуру. Этот список будет заведомо пуст или null в определённых случаях.
Введение такой информации усложняет сервер, но иначе процесс хода выглядел бы так — игрок перемещает фигуру, а сервер подтверждает или отвечает ему — «Можно» или «Нельзя!».
Конечно, всё это происходило бы с паузами не меньше 50-200ms на обработку запроса.
Сейчас же клиент принимает только разрешённые ходы. Естественно, сервер всё равно должен их проверить на случай манипуляций.
С перечислениями всё просто:

    public enum ColorDto {
        BLACK, WHITE
    }

    public enum PieceTypeDto {
        BISHOP,
        KING,
        KNIGHT,
        PAWN,
        QUEEN,
        ROCK;
    } 

Создаём объект с парой фигур, чтобы оценить результат сериализации в JSON:

        {
        "pieces": [
              {
                  "position": "e2",
                  "color": "WHITE",
                  "pieceType": "PAWN",
                      "validMoves": [
                          "e3",
                          "e4"
                      ]
              },
              {
                  "position": "f7",
                  "color": "BLACK",
                  "pieceType": "PAWN",
                  "validMoves": null
              }
        ],
        "currentPlayer": "WHITE",
        "lastOpponentMove": null
        }    

Думаю, что это разумный максимум того, что мы можем предоставить команде, занимающейся клиентом до завершения реализации сервера.
Так что теперь приступим к реализации правил и сервисов управления жизненным циклом игры (создание игры, подключение, ход, ожидание хода игрока, завершение партии).
Игровой «движок», отвечающий за проверку всех правил игры — более сложная и объёмная составляющая, поэтому начну с него.

Game engine

Реализация правил игры состоит из базовых типов и управляющих конструкций языка. Здесь нет взаимодействия с базой данных, веб-сервисов, многопоточности, масштабируемости,
контейнеров и фреймворков. Только несколько классов и функций с проверками, циклами, конструкциями switch. Кроме того, алгоритмы очень простые, не требующие оптимальности ввиду маленького игрового поля,
а также не использующие никаких сложных структур данных.
Код, который нам нужно написать должен проверять, что слон ходит по диагонали и не перепрыгивает через другие фигуру того же цвета. Ладья — по вертикали и горизонтали.
Пешка — на одно или два поля вперёд, а атакует по диагонали. Не забываем про рокировки и взятие на проходе (en passant).
В общем, много отдельных случаев, каждый из которых прост, но сложно заставить всё работать правильно сразу. Хочется минимизировать риск ошибок, таких как:
для белых проверяем, а для чёрных забыли; правую границу проверяем, а левую нет; пешка ходит не в ту сторону. В общем, всех ошибок, которые возможны когда в длинной цепочке
проверок и сравнений нескольких чисел потеряли одну, перепутали меньше-больше или плюс с минусом.

Давайте посмотрим на вот такой код. Что он делает, работает ли он? Сколько времени потребуется, чтобы разобраться с ним тому, кто его не писал?

    private List<Position> tricky(int x1, int y1) {
        List<Position> result = new LinkedList<>();

        for (int x2 = 1; x2 <= 8 ; x2++) {
            for (int y2 = 1; y2 <= 8; y2++) {
                int mx = Math.abs(x1 - x2);
                int my = Math.abs(y1 - y2);
                if( (mx + my == 3) && (mx * my == 2)
                    && Math.abs(x2 - 4.5) < 4.5
                    && Math.abs(y2 - 4.5) < 4.5) {
                        result.add(Position.of(x2,y2));
                }
            }
        }

        return result;
    }
    

Результат выполнения функции — все позиции, на которые можно переместить коня с поля (x1, y1). Кратко. Хитро. Не сразу придумаешь, особенно этот трюк с числом 4.5.
Его корректность можно показать следующими рассуждениями:

           x - в границе поля.
           1 <= x <= 8, x - целое число, следовательно
           0 < x < 9, следовательно,
        -4.5 < x - 4.5 < 4.5, то есть
           | x - 4.5 | < 4.5
    

В дополнение к коду придётся прикладывать математические выкладки.
Однако этот код очень эффективный, и если убрать перебор всех клеток поля, что не очень сложно, то вероятно, будет оптимальным.
Но нужна ли нам тиакая эффективность и оптимальность здесь? Думаю, что нет. Нам нужно, чтобы код был выразительным, понятным, поддерживаемым.
Подробнее в посте про алгоритмическую сложность.

Я постарался в своей реализации минимизировать операции с целыми числами и проверки границ поля. Вся логика приложения «раскручивается» от четырёх операций с объектом класса Position:
up1, down1, left1, right1. Up1 — значит на одно поле вверх. Понятие «вверх» одинаково для игрока за черных и за белых.

    public class Position {
        //from 1 to 8
        private final int x;
        private final int y;
        ...

        public Position up1() {
            return (y < 8) ? of(x, y + 1) : null;
        }
        public Position down1() {
            return (y > 1) ? of(x, y - 1) : null;
        }
        ...//right1, left1 - аналогично
    

Вместо null было бы неплохо использовать Optional, но тогда половина кода превращается в манипуляции с ним,
поэтому я оставил null для случаев, когда при перемещении мы выходим за границы поля.
Но за пределы класса Position значение null на практике не выходит. Также отмечу, что класс Position используется как неизменяемый (immutable) объект.
Любой сдвиг — создание нового экземпляра объекта. Теперь напишем функцию up(), возвращающую все поля выше текущего до границы поля.

    public Stream<Position> up() {
        return Stream.iterate(this, Objects::nonNull, Position::up1).skip(1);
    }
    

Создаём stream при помощи итерации от текущего поля до null, на каждом шаге применяем функцию up1. Первый элемент, являющийся текущей позицией, пропускаем.

Чтобы описать поведение коня, понадобится вспомогательная функция, применяющая несколько одинарных изменений позиции.
Приходится использовать цикл вместо стрима, потому что результат move может быть равен null, а Stream (реализация ReferencePipeline) этого не допускает.
По сути применяем move.apply(finalPosition) пока не закончатся элементы в массиве moves или операция не вернёт null.

    private Position move(UnaryOperator<Position>... moves) {
        Position finalPosition = this;
        for (int i = 0; i < moves.length && (finalPosition != null); i++) {
            UnaryOperator<Position> move = moves[i];
            finalPosition = move.apply(finalPosition);
        }
        return finalPosition;
    }
    

И вот, что в результате получается для коня:

    public List<Position> knight() {
        List<Position> moves = new LinkedList<>();
        moves.add(move(Position::up1, Position::up1, Position::right1));
        moves.add(move(Position::up1, Position::up1, Position::left1));
        moves.add(move(Position::down1, Position::down1, Position::right1));
        moves.add(move(Position::down1, Position::down1, Position::left1));
        moves.add(move(Position::right1, Position::right1, Position::up1));
        moves.add(move(Position::right1, Position::right1, Position::down1));
        moves.add(move(Position::left1, Position::left1, Position::up1));
        moves.add(move(Position::left1, Position::left1, Position::down1));

        moves.removeIf(Objects::isNull);
        return moves;
    }
    

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

    @Test
    void testKnight1() {
        Position p = Position.of(1, 1);
        List<Position> kn = p.knight();
        assertEquals(2, kn.size());
        assertTrue(kn.contains(Position.of(3, 2)));
        assertTrue(kn.contains(Position.of(2, 3)));
    }

    @Test
    void testKnight2() {
        Position p = Position.of(4, 3);
        Set<Position> kn = new HashSet<>(p.knight());
        assertEquals(8, kn.size());
    }
    

Я проверяю угловое поле и центральное, поверхностно. Планирую, что тесты на более высокоуровневую логику заодно проверят и эту.

Подобным образом в классе Position реализованы все перемещения фигур, которые не зависят от других фигур на доске или хода партии.
Сейчас мы работаем над тем, чтобы для каждой фигуры определить поля, на которые активный игрок вправе её переместить. Для продолжения нужно
научиться хранить все фигуры. За это отвечает класс Board.

    public class Board implements Cloneable {
        final Map<Position, Piece> whitePieces = new HashMap<>();
        final Map<Position, Piece> blackPieces = new HashMap<>();
        ...
    }
    public class ChessGame {
        final PieceColor currentPlayer;
        final List<Board> previousStates;
        final List<PieceMove> previousMoves;
        final Board board;
        ...
    }

    public abstract class Piece {
        final Position position;
        final PieceColor pieceColor;
        final Type pieceType;
        ...
        public abstract Set<Position> validPieceMoves(ChessGame game);

        public final Set<Position> finallyValidMoves(ChessGame game) {
            Set<Position> moves = validPieceMoves(game);

            //remove target positions where same team pieces are present
            moves.removeIf(pos ->
            game.at(pos).map(piece -> piece.pieceColor == pieceColor).orElse(false));

            //remove target positions where king will be in trouble after move
            PieceColor currentPlayer = game.getCurrentPlayer();
            moves.removeIf(m -> game.applyMoveNoValidate(new PieceMove(position, m)).kingUnderAttack(currentPlayer));

            return moves;
         }
    }
    

Абстрактный класс Piece определяет свойства и поведение общее для всех фигур: нельзя перемещать фигуры на поля, занятые другими фирурами того же игрока,
после хода король не может попасть или остаться под боем. При этом в общем случае требуется не только информация о текущем положении фигур (Board), но и информация о ходе партии, поэтому
в качестве параметра используется ChessGame.
Классы Pawn, King, Queen и другие наследуют Piece и отвечают за реализацию сильно специфичного поведения фигур, наподобие рокировок или ходов пешек.

Код классов Queen, Bishop, Knight, Rock — однотипный и тривиальный:

    public class Queen extends Piece {
        ...
        @Override
        public Set<Position> validPieceMoves(ChessGame game) {
            return position.moveUntilHit(position.queen(), game, pieceColor);
        }
        ...
    }
    

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

    Map<PieceColor, UnaryOperator<Position>> MOVE_FOWARD = Map.of(
        PieceColor.WHITE, Position::up1,
        PieceColor.BLACK, Position::down1
    );

    private Optional<Position> enPassant(ChessGame game) {
        if (!ENPASSANT_LINE.get(pieceColor).equals(position.getY())
            || game.getPreviousMoves().isEmpty()) {
            return Optional.empty();
        }
        UnaryOperator<Position> moveForward = MOVE_FOWARD.get(pieceColor);
        UnaryOperator<Position> moveBackward = MOVE_BACKWARD.get(pieceColor);
        Position fl = moveForward.apply(position).left1();
        Position fr = moveForward.apply(position).right1();

        for (Position forwardAttack : new Position[]{fl, fr}) {
             if (forwardAttack != null) {
                  Position near = moveBackward.apply(forwardAttack);
                  Position nearFrom = moveForward.apply(forwardAttack);

                  PieceMove lastOppMove = game.getPreviousMoves().get(game.getPreviousMoves().size() - 1);
                  if (game.at(near).map(p -> p.getPieceType() == Type.PAWN && p.pieceColor != pieceColor).orElse(false)
                        && lastOppMove.getFrom().equals(nearFrom)
                        && lastOppMove.getTo().equals(near)) {
                      return Optional.of(forwardAttack);
                  }
             }
        }

        return Optional.empty();
    }
    

С таким кодом придётся разбираться в любом случае, потому что запутанная логика исходит из бизнес требований, а не из неудачного подхода или
плохой реализации. Стараемся выделить подобный код в отдельную функцию и не смешивать с остальной частью, работающей по более-менее стандартной логике.
Важно унифицировать логику для белых и чёрных, иначе все тесты придётся писать для обеих сторон. Тесты писать муторно и скучно, потому что приходится
воссоздавать ситуацию на доске целиком, но этого не избежать. Во время реализации я проверил все сценарии в тестах, кроме повышения пешки до другой фигуры, которое я решил
воспроизвести уже с помощью браузера. И конечно же, там была ошибка по типу copy-paste: ход применялся к исходному полю, а не к его копии (классы Board, ChessGame и другие
хоть и имеют изменяемое внутреннее состояние, но перед манипуляциями создаются копии объектов).

Так выглядит тест, проверяющий, что единственный возможный ход для белого коня — атаковать чёрного коня, чтобы снять шах с короля.
В тестах много вспомогательных функций, чтобы сделать их более читаемыми.

    @Test
    void testValidMovesKnight1() {
         Piece whiteKing = new King(Position.of(2,2), PieceColor.WHITE);
         Piece blackKnight = new Knight(Position.of(4,3), PieceColor.BLACK);
         Piece whiteKnight = new Knight(Position.of(5,5), PieceColor.WHITE);

         Board board = new Board(Arrays.asList(whiteKing, whiteKnight, blackKnight));
         ChessGame chessGame = new ChessGame(PieceColor.WHITE, Collections.emptyList(), Collections.emptyList(), board);
         Set<Position> validMoves = whiteKnight.finallyValidMoves(chessGame);

         assertEquals(Sets.newSet(blackKnight.getPosition()), validMoves);
    }
    

Возможность для каждой фигуры определить список разрешённых ходов позволяет легко реализовать проверки окончания партии, шаха и мата.

    public boolean kingUnderAttack(PieceColor player) {
        Set<Position> attackPositions =
            board.pieces(player.negate()).stream()
                .flatMap(p -> p.validPieceMoves(this).stream())
                .collect(Collectors.toSet());

        return attackPositions.contains(board.king(player));
    }

    public void updateGameStatus() {
        this.validMovesForCurrentPlayer = validMovesForCurrentPlayer();

        boolean kingAttacked = kingUnderAttack(currentPlayer);
        boolean canMove = validMovesForCurrentPlayer.values().stream().anyMatch(s -> !s.isEmpty());

        if (canMove && kingAttacked) {
            status = GameStatus.CHECK;
        }
        if (!canMove && kingAttacked) {
            status = GameStatus.CHECKMATE;
            finished = true;
        }
        if (!canMove && !kingAttacked) {
            status = GameStatus.DRAW_STALEMATE;
            finished = true;
        }
    }
    

Я очень интенсивно использую Optional и Stream, поэтому полагаю, что код можем показаться сложнее, чем если бы
те же проверки осуществлялись простыми циклами и проверками if-else. В первом случаем код получается более декларативным, то есть он
описывает «что» нужно сделать, а второй — императивный, то есть «как» сделать, указав конкретные шаги.
Декларативный подход чаще всего лучше, но к нему нужно привыкнуть.

Когда поведение всех фигур, а также проверки окончания партии реализованы, можно написать тест, проверяющий всё в сборе.
Например, убедимся, что детский мат — действительно приводит к победе в три хода:

    @Test
    void testMoves() {
        ChessGame game = ChessGame.startGame();
        List<PieceMove> moves = movesFromString("e2e4 e7e5 d1h5 b8c6 f1c4 g8f6 h5f7").collect(Collectors.toList());
        for (PieceMove move : moves) {
            game = game.applyMove(move);
        }

        assertTrue(game.isFinished());
        assertEquals(GameStatus.CHECKMATE, game.getStatus());
    }
    

Мы всё ещё не можем быть уверены, что ошибок нет, очень много игровых ситуаций не проверено ни вручную, ни автоматически, хотя с учётом ещё нескольких тестов, которые я не упомянул,
покрытие строк кода почти полное. Но покрытие строк далеко от покрытия всех сценариев. Как же быть? Как правило, тестирование проводит отдельная команда, которая собирает
и поддерживает достаточный список проверок приложения (все или основные бизнес сценарии). По возможности эти проверки автоматизируются, но это уже не юнит тесты, а проверки на живом окружении,
в котором приложение работает в условиях, близких к продакшену. Давайте подумаем, как бы мы могли улучшить тесты, чтобы увеличить уверенность в том, что реализация работает верно.
Идею написать больше однотипных тестов оставим без рассмотрения как очевидную.
Один из вариантов — если бы существовала гарантированно правильная реализация, то мы могли бы сравнить поведение на большом количестве случайных партий.
Это очень удачный случай, но не часто будет такая реализация, которую мы можем назвать эталонной, можем использовать в тесте, но не в коде.
Другой вариант — если бы удалось получить архив реальных шахматных партий в текстовом формате — тогда можно проверить, что все ходы и результаты партий согласуются с нашей реализацией.
Правда, так мы проверим только заведомо разрешённые ходы. Сейчас же в плане юнит тестов для правил остановимся на достигнутом.

На данный момент приложение умеет:

  1. Создавать стартовое игровое поле с 16 фигурами
  2. Определять, на какие поля игрок может переставить каждую фигуру
  3. Проверять, разрешен ход или нет (используя предыдущий пункт)
  4. Менять состояние игрового поля, применяя перемещение фигуры
  5. Определять окончание партии победой игрока или вничью

Однако, весь процесс пока не имеет никакой связи с REST сервисом, который будет использоваться игровым клиентом.

REST

Для начала подумаем какие данные, какому игроку, в какой момент и в каком объёме нужно предоставить.
Что касается объёма данных, то, к счастью, оба игрока владеют полной информацией об игровом поле и ходе партии, в отличии, например, от игры в морской бой.
Также мы уже условились, что игровое поле передаётся целиком каждый раз, а не только те поля, которые изменили своё состояние.
Очевидно, что после завершения хода, то есть когда пользователь кликнет на поле или отпустит кнопку при переносе фигуры, должен быть вызван какой-то
сервис для передачи информации о ходе. Ещё нужен способ передать данные от сервера к клиенту, когда оппонент завершит ход. Это несвойственно протоколу HTTP, но необходимо для достижения требований:
ход должен передаваться без пауз, а если мы будем периодически опрашивать сервер, даже раз в секунду, то пауз не избежать.

Обсудим этот вопрос подробнее. Мне известны три способа передачи данных от сервера к клиенту-браузеру. По крайней мере, различных статьях сравнивают именно эти три подхода.

  1. Websockets
  2. Server sent events
  3. Long polling

Я остановился на Long Polling, так как раньше он использовался в Facebook (может и сейчас используется) для доставки личных сообщений и обновлений ленты, что очень похоже на нашу задачу по своей природе.
Не хочу описывать эти подходы подробно, потому что получится не точно, опыта работы с ними у меня нет.
Я понимаю области их применения так: websockets — онлайн игры с интенсивным трафиком, где дорог каждый байт и каждая миллисекунда.
Минусы — слишком низкоуровневый протокол, нужно делать собственную надстройку.
Может не поддерживаться определёнными браузерами, желательно использовать библиотеки, которые в случае чего переключаются на HTTP, что дополнительно усложнит код и тестирование.
Server sent events — хорошо подходит для просмотра графиков биржи с обновлениями в режиме реального времени. Многократные однотипные события через небольшие промежутки времени.
Определённым образом ограничивает количество соединений — это требует дополнительного изучения. Long Polling — простой трюк использования протокола HTTP — сервер возвращает данные
когда они появляются, а клиент работает как с обычным очень медленным запросом. Главное не попасть на таймауты от прокси сервера, но это решается периодическим повторением запроса.
Для нашей задачи, в которой ответ может прийти через секунды, минуты или часы, подходит лучше всего.

Теперь, выбрав Long polling в качестве способа ожидания действия соперника, наш сервис обретает две механики:
запросы, которые обрабатываются сразу и запросы, которые обрабатываются неопределённо долго.

Я сравнительно долго подбирал количество сервисов и распределял задачи между ними.
Пришлось учитывать то, как будет работать клиент, включая начало игры одним пользователем и последующим подключением второго, после которого сразу должна начаться игра.
От интерактивного взаимодействия между игроками решил пока отказаться, то есть нет чата, возможности сдаться или предложить ничью. Всё равно чтобы начать игру один игрок как-то
отправляет ссылку второму, значит общий чат у них уже есть.

Так как число одновременных партий никак не ограничено, каждая игра имеет уникальный идентификатор.
Чтобы отличать игроков друг от друга, каждому выдаётся свой уникальный (и секретный) токен, который генерируется классом UUID.
Так решаются две задачи — во-первых, защита партии от неавторизованных вызовов (ведь процесса аутентификации у нас нет), а во-вторых, идентификация и отличие игрока белыми и игрока чёрными.
Напомню, что сейчас информация о партии хранится в классе ChessGame, в котором нет упоминания идентификаторов или токенов.
Для этого введём дополнительный класс ChessGameMetadata.

    public class ChessGameMetadata {
        private String id;
        private Map<PieceColor, String> playerTokens = generateTokens();
        private ChessGame chessGame;
        private boolean secondPlayerJoined = false;
    

Эту информацию можно было бы включить и в класс ChessGame, но хочется оставить ядро системы независимым от способа взаимодействия с игроками и сервиса.
Классы ChessGame и ChessGameMedatada не предназначены для сериализации в JSON напрямую, поэтому существуют функции, преобразующие их к уже известному нам GameStateDto и
новому GameConnectionParamsDto. Второй класс потребовался для передачи токена в начале партии каждому игроку. Не хочется, чтобы токен передавался к игроку при каждом вызове, так
его намного легче перехватить, да и клиенту он не нужен, потому что не может измениться в процессе партии.

    public class GameConnectionParamsDto {
        private String id;
        private String token;
        private ColorDto playerSide;
        private GameStateDto gameState;//состояние игры также передаётся, чтобы не заставлять клиента делать лишний вызов.
        ...
    }
    

С учётом всего вышесказанного, полный жизненный цикл игры обеспечивается с помощью четырёх методов (endpoint). Все методы изменяют состояние сервера, поэтому
используют HTTP метод POST, даже если тело запроса не содержит параметров.

  1. /host Немедленно создаёт новую игру и возвращает GameConnectionParamsDto с id игры и токеном игрока за белых. Тело запроса пустое. Заголовки запроса отсутствуют.
    В будущем в тело запроса можно добавить настройки игры: таймер, сторону.
  2. /{id}/join Находит игру по идентификатору «id» и немедленно возвращает GameConnectionParamsDto с id игры и токеном игрока за чёрных.
    Тело запроса пустое. Заголовки запроса отсутствуют.
  3. /{id}/wait-for-my-move Long polling. Если вызвать на своем ходу, то немедленно возвращает GameStateDto. Если на чужом — то запрос «повисает» до тех пор,
    пока соперник не сделает ход. Таймаут на сервере установлен в 60 минут, после чего запрос завершится со статусом 408.
    Впрочем, реализация клиента повторяет запрос намного раньше, каждые пять минут. Тело запроса пустое. Заголовок запроса включает токен игрока.
    Чтобы не создавать отдельный сервис, подключение второго игрока считается первым ходом чёрных.
  4. /{id}/move Находит игру по id, изменяет её состояние, совершая один ход и немедленно возвращает обновленное состояние в виде GameStateDto.
    Тело запроса соответствует классу MoveDto, который включает два поля (from, to) и строку promotion, которая заполняется для повышения пешки, достигшей края поля.
    Заголовок запроса включает токен игрока. Гипотетически, можно было бы объединить c /wait-for-my-move, сразу инициируя ожидание хода коперника.
    Но тогда клиент должен уметь определить состояние доски после хода, а в случае рокировок и взятий на проходе это не тривиально и потребует логики на клиенте.

Использовать эти сервисы на стороне клиента предполагается следующим образом:

  1. Первый игрок открывает главную страницу, JS сразу выполняет запрос /host, получает id игры и формирует ссылку для второго игрока, также отправляет запрос /wait-for-my-move
  2. Второй игрок открывает ссылку. Это та же главная страница, но с параметром запроса ?joinid={id}. JS получает id игры из параметров URL и отправляет запрос /{id}/join.
    Результат запроса содержит стартовое игровое поле в ожидании хода белых. JS отображает это поле в браузере и отправляет запрос /wait-for-my-move в ожидании хода белых.
  3. Как только завершается вызов /join, первый запрос /wait-for-my-move возвращает результат, сообщая о том, что второй игрок подключился и партия началась.
    Первый игрок думает, потом совершает ход, что приводит к вызову /move и следом /wait-for-my-move.
  4. Как только завершается вызов /move, второй запрос /wait-for-my-move возвращает результат, сообщая о том, что первый игрок совершил ход, и ход переходит к чёрным.
    Второй игрок думает, потом совершает ход, что приводит к вызову /move и следом /wait-for-my-move.
  5. Цикл завершается, если в объекте GameStateDto поле gameFinished становится равным true.

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

Перейдём к коду. Чтобы не откладывать на потом и не вносить изменения в уже протестированные сервисы, начнём с обработки ошибок.
Для это вводим несколько собственных классов исключений, а с помощью аннотации @ExceptionHandler отдельно от основной реализации размещаем код, который для каждого типа ошибки
устанавливает соответствующий HTTP статус.

    @ExceptionHandler(GameNotFoundException.class)
    public ResponseEntity<?> handleNotFound() {
        return new ResponseEntity<Object>("game not found", new HttpHeaders(), HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(InvalidMoveException.class)
    public ResponseEntity<?> handleInvalidMove() {
        return new ResponseEntity<Object>("bad move", new HttpHeaders(), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(InvalidTokenException.class)
    public ResponseEntity<?> handleInvalidToken() {
        return new ResponseEntity<Object>("bad token", new HttpHeaders(), HttpStatus.FORBIDDEN);
    }
    

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

    public interface GameRepository {
        ChessGameMetadata newGame();
        Optional<ChessGameMetadata> find(String id);
        void save(ChessGameMetadata chessGameMetadata);
    }
    

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

    @Repository
    public class InMemoryGameRepository implements GameRepository {
        private Map<String, ChessGameMetadata> metadataMap = new ConcurrentHashMap<>();

        @Override
        public ChessGameMetadata newGame() {
            ChessGame chessGame = ChessGame.startGame();
            ChessGameMetadata chessGameMetadata = new ChessGameMetadata();
            chessGameMetadata.setChessGame(chessGame);
            chessGameMetadata.setId(generateId());

            metadataMap.put(chessGameMetadata.getId(), chessGameMetadata);
            return chessGameMetadata;
        }

        @Override
        public Optional<ChessGameMetadata> find(String id) {
            return Optional.ofNullable(metadataMap.get(id));
        }

        @Override
        public void save(ChessGameMetadata chessGameMetadata) {
            metadataMap.put(chessGameMetadata.getId(), chessGameMetadata);
        }
        ...
    }
    

Так как данный код будет использоваться в REST сервисе, который по природе своей работает многопоточно,
то выбираем потокобезопасную реализацию Map — ConcurrentHashMap. Вообще, многопоточность — очень скользкая и опасная тема, и проверить или
доказать, что использования ConcurrentHashMap достаточно, довольно трудно. Будем стараться делать лучше, чем хуже и тестировать код.
Расставлять везде synchronized, чтобы гарантированно избегать коллизий — это плохое решение.
Репозиторий также отвечает за генерацию идентификатора — так же, как и обычно при работе с базами данных.

Те сервисы, которые возвращают результат сразу реализованы стандартно через Spring web:

    @PostMapping("/host")
    public ResponseEntity<?> host() {
        ChessGameMetadata metadata = repository.newGame();
        String whitePlayerToken = metadata.getPlayerTokens().get(PieceColor.WHITE);

        return ResponseEntity.ok(new GameConnectionParamsDto(metadata.getId(), whitePlayerToken,
            ColorDto.WHITE, metadata.getGameStateDtoForPlayer(whitePlayerToken)));
    }
    

Логика контроллера заключается только в преобразовании ответа к Dto объектам.

А вот Long Polling — вещь нетривиальная, и на ней остановимся подробнее.
В первую очередь, чтобы не занимать поток исполнения веб сервера (tomcat) в качестве возвращаемого результата используется
не Dto объект, а специальный DeferredResult:

    @PostMapping("{id}/wait-for-my-move")
    public DeferredResult<?> waitForMove(@RequestHeader("ptoken") String playerToken,
          @PathVariable("id") String id)
    

Сам метод контроллера отработает без пауз, а вот ответ на запрос будет отправлен только тогда, когда явно будет вызван метод deferredResult.setResult.
Мы знаем, что это нужно сделать сразу после действия другого игрока. Причём, очевидно, это будет вызов через неопределённое время и из другого потока.
Spring web mvc не даёт никаких указаний на счёт того, как это реализовать, нужно решать самостоятельно, выбирая из тех механизмов работы с потоками, которые нам доступны
в силу нашего опыта и специфики приложения.

Изолируем задачу: в один момент времени создаётся объект deferredResult в одном потоке. Затем после наступления определённого события нужно выполнить действие setResult.
Приведём к стандартной модели Publish-Subscribe. Один поток подписывается на событие — завершение хода другим игроком,
а второй поток публикует это событие как только вызывается метод сервиса /move.

Какие же варианты реализации Publish-Subscribe существуют? Вообще их безумно много.

  1. Wait-notify. На собеседованиях иногда предлагают реализовать ping-pong между двумя потоками.
    Полезно в образовательных целях, в реальных проектах нежелательно, слишком легко допустить ошибку, которая проявится в самый неподходящий момент.
  2. Блокирующие очереди, например, LinkedBlockingQueue. Метод .take() блокируется и ожидает добавления элементов в очередь. Вполне рабочий способ.
  3. Способы с использованием библиотек или внешних сервисов: Kafka, JMS, ActiveMQ, Reactor Event Bus. Хорошие, но тяжеловесные решения. Сейчас они излишни, а вот если
    переносить приложение в облако контейнеров, то придётся к ним вернуться.
  4. Java 9 Reactive Streams. Самый современный способ. Выберем его, чтобы попрактиковаться и оценить результат.
    Крайне рекомендую посмотреть пример на Baeldung, иначе будет совсем непонятно.

Логику по работе с Publisher/Subscriber инкапсулируем в отдельном классе GameManager, чтобы не перегружать контроллер.
Для начала создадим структуру для хранения обработчиков. Для каждой пары (игра, игрок) отображение содержит ровно один обработчик,
который в качестве параметра принимает изменённое состояние доски — после хода соперника.

    public class GamePlayerDesc {
        private final String gameId;
        private final String playerToken;
        ...
    }

    @Service
    public class GameManager {
        final ConcurrentMap<GamePlayerDesc, Consumer<ChessGameMetadata>> handlers = new ConcurrentHashMap<>();
        ...
    }
    

Теперь когда сервис /wait-for-my-move инициирует ожидание, в handlers добавляется обработчик:

    //GameManager
    public void awaitOpponentMove(GamePlayerDesc gamePlayerDesc, Consumer<ChessGameMetadata> handler) {
        handlers.put(gamePlayerDesc, handler);
    }
    //GameController
    gameManager.awaitOpponentMove(new GamePlayerDesc(id, playerToken), m -> {
        deferredResult.setResult(m.getGameStateDtoForPlayer(playerToken));
        log.debug("await move completed {} {}", id, playerToken);
    });
    

А когда обрабатывается /move, то по цепочке вызовов выполняется публикация события:

    gameUpdatesPublisher.submit(chessGameMetadata);
    

В соответствии с принципами работы Reactive Streams это в свою очередь приводит в вызову метода onNext() где-то там на специальном классе, реализующем интерфейс Subscriber.
Метод onNext находит обработчик, удаляет его из отображения (Map) и вызывает метод accept. Метод accept вызывает setResult на deferredResult, отправляется ответ на запрос /waitj-for-my-move,
тем самым круг замыкается.

    class GameUpdatesSubscriber implements Flow.Subscriber<ChessGameMetadata> {
    ...
        @Override
        public void onNext(ChessGameMetadata metadata) {
             GamePlayerDesc gamePlayerDesc = metadata.currentPlayerDesc();
             Optional.ofNullable(handlers.remove(gamePlayerDesc)).ifPresent(h -> {
                 h.accept(metadata);
             });

             subscription.request(1);
        }
     ...
    }  

Сложно, запутанно, непонятно. А код юнит тестов едва ли не сложнее основного. Но с этим особенно ничего не поделаешь,
с многопоточностью так всегда, а упрощать задачу, например, заменив long polling на частые вызовы со стороны клиента, не хотелось.
Что касается Reactive Streams, то думаю, что буду применять эту технологию и в дальнейшем. В данном случае она решает задачу хорошо, но кроме того обладает уникальной
дополнительной функциональностью неблокирующего «обратного давления» (back pressure), за счёт которого можно избегать переполнения очереди подписчика, если он не успевает обрабатывать события.
Эта возможность сама собой достигается при синхронных вызовах, но при асинхронных её было довольно трудно достичь.

Так или иначе, сервер практически готов. На все сервисы по ходу реализации я добавлял юнит тесты. Они получились довольно объёмными и в процессе
я нашёл несколько ошибок, но не в коде, а в самих тестах! Например, вот так я отправляю POST запрос:

    private <T, R> T post(String subUrl, Optional<String> pheader, R body, Class<T> clazz) {
         HttpHeaders httpHeaders = new HttpHeaders();
         pheader.ifPresent(h -> {
              httpHeaders.add("ptoken", h);
         });

          HttpEntity<R> request = new HttpEntity<>(body, httpHeaders);
          ResponseEntity<T> responseEntity =
              restTemplate.postForEntity("http://localhost:" + port + "/api/game/" + subUrl, request, clazz);

          assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
          return responseEntity.getBody();
    }
    

А вот так я ожидаю, когда завершится вызов wait-for-my-move:

    private CompletableFuture<GameStateDto> awaitMove(String id, String pheader) {
          return CompletableFuture.supplyAsync(() ->
          post(id + "/wait-for-my-move", Optional.of(pheader), "", GameStateDto.class), cfPool);
    }
    ...
    for (MoveDto move : moves) {
         try {
             state = awaitMove(id, token).get(TIMEOUT, TimeUnit.SECONDS);
         } catch (InterruptedException | ExecutionException | TimeoutException e) {
             e.printStackTrace();
             throw new AssertionFailedError(e.getMessage());
         }
         assertTrue(state.isMyTurn());
         state = move(id, token, move);
         assertFalse(state.isMyTurn());
    }  

В целом схема простая: у каждого игрока есть последовательность ходов, и поочерёдно вызываются сервис /move и /wait-for-my-move.
Для каждой партии создаётся два потока под этот цикл: для чёрных и для белых, а в конце тест снова использует CompletableFuture.get(…). чтобы дождаться
окончания партии и проверить результат. В тесте, проверяющем одну партию всё работало. Две партии тоже работали. Три не помню, но четыре непременно приводили к
повисанию системы навечно. В дампе потоков было видно, что какой-то из пулов потоков полностью исчерпан и все потоки в состоянии WAITING (или TIMED_WAITING, не помню).
Я грешил на то, что неправильно использую Reactive Streams. Или может DeferredResult не освобождает поток, вопреки документации. Всё это не подтвердилось.

Короче говоря, проблема оказалась в том, что в тесте на одну партию запускалось 3-4 метода CompletableFuture.get(…), но каждый из них естественно занимает один поток.
Он не занимает процессор, память, сеть или диск, то есть ресурсы не тратятся, всё как положено. Но поток-то занят! Да этих потоков можно создать хоть 32 000, а то и больше,
но это если создавать из через new Thread(), забивая планировщик задач. Но во всех механизмах работы с потоками, которые сейчас есть в приложении, потоки не создаются бесконтрольно,
используются пулы потоков. Это и потоки самого сервера Tomcat, обрабатывающие HTTP запросы (по умолчанию максимум 200 потоков). Это и потоки, которыми пользуется SubmissionPublisher
из Reactive Streams. Это и потоки, которые обрабатывают Completable Future в тесте при запуске параллельных партий.

Пул потоков для Tomcat в расчёт не берем, он живет своей жизнью. А вот все остальные по умолчанию используют встроенный ForkJoinPool.commonPool из восьми потоков.
Очень быстро все потоки уходят в ожидание результата по CompletableFuture.get(…), а для обработки ходов, которые бы привели к результату и окончанию этого ожидания потоков просто нет.
Это вполне можно назвать ситуацией Dead-Lock, потому что цикл разрывается только когда CompletableFuture.get(…) завершается по таймауту, но тогда последовательность
выполнения нарушается и результат оказывается неверный. Решение — использовать отдельные пулы для разных категорий обработчиков и, в первую очередь для CompletableFuture.
За это отвечает последний параметр cfPool:

    ExecutorService cfPool = Executors.newFixedThreadPool(12, r -> new Thread(r, "completable future pool"));

    ...
    CompletableFuture<GameStateDto> whiteMoveseq = CompletableFuture.supplyAsync(() ->
    moveSequence(white.getId(), white.getToken(), whiteMoves), cfPool);

    CompletableFuture<GameStateDto> blackMoveseq = CompletableFuture.supplyAsync(() ->
        moveSequence(black.getId(), black.getToken(), blackMoves), cfPool);
    

Теперь тест проходит и появляется уверенность (надежда), что большое количество одновременных партий никак не влияют друг на друга, то есть вся эта ужасающая многопоточность
работает не только в теории, но и на практике. Лишний раз убеждаюсь в том, что если и работать с многопоточными приложениями, то это нужно делать постоянно и глубоко разбираться в теории,
а иначе лучше лишний раз не рисковать.

И вот реализация сервера практически готова. Можно приступать к использованию всех этих сервисов в клиенте. Если работой над клиентом занимается тот же разработчик или, по крайней мере,
java программист, то можно надеяться, что документация не потребуется, либо будет достаточно общего описания. А если нет? Если придётся описывать все сервисы, параметры, коды ошибок, приводить примеры запросов
где-нибудь на confluence или вообще в Word документе?

Да и как нам самим проверить приложение? Тесты это прекрасно, но хочется запустить приложение целиком и выполнить несколько HTTP запросов так, как это будет делать клиент.
Можно использовать клиент Postman, но форматы сообщений и пути к сервисам придётся аккуратно формировать по коду приложения.
Чтобы не тратить на всё это время, можно сгенерировать одновременно документацию и клиент с помощью Swagger, при этом затратив минимум усилий.
Добавляем две зависимости в проект:

    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>

    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    

И ещё создаём конфигурацию:

    @Configuration
    @EnableSwagger2
    public class SpringFoxConfig {
        @Bean
        public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
             .select()
             .apis(RequestHandlerSelectors.any())
             .paths(PathSelectors.any())
             .build();
        }
    }
    

Swagger клиент доступен по адресу http://localhost:8085/swagger-ui.html#/ . Можно изучать сервисы, параметры и формат данных.
Удобно сразу указывать параметры POST запросов, включая заголовки. Интерфейс простой и интуитивный. Можно целую шахматную партию сыграть, не выходя из него.
Пара скриншотов для наглядности.

картинки нет, но вы держитесь
картинки нет, но вы держитесь

Фронтенд (Front-end)

Теперь приступим к реализации интерфейса. Как правило, во внутренних корпоративных системах фронтендом могут заниматься Java разработчики.
В таких приложения от сайта не требуется работать во всех браузерах на всех устройствах, иметь красивые современные стили и анимации и так далее.
В общем, интерфейс должен быть функциональным и рабочим, но конкурировать ни с кем не приходится. Если же нужен качественный, можно сказать, профессиональный сайт для широкой аудитории,
то чаще его разрабатывает отдельная команда.

Для наших шахмат попробуем реализовать простой функциональный интерфейс без изысков, но и не в стиле 90-x, по возможности.
Тем не менее, вполне вероятно, что мы распределили задачи по разработке бэкенда и фронтенда между сотрудниками отдела.
Поэтому будем исходить из предположения, что сервер ещё не готов, максимум существует REST интерфейс,
который поддерживает несколько простых вызовов, чтобы мы могли сориентироваться по формату данных.

Для начала создадим минимальный набор файлов: index.html, main.js, main.css, а также скопируем bootstrap и jquery. Пока что все файлы помещаем в корень директории static.
Если файлы будут множиться, создадим отдельные директории для Javascript или CSS файлов. Так же сразу в index.html добавляем минимальный head, пустой body м подключаем css и js.

Теперь подумаем над задачей. Я думаю, что лучше сначала сконцентрироваться на «движке» — отображении шахматной доски и фигур, обработку общения с сервером и совершение ходов.
Эту часть работы можно выполнить максимально изолированно, пока остальные требования будут, возможно, уточняться.
Также, по первому впечатлению, это самая технически сложная и объёмная часть.

Как же отобразить шахматное поле? Можно воспользоваться canvas — будем рисовать все границы, линии и фигуры самостоятельно.
Canvas позволит нарисовать всё, что угодно, но кажется слишком сложно, долго и муторно.
Если можно сделать отображение на чистом html — это было бь лучше. Какие возможности интерфейса нам нужны?

  1. Доска — всегда 8*8, никак не трансформируется
  2. Ход — клик на одно поле, подсветка полей, на которые можно переместить фигуру и клик на второе поле
  3. Поле — квадрат фиксированного размера, чёрное или белое, может содержать одну из шести шахматных фигур двух цветов

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

картинки нет, но вы держитесь

Так, само поле 8*8, но ещё есть обозначения по краям — то есть заведём таблицу 10*10. Для начала из пары строк:

    <table class="table-bordered">
        <tr>
            <td></td><td>a</td><td>b</td><td>c</td><td>d</td><td>e</td><td>f</td><td>g</td><td>h</td><td></td>
        </tr>
        <tr>
            <td>8</td>
            <td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td>8</td>
        </tr>
    </table>
    

Вроде получается, но сразу понятно, что нам нужно контролировать размеры ячеек и отличать чёрные от белых.
Поэтому проставим css классы cell, cell-white, cell-black в таблице и объявим их в main.css с минимальными настройками.

    <tr>
         <td>8</td>
         <td class="cell cell-black"></td>
         <td class="cell cell-white"></td>
    ...
    
     .cell {
         width: 60px;
         height: 60px;
     }
     .cell-white {
         background-color: #ffff80;
     }
     .cell-black {
        background-color: #a3a264;
     }
   

Я выбрал цвета с оттенком желтого. Хочу, чтобы было похоже на старые советские деревянные лакированные шахматы.
Результат:

Буквы не по центру, лишние границы по краям, в остальном движемся верно. Подобные мелочи оставлю за кадром.
Давайте ещё добавим фигуры и подсветку полей, на которую можно перемещать фигуру, выбранной фигуры и последнего хода соперника (зелёный, чёрный и красный, соответственно).
Изображения возьмём векторные и наличие фигуры на поле будет определяться css стилем или data атрибутами, а не содержимым ячейки таблицы.
Так будет проще изменять состояние, потому что не нужно создавать или удалять html элементы.
Я выбрал data атрибуты, по сути для подобных целей они идеально подходят.

    <td class="cell cell-black" data-piece="bishop-black"></td>
    <td class="cell cell-white" data-piece-state="move"></td>
    <td class="cell cell-black"></td>
    <td class="cell cell-white" data-piece-state="eat"></td>
    <td class="cell cell-black"></td>
    <td class="cell cell-white" data-piece-state="selected"></td>
    
    td[data-piece='bishop-black']{
       background-image: url("/img/posts/chess/bishop.svg");
       background-size: contain;
    }

    td[data-piece-state='selected']{
       border: 5px solid black !important;
    }

    td[data-piece-state='eat']{
      border: 5px solid red !important;
    }

    td[data-piece-state='move']{
        border: 5px solid green !important;
    }
    

Выглядит неплохо. Не получается пока обойтись одной svg картинкой для каждой фигуры, вероятно придётся
отдельно иметь bishop-black.svg и bishop-white.svg (fill:white почему-то не помогает). Опять же, не зацикливаемся на подобных мелочах.

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

Клиент будет работать по принципу одностраничного сайта. Html код страницы загружается по первому запросу, но содержит только скелет приложения и заголовки.
Наполнение страницы формируется динамически по результатам AJAX запросов с использованием библиотеки JQuery.
Нам уже известен формат объекта GameStateDto, который описывает состояние шахматной доски.
Остальные сервисы пока в разработке (гипотетически).

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

Итак, чтобы создать пустое поле нужно сгенерировать восемь строк по восемь столбцов, а для чёрных нужно всё инвертировать (перевернуть доску).
Каждое второе поле помечаем белым (cell-white), остальные — чёрным (cell-black). На всех полях требуется идентификатор позиции (data-position=’e2′), чтобы потом можно было расставить фигуры.
Каждому полю требуется обработчик onClick, для которого пока заведём пустую функцию. Также не забываем про буквы и цифры по краям таблицы.
Вот что получилось:

    function emptyBoard() {
        const board = $("#board");
        let trs = [];

        let rowNums = [1, 2, 3, 4, 5, 6, 7, 8];
        let cols = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
        let styles = ['cell-black', 'cell-white'];

        if (state.isBlack) {
            cols.reverse();
        } else {
            rowNums.reverse();
        }

        board.append(topTr(cols));
        rowNums.forEach((r, i) => {
            styles.reverse();
            board.append(boardTr(r, cols, styles));
        });
        board.append(topTr(cols));
    }

    function topTr(cols) {
         let tr = $('<tr class="text-center">');
         tr.append($('<td>'));
         cols.forEach((c, i) => {
             tr.append($('<td>').text(c));
         });
         tr.append($('<td>'));
         return tr;
    }

    function boardTr(row, cols, styles) {
        let tr = $('<tr>');
        tr.append($('<td>').text(row));
        cols.forEach((c, i) => {
            let pos = c + row;
            tr.append($('<td>')
                .addClass('cell')
                .addClass(styles[i % 2])
                .attr('data-position', pos)
                .click(() => {
                     handleBoardClick(pos);
                 }));
        });
        tr.append($('<td>').text(row));
        return tr;
    } 

Не знаю, как прокомментировать javascript код. При его виде, честно говоря, хочется перейти хотя бы на Typescript, чтобы иметь
больше возможностей отделить бизнес логику от манипуляций с DOM. Но в целом, он не очень сложный, ведь это просто таблица 10*10.
Пустую доску можно было бы вообще зафиксировать в html, но пришлось бы прописывать атрибуты для 64 элементов таблицы для ориентации от белого игрока и ещё столько же от чёрного.
Результат:

картинки нет, но вы держитесь

Теперь разместим на поле фигуры. Предполагаем, что в глобальной переменной state.gameState находится объект отражающий структуру GameStateDto.

    function atPosition(pos) {
        return $('#board td[data-position=' + pos + ']');
    }

    function cleanupPieces() {
        foreachPosition(p => {
            atPosition(p).attr('data-piece', '');
        });
    }

    function cleanupSelections() {
        foreachPosition(p => {
            atPosition(p).attr('data-piece-state', '');
        });
    }

    function updateBoardState() {
        cleanupPieces();
        cleanupSelections();

        state.gameState.pieces.forEach((p, i) => {
           setPiece(p);
        });
    }

    function setPiece(piece) {
        const square = atPosition(piece.position);
        const dp = piece.pieceType.toLowerCase() + '-' + piece.color.toLowerCase();
        square.attr('data-piece', dp);
        square.attr('width', '200px');
    }
    

В последней функции мы преобразовываем два поля pieceType (e.g KNIGHT) и color (e.g. WHITE) в строку, которая используется в css стилях (e.g. knight-white).
Вот наше поле. Я заменил svg картинки на png, но более красивые из-за того, что просто поменяв заливку на белую, все иконки превратились в непонятную бесконтурную кляксу.

картинки нет, но вы держитесь

Напомню, что все фигуры на доске определяются наличием специальных атрибутов (data-piece).
Мы не добавляем и удаляем img теги, а только устанавливаем и очищаем атрибуты, а фигуры появляются на поле благодаря css селекторам.
То же самое с подсветкой фигур.

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

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

    <div id="hostscreen" class="ontop">
        <div class="container">
            <div class="row">
                <div class="col-lg-2 col-md-2">
                </div>
                <div class="col-lg-10 col-md-10">
                    <h3>Отправьте эту ссылку второму игроку:</h3>
                    <p>
                        <span id="joinlink"></span>
                    </p>
                    <h4>Ожидаем подключения соперника...</h4>
                </div>
            </div>
         </div>
    </div>
    
    .ontop {
          display: none; /* Hidden by default */
          position: fixed; /* Stay in place */
          z-index: 1; /* Sit on top */
          padding-top: 200px; /* Location of the box */
          left: 0;
          top: 0;
          width: 100%; /* Full width */
          height: 100%; /* Full height */
          overflow: auto; /* Enable scroll if needed */
          background-color: rgb(255,255,255); /* Fallback color */
          background-color: rgba(255,255,255,0.95); /* Black w/ opacity */
    } 

картинки нет, но вы держитесь

Первое, что делаем при открытии страницы — проверяем, есть ли параметр URL joinid:

    $(document).ready(function () {
        const searchParams = new URLSearchParams(window.location.search);
        if (!searchParams.has('joinid')) {
            host();
        } else {
            join(searchParams.get('joinid'));
        }
    }); 

Все наши REST сервисы запрашиваем по общей схеме используя функцию ajax, которая принимает все параметры, заголовки, тело запроса и
функции — обработчики успешного вызова и ошибок.

    function host() {
    $.ajax({
         type: "POST",
         url: "/api/game/host",
         data: JSON.stringify({}),
         contentType: "application/json; charset=utf-8",
         dataType: "json",
         success: function (data) {
             initGameParams(data);
         },
         error: function (errMsg) {
             alert(errMsg);
         }
         });
    }
    function initGameParams(params) {
         state.gameId = params.id;
         state.ptoken = params.token;
         state.isBlack = "black" === params.playerSide.toLowerCase();
         state.playerSide = params.playerSide;
         state.gameState = params.gameState;
         emptyBoard();

         updateStatusMessage();
         updateBoardState();

         setJoinLink(state.gameId);
         if (!state.isBlack) {
             $("#hostscreen").toggle();
         }

         await(1000 * 60 * 5, 50);
    } 

Чувствую, что начинаю просто весь javascript код сюда копировать, особо не объясняя. Но здесь самое интересное — это, пожалуй — ожидание действия соперника, а именно метод await.
Он вызывает сервис wait-for-my-move и ожидает 5 минут, после чего повторяется. Я ограничил количество вызовов числом 50, чтобы забытые вкладки в браузере не выполняли код бесконечно.

    function await(timeoutms, max) {
    if (max === 0) {
    return;
    }

    $.ajax({
        type: "POST",
        url: "/api/game/" + state.gameId + "/wait-for-my-move",
        data: JSON.stringify({}),
        contentType: "application/json; charset=utf-8",
        headers: {'ptoken': state.ptoken},
        dataType: "json",
        error: function (err) {
            if (err.statusText === 'timeout') {
                await(timeoutms, max - 1);
            } else {
                console.log(err.status + " " + err.statusText);
            }
        },
        success: function (data) {
            handleBoardUpdate(data);
        },
        timeout: timeoutms
        });
    } 

При обработке нажатия на поле мы сначала помечаем соответствующим состоянием (data-piece-state = ‘move’) поля, на которые можно переместить фигуру.
А если поле уже отмечено, то воспринимаем это как завершение хода и отправляем соответствующий запрос /move и вызываем await().
Последний ход оппонента у нас отмечен красным выделением, обновляем его каждый раз, чтобы этот стиль не терялся при коллизиях.
Каждый раз обрабатывая обновление игрового поля, сохраняем все разрешённые ходы в общую переменную validMoves, чтобы не искать их там потом линейным проходом по списку фигур.

    function handleBoardClick(pos) {
        if (atPosition(pos).attr('data-piece-state') === 'move') {
            handleMove(pos);
        } else {

        cleanupSelections();
        highlightLastOppMove();
        if (!state.gameState.gameFinished
            && state.gameState.myTurn
            && atPosition(pos).attr('data-piece')
            && atPosition(pos).attr('data-piece').includes(state.playerSide.toLowerCase())) {
                handleMyPieceClickOnMyMove(pos);
            }
        }
    }

    function handleMyPieceClickOnMyMove(pos) {
        atPosition(pos).attr('data-piece-state', 'selected');
        state.selectedPosition = pos;
        const validMoves = state.validMoves[pos];
        if (validMoves) {
            validMoves.forEach((m, i) => {
                atPosition(m).attr('data-piece-state', 'move');
            });
        }
    }  

картинки нет, но вы держитесь

В игре есть ещё немного функционала, который я не описал: верхнее меню, отображение активного игрока (белые или чёрные), выбор фигуры, когда пешка доходит до границы поля, а
также всякие мелочи. Но ничего принципиально нового или примечательно в реализации этого функционала нет. Что-то не реализовано: шахматный таймер и список ходов, но код и так уже слишком
длинный для подобного формата и опять же, ничего принципиально нового для реализации тут не потребуется. Таймер — просто целое число, которое бы присылал сервер, а javascript уменьшал каждую секунду
при помощи setInterval. Настраивать начальный таймер можно дополнительными параметрами метода /host.

Отмечу, что мне не очень нравится, как структурирован Javascript код: всё в одном файле, все функции и переменные глобальные. Нужные функции постоянно приходится искать поиском.
Кроме того, нет ни единого юнит теста, поэтому при внесении изменений приходится вручную проверять основные сценарии.
Для примера и демонстрации отдельных функций сгодится, но для серьезного приложения нужно изучать более продвинутые практики. Например, использовать прототипы
и таким образом распределять код на разные классы. Возможно, следует писать на typescript, а не чистом javascript. Frontend разработчики успешно пользуются этими приемами.
Однако в простых приложениях это выглядит избыточным.

Долго ли, коротко ли, а наша реализация подошла к концу. Остаётся только открыть две вкладки в браузере и поиграть, чтобы проверить различные сценарии, рокировки, шахи, маты
и прочее. Видно, что ход в одной вкладке мгновенно отображается в другой. Также можно уйти на полдня, сделать ход, и игра успешно продолжится.
Однако, если случайно обновить вкладку, то игра будет потеряна, вернуться в неё нельзя — но это соответствует изначальному проекту и требованиям.
То, что мы избежали сессии и даже cookie — это может быть намного полезнее в отдельных случаях.
Жаль только, что у меня нет сейчас под рукой сервера, чтобы захостить эту реализацию, но локально всё можно запустить из github репозитоия (главный класс ChessApplication),
никаких дополнительных настроек не требуется.

Заключение

Цель данного повествования — показать ключевые моменты планирования, проектирования и разработки. Даже немного затронута коммуникация между отдельными командами или разработчиками.
Полноценным учебником (гайдом, туториалом), по которому можно повторить все шаги и получить в результате рабочий код, это назвать нельзя.
Кроме того, если такое приложение действительно выкладывать в общий доступ и привлекать живых пользователей, то предстоит дополнительный и огромный объём работ разработки и тестирования.
Текущая версия — это скорее прототип. Однако реализовывать подобные приложения просто в качестве обучения или практики для расширения кругозора или работы с frontend, с которым традиционно
небольшой опыт у java разработчиков, очень полезно.

Chess

This is my implementation of Chess in Java. During the SARS-COV-2 pandemic, I began playing more chess. This lead me to want to build my own chess game, with the hopes of improving my Java skills.

Table of Contents

  • Description
  • 1 — Basic Logic
  • 2 — Enums
    • ID
    • COLOUR
    • BOARD
  • 3 — Key Classes
    • Coordinate
    • Piece
    • Pieces
    • Move
  • 4 — The UI
    • Printing the Interface
    • Handling movements
  • 5 — Saving a Game
  • 6 — To Do List
  • 7 — Pictures
    • Terminal Interface
    • UI

Description

Initially, I thought I could only develop a terminal based game, as that was all that I maanged when I had made games before. However, feeling usnatisfied, I learnt Swing to develop a UI that definetely improved the game experience. Both implementations have the same underlying processes. To ensure the correct functioning of the whole project, I created JUNIT tests for most classes

1 — Basic Logic ♟

The logic for the terminal based game and the UI is essentially the same:

  1. Instantiate a pieces class. This is the board of the game, and contains all of the pieces.
  2. Select a piece and select a coordinate. If the destination coordinate is a valid coordinate for your given piece, and its the piece’s colour turn to move, then make a move.
  • this is represented by changing pieces.
  • the piece’s key changes from its origin to its destination coordinate
  • if a piece from the opposite colour was occupying the coordiante, that piece is eliminated
  1. Check if its check mate. If so, the game ends. Otherwise, it is the turn of the other colour.

2 — Enums ♞

Enums were an integral part of this project, as I used them to represent important constants for the game. The enums I created were ID, COLOUR and BOARD.

ID

Used as an identifier for a piece. The types of pieces are:

  • KING
  • QUEEN
  • ROOK
  • BISHOP
  • KNIGHT
  • PAWN

This enum contained 2 toString() methods. One (toString()) is used to print the piece’s letter for describing moves. For example, if a bishop moves to e6, such move would be described as Be6. The other one (toFullString()) is mainly used for testing and «human» printing purposes. It returns the full name of the String. For example, ID.KING.toFullString() would return "King".

COLOUR

Used as a colour identifier for a piece. These are:

  • B (a black piece)
  • W (a white piece)

This enum also contains 2 toString() methods, albeit these are barely used (mainly only for tests). The most important method is the not(COLOUR) method. It is used to return the opposite colour to the argument it takes. Hence, COLOUR.not(COLOUR.B) would return COLOUR.W. This is extremely useful, for example when handling the turn of play, or calculating when a move leads to check.

BOARD

Used to contain the dimensions of the board. These are determined by 4 constants:

  • FIRST_FILE(‘a’)
  • LAST_FILE(‘h’)
  • FIRST_RANK(1)
  • LAST_RANK(8)

A file is used to represent a column, and is represented by a character from a to h. A rank represents a row, and is represented by an integer from 1 to 8. BOARD contains methods to access the values associated with these constants.

3 — Key Classes ♝

There are 4 key classes that sustain this project: Coordinate, Piece, Pieces and Move. The first 3 are used to create objects to represent the chess board and its pieces. They all contain getters, setters, alongside functionality to create deep copies of its instances. This is paramount, as will be explained later. The methods toString(), equals(Object o) and hashCode() have all been overridden. The last class, Move simply contains methods that are essential for the correct functioining of the project.

Coordinate

Uses a char (file) and an int (rank) to determine a square within a board, according to Chess nomenclature. Includes functionality to ensure that the arguments provided represent a valid coordinate within the board.

Piece

A class identifying the pieces of the game. A piece is initialised with an ID (type of piece), a COLOUR (black or white) and its initial Coordinate within the board. It acts as a super class to the more specific pieces: King, Queen, Rook, Bishop, Knight, and Pawn. The most important method in Piece have to do with the creation, updating and validation of the moves that a piece can move. We define raw moves as those moves that a piece can make independently of whether the King is in check of not. Potential moves are the actual moves that a piece can make, taking checks into accounts. Piece contains abstract methods that are then individually defined within the children classes. For example, getRawMoves(Pieces pieces) is used to obtain the raw moves that are available to an individual piece. Since each piece moves differently, the details of getRawMoves(Pieces pieces) are defined individually.

Perhaps the most important of all its methods is removeOwnCheck(Pieces pieces). This method is used to take in the raw moves available to a piece, and then filter out all of the moves that are impossible; namely those that would either:

  • lead to check
  • not stop a check (i.e if a piece moves away from the King, leading to a check by the opposition)

In order to do this, we must create a deep copy of the board. From the raw moves of the piece, we make the piece execute the move within the copied board. We then check if that has lead to situation of check by the opposition. If it has, said move is deleted. Otherwise, it is maintained. This is a crucial process, as it allows the pieces to determine all of their moves, so checking whether the move provided by the user is legal becomes trivial. Moreover, for the UI, it allows us to display all the moves avaialbale to the given piece.

Pieces

Contains a HashMap with Coordinate-Piece key-value pairs. It contains all the methods used to handling the positioning of the pieces throughout the game. For example, we can use it to find the King of a certain colour, determine which pieces lie on the same file or whether it is the end of the game (via check mate or a draw/stalemate). Pieces also contains the method that executes the moves provided by the user. It is a particularly long method, which must check for all moves that constitute special cases, such as a King castling or a pawn queening/capturing in diagonal/en passant.

Move

This class contains all the classes pertinent to the movement of the pieces. It contains functionality to, given a board (Pieces) and a piece determine which range of movement it has. We can determine available moves in vertical, diagonal and horizontal direction, alongside the moves available to a Knight. It is these methods that are used within a Piece to determine the raw moves available to them. It must be noted that there are pieces, such as the King or the Pawn that have a special range of moves available to them. The handling of these moves is made directly within their classes.

4 — The UI ♜

To create the UI, I used the Swing package.

Printing the Interface

The interface is mainly made through the superposition of JPanels. To create the chess board, I created a 2D array of JButtons, each of alternating colours. I used a nested for loop to print the whole chess board. I also used this to assign coordinates to each JButton square, which allowed me to display a picture representing a piece on top of the square. Then, with each move, I executed the for loop again, but with pieces updated to reflect the current game. I also created an area to the right of the board that contained a section to see the moves played, alongside a button for saving a game, and an area displaying the outcome of a game (a draw, stalemate or win).

Handling movements

To make a move, the user needs to select a piece, and then select a destination square. I created a flag that would allow me to check whether the user has clicked twice, as this would represent a move. When the user clicks a square (JButton), I looped through the array of JButtons until I found the JButton that had been clicked. I then turned this information into a Coordinate, which then allowed me to find the Piece occupying the square. This then made it so the squares corresponding to the potential moves of the piece got illuminated. It also set the flag to true. Once there was a second click the program checked to see if the selected square corresponded to one of the potential moves of the piece. If so, the move was executed and the board was updated, resetting the flag. Otherwise, the potential moves of the selected piece would be shown.

5 — Saving a Game ♛

The FileIO class is used to handle game saving. In order to save a game, we create a txt file containing the moves, as per pgn (portable game notation) format (albeit without additional information, such as the date, location, players involved, etc …). FileIO contains a method that handles the creation of Strings representing sets of moves. The user can then introduce the name of the file to be saved.

6 — To Do List ♚

I believe I have merely constructed the beginning of the project. To further improve it, I would like to (in order to feasability and ease):

  • add functionality to parse pgn/txt files and load their games
  • add functionality so that a user can drag a piece to move it (currently need 2 clicks)
  • add a player vs player functionality (switching the position of the board to face the player)
  • add a timer
  • add an opening move handbook
  • create an AI of varying difficulty (player vs computer)
  • create an ML algorithm

7 — Pictures

Terminal Interface

UI

The initial set up:

Clicking on the Knight revels it has 2 potential moves (f3 & h3):

The Knight has moved, as shown in the game log to the right; the black pawn has 2 potential moves (d6 & d5):

We can choose to save the game under any name we choose (as long as it doesn’t already exist!):

The black Pawn can now be promoted:

The white King is in check, so its movements are limited:

If we want to save the King, the white Bishop only has 1 square available:

A situation of checkmate:

I would like submitting a simple chess board drawing example using Unicode characters. There 3 classes involved into this tiny project.

ChessLabel.java

import java.awt.Color;
import java.awt.Font;
import javax.swing.JLabel;
import javax.swing.SwingConstants;


public class ChessLabel extends JLabel {

    Font font     = new Font("Ariel", Font.PLAIN, 24);
    Color bgLight = new Color(222, 184, 135);
    Color bgDark  = new Color(139, 69, 19);

    ChessLabel(String s)
    {
        super(s);
    }

    void set(int idx, int row)
    {
      setFont(font);
          setOpaque(true);
          setBackground((idx+row)%2 == 0 ? bgDark : bgLight);
          setHorizontalAlignment( SwingConstants.CENTER );
    }

}

Board.java

import java.awt.*;
import javax.swing.JFrame;


public class Board extends JFrame {


   //Initialise arrays to hold panels and images of the board

    private ChessLabel[] labels = new ChessLabel[] {

    // white
    new ChessLabel("u2656"), new ChessLabel("u2658"), new ChessLabel("u2657"), 
    new ChessLabel("u2655"), new ChessLabel("u2654"), new ChessLabel("u2657"), 
    new ChessLabel("u2658"), new ChessLabel("u2656"), new ChessLabel("u2659"), 
    new ChessLabel("u2659"), new ChessLabel("u2659"), new ChessLabel("u2659"),
    new ChessLabel("u2659"), new ChessLabel("u2659"), new ChessLabel("u2659"), 
    new  ChessLabel("u2659"), 
    // empty
    new ChessLabel(" "), new ChessLabel(" "), new ChessLabel(" "), 
    new ChessLabel(" "), new ChessLabel(" "), new ChessLabel(" "), 
    new ChessLabel(" "), new ChessLabel(" "), new ChessLabel(" "), 
    new ChessLabel(" "), new ChessLabel(" "), new ChessLabel(" "), 
    new ChessLabel(" "), new ChessLabel(" "), new ChessLabel(" "), 
    new ChessLabel(" "), new ChessLabel(" "), new ChessLabel(" "), 
    new ChessLabel(" "), new ChessLabel(" "), new ChessLabel(" "), 
    new ChessLabel(" "), new ChessLabel(" "), new ChessLabel(" "),
    new ChessLabel(" "), new ChessLabel(" "), new ChessLabel(" "), 
    new ChessLabel(" "), new ChessLabel(" "), new ChessLabel(" "), 
    new ChessLabel(" "), new ChessLabel(" "),
    // black
    new ChessLabel("u265F"), new ChessLabel("u265F"), new ChessLabel("u265F"), 
    new ChessLabel("u265F"), new ChessLabel("u265F"), new ChessLabel("u265F"), 
    new ChessLabel("u265F"), new ChessLabel("u265F"), new ChessLabel("u265C"), 
    new ChessLabel("u265E"), new ChessLabel("u265D"), new ChessLabel("u265B"), 
    new ChessLabel("u265A"), new ChessLabel("u265D"), new ChessLabel("u265E"), 
    new ChessLabel("u265C")
    };

    public Board() 
    {

    } // Board()

    void display()
    {
        setTitle("Chess board with unicode images");

        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        Container contentPane = getContentPane();
        GridLayout gridLayout = new GridLayout(8, 8);

        contentPane.setLayout(gridLayout);

        int row = -1;
        for (int i = 0; i < labels.length; i++) 
        {
            if(i % 8 == 0) row ++; // increment row number
            labels[i].set(i, row);
            contentPane.add(labels[i]);
        } // i

        setSize(600, 600);
        setLocationRelativeTo(null);
        setVisible(true);
     } // display()

} // class Board

And ChessBoardTest.java

public class ChessBoardTest {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) 
    {
        Board board = new Board();

        board.display();
    }

}

Written on 27 Мая 2011. Posted in Java

Пример снимка экрана программы «Шахматы», написанной на JavaFX

Введение

Компания Sun недавно выпустила новый язык программирования для платформы Java, называемый JavaFX. Его главной задачей является облегчение разработки сетевых приложений с широкими возможностями (RIA), которые могут запускаться на различных устройствах, включая компьютеры, мобильные телефоны и Blu-ray проигрыватели. Чаще всего его сравнивают с новыми языками программирования сетевых приложений с широкими возможностями (RIA) от Microsoft (Silverlight) и Adobe (AIR). JavaFX не ограничен областью создания RIA. В этой статье описана разработка приложения «Шахматы», запускающегося на рабочем столе. Приложение называется моделью шахмат, потому что использованный алгоритм всего лишь выбирает ходы случайным образом. Так что если вы не сможете победить эту шахматную программу, значит вы худший игрок в шахматы. И это весьма плохо. Вероятно, в будущем будет написана лучшая программа для игры в шахматы и  появится статья под названием «Умные шахматы».

Предпосылки 

Наилучшим способом самостоятельного изучения JavaFX будет начало разработки программного проекта на тему, которой вы интересуетесь, и завершение проекта на новом языке программирования. Примерно в течение месяца мы писали программу «Шахматы» на JavaFX. Эта статья описывает опыт обучения и дает введение в основы программирования на JavaFX.

Многие ожидают, что JavaFX будет похож на Java, только с новым набором основных классов, которые нужно будет изучить. Они ошибаются. JavaFX — это полностью отличающийся язык программирования. Чтобы помочь опытным программистам Java, читающим данную статью, мы привели примеры преобразования кода из JavaFX в Java, что помочь вам лучше понять пример кода JavaFX, приведенный в этой статье.

Использование кода

Мы используем интегрированную среду разработки (IDE) при работе (Eclipse и Visual Studio) и действительно получаем  удовольствие от работы с ними. Однако для данного проекта мы решили не использовать IDE. Ниже описывается, какие программные продукты вам нужно будет загрузить, в зависимости от того, используете вы командную строку или IDE:

  • Чтобы скомпоновать программу «Шахматы» из командной строки, вы должны загрузить JavaFX 1.1 SDK.
  • Если вы предпочитаете работать в IDE, вы можете загрузить NetBeans IDE 6.5 для JavaFX 1.1.
  • Если вы предпочитаете Eclipse, а не NetBeans, то для Eclipse доступен плагин.
  • Чтобы преобразовать SVG в графическое представление JavaFX, также необходимо скачать JavaFX 1.1 Production Suite (производственный комплект).

Все это можно загрузить по адресу javafx.com, исключая плагин Eclipse, который доступен по адресу kenai.com. Мы никогда не использовали NetBeans или Eclipse для компоновки программы JavaFX, так что мы не уверены, насколько хорошо каждая из них работает.

Файлы исходного кода содержат файлы исходного кода на Java 1.6 и JavaFX 1.1. То есть вам также потребуется компилятор Java 1.6 для компоновки кода. NetBeans и Eclipse поставляются сразу с компилятором Java. Если вы выполняете компоновку с помощью командной строки, то вам потребуется скачать последнюю версию JDK по адресу java.sun.com.

Чтобы скомпоновать исходный код из командной строки и запустить программу «Шахматы», введите следующие команды в командную строку (выполняйте это из директории, которая содержит исходный код):

Компиляция и запуск программы «Шахматы»

javac *.java
javafxc *.fx
javafx Main

javac – это компилятор командной строки, который компилирует файлы исходного кода Java, javafxc — это компилятор командной строки, который компилирует файлы исходного кода JavaFX, и javafx запускает программу «Шахматы» с помощью запуска специально определенного файла JavaFX (в данном случае начинает выполняться код, скомпилированный из файла Main.fx).

Точки интереса

JavaFX — это очень молодой язык программирования и определенно  воспринимается как бета-версия продукта. Раньше мы никогда не находили столько ошибок в языке программирования, и при этом использовали его всего лишь в течение месяца для написания одного приложения. Нам удалось найти способы обойти некоторые ошибки, но не все из них. Мы так и не смогли изменить иконку для приложения.

Язык подает немалые надежды,  в числе которых и то, что эти ошибки будут устранены, когда продукт разовьется до серьезного уровня. Нам нравиться функция связывания, и создавать GUI (Графический интерфейс пользователя) нам  было намного легче по сравнению с первым опытом создания GUI в Java. Java – это очень развитый язык (он существует почти десятилетие), и для Java было написано очень много кода. Было бы хорошо иметь возможность создавать экземпляр этой большой библиотеки существующего кода Java непосредственно из кода JavaFX.

Если вы решите заниматься программированием на JavaFX, учтите, что много примеров кода на JavaFX, найденных  нами в Google, не работают в последней версии JavaFX (v. 1.1). JavaFX значительно изменился с середины 2008 года. Многие статьи, опубликованные до середины 2008 года, не работают в версии 1.1, при этом большая часть кода, опубликованная в конце 2008-го и в 2009 годах, работает прекрасно.

Начало работы

Давайте начнем с простой программы Hello World (здравствуй мир) и постепенно приступим к созданию шахматной программы. Как вы вскоре увидите, фактически программа Hello World научит вас многому. Здесь приводится код для Hello World на JavaFX:

Программа Hello World, написанная на JavaFX

println("Hello World!");

Это всего лишь одна строка кода. Простая программа, состоящая из одной строки, многому нас научит. Эквивалентная программа на Java выглядит так:

Программа Hello World, написанная на Java

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

Отсутствие необходимости в main или классе

Чему данный пример может научить? Если вы раньше работали с Java, то вы привыкли помещать все в классы. Потому что в Java никакой код не может существовать вне класса. Как видите, в JavaFX это не так.

В Java требуется создавать метод main, который объявляется как public static void, принимающий единственный параметр String[] args (аргументы командной строки). В JavaFX это не требуется. Вы просто помещаете инструкции в файл сценария JavaFX и запускаете файл. Метод main не требуется.

Если вам нужно получить доступ к аргументам командной строки, вы можете сделать это с помощью специальной функции run. run – это специальная функция, служащая главной точкой входа в сценарий. Функция run выглядит примерно так:

«run» – это вспомогательная функция, служащая главной точкой входа в сценарий JavaFX

function run(args: String[]) {
    println("{args[0]}");
}

Эта программа просто выводит на экран первый аргумент командной строки. Удивительно, но она выполняется без ошибки: она просто выводит новую строку, если не было передано ни одного аргумента командной строки.

Последовательности против массивов

Давайте продолжим обсуждение функции run. Она принимает единственный параметр, являющийся последовательностью объектов типа String и называющийся args. Последовательности в JavaFX выглядят как массивы в Java. Они объявляются с помощью помещения символа квадратных скобок []после типа. Например, String[] объявляет последовательность Strings. Последовательности неизменяемы, так же, как и массивы. Однако можно написать код, вставляющий элементы в последовательность и удаляющий элементы из последовательности, примерно так:

Вы можете добавлять и удалять элементы из последовательностей

var colors = ["Red"];
insert "Blue" into colors;
insert "Green" after colors[1];
insert "White" before colors[0];
delete "White" from colors;
delete colors[0];
println("{sizeof colors}");

Поскольку последовательности неизменяемы, Javafx создает новую последовательность, когда элементы вставляются в или удаляются из последовательности. Таким же образом работают Strings в Java. Вы можете изменять String в коде, но поскольку Strings неизменяемы, среда выполнения создает новый объект String,String. если вы изменяете объект

JavaFX имеет несколько способов создания числовых последовательностей путем использования специального (…) кода. Следующий код показывает несколько примеров создания числовых последовательностей:

Примеры создания числовых последовательностей в JavaFX

def seq1 = [1..10];  // seq1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def seq2 = [1..10 step 2];  // seq2 = [1, 3, 5, 7, 9]
def seq3 = [10..1 step -1];  // seq3 = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

Цикл for в JavaFX работает только с последовательностями. Он имеет вид for(имя(  )Переменной in. Следующий код показывает, как преобразовать простой цикл Java for в цикл JavaFX: последовательность) { … }

Различия между циклами «for» в Java и JavaFX

// цикл for Java
for(int i=0; i<10; i++) {
    System.out.println("Hello World!");
}

// эквивалентный цикл for JavaFX
for(i in [0..9]) {
    println("Hello World!");
}

В JavaFX можно использовать ключевое слово indexof, чтобы получить порядковый номер элемента в последовательности. Например:

Организация цикла по числовой последовательности и использование ключевого слова «indexof»

for(i in [100..110 step2]) {
    println("{indexof i} = {i}");
}

// Вывод
0 = 100
1 = 102
2 = 104
3 = 106
4 = 108
5 = 110

Переменные и параметры

Можно увидеть два различных вида объявления переменных/параметров в коде JavaFX. Первый вид имеет параметр args в функции run (объявленный как тип String[]) и второй вид был у переменной colors в примере последовательности (нет объявления типа). Все переменные и параметры должны быть объявлены в JavaFX, однако указывать тип необязательно. Если тип переменной не объявляется, среда выполнения определяет его на основе типа объекта, назначенного этой переменной. Многие предпочитают указывать тип переменной. Это можно сделать, поместив символ : после имени и вслед за двоеточием указать тип переменной. Например, так можно назначить тип переменной colors:

Объявлять тип переменной необязательно

var colors: String[] = ["Red"];

Параметры функций просто объявляются с именем переменной и необязательным типом. Переменные объявляются как var или как def. Различие между двумя видами объявления заключается в том, что vardef нельзя изменить. Переменная, объявленная как def, сходна с переменной, объявленной как final в Java. Ниже приводится правильный код: можно изменить, а

Переменные, объявленные как «def», можно изменить

// правильный код
var i = 7;
i = 2;

Однако этот код не будет компилироваться:

Переменные, объявленные как «def», нельзя изменить

// неправильный код, не будет компилироваться
def i = 7;
i = 2;

Параметры функций не могут содержать ключевые слова var или def. Все параметры функций рассматриваются как defs, и поэтому их нельзя изменить.

Функции

Все функции должны объявляться с использованием ключевого слова function. Для нас это требование было довольно неожиданным. Языки сценариев обычно известны своей краткостью. Это ключевое слово не кажется необходимым и является довольно большим, целых 8 символов. Компилятор должен иметь возможность распознать функцию, когда он видит ее.

Каждая функция имеет возвращаемый тип (тип возвращаемого результата). Если не объявлять возвращаемый тип, по умолчанию он будет равен Void. Это не опечатка, в Java void записывается с ‘v’ в нижнем регистре, при этом в Javafx Void записывается с заглавной буквы ‘V’.

Возвращаемые функцией типы помещаются по аналогии с типами в переменные и параметры. Возвращаемый тип объявляется путем помещения символа двоеточия  : после закрывающей круглой скобки, вслед за которой указывается возвращаемый тип. Например, следующая функция возвращает Integer (целое число):

Функция в JavaFX, возвращающая «Integer»

function foo(i: Integer): Integer {
    i * 4;
}

Встроенные типы данных

Как видно из последнего примера кода, ключевое слово Integer представляет целочисленный тип данных. В Java это эквивалентно int. Если вам нужна десятичная дробь, можно использовать встроенный тип Number,Byte, Short, Number, Integer, Long, Float, Double, и Character (все начинаются с заглавной буквы). который представляет число с плавающей точкой. Обычно есть два числовых типа, которые можно использовать, но если вам нужны переменные определенного размера, вы можете использовать любой из следующих встроенных числовых типов:

Другие встроенные типы данных являются Boolean (логическими), они подобны bool в Java, Duration,String. При присвоении значения типу Duration type по существу указывается количество времени вместе с единицами его измерения. Например: обозначающий продолжительность времени, и

Различные «durations» (длительности) в JavaFX

100ms;  // равняется 100 миллисекундам
200s;   // 200 секунд
300m;   // 300 минут
400h;   // 400 часов

Strings действует одинаково в JavaFX и в Java. Объекты JavaFX String имеют такие же методы, что и объекты Java String, но конкатенация (соединение) строк Strings действует по-разному. Следующие примеры показывают операции над строками String в Java вместе с эквивалентными операциями в JavaFX:

Объединение строк «String» в Java и в JavaFX

// код Java 
String strA = "A";
String strB = "B";
String strC = strA + strB;
String strD = strC.replaceAll("A", "C");

// эквивалентный код JavaFX
var strA: String = "A";
var strB: String = "B";
var strC: String = "{strA}{strB}";
var strD: String = strC.replaceAll("A", "C");

Выражения

Это второй раз, когда мы встречаемся с использованием символа {} внутри String. Помещение внутри строки символа {} преобразует заключенное в скобки выражение в String. Это можно было увидеть в функции run,println(«{args[0]}»). Также этот символ используется для объединения Strings, как показано в предыдущем примере кода. Можно поместить любое выражение внутрь фигурных скобок {}. Например, следующий код выводит The value of foo(7)is 49: где на экран выводился первый аргумент командной строки с помощью кода

Преобразование выражений к типу «Strings» в JavaFX

function foo(i: Integer): Integer {
    i * i;
}

def i = 7;
println("Значением foo({i}) является {foo(i)}");

{i} преобразует значение i к типу String, в то время как {foo(i)} преобразует результат foo(i) к типу String. Ониобъединяются со Strings (строкой) «The value of foo(«, «) is «, and «)». Эквивалентный код на Java:

Преобразование выражений к типу «Strings» в Java

int foo(int i) {
    return i * i;
}

int i = 7;
System.out.println("Значением foo(" + i + ") является " + Integer.toString(foo(i)));

Возврат дополнительного ключевого слова

В случае если вы не заметили, мы написали две функции foo(), каждая из которых возвращает Integers. Однако никакая из функций не использует ключевое слово return. Ключевое слово return не обязательно в JavaFX. Если вы не включите ключевое слово return, возвращается значение последнего выполненного выражения. Можно использовать ключевое слово return так же, как в следующем примере:

Вы можете использовать ключевое слово «return», если хотите

function foo(i: Integer): Integer {
    return i * i;
}

def i = 7;
println("Значением foo({i}) является {foo(i)}");

Графический интерфейс пользователя

Давайте теперь приступим к созданию шахматной программы. Сначала необходимо создать окно для нашего приложения. В Java для этого нужно использовать класс JFrame. Эквивалентным классом в JavaFX является Stage. Ниже показано, как окно создается в Java и в JavaFX:

Создание окон в Java и в JavaFX

// код Java
import java.io.*;
import javax.swing.*;

public class Main {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Chess");
        frame.setBounds(100, 100, 400, 400);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        String workingDir = System.getProperty("user.dir");
        String iconFilename =
        workingDir + File.separator + "res" + File.separator + "Icon32.png";
        ImageIcon icon = new ImageIcon(iconFilename);
        frame.setIconImage(icon.getImage());
        frame.setVisible(true);
    }
}

// код JavaFX
import java.io.*;
import java.lang.*
import javafx.scene.image.*;
import javafx.stage.*;

def stage: Stage = Stage {
    title: "Chess"
    x: 100, y: 100, width: 100, height: 100
    icons: [ Image {url: "{__DIR__}res{File.separator}Icon32.png" } ]
    onClose: function() {
        System.exit(0);
    }
}

JavaFX содержит множество ошибок

Ниже приводятся скриншоты каждого из двух приложений. Java приложение имеет пользовательскую иконку с изображением шахмат, в то время как JavaFX приложение имеет заданную по умолчанию иконку Java приложения. Это первая из тех нескольких ошибок, которые мы нашли в JavaFX. Невозможно загрузить пользовательскую иконку в приложение на JavaFX. Есть форумы, в которых обсуждаются ошибки — bug1 и bug2. Согласно документации JavaFX 1.1 docs, вы должны иметь возможность установить иконку для Stage. К сожалению, это не работает. Так что придется оставить иконку Java по умолчанию. Есть надежда, что эта проблема вскоре будет разрешена.

JavaFX версии 1.1 все еще содержит много ошибок, к примеру, вы не можете изменить иконку для главного окна.

Создание объектов

В Java новые классы создаются с помощью ключевого слова new. В JavaFX вы не должны использовать ключевое слово new. Обычно новый объект создается путем присваивания переменной имени класса и, после этого, присваивания начальных значений атрибутам класса внутри фигурных скобок ({}). Начальное имя атрибута класса устанавливается путем указания имени атрибута, вслед за ним указывается символ двоеточия : , а после двоеточия указывается начальное значение атрибута класса. Вы можете помещать запятые между каждым присвоением значений атрибутам класса, но это необязательно. В предыдущем примере запятые используются только между атрибутами, которым мы присваиваем значения в одной и той же строке (атрибуты x, y, width и height). Атрибут icons является последовательностью, поэтому мы помещаем иконку для Stage внутрь квадратных скобок []. Иконка является экземпляром объекта Image. Экземпляр этого класса создается аналогичным образом путем присвоения начальных значений атрибутам класса внутри фигурных скобок. В этом случае мы записываем в атрибут url местоположение графического файла, используемого в качестве иконки. Атрибут onClose инициализируется значением указателя на функцию. Функция просто вызывает System.exit(0).

Вы можете использовать ключевое слово new для создания экземпляров классов. В данном примере программа JavaFX работает аналогично предыдущему примеру, но использует new для создания Stage. Классы в JavaFX не имеют конструкторов. Поскольку Stage – это класс JavaFX, нельзя назначить начальные значения объекту с помощью передачи значений в конструктор (так как конструкторов нет). Так что значения атрибутам класса назначаются после создания нового объекта, как показано далее в примере кода:

Вы можете использовать ключевое слово «new» для создания объектов JavaFX.

import java.io.*;
import java.lang.*;
import javafx.scene.image.*;
import javafx.stage.*;

def stage: Stage = new Stage();
stage.title = "Chess";
stage.x = 100;
stage.y = 100;
stage.width = 400;
stage.height = 400;
stage.icons = [ Image { url: "{__DIR__}res{File.separator}Icon32.png" } ];
stage.onClose = function() {
    System.exit(0);
};

При создании объектов Java важно создавать их с помощью ключевого слова new, потому что объекты Java имеют конструкторы, и этим конструкторам можно передавать параметры, используя ключевое слово new. При создании объектов JavaFX мы предпочитаем не использовать ключевое слово new. По-нашему, код выглядит понятнее, когда мы создаем объекты без него и присваиваем  начальные значения всем атрибутам класса, используя закрывающие круглые скобки, {}, как показано в первом примере.

Сохранение настроек приложения

Мы всегда предпочитаем сохранять настройки приложения в домашнем каталоге пользователя. В случае приложений с графическим пользовательским интерфейсом самое меньшее, что нужно сделать — это сохранить местоположение и размер главного окна приложения. Java существует в течение долгого времени и уже имеет набор классов, помогающих выполнить эту задачу. Давайте рассмотрим, как это делается в Java, и затем сделаем то же самое в JavaFX.

Сохранение настроек приложения в Java

// код Java
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import javax.swing.*;

public class Main {
    public static void main(String[] args) {
        // получаем домашний каталог пользователя
        String homeDir = System.getProperty("user.home");

        // они объявлены как final, так что к ним можно получить доступ

        // во внутреннем анонимном классе ниже
        final String settingsFilename =
        homeDir + File.separator + "mySettings.properties";
        final Properties props = new Properties():

        // Загрузка сохраненных настроек
        try {
            FileInputStream input = new FileInputStream(settingsFilename);
            props.load(input);
            input.close();
        } catch(Exception ignore) {
            // исключение игнорируется, поскольку ожидалось, что
       // файл установочных параметров иногда может не существовать
            // при первом запуске приложения он точно не будет существовать
        }

        int savedX;
        try {
            savedX = Integer.parseInt(props.getProperty("xPos", "100"));
        } catch(NumberFormatException e) {
            savedX = 100;
        }

        // похожий код для загрузки savedY, savedWidth, savedHeight пропущен для краткости

        // Создаем и отображаем окно, такой же код, как и выше, исключая то, что при закрытии ничего не делается
        // Также делаем JFrame конечным, чтобы к нему можно было получить доступ
        // во внутреннем анонимном классе
        final JFrame frame = new JFrame("Chess");
        frame.setBounds(savedX, savedY, savedWidth, savedHeight);
        // отличается от предыдущего примера
        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        String workingDir = System.getProperty("user.dir");
        String iconFilename = workingDir + File.separator +
            "res" + File.separator + "Icon32.png";
        ImageIcon icon = new ImageIcon(iconFilename);
        frame.setIconImage(icon.getImage());
        frame.setVisible(true);

        frame.addWindowListener(new WindowAdapter() {
            @Override public void windowClosing(WindowEvent e) {
                // Сохраняем настройки при выходе
                Rectangle frameBounds = frame.getBounds();
                props.setProperty("xPos", frameBounds.x);
                props.setProperty("yPos", frameBounds.y);
                props.setProperty("width", frameBounds.width);
                props.setProperty("height", frameBounds.height);

                try {
                    FileOutputStream output = new FileOutputStream(settingsFile);
                    props.store(output, "Saved settings");
                    output.close();
                } catch(Exception ignore) {
                    // если не получается сохранить настройки,
               // в следующий раз будут использоваться настройки по умолчанию
                }

                // выход из приложения
                System.exit(0);
            }
        });
    }
}

Свойства

Класс Properties Java используется для сохранения настроек приложения. Этот класс содержит словарь, состоящий из пар ключ/значение. Метод load используется для загрузки сохраненных свойств, и метод storegetProperty вызывается, чтобы получить значение для определенного ключа. Также передается значение параметра по умолчанию, это значение возвращается, если не установлен ключ в объекте Properties. Мы сохраняем properties, используя метод setProperty. Это один из нескольких способов сохранения настроек приложения в Java. используется для сохранения свойств. Метод

WindowListener (слушатель окна)

Нужно загрузить настройки приложения перед отображением окна, потому что требуется установить сохраненную позицию и местоположение окна. Можно сохранить настройки приложения, добавив WindowListener в JFrame. WindowListener будет отправлено сообщение, когда пользователь попытается закрыть окно приложения. Когда это происходит, размер и местоположение JFrame сохраняются в объекте Properties и затем сохраняются в домашнем каталоге пользователя. Можно выйти из приложения, вызвав System.exit(0).

Сохранение настроек в JavaFX

Можно использовать похожие классы для сохранения настроек приложения в нашем JavaFX классе. Единственное отличие состоит в том, что не требуется добавлять WindowListener, просто нужно обновить функцию onClose класса Stage, чтобы сохранить настройки приложения перед выходом из приложения. Это показано в следующем фрагменте кода:

Сохранение настроек приложения в JavaFX

import java.io.*;
import java.lang.*;
import java.util.*;
import javafx.scene.image.*;
import javafx.stage.*;

// ищем файл настроек в домашнем каталоге пользователя
def homeDir = System.getProperty("user.home");
def settingsFile = "{homeDir}{File.separator}"mySettings.properties";

// загружаем настройки в класс Properties
def props: Properties = new Properties();
try {
    def input: FileInputStream = new FileInputStream(settingsFile);
    props.load(input);
    input.close();
} catch(ignore: Exception) {
// если файл не существует, будут использоваться значения по умолчанию
}

// чтение сохраненных настроек
var savedX: Number;
try {
    savedX = Double.parseDouble(props.getProperty("xPos", "100"));
} catch(e: NumberFormatException) {
    savedX = 100;
}
// похожим образом загружаем настройки savedY, savedWidth и savedHeight

// создаем окно, как и раньше, только добавив вызов saveSettings() в функцию onClose

def stage: Stage = Stage {
    title: "Chess"
    x: savedX, y: savedY, width: savedWidth, height: savedHeight
    icons: [ Image {url: "{__DIR__}res{File.separator}Icon32.png" } ]
    onClose: function() {
        saveSettings();  // save settings before exiting
        System.exit(0);
    }
}

function saveSettings(): Void {
    props.setProperty("xPos", "{stage.x}");
    props.setProperty("yPos", "{stage.y}");
    props.setProperty("width", "{stage.width}");
    props.setProperty("height", "{stage.height}");

    try {
        def output: FileOutputStream = new FileOutputStream(settingsFile);
        props.store(output, "Saved Chess Settings");
        output.close();
    } catch(ignore: Exception) {
        // если настройки не получается сохранить, в следующий раз будут использоваться настройки по умолчанию
    }
}

Как видите, можно использовать такие же классы Java для сохранения настроек приложения в JavaFX, как и используемые в Java приложениях. Это одна из лучших возможностей JavaFX, можно использовать большую библиотеку существующих классов Java в программах на JavaFX. Опыт работы с Java Framework можно применить для программирования на JavaFX.

Нужно заметить, что атрибуты x, y, width и height класса Stage все определены как Numbers. Поэтому, если мы сохраняем их на диске, они все сохраняются с десятичной точкой. Это кажется странным, так как размер и местоположение объекта Stage задается в виде числа пикселей. Не имеет смысла использовать десятичную дробь, так как задаются только целые пиксели, 1.5 пикселя не имеет смысла, может быть только 1 или 2 пикселя. Неизвестно, ошибка это или нет. Мы уже отправляли два сообщения об ошибках в JavaFX, а потому  не уверены,  есть ли смысл в данном случае отправлять другое сообщение об ошибке. Однако кажется странным решение использовать Number вместо Integer (целое число).

Программа «Шахматы»

После создания окна следующим шагом является создание шахматной доски. Мы создадим пользовательский компонент шахматной доски для графического пользовательского интерфейса на JavaFX. Если бы мы делали это в Java, наша шахматная доска расширяла бы JPanel и мы бы добавили эту JPanel к нашей JFrame. В JavaFX наша шахматная доска будет расширять Scene и будет добавлена к нашему объекту Stage.

Перед созданием класса Board extends (расширяет) Scene нужно рассказать немного о доске, которую мы хотим создать. При создании программы на JavaFX мы решили использовать преимущество, предоставляемое большими возможностями нового языка для работы с графикой. В этом случае нам стоит использовать графику SVG для наших шахматных фигур, чтобы они хорошо выглядели в любом разрешении. Пользователь должен иметь возможность изменять размер шахматной доски, и она должна выглядеть хорошо независимо от расположения границ главного окна приложения. Следующий скриншот объясняет, как должна выглядеть доска при изменении размеров главного окна.

Шахматная доска изменяет свои размеры и положение в зависимости от размеров главного окна

Доска пытается занять как можно большую часть окна, насколько это возможно при сохранении квадратной формы. Если ширина окна больше, чем его высота, или его высота больше, чем ширина, то доска центрируется в окне. Если окно имеет квадратную форму, доска не займет окно полностью, но займет большую часть окна. При этом вокруг доски всегда остается небольшая рамка. Ширина и высота этой рамки по крайней мере равняются размеру одного квадрата на шахматной доске. Так что доска имеет следующие 3 атрибута:

  • squareSize – Размер одного квадрата на шахматной доске
  • xOffset – Если ширина окна больше, чем его высота, этот атрибут говорит о том, насколько далеко нужно передвинуть доску, чтобы расположить ее по центру окна
  • yOffset – Если высота окна больше, чем ширина, этот атрибут говорит о том, насколько далеко нужно передвинуть доску, чтобы расположить ее по центру окна.

Связывание в JavaFX

Связывание – это новая возможность в JavaFX, позволяющая связать значение одной переменной с другой переменной или связать значение метода с другой переменной. Связывание выполняется с помощью ключевого слова bind. Так как значения атрибутов squareSize, xOffset и yOffset зависят от размера шахматной доски, свяжем их значения с размером шахматной доски. Ниже приводится код для шахматной доски:

Шахматная доска использует «bind» для динамического изменения своего размера и положения

import javafx.scene.*;
import javafx.scene.paint.*;
import javafx.scene.shape.*;

public class Board extends Scene {
    def LIGHT_COLOR: Color = Color.web("lemonchiffon");
    def DARK_COLOR: Color = Color.web("brown");

    public-read var squareSize = bind {
        if(width > height) {
            height / 10;
        } else {
            width / 10;
        }
    }

    public-read var xOffset = bind {
        if (width > height) {
            (width - height) / 2;
        } else }
            0;
        }
    }

    public-read var yOffset = bind {
        if (width > height) {
            0;
        } else {
            (height - width) / 2;
        }
    }

    def board = [ Coord.A8, Coord.B8, Coord.C8, Coord.D8,
            Coord.E8, Coord.F8, Coord.G8, Coord.H8,
                  Coord.A7, Coord.B7, Coord.C7, Coord.D7,
            Coord.E7, Coord.F7, Coord.G7, Coord.H7,
                  Coord.A6, Coord.B6, Coord.C6, Coord.D6,
            Coord.E6, Coord.F6, Coord.G6, Coord.H6,
                  Coord.A5, Coord.B5, Coord.C5, Coord.D5,
            Coord.E5, Coord.F5, Coord.G5, Coord.H5,
                  Coord.A4, Coord.B4, Coord.C4, Coord.D4,
            Coord.E4, Coord.F4, Coord.G4, Coord.H4,
                  Coord.A3, Coord.B3, Coord.C3, Coord.D3,
            Coord.E3, Coord.F3, Coord.G3, Coord.H3,
                  Coord.A2, Coord.B2, Coord.C2, Coord.D2,
            Coord.E2, Coord.F2, Coord.G2, Coord.H2,
                  Coord.A1, Coord.B1, Coord.C1, Coord.D1,
            Coord.E1, Coord.F1, Coord.G1, Coord.H1 ];

    postinit {
        for (square in board) {
            def i: Integer = indexof square;
            insert Rectangle {
                fill: if (square.getIsWhite()) LIGHT_COLOR else DARK_COLOR
                x: bind xOffset + ((i mod 8) + 1) * squareSize
                y: bind yOffset + ((i / 8) + 1) * squareSize
                width: bind squareSize
                height: bind squareSize
            } into content;
        }
    }
}

Coord – это просто enum (перечисление), обеспечивающий более легкое размещение шахматных фигур. Coord также отслеживает, является ли квадрат с теми координатами темным или ярко окрашенным квадратом. В шахматах королева всегда помещается на ее цвет, и нижний левый квадрат доски всегда темный. Это помогает не допустить помещения короля или королевы не на то место доски. JavaFX не может создавать enum, поэтому класс Coord написан на Java. Вот почему вам необходимы компиляторы Java и JavaFX для компоновки этого проекта. Нужно скомпоновать файл Coord.java, используя компилятор Java, перед компоновкой файлов на JavaFX с помощью компилятора JavaFX.

Этот класс использует множество связываний. Первое связывание применяется для атрибута класса class squareSize. squareSize в данном случае связывается с выражением. Если значение выражения изменяется, значение атрибута squareSize будет автоматически изменяться на соответствующее новому значению выражение. Выражение, с которым связан этот атрибут, является блоком кода. Значение этого блока кода равняется последней строке, выполненной в блоке кода. В данном случае, последняя выполненная строка зависит от значений атрибутов width и height объекта Scene. Значение squareSize будет установлено в height / 10; или width / 10;.

Класс Board будет помещен в класс Stage (главное окно приложения). Board будет заполнять всю клиентскую область Stage, обычно это целое окно, исключая рамку. Если размер окна изменяется, то атрибуты width и height класса Board будут изменяться. Когда их значения изменяются, среда выполнения JavaFX будет автоматически изменять значение атрибута squareSize.

Аналогично, атрибуты xOffset и yOffset связаны со значениями width и height.

postinit вместо конструкторов

Следующие связывания можно увидеть в блоке кода postinit. Как мы говорили раньше, классы JavaFX не имеют конструкторов. Однако всегда нужно выполнять какой-то код при первоначальном создании объекта. JavaFX позволяет определить код, который будет выполняться при создании объекта, с помощью использования ключевого слова postinit. Код, помещенный в блок кода postinit, будет автоматически выполняться при создании объекта, подобно вызову конструктора в Java.

В блоке Boards postinit создается Rectangle (прямоугольник) для каждого квадрата на шахматной доске. Местоположение и размер каждого из Rectangles на доске связаны со значениями squareSize, xOffset и yOffset. Это делается с помощью помещения ключевого слова bind после атрибутов x, y, width и height каждого из Rectangles, используемых для каждого из квадратов, и помещения выражения, с которым атрибуты должны быть связаны, после ключевого слова bind.

JavaFX имеет новый набор модификаторов доступа

Класс Board использует модификатор доступа, который не существует в языке программирования Java, public-read. JavaFX использует новый набор модификаторов доступа, описанный ниже:

  • default (по умолчанию) – Если вы не используете модификатор доступа для переменной или функции, по умолчанию устанавливается доступ только для сценария (script-only). Это означает, что можно получить доступ к переменной/функции только в пределах текущего файла сценария. JavaFX не имеет модификатора доступа private, модификатор доступа по умолчанию в JavaFX эквивалентен private в Java. (Замечание: мы используем термины «переменная» и «функция», потому что в JavaFX не требуется использовать классы, однако если вы используете класс, лучше применять термины «атрибут» и «метод».)
  • package – Этот модификатор делает переменную или функцию доступной для любого другого кода в этом же пакете. Это аналогично модификатору доступа по умолчанию в Java (Java не использует ключевое слово package как модификатор доступа).
  • protected — Этот модификатор делает переменную или функцию доступной для любого другого кода в этом же пакете и также для всех подклассов. Java использует такое же ключевое слово, имеющее такое же значение.
  • public — Этот модификатор делает переменную или функцию доступной в любом месте для доступа с целью чтения или записи. Java использует такое же ключевое слово, имеющее такое же значение.
  • public-read – Этот модификатор доступа используется в классе Board. Этот модификатор доступа можно применять только к переменным, его нельзя применять к функциям. Он открывает доступ к переменной с целью чтения из любого места, ограничивая доступ с целью записи рамками данного сценария. Java не имеет подобного модификатора доступа. Можно получить такую же функциональность в Java, объявив атрибут private и предоставив метод public, извлекающий значение атрибута. Пример этого показан в коде ниже.
  • public-init – Переменная, обозначенная как public-init, может записываться из любого места при первоначальном создании объекта. После создания объекта ее можно прочитать из любого места, но запись в переменную возможна только с помощью сценария, в котором определяется переменная. Как и модификатор доступа public-read, модификатор доступа public-init можно использовать только для переменной, но нельзя использовать для функции. Эквивалентный модификатор доступа не существует в Java. Вы можете получить такую же функциональность в Java, объявив атрибут private и предоставив метод public, извлекающий значение атрибута, и позволив устанавливать начальное значение атрибута через параметр, передаваемый конструктору. Пример этого дается в коде ниже:

Код «public-read» and «public-init» на JavaFX, преобразованный в код Java

// код JavaFX
public-read size: Integer;
public-init height: Integer;

// Такой же код на Java
public class MyClass {
    // только этот класс может записывать в атрибут size
    private int size;
    private int height;

    // другие классы могут записывать только в атрибут height с помощью этого конструктора
    // после создания объекта только этот класс может записывать в атрибут height
    public MyClass(int initHeight) {
        height = initHeight;
    }

    // любой может прочитать значение атрибута size
    public int getSize() {
        return size;
    }

    // любой может прочитать значение атрибута height
    public int getHeight() {
        return height;
    }
}

Есть одна вещь, которую мы можем сделать в Java, но которую не смогли сделать в JavaFX. В Java мы всегда можем объявить атрибут класса как final, в JavaFX можем это сделать, используя ключевое слово def. Однако, в Java можно позволить любому устанавливать начальное значение этого атрибута, передавая значение в конструктор. В JavaFX этого сделать нельзя. Мы пытались объявить переменную как public-init def, но это вызывает ошибку, если не присвоить значение переменной в том же месте.

В JavaFX вы не можете инициализировать переменную «def» (то есть final) из другого класса

// ниже приведен корректный код Java, для которого
// не существует аналога в JavaFX
public class Test {
    public final int i;

    public Test(int initI) {
        i = initI;
    }
}

// следующий код в JavaFX не будет компилироваться
public class Test {
    public-init def i: Integer;
}

def test: Test = Test { i: 7 }

// здесь есть сообщение об ошибке
Test.fx:2: The 'def' of 'i' must be initialized with a value here.
                Perhaps you meant to use 'var'?
    public-init def i: Integer

1 error

// если устанавливать значение атрибута i, получается еще больше сообщений об ошибке
class Test {
    public-init def i: Integer = 4;
}

def test: Test = Test { i: 7 }

// здесь есть сообщения об ошибке
Test.fx:2: modifier public-init not allowed on a def
    public-init def i: Integer = 4;

Test.fx:5: You cannot change the value(s) of 'i'
    because it was declared as a 'def', perhaps it should be a 'var'?
    def test: Test = Test { i: 7 }

2 errors

Полезной окажется возможность объявить переменную как public-init def. Мы хотели использовать это для шахматных фигур, объявляя фигуру как черную или белую, когда мы создали фигуру. Когда цвет фигуры установлен, его больше нельзя изменить, поэтому мы хотели объявить этот параметр как def, но не смогли этого сделать. Мы вынуждены объявить его как public-init var и проверять, что он не изменяется после первоначального присвоения значения. Это второе решение создателей JavaFX, которое кажется нам неправильным. (Первым было решение объявить атрибуты x, y, width и height класса Stage как NumbersIntegers.) вместо

Шахматные фигуры SVG

Шахматные фигуры, которые мы использовали в этой шахматной программе, взяты по адресу openclipart.org. Этот сайт имеет хороший выбор масштабируемой векторной графики (SVG) с открытым исходным кодом, которую вы можете использовать в своих приложениях. Благодаря им мы смогли использовать красивую графику в своем приложении, не являясь художниками. Все были бы впечатлены, если бы JavaFX позволял непосредственно загружать графику SVG. Но это не так. Чтобы использовать графику, нам пришлось преобразовать ее из SVG в графический формат JavaFX. Чтобы преобразовать SVG в графику JavaFX, нужно загрузить JavaFX 1.1 Production Suite по адресу javafx.com. Для преобразования графики SVG в графику JavaFX, нужно использовать программу «SVG to JavaFX Graphics Converter», поставляемую вместе с набором средств. Здесь приводится скриншот шахматных фигур, используемых в программе:

Используемые шахматные фигуры в графике формата SVG

Загрузка и отображение шахматных фигур

Мы обнаружили очередную ошибку в JavaFX, пытаясь загрузить графические изображения шахматных фигур в формате JavaFX. Согласно документации, можно загрузить фигуры с помощью следующего кода:

Этот код должен загружать графические файлы JavaFX, но он не работает

// Загрузка графического файла JavaFX 
var pieceNode = FXDLoader.load("{__DIR__}res/WKing.fxz");
// добавление (в) графики в объект Group
var group = Group {
    content: [
        pieceNode
    ]
}
// добавление графики к доске
insert group into board.content

Приведенный выше код не работает. __DIR__ является специальной константой, возвращающей URL каталога, в котором содержится текущий файл исходного кода JavaFX. Во всех примерах кода, которые мы видели, все ресурсы (графика, иконки и т.д.) для программ на JavaFX доступны относительно __DIR__. Это работает в большинстве случаев. Например, для загрузки изображения вы можете использовать следующий код:

Загрузка изображения в JavaFX

var myImage = Image { url: "{__DIR__}images/SampleImage.jpg" backgroundLoading: true};

Однако если попытаться загрузить изображение с помощью класса FXDLoader, используя этот метод, произойдет ошибка. Проблема в том, что значение, возвращаемое __DIR__ , является URL. Это означает, что пробелы преобразуются в %20. FXDLoader выдает сообщение об ошибке, говорящее, что файл не найден. Для решения этой проблемы нужно преобразовать 20% обратно в пробелы, как это делается в следующем коде:

Решение проблемы с загрузкой графики в JavaFX

// решение проблемы, преобразование 20% в пробел, чтобы FXDLoader мог загрузить графику
def rootDir = "{__DIR__}".replaceAll("%20", " ");

// загрузка графического файла JavaFX
var pieceNode = FXDLoader.load("{rootDir}res/WKing.fxz");
// добавление графики в объект Group
var group = Group {
    content: [
        pieceNode
    ]
}
// добавление графики к доске
insert group into board.content

Мы отправили очередной отчет об ошибке, сообщающий, что FXDLoader нужно обновить, чтобы он мог обрабатывать URL, как в случае с классом Image. В примере кода от Sun рекомендуется использовать __DIR__ для загрузки других ресурсов. Он должен работать таким же образом с графическими объектами JavaFX, используя класс FXDLoader.

Пользовательский курсор мыши в JavaFX

Мы решили загрузить пользовательский курсор мыши. Хотелось, чтобы наша шахматная программа подсвечивала фигуру, над которой находится мышь, и курсор мыши принимал форму открытой руки, показанную на рисунке ниже, и принимал форму закрытой руки, когда на фигуре выполнен щелчок мыши, и она перетаскивается. Также должен подсвечиваться квадрат, на который фигура будет помещена, если отпустить кнопку мыши. Это полезно в случаях, когда пользователь перетаскивает фигуру над углом квадрата. Без подсказки пользовательского интерфейса вы можете не знать, в какой квадрат фигура будет помещена при отпускании кнопки мыши.

Подсвеченные фигуры и пользовательские фигуры при перемещении мыши над фигурой и при перетаскивании фигуры с помощью мыши

JavaFX имеет свой собственный набор установленных  курсоров, доступный для использования. Если вы хотите создать ваш собственный пользовательский курсор, вы должны создать новый объект java.awt.Cursor для вашего пользовательского курсора, как показано в следующем коде:

Пользовательский (специализированный) курсор

import java.awt.*;
import java.net.*;
import javax.swing.*;
import javafx.scene.Cursor;

public class CustomCursor extends Cursor {
    public-init var imageURL: String;
    public-init var cursorName: String;

    public override function impl_getAWTCursor(): java.awt.Cursor {
        var toolkit = Toolkit.getDefaultToolkit();
        var image: Image = toolkit.getImage(new URL(imageURL));
        var point: Point = new Point(16, 16);
        var cursor: java.awt.Cursor =
        toolkit.createCustomCursor(image, point, cursorName);
    }
}

Этот код немного сбивает с толку, потому что используются два различных Cursors. Новый класс CustomCursor расширяет класс JavaFX Cursor (javafx.scene.Cursor). Другой класс создает новый объект Java Cursor (java.awt.Cursor). Это два различных класса Cursor. Мы заметили, что замененный нами метод impl_getAWTCursor() не включен в список документации JavaFX 1.1 API. Причиной этого может быть неполноценность документации JavaFX API в настоящий момент (она выглядит совсем не так, как полная документация Java API) или это может быть скрытым API, который нельзя заменять.

Поскольку документация JavaFX API хуже, чем документация Java API, это не было для нас неожиданностью. Java существует намного дольше и является более развитым языком программирования. Однако, есть одна вещь, которая нам очень не понравилась в документации JavaFX API. В документации Java все классы перечислены в формате HTML в одной оконной панели. Так что если мы поищем определенный класс, его легко найти с помощью браузера. В JavaFX, если вы знаете имя класса, который вы ищете, но не знаете, в какой пакет входит этот класс, найти данный класс в документации будет довольно сложно. В документации JavaFX API все классы не перечислены в боковой оконной панели (как в документации Java), однако в боковой оконной панели перечислены все имена пакетов. Вы вынуждены щелкнуть мышкой на имени пакета, чтобы увидеть классы, входящие в этот пакет. Так что если вы хотите найти документацию для класса Cursor и не знаете, что он входит в пакет javafx.scene, этот класс будет сложно найти. Документация может выглядеть красивее, но она не удобная.

Вернемся к загрузке пользовательского курсора. Следующий код загружает пользовательский курсор в шахматную программу:

Загрузка пользовательского курсора

def rootDir = "{__DIR__}".replaceAll("%20", " ");
def grabCursor = CustomCursor {
    imageURL: "{rootDir}res/grab.png"
    cursorName: "Grab"
}
def grabbingCursor = CustomCursor {
    imageURL: "{rootDir}res/grabbing.png"
    cursorName: "Grabbing"
}

var currentCursor = Cursor.Default;
// Внутри класса Piece (фигура) создаются пользовательские функции слушателей событий
// мыши для обеспечения возможности изменения курсора
onMouseEntered: function(e: MouseEvent) {
    currentCursor = grabCursor;
    board.cursor = currentCursor;
}
onMouseExited: function(e: MouseEvent) {
    currentCursor = Cursor.Default;
    board.cursor = currentCursor;
}
// аналогично для других функций мыши - отпущенный, нажатый, произошел щелчок по кнопке мыши

  Загрузить исходный код — 65.49 KB

Improve Article

Save Article

  • Read
  • Discuss
  • Improve Article

    Save Article

    Given task is to draw a Chessboard in Java Applet 

    Approach:

    1. Create a rectangle with length and breadth of 20 unit each, with 10 rows and columns of chess.
    2. As soon as even position occurs in row and column change the color of a rectangle with BLACK, else it will be WHITE

    Below is the implementation of the above approach: 

    Applet Program: 

    Java

    import java.applet.*;

    import java.awt.*;

    public class Chess extends Applet {

        static int N = 10;

        public void paint(Graphics g)

        {

            int x, y;

            for (int row = 0; row & lt; N; row++) {

                for (int col = 0; col & lt; N; col++) {

                    x = row * 20;

                    y = col * 20;

                    if ((row % 2 == 0) == (col % 2 == 0))

                        g.setColor(Color.BLACK);

                    else

                        g.setColor(Color.WHITE);

                    g.fillRect(x, y, 20, 20);

                }

            }

        }

    }

    Output:

    Note: To run the applet in command line use the following commands.

    > javac Chess.java
    > appletviewer Chess.java

    You can also refer to: https://www.geeksforgeeks.org/different-ways-to-run-applet-in-java to run applet program.

    This is my very first project in java. I would love some feedback on my design. How the same implementation can be better written. Idioms, conventions, anything that comes to your mind. I hope I have not included a lot of code here. I thought it would be hard to review my code without the basic classes.

    The GUI is not included here but if you would like to look at it then here is my repository.

    I have got some specific questions about the implementation that I would love to have answered

    1) The BoardManager.java class seems to be pretty large. Is there any way I can clean it up (divide it up maybe)? Something that bugs me is the implementation of isValidCastling() function and move() function. Is there any way to improve on these?

    2) I have learned a convention of assigning a null value to all objects if they are empty/not present (If a square does not contain a piece, then null is assigned). Is there a better option? Due to this, I get a lot of NullPointerExceptions while debugging? Is this a design flaw or is this something I have to deal with?

    PieceType.java

    package pieces;
    
    /**
     * @author gnik
     *
     */
    public enum PieceType {
        KING("k"), KNIGHT("n"), ROOK("r"), QUEEN("q"), BISHOP("b"), PAWN("p");
    
        private String value;
    
        PieceType(String value) {
            this.value = value;
    
        }
    
        @Override
        public String toString() {
            return this.value;
        }
        public static PieceType fromString(String value){
            for (PieceType piece :PieceType.values()) {
                if (piece.value.equalsIgnoreCase(value)) {
                  return piece;
                }
              }
              return null;
        }
    
    }
    

    PlayerType.java

    package player;
    
    /**
     * @author gnik
     *
     */
    
    public enum PlayerType {
        WHITE("W"), BLACK("B");
    
        private String value;
    
        PlayerType(String value) {
            this.value = value;
        }
    
        @Override
        public String toString() {
            return value;
        }
    }
    

    Piece.java

    package pieces;
    
    import player.PlayerType;
    import game.Coordinate;
    
    
    /**
     * @author gnik
     *
     */
    public abstract class Piece {
        private PieceType type;
        private PlayerType player;
    
        /**
         * Initialize a piece with a playerType.
         * @param player The player the piece belongs to 
         * @param type The piece type
         */
        public Piece(PlayerType player,PieceType type){
            this.type=type;
            this.player=player;
        }
    
        /* (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        public String toString(){
            return player.toString()+type.toString();
        }    
    
        /**
         * Returns the playerType.
         * @return PlayerType player
         */
        public PlayerType getPlayer(){return player;} 
    
        /**
         * Returns the type of piece.
         * @return PieceType Piece
         */
        public PieceType getType(){return type;}
    
    
    
        /**
         * Checks if the move is a valid move by the piece.
         * @param initPos Initial Coordinate 
         * @param finalPos Final Coordinate
         * @return boolean If the move is Valid
         */
        public abstract boolean isValidMove(Coordinate initPos,Coordinate finalPos);
    
    
        /**
         * Return the path for movement.
         * @param initPos The initial Coordinate
         * @param finalPos The final Coordinate
         * @return Coordinate[] The Path for the movement 
         */
        public abstract Coordinate[] getPath(Coordinate initPos,Coordinate finalPos);
    
    
    }
    

    Pawn.java

    package pieces;
    
    import player.PlayerType;
    import game.Coordinate;
    
    /**
     * @author gnik
     * 
     */
    public class Pawn extends Piece {
    
        /**
         * Create a pawn
         * 
         * @param player
         *            The player the piece belongs to
         */
        public Pawn(PlayerType player) {
            super(player, PieceType.PAWN);
        }
    
        @Override
        public boolean isValidMove(Coordinate initPos, Coordinate finalPos) {
            if (initPos.equals(finalPos)) {
                return false;
            }
    
            // This is for normal pawn moves.
            if (Math.abs(initPos.getY() - finalPos.getY()) == 1
                    && Math.abs(initPos.getX() - finalPos.getX()) == 0) {
                // White can only move forward
                if (this.getPlayer() == PlayerType.WHITE) {
                    if (initPos.getY() < finalPos.getY()) {
                        return true;
                    }
                }
                // Black can only move backward in a sense.
                if (this.getPlayer() == PlayerType.BLACK) {
                    if (initPos.getY() > finalPos.getY()) {
                        return true;
                    }
                }
    
            }
    
            // This is for first pawn move.
            if (Math.abs(initPos.getY() - finalPos.getY()) == 2
                    && Math.abs(initPos.getX() - finalPos.getX()) == 0
                    && (initPos.getY() == 1 || initPos.getY() == 6)) {
    
                // White can only move forward
                if (this.getPlayer() == PlayerType.WHITE) {
                    if (initPos.getY() < finalPos.getY()) {
                        return true;
                    }
                }
                // Black can only move backward in a sense.
                if (this.getPlayer() == PlayerType.BLACK) {
                    if (initPos.getY() > finalPos.getY()) {
                        return true;
                    }
                }
    
            }
    
            // This if for normal pawn captures.
            // this is for Enpassant.
    
            return false;
        }
    
        @Override
        public Coordinate[] getPath(Coordinate initPos, Coordinate finalPos) {
            //This is for pawn captures
            if (initPos.getX()!=finalPos.getX()){return new Coordinate[]{initPos,finalPos};}
            //This is for normal pawn moves and first pawn moves.
            int pathLength = Math.abs(initPos.getY() - finalPos.getY()) + 1;
            Coordinate[] path = new Coordinate[pathLength];
    
            for (int cnt = 0; cnt < pathLength; cnt++) {
                path[cnt] = new Coordinate(initPos.getX(), Math.min(initPos.getY(),
                        finalPos.getY()) + cnt);
            }
    
            return path;
        }
    }
    

    Queen.java

    package pieces;
    
    import player.PlayerType;
    import game.Coordinate;
    
    /**
     * @author gnik
     *
     */
    public class Queen extends Piece{
    
        /**
         * Create a Queen
         * @param player The player the queen belongs to
         */
        public Queen(PlayerType player){
            super(player,PieceType.QUEEN);
        }
    
    
        @Override
        public boolean isValidMove(Coordinate initPos,Coordinate finalPos) {
            if (initPos.equals(finalPos)){return false;}
    
            //This is the bishop move.
            int diffX=Math.abs(initPos.getX()-finalPos.getX());
            int diffY=Math.abs(initPos.getY()-finalPos.getY());
            if (diffX==diffY) return true;
    
            //This is the rook move.
            if (initPos.getX()==finalPos.getX() ||
                    initPos.getY()==finalPos.getY())
                {return true;}
    
            return false;
        }
    
        @Override
        public Coordinate[] getPath(Coordinate initPos,Coordinate finalPos) {
            Coordinate[] path;
            //If it is a rook move
            if (initPos.getX()==finalPos.getX() ||
                    initPos.getY()==finalPos.getY())
            {
                int pathLength=Math.abs(initPos.getX()-finalPos.getX())
                        +Math.abs(initPos.getY()-finalPos.getY())+1;
                path=new Coordinate[pathLength];
                for (int cnt=0;cnt<pathLength;cnt++)
                {
                    if ((initPos.getX()==finalPos.getX())){
                        path[cnt]=new Coordinate(initPos.getX(),Math.min(initPos.getY(),finalPos.getY())+cnt);
                    }
                    else{
                        path[cnt]=new Coordinate(Math.min(initPos.getX(),finalPos.getX())+cnt,initPos.getY());
                    }
                }
    
            }
            else
            {
                //If it a bishop move.
                int pathLength=( Math.abs(initPos.getX()-finalPos.getX())+
                        Math.abs(initPos.getY()-finalPos.getY()) )/2+1;
                path=new Coordinate[pathLength];
    
                //Integer.signum(a) provides the sign of a number 1 if positive and -1 if negative.
                //In this case i am considering initPos as the first point and finalPos as second
                int i_X=Integer.signum(finalPos.getX()-initPos.getX());
                int i_Y=Integer.signum(finalPos.getY()-initPos.getY());
    
                for (int cnt=0;cnt<pathLength;cnt++)
                {
                    path[cnt]=new Coordinate(initPos.getX()+i_X*cnt,initPos.getY()+i_Y*cnt);
                }
            }
    
    
    
    
    
            return path;
        }
    
    }
    

    Knight.java

    package pieces;
    
    import player.PlayerType;
    import game.Coordinate;
    
    /**
     * @author gnik
     *
     */
    public class Knight extends Piece {
    
        /**
         * Create a Knight
         * @param player The player the Knight belongs to
         */
        public Knight(PlayerType player){
            super(player,PieceType.KNIGHT);
        }
    
    
        @Override
        public boolean isValidMove(Coordinate initPos,Coordinate finalPos) {
            if (initPos.equals(finalPos)){return false;}
    
            int diffX=Math.abs(initPos.getX()-finalPos.getX());
            int diffY=Math.abs(initPos.getY()-finalPos.getY());
            if ((diffX+diffY)==3 && diffX!=0 && diffY!=0)
            {return true;}
    
            return false;
        }
    
        @Override
        public Coordinate[] getPath(Coordinate initPos,Coordinate finalPos) {
            //The knight can jump over pieces.
            return new Coordinate[]{initPos,finalPos};
        }
    
    }
    

    I think the I don’t need place all the code of all the pieces for a full code review. Therefore I am excluding some of the other pieces. If you feel like you need to look at it, I have provided a link to my repository.

    Coordinate.java

    package game;
    
    /**
     * @author gnik
     * 
     */
    public class Coordinate {
        int positionX;
        int positionY;
    
        /**
         * Initializes a new coordinate with the x and y values.
         * 
         * @param x
         *            The x coordinate
         * @param y
         *            The y coordinate
         */
        public Coordinate(int x, int y) {
            positionX = x;
            positionY = y;
    
        }
    
        /**
         * Creates a coordinate from a UCI move string
         * @param coordinate The coordinate in string format (UCI)
         */
        public Coordinate(String coordinate){
            positionX=(char)coordinate.toCharArray()[0]-97;
            positionY=Integer.parseInt(coordinate.substring(1,2))-1;
    
        }
        /**
         * Checks if a Coordinate is valid or not
         * 
         * @return boolean If coordinate is valid
         */
        public boolean isValid() {
            if ((positionX >= 0 && positionX < 8)
                    && (positionY >= 0 && positionY < 8)) {
                return true;
            }
            return false;
        }
    
        /**
         * Gets the x coordinate
         * 
         * @return int The x coordinate
         */
        public int getX() {
            return positionX;
        }
    
        /**
         * Gets the y coordinate
         * 
         * @return int The y coordinate.
         */
        public int getY() {
            return positionY;
        }
    
        /**
         * Sets the x coordinate
         * 
         * @param x
         *            The x coordinate
         */
        public void setX(int x) {
            positionX = x;
        }
    
        /**
         * Sets the y coordinate
         * 
         * @param y
         *            The y coordinate
         */
        public void setY(int y) {
            positionY = y;
        }
    
        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#toString()
         */
        public String toString() {
            return Integer.toString(positionX) + "," + Integer.toString(positionY);
        }
    
        /**
         * Checks if two coordinates are equal. The x and y coordinates should be
         * equal.
         * 
         * @param coordinate
         *            A coordinate
         * @return boolean If the coordinates have equal value
         */
        public boolean equals(Coordinate coordinate) {
            if ((positionX == coordinate.getX())
                    && (positionY == coordinate.getY())) {
                return true;
            }
            return false;
        }
    
        /**
         * This converts the coordinate to UCI chess notation
         * @return String The string representation of the square in UCI form
         */
        public String getParsedCoordinate() {
            String parsedString = "";
            parsedString = (char) (positionX + 97)
                    + Integer.toString(positionY + 1);
            return parsedString;
        }
    }
    

    Square.java

    package game;
    
    import pieces.Piece;
    
    /**
     * @author gnik
     *
     */
    public class Square {
        /**
         * The coordinate of the square
         */
        private Coordinate coordinate;
        /**
         * The piece object
         */
        private Piece piece = null;
    
        /**
         * Creates a new square.
         * @param coordinate The coordinate of the square
         * @param piece The piece if the square contains any.
         */
        public Square(Coordinate coordinate, Piece piece) {
            this.coordinate = coordinate;
            this.piece = piece;
        }
    
        /**
         * This is the alternative way to construct a square. Use if no piece is present
         * This calls Square(Coordinate,null)
         * @param coordinate Coordinate of the square
         */
        public Square(Coordinate coordinate) {
            this(coordinate, null);
        }
    
        /**
         * This returns the coordinate of the square.
         * @return coordinate Coordinate of square
         */
        public Coordinate getCoordinate() {
            return coordinate;
        }
    
        /**
         * This gets the piece the square is in. 
         * @return piece Piece
         */
        public Piece getPiece() {
            return piece;
        }
    
        /**
         * Checks if two square have the same coordinate
         * @param square Square
         * @return boolean If the squares are equal.
         */
        public boolean equals(Square square) {
            if (square.getCoordinate().equals(coordinate))
                return true;
            return false;
        }
    
        /**
         * Checks if the square contains a piece
         * @return boolean If square contains a piece 
         */
        public boolean isOccupied() {
            if (piece == null) {
                return false;
            }
            return true;
        }
    
        /**
         * Gets a string representation of the square
         * @return string String representation of the square
         */
        public String getPieceString() {
            if (piece == null) {
                return "  ";
            }
            return piece.toString();
        }
    
        /**
         * Removes any piece that is currently in the square
         */
        public void releasePiece() {
            piece = null;
        }
    
        /**
         * Sets a piece to the square
         * @param piece The piece object
         */
        public void setPiece(Piece piece) {
            this.piece = piece;
        }
    
    }
    

    Board.java

    package game;
    
    import pieces.*;
    import player.PlayerType;
    
    
    
    /**
     * @author gnik
     *
     */
    public class Board {
    
        private Square[][] squares=new Square[8][8];
    
    
        /**
         * This initialized the board to it's original position.
         */
    
        public Board()
        {
            setSquares();
            setWhitePieces();
            setBlackPieces();
    
        }
    
        /**
         * Resets the board to it's original position
         */
        public void resetBoard(){
            setSquares();
            setWhitePieces();
            setBlackPieces();
        }
    
        /**
         * Initializes all the squares
         */
        private void setSquares(){
            for (int x=0;x<8;x++)
            {
                for (int y=0;y<8;y++)
                {
                    squares[x][y]=new Square(new Coordinate(x,y));
                }
            }
    
        }
    
        /**
         * Initializes and assigns all white Pieces.
         */
        private void setWhitePieces(){
            squares[2][0].setPiece(new Bishop(PlayerType.WHITE));
            squares[5][0].setPiece(new Bishop(PlayerType.WHITE));
            squares[1][0].setPiece(new Knight(PlayerType.WHITE));
            squares[6][0].setPiece(new Knight(PlayerType.WHITE));
            squares[0][0].setPiece(new Rook(PlayerType.WHITE));
            squares[7][0].setPiece(new Rook(PlayerType.WHITE));
            squares[3][0].setPiece(new Queen(PlayerType.WHITE));
            squares[4][0].setPiece(new King(PlayerType.WHITE));
            squares[0][1].setPiece(new Pawn(PlayerType.WHITE));
            squares[1][1].setPiece(new Pawn(PlayerType.WHITE));
            squares[2][1].setPiece(new Pawn(PlayerType.WHITE));
            squares[3][1].setPiece(new Pawn(PlayerType.WHITE));
            squares[4][1].setPiece(new Pawn(PlayerType.WHITE));
            squares[5][1].setPiece(new Pawn(PlayerType.WHITE));
            squares[6][1].setPiece(new Pawn(PlayerType.WHITE));
            squares[7][1].setPiece(new Pawn(PlayerType.WHITE));
    
        }
    
        /**
         * Initializes and sets all Black Pieces.
         */
        private void setBlackPieces(){
            squares[2][7].setPiece(new Bishop(PlayerType.BLACK));
            squares[5][7].setPiece(new Bishop(PlayerType.BLACK));
            squares[1][7].setPiece(new Knight(PlayerType.BLACK));
            squares[6][7].setPiece(new Knight(PlayerType.BLACK));
            squares[0][7].setPiece(new Rook(PlayerType.BLACK));
            squares[7][7].setPiece(new Rook(PlayerType.BLACK));
            squares[3][7].setPiece(new Queen(PlayerType.BLACK));
            squares[4][7].setPiece(new King(PlayerType.BLACK));
            squares[0][6].setPiece(new Pawn(PlayerType.BLACK));
            squares[1][6].setPiece(new Pawn(PlayerType.BLACK));
            squares[2][6].setPiece(new Pawn(PlayerType.BLACK));
            squares[3][6].setPiece(new Pawn(PlayerType.BLACK));
            squares[4][6].setPiece(new Pawn(PlayerType.BLACK));
            squares[5][6].setPiece(new Pawn(PlayerType.BLACK));
            squares[6][6].setPiece(new Pawn(PlayerType.BLACK));
            squares[7][6].setPiece(new Pawn(PlayerType.BLACK));
    
        }
    
    
        /**
         * Returns all the squares on the board. 
         * @return Square[][] A dimensional array of all the Squares
         */
        public Square[][] getSquares(){
            return squares;
        }
    
    
        /**
         * This gets the square with a specific coordinate.
         * @param coordinate Coordinate of the square
         * @return Square The Square object
         */
        public Square getSquare(Coordinate coordinate){
            Square result=null;
            for (int x=0;x<8;x++)
            {
                for (int y=0;y<8;y++){
                    if (squares[x][y].getCoordinate().equals(coordinate))
                    {
                        result=squares[x][y];
                    }
                }
            }
            return result;
        }
    
        /**
         * Makes a Move from initial Coordinate to final coordinate
         * @param initCoordinate The initial Coordinate.
         * @param finalCoordinate The final Coordinate.
         */
        public void makeMove(Coordinate initCoordinate,Coordinate finalCoordinate)
        {
            makeMove(getSquare(initCoordinate),getSquare(finalCoordinate));
        }
    
        /**
         * This set the piece in the specified coordinate
         * @param coordinate Coordinate of the piece
         * @param piece The piece object to be set.
         */
        public void setPiece(Coordinate coordinate,Piece piece){
            getSquare(coordinate).setPiece(piece);
        }
        /**
         * Captures the piece that is present in the square.
         * @param square The square of the piece
         */
        public void capturePiece(Square square){
            if(square.isOccupied())
            {
                square.releasePiece();
            }
        }
    
        /**
         * This makes a move from a square to another. **Move may be invalid**
         * @param initSquare The initial Square.
         * @param finalSquare The final Square
         */
        public void makeMove(Square initSquare,Square finalSquare){
            //Has a piece been captured;
            if(finalSquare.isOccupied())
            {
                capturePiece(finalSquare);
            }
            //Make the move
            finalSquare.setPiece(initSquare.getPiece());
            initSquare.releasePiece();
        }
    
        /**
         * This prints the board in the command line.
         */
        public void printBoard(){
            for (int y=7;y>=0;y--){
                for(int x=0;x<8;x++)
                {
                    System.out.print(squares[x][y].getPieceString()+" ");
                }
                System.out.print('n');
            }
        }
    
    
    
    }
    

    BoardManager.java

    package game;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import pieces.Bishop;
    import pieces.Knight;
    import pieces.Pawn;
    import pieces.Piece;
    import pieces.PieceType;
    import pieces.Queen;
    import pieces.Rook;
    import player.PlayerType;
    
    /**
     * @author gnik
     * 
     */
    public class BoardManager {
        /**
         * The board object
         */
        private Board board;
    
        /**
         * Current Player which is to move. Default is PlayerType.WHITE
         */
        private PlayerType currentPlayer = PlayerType.WHITE;
    
        /**
         * This is the list that holds all the moves made by the user.
         */
        private List<Move> moveList = new ArrayList<Move>();
    
        /**
         * Constructs a new BoardManager object
         */
        public BoardManager() {
            this.board = new Board();
        }
    
        /**
         * Resets the board to it's initial state
         */
        public void resetBoard() {
            moveList = new ArrayList<Move>();
            board.resetBoard();
            currentPlayer = PlayerType.WHITE;
        }
    
        /**
         * Switches the player currently to move.
         */
        private void switchCurrentPlayer() {
            if (currentPlayer == PlayerType.WHITE) {
                currentPlayer = PlayerType.BLACK;
            } else {
                currentPlayer = PlayerType.WHITE;
            }
    
        }
    
        /**
         * Return the player who is to move
         * 
         * @return PlayerType The player
         */
        public PlayerType getCurrentPlayer() {
            return currentPlayer;
        }
    
        /**
         * Returns a list of moves that the player has made.
         * 
         * @return List The list of moves
         */
        public List<Move> getMoveList() {
            return moveList;
        }
    
        /**
         * Returns the board object
         * 
         * @return board The board object
         */
        public Board getBoard() {
            return board;
        }
    
        /**
         * Promotes a pawn to a newer piece. Calls isValidPromotion function first
         * 
         * @param square
         *            Promotion Square
         * @param pieceType
         *            The type of piece to promote to. If none is provided it
         *            defaults to Queen.
         * @return boolean If the promotion was made
         */
        public boolean promote(Square square, PieceType pieceType) {
            if (isValidPromotion(square)) {
                Piece piece;
                if (pieceType == PieceType.BISHOP) {
                    piece = new Bishop(square.getPiece().getPlayer());
                } else if (pieceType == PieceType.KNIGHT) {
                    piece = new Knight(square.getPiece().getPlayer());
                } else if (pieceType == PieceType.ROOK) {
                    piece = new Rook(square.getPiece().getPlayer());
                } else {
                    piece = new Queen(square.getPiece().getPlayer());
                }
                moveList.add(new Move(square.getCoordinate(), square
                        .getCoordinate(), piece, square));
                square.setPiece(piece);
                return true;
            }
            return false;
        }
    
        /**
         * Checks if the square contains a pawn that can be promoted.
         * 
         * @param square
         *            Square of the pawn
         * @return boolean If the pawn can be promoted
         */
        public boolean isValidPromotion(Square square) {
            if (!square.isOccupied() == true) {
                return false;
            }
            if (square.getPiece().getType() == PieceType.PAWN) {
                int col = 7;
                if (square.getPiece().getPlayer() == PlayerType.BLACK) {
                    col = 0;
                }
                if (square.getCoordinate().equals(
                        new Coordinate(square.getCoordinate().getX(), col))) {
                    return true;
                }
    
            }
            return false;
        }
    
        /**
         * Returns if either of the players are checkmated.
         * 
         * @return boolean
         */
        public boolean isGameOver() {
            if (isCheckmate(PlayerType.WHITE) || isCheckmate(PlayerType.BLACK)) {
                return true;
            }
            return false;
        }
    
        /**
         * Returns if the player is checkmated or not.
         * 
         * @param player
         *            The player to check checkmate for
         * @return boolean
         */
        public boolean isCheckmate(PlayerType player) {
            Square[] attackers = getAttackingPieces(player);
            // If there are no attackers
            if (attackers.length == 0) {
                return false;
            }
    
            // If there is more than one attacker then there are many options check all.
            boolean checkmate = true;
            Square attackerSquare = attackers[0];
            Square kingSquare = squareOfKing(player);
            Coordinate[] attackPath = attackerSquare.getPiece().getPath(
                    attackerSquare.getCoordinate(), kingSquare.getCoordinate());
            Square[][] allSquares = board.getSquares();
            for (int x = 0; x < 8; x++) {
                for (int y = 0; y < 8; y++) {
    
                    // If the king can move to a different square.
                    if (isValidMove(squareOfKing(player), board.getSquares()[x][y])
                            && squareOfKing(player) != board.getSquares()[x][y]) {
                        return false;
                    }
                    for (Coordinate coordinate : attackPath) {
                        Square tmpSquare = allSquares[x][y];
                        // The square must be occupied
                        if (tmpSquare.isOccupied()) {
    
                            // The player must move his own piece between the paths
                            // of the attacker and the King.
                            // If it can do so then there is no checkmate
                            if (tmpSquare.getPiece().getPlayer() == kingSquare
                                    .getPiece().getPlayer()
                                    && isValidMove(tmpSquare,
                                            board.getSquare(coordinate))) {
                                checkmate = false;
                            }
                        }
    
                    }
                }
    
            }
            return checkmate;
    
        }
    
        /**
         * This undoes the previous move.
         */
        public void undoMove() {
            if (moveList.isEmpty()) {
                return;
            }
            Move lastMove = moveList.get(moveList.size() - 1);
            if (lastMove.getFinalCoordinate() != lastMove.getInitCoordinate()) {
                board.makeMove(lastMove.getFinalCoordinate(),
                        lastMove.getInitCoordinate());
    
                if (lastMove.isCapture()) {
                    board.setPiece(lastMove.getCaptureCoordinate(),
                            lastMove.getCapturedPiece());
                }
            } else {
                // If the move was a promotion.
                moveList.remove(moveList.size() - 1);
                lastMove = moveList.get(moveList.size() - 1);
                board.setPiece(lastMove.getFinalCoordinate(), new Pawn(lastMove
                        .getPiece().getPlayer()));
            }
            // Flush the lastmove.
            moveList.remove(moveList.size() - 1);
            // Switch the current players.
            switchCurrentPlayer();
        }
    
        /**
         * This function returns all the valid moves a square/piece can make
         * 
         * @param coordinate
         *            The coordinate of the piece/square.
         * @return Square[] The array of possible squares.
         */
        public Square[] getValidMoves(Coordinate coordinate) {
            List<Square> moves = new ArrayList<Square>();
            for (int x = 0; x < 8; x++) {
                for (int y = 0; y < 8; y++) {
                    if (isValidMove(board.getSquare(coordinate),
                            board.getSquares()[x][y])) {
                        moves.add(board.getSquares()[x][y]);
                    }
                }
            }
            return moves.toArray(new Square[0]);
        }
    
        /**
         * Returns the array of squares of the pieces that are attacking the King If
         * no piece is attacking it then empty array is returned.
         * 
         * @param player
         *            The player whose king is under attack
         * @return Squares[] The array of squares of pieces that are attacking the
         *         King. Max Size of array is 2
         */
        public Square[] getAttackingPieces(PlayerType player) {
            List<Square> squares = new ArrayList<Square>();
            Square[][] allSquares = board.getSquares();
            Square kingSquare = squareOfKing(player);
            for (int x = 0; x < 8; x++) {
                for (int y = 0; y < 8; y++) {
                    Square tmpSquare = allSquares[x][y];
                    if (tmpSquare.isOccupied()) {
                        if (isValidMovement(tmpSquare, kingSquare)
                                && kingSquare.getPiece().getPlayer() != tmpSquare
                                        .getPiece().getPlayer()) {
                            squares.add(tmpSquare);
                        }
                    }
    
                }
            }
            return squares.toArray(new Square[0]);
        }
    
        /**
         * Makes a move from initial coordinate to final one. It calls
         * isValidMove(),isValidCastling() and isValidEnpassant()
         * 
         * @param initCoordinate
         *            Initial Coordinate
         * @param finalCoordinate
         *            Final Coordinate
         * @return boolean If the move was made
         */
        public boolean move(Coordinate initCoordinate, Coordinate finalCoordinate) {
            if(initCoordinate==null || finalCoordinate==null){return false;}
            // Only valid coordinates are allowed.
            if (!(initCoordinate.isValid() && finalCoordinate.isValid())) {
                return false;
            }
            Square s1 = board.getSquare(initCoordinate);
            Square s2 = board.getSquare(finalCoordinate);
            //Checks for sane moves.
            if(!isSaneMove(s1,s2)){return false;}
            // Only the current player can move the piece.
            if (currentPlayer == s1.getPiece().getPlayer()) {
                if (isValidCastling(s1, s2)) {
                    Piece tmp = s1.getPiece();
                    castle(s1, s2);
                    switchCurrentPlayer();
                    moveList.add(new Move(s1.getCoordinate(), s2.getCoordinate(),
                            tmp));
                    return true;
                } else if (isValidEnpassant(s1, s2)) {
                    Piece tmp = s1.getPiece();
                    Square capture = board
                            .getSquare((moveList.get(moveList.size() - 1)
                                    .getFinalCoordinate()));
                    enpassant(s1, s2);
                    switchCurrentPlayer();
                    moveList.add(new Move(s1.getCoordinate(), s2.getCoordinate(),
                            tmp, capture));
                    return true;
                } else if (isValidMove(s1, s2)) {
                    switchCurrentPlayer();
                    moveList.add(new Move(s1.getCoordinate(), s2.getCoordinate(),
                            s1.getPiece(), s1));
                    board.makeMove(s1, s2);
                    return true;
                }
            }
            return false;
        }
    
        /**
         * Checks if the move is valid enpassant move.
         * 
         * @param s1
         *            Initial Square
         * @param s2
         *            Final Square
         * @return boolean If enpassant valid
         */
        private boolean isValidEnpassant(Square s1, Square s2) {
            // The final square should be empty
            if (s2.isOccupied()) {
                return false;
            }
    
            // The first piece should be a pawn.
            if (s1.getPiece().getType() != PieceType.PAWN) {
                return false;
            }
            // Move type is different according to player color
            if (s1.getPiece().getPlayer() == PlayerType.WHITE) {
                if (s1.getCoordinate().getY() > s2.getCoordinate().getY()) {
                    // White can only move forward
                    return false;
                }
            } else {
                if (s1.getCoordinate().getY() < s2.getCoordinate().getY()) {
                    // Black can only move backward
                    return false;
                }
            }
            // The move should be like a bishop move to a single square.
            if (Math.abs(s1.getCoordinate().getX() - s2.getCoordinate().getX()) == 1
                    && Math.abs(s1.getCoordinate().getY()
                            - s2.getCoordinate().getY()) == 1) {
                // There should be a pawn move before enpassant.
                if (moveList.isEmpty()) {
                    return false;
                }
                Move lastMove = moveList.get(moveList.size() - 1);
                if (lastMove.getPiece() == null) {
                    return false;
                }
                if (board.getSquare(lastMove.getFinalCoordinate()).getPiece()
                        .getType() == PieceType.PAWN) {
                    // The pawn should be moving two steps forward/backward.
                    // And our pawn should be moving to the same file as the last
                    // pawn
                    if (Math.abs(lastMove.getFinalCoordinate().getY()
                            - lastMove.getInitCoordinate().getY()) == 2
                            && lastMove.getFinalCoordinate().getX() == s2
                                    .getCoordinate().getX()) {
                        return true;
                    }
                }
            }
            return false;
        }
    
        /**
         * Makes a Enpassant move
         * 
         * @param initSquare
         *            Initial Square
         * @param finalSquare
         *            Final Square
         */
        private void enpassant(Square initSquare, Square finalSquare) {
            Move lastMove = moveList.get(moveList.size() - 1);
            board.capturePiece(board.getSquare(lastMove.getFinalCoordinate()));
            board.makeMove(initSquare, finalSquare);
    
        }
    
        /**
         * Checks if the given move makes check for the moving player
         * 
         * @param initSquare
         *            Initial Square
         * @param finalSquare
         *            Final Square
         * @return boolean If the move makes check.
         */
        private boolean moveMakesCheck(Square initSquare, Square finalSquare) {
            Piece temporaryPiece = finalSquare.getPiece();
            finalSquare.setPiece(initSquare.getPiece());
            initSquare.releasePiece();
            boolean enpassant = false;
            Piece tmp = null;
            Square lastMove = null;
            // if it is a enpassant move then you must also remove a piece from the
            // board temporarily.
            if (isValidEnpassant(initSquare, finalSquare)) {
                enpassant = true;
                lastMove = board.getSquare(moveList.get(moveList.size() - 1)
                        .getFinalCoordinate());
                tmp = lastMove.getPiece();
                lastMove.releasePiece();
            }
    
            if (isCheck(finalSquare.getPiece().getPlayer())) {
                initSquare.setPiece(finalSquare.getPiece());
                finalSquare.setPiece(temporaryPiece);
                if (enpassant) {
                    lastMove.setPiece(tmp);
                }
                return true;
            } else {
                initSquare.setPiece(finalSquare.getPiece());
                finalSquare.setPiece(temporaryPiece);
                if (enpassant) {
                    lastMove.setPiece(tmp);
                }
            }
            return false;
        }
    
        /**
         * Gets the square of the King
         * 
         * @param player
         *            The player whose king it is
         * @return Square The square of the king
         */
        private Square squareOfKing(PlayerType player) {
            Square[][] squares = board.getSquares();
            Square squareOfKing = null;
            for (int x = 0; x < 8; x++) {
                for (int y = 0; y < 8; y++) {
                    Square square = squares[x][y];
                    if (square.isOccupied()) {
                        if (square.getPiece().getType() == PieceType.KING
                                && square.getPiece().getPlayer() == player) {
                            squareOfKing = square;
                        }
                    }
                }
            }
            return squareOfKing;
        }
    
        /**
         * Checks if there is check for the player
         * 
         * @param player
         *            Is this player in check
         * @return boolean If the player is in check
         */
        public boolean isCheck(PlayerType player) {
            if (getAttackingPieces(player).length > 0) {
                return true;
            } else {
                return false;
            }
        }
    
        /**
         * Checks if the move is valid pawn capture move
         * 
         * @param initSquare
         *            Initial Square
         * @param finalSquare
         *            Final Square
         * @return boolean If the pawn capture is valid
         */
        private boolean isValidPawnCapture(Square initSquare, Square finalSquare) {
            // If the piece is not a pawn OR this is not a capture.
            if (!finalSquare.isOccupied()
                    || initSquare.getPiece().getType() != PieceType.PAWN) {
                return false;
            }
            Coordinate initPos = initSquare.getCoordinate();
            Coordinate finalPos = finalSquare.getCoordinate();
            PlayerType player = initSquare.getPiece().getPlayer();
    
            // This is for normal pawn capture moves.
            if (Math.abs(initPos.getY() - finalPos.getY()) == 1
                    && Math.abs(initPos.getX() - finalPos.getX()) == 1) {
                // White can only move forward
                if (player == PlayerType.WHITE) {
                    if (initPos.getY() < finalPos.getY()) {
                        return true;
                    }
                }
                // Black can only move backward in a sense.
                if (player == PlayerType.BLACK) {
                    if (initPos.getY() > finalPos.getY()) {
                        return true;
                    }
                }
    
            }
            return false;
        }
    
        /**
         * @param square
         *            The square of the piece
         * @return boolean If this piece has been moved or captured.
         */
        private boolean hasPieceMoved(Square square) {
            for (Move move : moveList) {
                if (move.getInitCoordinate() == square.getCoordinate()
                        || move.getFinalCoordinate() == square.getCoordinate()) {
                    return true;
                }
            }
            return false;
        }
    
        /**
         * Checks if it is valid Castling move
         * 
         * @param kingSquare
         *            The square of the king
         * @param rookSquare
         *            The square of the rook
         * @return boolean If this is valid Castling
         */
        private boolean isValidCastling(Square kingSquare, Square rookSquare) {
            // Check if the squares are occupied.
            if (!(kingSquare.isOccupied() && rookSquare.isOccupied())) {
                return false;
            }
            // Check if the pieces have been moved or not.
            if (hasPieceMoved(kingSquare) || hasPieceMoved(rookSquare)) {
                return false;
            }
    
            // First check if the move is valid.
            if (!rookSquare.getPiece().isValidMove(kingSquare.getCoordinate(),
                    rookSquare.getCoordinate())) {
                return false;
            }
            // Check if the path is clear
            if (!isPathClear(
                    rookSquare.getPiece().getPath(rookSquare.getCoordinate(),
                            kingSquare.getCoordinate()),
                    rookSquare.getCoordinate(), kingSquare.getCoordinate())) {
                return false;
            }
            // Now check if the movement of the castling is fine
            // First check if the piece is king and rook
            if (kingSquare.getPiece().getType() == PieceType.KING
                    && rookSquare.getPiece().getType() == PieceType.ROOK) {
    
                int col = 0;
                if (kingSquare.getPiece().getPlayer() == PlayerType.BLACK) {
                    col = 7;
                }
                // The peices are in correct position for castling.
    
                if (kingSquare.getCoordinate().equals(new Coordinate(4, col))
                        && (rookSquare.getCoordinate().equals(
                                new Coordinate(0, col)) || rookSquare
                                .getCoordinate().equals(new Coordinate(7, col)))) {
    
                    // Check if there is check in any way between the king and final
                    // king square
                    int offset;
                    if (Math.signum(rookSquare.getCoordinate().getX()
                            - kingSquare.getCoordinate().getX()) == 1) {
                        offset = 2;
                    } else {
                        offset = -2;
                    }
                    // Calculates final kings X coordinate
                    int kingX = kingSquare.getCoordinate().getX() + offset;
                    for (Coordinate coordinate : rookSquare.getPiece()
                            .getPath(
                                    kingSquare.getCoordinate(),
                                    new Coordinate(kingX, kingSquare
                                            .getCoordinate().getY()))) {
                        if (kingSquare.equals(board.getSquare(coordinate))) {
                            // This removes a nasty null pointer exception
                            continue;
                        }
                        if (moveMakesCheck(kingSquare, board.getSquare(coordinate))) {
                            return false;
                        }
                    }
    
                    return true;
                }
            }
            return false;
        }
    
        /**
         * Makes a castle move.
         * <p>
         * It calls the isValidCastling() first.
         * 
         * @param kingSquare
         *            The square of the King
         * @param rookSquare
         *            The square of the Rook
         */
        private void castle(Square kingSquare, Square rookSquare) {
            int offset;
            if (Math.signum(rookSquare.getCoordinate().getX()
                    - kingSquare.getCoordinate().getX()) == 1) {
                offset = 2;
            } else {
                offset = -2;
            }
            int kingX = kingSquare.getCoordinate().getX() + offset;
            int rookX = kingX - offset / 2;
            board.makeMove(kingSquare.getCoordinate(), new Coordinate(kingX,
                    kingSquare.getCoordinate().getY()));
            board.makeMove(rookSquare.getCoordinate(), new Coordinate(rookX,
                    rookSquare.getCoordinate().getY()));
        }
    
        /**
         * Checks if there are any obstacles between the pieces.
         * 
         * @param path
         *            The path between the pieces
         * @param initCoordinate
         *            Initial Coordinate to ignore
         * @param finalCoordinate
         *            Final Coordinate to ignore
         * @return boolean Is path clear
         */
        private boolean isPathClear(Coordinate[] path, Coordinate initCoordinate,
                Coordinate finalCoordinate) {
            Square[][] squares = board.getSquares();
            for (Coordinate coordinate : path) {
                if ((squares[coordinate.getX()][coordinate.getY()].isOccupied())
                        && (!coordinate.equals(initCoordinate))
                        && (!coordinate.equals(finalCoordinate))) {
                    return false;
                }
            }
            return true;
        }
    
        /**
         * Checks trivial movement. If a sane move is being made it returns true.
         * @param initSquare The initial square
         * @param finalSquare The final square
         * @return boolean If a move is sane.
         */
        private boolean isSaneMove(Square initSquare, Square finalSquare) {
            //Check if the coordinates are valid
            if(!initSquare.getCoordinate().isValid() || !initSquare.getCoordinate().isValid() )
            {
                return false;
            }
            // If the player tries to move a empty square.
            if (!initSquare.isOccupied()) {
                return false;
            }
            // If it is moving to the same square.
            // This is also checked by every piece but still for safety
            if (initSquare.equals(finalSquare)) {
                return false;
            }
    
            return true;
        }
    
        /**
         * Checks if the piece can make a valid movement to the square.
         * 
         * @param initSquare
         *            Initial Square
         * @param finalSquare
         *            Final Square
         * @return boolean If movement is valid
         */
        private boolean isValidMovement(Square initSquare, Square finalSquare) {
            if(!isSaneMove(initSquare,finalSquare)){
                return false;
            }
            // If the player tries to take his own piece.
            if (finalSquare.isOccupied()) {
                if (initSquare.getPiece().getPlayer() == finalSquare.getPiece()
                        .getPlayer())
                    return false;
            }
            // Check all movements here. Normal Moves, Pawn Captures and Enpassant.
            // Castling are handled by the move function itself.
            // If the piece cannot move to the square. No such movement.
            if (!initSquare.getPiece().isValidMove(initSquare.getCoordinate(),
                    finalSquare.getCoordinate())
                    && !isValidPawnCapture(initSquare, finalSquare)
                    && !isValidEnpassant(initSquare, finalSquare)) {
                return false;
            }
            // Pawns cannot capture forward.
            if (initSquare.getPiece().getType() == PieceType.PAWN
                    && finalSquare.isOccupied()
                    && !isValidPawnCapture(initSquare, finalSquare)) {
                return false;
            }
    
            // If piece is blocked by other pieces
            Coordinate[] path = initSquare.getPiece().getPath(
                    initSquare.getCoordinate(), finalSquare.getCoordinate());
            if (!isPathClear(path, initSquare.getCoordinate(),
                    finalSquare.getCoordinate())) {
                return false;
            }
            return true;
        }
    
        /**
         * Checks if the given move is valid and safe. Calls the isValidMovement()
         * and moveMakesCheck().
         * 
         * @param initSquare
         *            The initial Square
         * @param finalSquare
         *            The final Square
         * @return boolean Whether move is valid
         */
        public boolean isValidMove(Square initSquare, Square finalSquare) {
            if (isValidCastling(initSquare, finalSquare)) {
                return true;
            }
            if (!isValidMovement(initSquare, finalSquare)) {
                return false;
            }
            if (moveMakesCheck(initSquare, finalSquare)) {
                return false;
            }
            return true;
        }
    
    }
    

    Move.java

    package game;
    
    import pieces.Piece;
    
    /**
     * @author gnik
     * 
     */
    public class Move {
        /**
         * This is the initial coordinate of the move
         */
        private Coordinate initCoordinate;
    
        /**
         * This is the final coordinate of the move
         */
        private Coordinate finalCoordinate;
    
        /**
         * This is the type of piece
         */
        private Piece piece;
    
        /**
         * This is the piece that was captured.
         */
        private Piece capturedPiece=null;
    
        /**
         * This is the coordinate of the captured piece 
         */
        private Coordinate captureCoordinate=null;
    
        /**
         * Creates a move object. Promotions are represented as movement to the same
         * square and the piece represented is the piece to be promoted to.
         * 
         * @param initCoordinate
         *            The initial move coordinate.
         * @param finalCoordinate
         *            The coordinate of the final move.
         * @param piece
         *            The piece that was moved
         */
        public Move(Coordinate initCoordinate, Coordinate finalCoordinate,
                Piece piece) {
            this(initCoordinate,finalCoordinate,piece,null);
        }
    
        /**
         * Creates a move object. Promotions are represented as movement to the same
         * square and the piece represented is the piece to be promoted to.
         * 
         * @param initCoordinate
         *            The initial move coordinate.
         * @param finalCoordinate
         *            The coordinate of the final move.
         * @param piece
         *            The piece that was moved
         * @param captureSquare The square of the piece that was captured.
         */
        public Move(Coordinate initCoordinate, Coordinate finalCoordinate,
                Piece piece,Square captureSquare) {
            this.initCoordinate = initCoordinate;
            this.finalCoordinate = finalCoordinate;
            this.piece = piece;
            if(captureSquare!=null){
            this.capturedPiece=captureSquare.getPiece();
            this.captureCoordinate=captureSquare.getCoordinate();
            }
        }
    
        /**
         * It returns the initial move coordinate
         * 
         * @return Coordinate The initial Coordinate
         */
        public Coordinate getInitCoordinate() {
            return initCoordinate;
        }
    
        /**
         * It returns the final move coordinate
         * 
         * @return Coordinate The final Coordinate
         */
        public Coordinate getFinalCoordinate() {
            return finalCoordinate;
        }
    
        /**
         * It returns the piece that was moved.
         * 
         * @return piece The piece that was moved.
         */
        public Piece getPiece() {
            return piece;
        }
    
        /**
         * Checks if the move was a capture
         * @return boolean If the move was a capture move.
         */
        public boolean isCapture(){
            if (capturedPiece==null){return false;}
            return true;
        }
    
    
        /**
         * Returns the piece that was captured when the move was made.
         * @return Returns the captured piece
         */
        public Piece getCapturedPiece(){
            return capturedPiece;
        }
        /**
         * Returns the coordinate of the capture.
         * @return The coordinate were the capture occured.
         */
        public Coordinate getCaptureCoordinate(){
            return captureCoordinate;
        }
    }
    

    MoveParser.java

    package game;
    
    import java.util.List;
    
    /**
     * @author gnik
     * 
     */
    public class MoveParser {
    
        /**
         * @param moveList
         *            The list of moves of the game
         * @return The parsed string of the moves
         */
        public static String parse(List<Move> moveList) {
            String parsedMoves = "";
            for (Move move : moveList) {
                //This if else is for promotion
                if (parseMove(move).length() != 1) {
                    parsedMoves += " " + parseMove(move);
                } else {
                    parsedMoves += parseMove(move);
                }
    
            }
            return parsedMoves;
        }
    
        /**
         * @param move
         *            The move object
         * @return The parsed string of one move
         */
        private static String parseMove(Move move) {
            if (move.getInitCoordinate().equals(move.getFinalCoordinate())) {
                return move.getPiece().getType().toString();
            }
            return move.getInitCoordinate().getParsedCoordinate()
                    + move.getFinalCoordinate().getParsedCoordinate();
        }
    
    }
    

    Stockfish.java

    package game;
    
    import java.io.BufferedReader;
    import java.io.BufferedWriter;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.OutputStreamWriter;
    
    
    /**
     * @author gnik
     *
     */
    public class Stockfish {
        /**
         * This is the Stockfish process that runs in the background.
         */
        private Process stockfish;
    
        /**
         * This is the input BufferedWriter object to give commands to Engine.
         */
        private BufferedWriter stockfishInput;
    
        /**
         * This is the output object to recive output from Engine.
         */
        private BufferedReader stockfishOutput;
    
    
        private int stockfishLevel;
    
        /**
         * This starts a new process for the stockfish engine(if available only).
         * Also sets it in UCI_Chess960 mode with default options. Else it prints a stacktrace.
         * @param level The difficulty level from 0-20.
         */
        public Stockfish(int level) {
            stockfishLevel=level;
            try {
                stockfish = Runtime.getRuntime().exec("stockfish");
            } catch (IOException e) {
                e.printStackTrace();
            }
            stockfishInput = new BufferedWriter(new OutputStreamWriter(
                    stockfish.getOutputStream()));
            stockfishOutput = new BufferedReader(new InputStreamReader(
                    stockfish.getInputStream()));
            try {
                stockfishInput.write("setoption name UCI_Chess960 value truen");
                stockfishInput.write("setoption name Skill Level value "+Integer.toString(stockfishLevel)+"n");
                stockfishInput.flush();
                stockfishOutput.readLine();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * Starts a new engine at max difficulty. Same as Engine(20)
         */
        public Stockfish()
        {
            this(20);
        }
    
    
        /**
         * Sets the level of difficulty of the engine
         * @param level The level of difficulty (0-20)
         */
        public void setLevel(int level ){
            stockfishLevel=level;
            try {
                stockfishInput.write("setoption name Skill Level value "+Integer.toString(stockfishLevel)+"n");
                stockfishInput.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
        /**
         * This returns the best possible move 
         * If there is no possible move(Checkmate) then it send a blank string.(BUGGY)
         * @param position The current board position in UCI format
         * @return String The best possible move in UCI format
         */
        public String getBestMove(String position) {
            String output = "Stockfish api";
            try {
                stockfishInput.write("position startpos moves " + position + "n");
                stockfishInput.write("go" + "n");
                stockfishInput.flush();
                while (!output.substring(0, 8).equals("bestmove")) {
                    output = stockfishOutput.readLine();
                }
                stockfishInput.write("stop" + "n");
                if (output.equals("bestmove (none)")){return "";}
                output = output.split(" ")[1];
    
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return output;
        }
    
    
        /**
         * Closes the to the Stockfish engine process gracefully. 
         */
        public void quit(){
            try {
                stockfishInput.write("quitn");
            } catch (IOException e) {
                e.printStackTrace();
            }
            stockfish.destroy();
        }
    
    }
    

    I love playing chess. I also love programming. How to marry the two? How about playing chess against a chess engine (chess playing program) that I have designed and wrote? If you are intrigued- read on! This is the first blog post in the series about building my chess engine in Java.

    The rules of chess

    I assume that you are more-less familiar with the rules of chess. You have two “armies” facing each other, making a move after a move, until one side is guaranteed to capture the enemy king (the checkmate), or neither side can win- a draw.

    If you need to refresh your memory on the exact rules, there are Rules of chess on Wikipedia. If you are a less serious player I would point out a few overlooked and misunderstood rules:

    • Castling – different both ways
    • Promoting a pawn to any piece once they cross the board
    • En-passant – special capture with a pawn

    There are also a few rules regarding checks – you have to defend. Assuming you have the basics covered, let’s look at designing a clean Object Oriented Java Chess Engine.

    Our “Mechanical Turk” will be Java powered

    What are the key elements of building a chess engine? In fact, it is not that complicated. You need the following parts:

    • A way of storing the chess position
    • A way of generating all legal positions that could follow from given positions (all legal moves)
    • A way of evaluating every position (how good the move is)
    • An algorithm to effectively select the move, searching the tree of possibilities

    Ideally, you would want all these steps to be done with a reasonable efficiency- looking at millions of possible resulting positions before selecting a move.

    In the design phase, I will focus on the approach to the first two parts of the problem.

    First, it would be good to have a Class for storing the overall state of the game. This will be the main Class that we are interacting with. Let’s call it the ChessGame.

    Let’s have positions describe the flow of the game (could have moves, but I like the immutability for now). The currentPosition will be… well, the current position on the board. We want to be able to makeMove – to apply a new position and to find the bestMove.

    I hope that all makes sense so far. At that point let’s think what are we trying to achieve in this design:

    • Simple to understand and read
    • Efficient to work with
    • Focused on immutability for easy multithreading

    Following these guidelines, this is the design for the Position that I came up with:

    We will have the 64 squares represented as an array of bytes. Each byte corresponding to a piece (white or black) or an empty square. This is done for efficiency, although it is not the most efficient possible implementation. We will look at that later.

    There is also a number of flags attached to a Position. We need to know if castling happened if there is an en-passant possible and which move we are on. This is to determine if it is white or black move. We are not dealing with the 50 move draw rule here.

    Here I would ask for your ideas. Can we do much better than an array of bytes while maintaining readability and performance? If you have good ideas, let me know on twitter (@e4developer) or in the comments.

    The last part is looking at the pieces. All we need to know are the possible moves, leaving us with a rather simple design:

    All we need to get from each Piece is the set of legal transitions possible from a given position. These are going to be simply static methods, as pieces are actually recorded as bytes (for now).

    This leaves me with the following initial design to implement:

    Java Chess Engine – next steps

    So far I have talked about the basic design. This leaves me with the following next steps to carry out:

    • Implement the design and share the implementation with you on my blog
    • Start working on the evaluatePosition() function – this will be a lot of fun!
    • Implement a min-max algorithm using alpha-beta pruning. This is mandatory for a decent chess engine.

    If you can think of improvements over this plan, I will be keen to listen!

    Summary

    The goal of this article was to present the steps required to implement an Object Oriented Java Chess Engine. I also want to take you on the journey towards the implementation. I hope you will enjoy the ride!

    Понравилась статья? Поделить с друзьями:
  • Как написать шахматный движок
  • Как написать шарию личное сообщение
  • Как написать шапку характеристики
  • Как написать шапку реферата
  • Как написать шапку объяснительной