Игра Морской бой на JavaScript. Рандомная расстановка кораблей.

Вступление.

Игру «Морской бой», думаю, представлять не надо. Вряд ли найдётся хоть один человек, который хотя бы раз не играл в неё. Давайте и мы создадим полноценный «Морской бой», используя HTML-вёрстку для отображения структуры игры на экране монитора, визуально оформим её с помощью таблицы стилей, а с помощью чистого JavaScript напишем «движок», определяющий поведение игры.

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

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

Игра Морской бой. Пример игрового поля во время игры.

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

Техническое задание на создание игры «Морской бой»

  • 1

    Количество и типы кораблей, их расположение.

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

  • 2

    Расстановка кораблей и редактирование их положения.

    Корабли игрока могут расставляться по случайному закону или самим игроком, путём перетаскивания их на игровое поле. И в первом, и во втором случае можно редактировать направление положения палуб (вертикальное или горизонтальное), путём клика правой кнопкой мышки по первой палубе и расположение, путём перетаскивания. Первая палуба — при горизонтальном расположении корабля — самая левая, при вертикальном — самая верхняя.
    При перетаскивании или повороте корабля скрипт постоянно отслеживает корректность координат корабля. Если корабль будет соприкасаться с соседним или выходить за пределы игрового поля, то рамка корабля окрасится в красный цвет, а корабль, после отпускания кнопки мышки, вернётся в исходное положение. В противном случае, рамка корабля будет зелёная.
    Нет необходимости пытаться установить корабль точно по границам клеток — программа сама откорректирует его положение, установив в ближайшие валидные координаты.
    В момент начала игры, редактирование положения кораблей отключается.

  • 3

    Ведение морского боя

    Если сражение первое, рандомно определяем, кто стреляет первым. Далее, первым стреляет победитель.
    Координаты выстрела передаются путём клика по выбранной клетке — ввода координат с клавиатуры не будет.
    Попадание отмечается красным крестиком, промах — точкой, клетку, где корабля точно быть не может, отмечаем / снимаем отметку кликом правой кнопкой мышки.
    Сообщений «ранил», «убил / потопил» не будет — самостоятельно определяем тип корабля.
    Под игровыми полями выводятся сообщения о том, чей выстрел и результат выстрела.

  • 4

    Искусственный интеллект противника.

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

    На этих требования к ИИ мы пока и остановимся, хотя можно рассмотреть варианты размещения кораблей компьютером, при которых сильно уменьшается вероятность попадания в однопалубные корабли.

  • 5

    Окончание морского боя.

    Выводится сообщение о победителе. Если выиграл компьютер, то отображаются его не потопленные корабли.

HTML-вёрстка для игры «Морской бой».

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

  1. Основной родительский элемент, background которого похож на лист тетрадки в клетку.
  2. Два игровых поля — игрока, где мы будем расставлять свои корабли и контролировать выстрелы компьютера, и компьютера, где мы будут отмечаться наши попадания и промахи. Им задаётся background в виде рамки размером 10х10 клеток с буквенным обозначением строк и цифровым обозначением колонок.
  3. Контейнер с инструкцией по ручной расстановке кораблей и набором кораблей, которые необходимо перетащить на своё игровое поле.
  4. Два блока, верхний и нижний, для вывода информационных сообщений.
  5. Кнопка запуска игры.

Первоначально, при загрузке страницы, в контейнере инструкции отображаются только элементы для выбора способа размещения кораблей:
— рандомное размещение при помощи js-скрипта;
— самостоятельное размещение игроком.
Кнопка запуска игры изначально не видна. Она появится только после того, как игрок разместит свои корабли.

Исходный код разметки HTML:

Самый сложный блок, как вы заметили, это набор кораблей.

Таблица стилей для игры «Морской бой».

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

Начинаем писать Javascript для игры «Морской бой».

Весь наш код разместим в анонимной самозапускающейся функции для ограничения области видимости:

При написании js-скрипта мы будем использовать прототипное наследование. Почему выбран такой стиль программирования?
У нас есть два игровых поля — игрока и компьютера. Эти поля совершенно идентичны. У них есть ряд одинаковых свойств и одинаковые методы, с помощью которых можно получить координаты кораблей и разместить их на этих полях. Кроме игровых полей, у нас есть множество объектов кораблей, которые также обладают рядом одинаковых свойств и методов.

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

Игра «Морской бой». Конструктор игровых полей.

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

Рассмотрим подробно функцию-конструктор и её свойства, которые будут наследоваться созданными ею объектами user и comp:

При дальнейшей написании js-кода игры «Морской бой», нам неоднократно придётся получать элементы по их id. Напишем маленькую функцию, которая будет выполнять эту работу, немного сократив этим наш код:

На данном этапе, создаём только экземпляр поля игрока — user. Объект comp создадим с помощью конструктора позже, когда будем расставлять корабли компьютера.

Игра «Морской бой». Рандомная расстановка кораблей в игре.

Игра «Морской бой». Алгоритм расстановки кораблей шаг за шагом.

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

  • 1

    У нас есть два интерактивных элемента, клик по которым запускает js-скрипт расстановки кораблей — рандомную или самостоятельную. Чтобы не вешать два обработчика события, воспользуемся делегированием и с помощью функции addEventListener вешаем один обработчик на родителя этих элементов. Родительским элементом является:

    Запомните, это очень важно.
    Для управления элементами текущей страницы (показать / скрыть, изменить стиль, переместить, подгрузить и т.д.) должны использоваться элементы <span>, <button>, <div>, <li>. Именно на них вешаются обработчики событий.
    Ни в коем случае, не используйте для этой цели тег <a>. Этот тег должен использоваться только для формирования ссылок, ведущих на другие страницы сайта или другой интернет-ресурс.

    Используя метод делегирования событий определяем, по какому элементу был сделан клик и считываем значение его атрибута data-target. Если это значение равно random, то вызываем функцию randomLocationShips.

  • 2

    Функции randomLocationShips перебирает массив с данными кораблей, который включает в себя количество палуб и тип по каждому кораблю, при этом индекс массива равен количеству кораблей данного типа. Чтобы было более понятно, рассмотрим пример:

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

  • 3

    Проверяем полученные координаты с помощью функции checkLocationShip. Зная координаты первой палубы и направление расположения палуб (горизонтальное или вертикальное), получаем координаты остальных палуб. При этом проверяем три условия:

    1. в полученных координатах не должны располагаться палубы ранее созданного корабля;
    2. в соседних клетках, включая диагональные, не должны находиться палубы ранее созданного корабля;
    3. корабль не должен выходить за пределы игрового поля.

    Если какое-то из этих условий не выполняется, снова вызываем функцию randomLocationShips.

  • 4

    Создаём экземпляр объекта корабля, используя конструктор кораблей Ships. Заносим координаты палуб корабля в двумерный массив объекта user и массив squadron, в котором содержаться данные по каждому кораблю эскадры.

Игра «Морской бой». Обработчик события запуска генерации начальных координат кораблей.

Ещё раз обратим внимание на следующую HTML-разметку:

Как было написано выше, обработчик события повесим на <div> с id="type_placement".

Игра «Морской бой». Генерация начальных координат кораблей.

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

Запишем этот код в самый конец нашего скрипта.

При вызове функции randomLocationShips, как было сказано ранее, запускается цикл, который перебирает массив с данными кораблей по каждому типу.
Эта функция является методом конструктора Field, другими словами, Field — прототип для randomLocationShips, поэтому будем вызывать её через Field.prototype.

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

Теперь полный код функции randomLocationShips выглядит так:

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

Запомните, это очень важно.
Данные о координатах корабля будут храниться в матрице (двумерном массиве), поэтому вместо буквенно-цифровых координат будут формироваться только цифровые. Отсчёт элементов массива начинается с 0. Строки матрицы — это координата ‘X’, а столбцы — координата ‘Y’.
Например, координата «Б3» в двумерном массиве будет иметь значение [1,2]. Как видно, координаты палубы корабля есть не что иное, как индексы элемента в двумерном массиве.
Игра Морской бой. Ограничения на координаты первой палубы.

Разобравшись, как накладываются ограничения на формирование координат, мы можем получить зависимость максимальной координаты от количества палуб корабля. Для координаты ‘Y’, при горизонтальном размещении корабля, это будет выглядеть так:
y = (9 - decks) + 1 или короче y = 10 - decks, где decks — количество палуб у корабля.
При этом, координата ‘X’ может принимать любое значение в диапазоне от 0 до 9.

Аналогичные ограничения накладываются и при вертикальном расположении корабля.

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

Рассмотрим по шагам, как работает функция getCoordinatesDecks:

  • 1

    Случайным образом определяем направление расположения корабля, присваивая значения специальным коэффициентам kx и ky. Если kx == 0 и ky == 1 — корабль расположен горизонтально, если kx == 1 и ky == 0, то вертикально.

  • 2

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

  • 3

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

  • 4

    Возвращаем объект, в котором содержаться значения координат x и y, а также коэффициентов kx и ky.

Теперь представим функцию getCoordinatesDecks в её полном виде:

Как вы видите, эта функция также наследуется от объекта, созданного конструктором Field.

Игра «Морской бой». Валидация координат всех палуб корабля.

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

Игра Морской бой. Отображение координат требующих проверки.

Как видно на приведённом рисунке, проверяется группа координат, совпадающая с координатами палуб корабля и прилегающих к ним клеток. Эта группа может быть меньше, если корабль примыкает к одной или сразу двум границам игрового поля. Значение проверяемых координат берётся из матрицы игрового поля, принадлежащей объекту, от которого в данный момент наследуется функция getCoordinatesDecks. Это может быть или поле игрока, или поле компьютера. Напоминаю, что в данный момент мы рассматриваем рандомную расстановку кораблей для игры «Морской бой» на поле игрока.
Полученное из матрицы значение можно сравнить или с 0 — это значит, что клетка (элемент двумерного массива) пустая, или с 1 — в проверяемой клетке есть палуба уже установленного корабля.

Проверка реализована двумя вложенными циклами for. Зная координаты первой палубы и направление расположения палуб, мы можем вычислить индексы начала и конца цикла. Для внешнего цикла это будут fromX и toX, для вложенного — fromY и toY. При вычислении мы будем также учитывать — стоит корабль по середине игрового поля или примыкает к его границе.

Игра «Морской бой». Создаём экземпляр корабля и выводим его на экран.

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

Вернёмся к функции randomLocationShips. В конце функции есть такие строки:

Настало время рассмотреть их подробнее.

Игра «Морской бой». Конструктор кораблей.

В качестве конструктора кораблей используем функцию Ships. В качестве аргументов конструктор принимает два параметра:

  1. this, указывает, для кого создаётся данный корабль, для игрока или компьютера.
  2. fc, это объект, имеющий свойства:
    fc.x и fc.y — координаты первой палубы;
    fc.kx и fc.ky — направлении расположения палуб;
    fc.decks — количество палуб корабля;
    fc.shipname — уникальное имя корабля, которое будет использоваться в качестве его идентификатора.

Игра «Морской бой». Создание корабля и сохранение информации о нём.

При создании корабля нам нужно в цикле записать координаты его палуб в два массива matrix. Один массив является свойством объекта созданным конструктором Field, а второй массив — свойством объекта созданным конструктором Ships.
Массив player.matrix необходим для:

  1. редактирования положения корабля;
  2. сохранения координат выстрела, попадания и клетки, где точно не может находиться корабль;
  3. получения текущих координат корабля при ручной расстановке эскадры.

В массив this.matrix заносится координата каждой палубы корабля. После это, сам массив записывается в массив эскадры player.squadron. Таким образом, в массиве эскадры храниться полная информация по каждому кораблю, что потребуется в дальнейшем при создании выстрела и оценки его результата.

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

Игра «Морской бой». Вывод корабля на экран монитора.

Прежде чем рассматривать реализацию функции showShip, отвечающую за отображение корабля, нужно разобрать, каким образом осуществляется позиционирование корабля на игровом поле с учётом его координат в двумерном массиве.
Все корабли формируются элементами div с абсолютным позиционированием относительно своего родительского элемента:

Зная размер палубы корабля, он совпадает с размером клетки игрового поля и равен 33px, и зная координаты, можно преобразовать их в смещение в пикселях относительно родительского элемента.

Игра Морской бой. Преобразование координат.

Кроме смещения, элементу div нужно добавить класс по названию совпадающий с именем корабля. Этот класс определит стиль корабля. Если корабль размещён вертикально, то необходимо добавить ещё класс vertical.

Поскольку прототипом функции showShip является Ships, то вызывать её мы будем через Ships.prototype. Полный код функции:

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

А что если нам не понравится, как рандомно расставлены корабли и мы заходим создать эскадру по-новому, кликнув ещё раз на слова «Случайным образом»? В лучшем случае, смешаются старая и новая эскадры.

Игра «Морской бой». Очистка игрового поля.

Итак, мы выяснили, что нам необходима некая функция, выполняющая следующие задачи:

  1. удаление ранее установленных кораблей с игрового поля;
  2. очистка массива squadron от записанных в него объектов кораблей.

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

Для функции cleanField так же прототипом является Field, поэтому будем вызывать её через Field.prototype. Вот полный код этой функции с комментариями:

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

В следующей статье будет рассказано, как с помощью JavaScript реализовать размещение кораблей игрока перетаскиванием на игровое поле, используя метод Drag’n’Drop.

Комментарии

Всего: 12 комментариев
Требования при посте комментариев:
  1. Комментарии должны содержать вопросы и дополнения по статье, ответы на вопросы других пользователей.
    Комментарии содержащие обсуждение политики, будут безжалостно удаляться.
  2. Для удобства чтения Вашего кода, не забываейте его форматировать. Вы его можете подсветить код с помощью тега <pre>:
    <pre class="lang:xhtml"> - HTML;
    <pre class="lang:css"> - CSS;
    <pre class="lang:javascript"> - JavaScript.
  3. Если что-то не понятно в статье, постарайтесь указать более конкретно, что именно не понятно.
  • Добрый день!
    Интересная статья . Вопрос — » вы можете скачать её и спрайт с картинками в архиве» ссылки на этот архив на сайте не нашел?
    Спасибо.

  • В ближайшие дни выложу архив игры «Морской бой» на сервер. Линк на него будет рядом с кнопкой «Посмотреть игру».

  • Выложил полный архив игры «Морской бой».
    В процессе написания статей, в js-код будут вносится изменения дополнения и т. д. Код в файле function.js не всегда будет соответствовать коду, приведённому в тексте статьи. Прошу это учесть.

  • Нет проверки на — какие корабли потоплены и стоит ли стрелять дальше в соседнею клетку. А значит ПК изначально уступает человеку.

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

  • Хорошо что поле квадратное, поэтому проблемы не видно. На самом деле допущена серёзная логическая ошибка в процессе кодирования. В какой-то момент автор запутался и поле повернулось на 90 градусов. Координаты x и y поменялись местами. Мне кажется, отсюда растёт проблема с расстановкой кораблей, которые в ручном режиме становится очень сложно правильно выстроить.
    Например: чтобы не быть голословным приведу пример >

    Ships.prototype.showShip = function () {
    ~263 стр …
    // через атрибут ‘style’ задаём позиционирование кораблю относительно
    // его родительского элемента
    // смещение вычисляется путём умножения координаты первой палубы на
    // размер клетки игрового поля, этот размер совпадает с размером палубы
    div.style.cssText = ‘left:’ + (this.y0 * player.shipSide) + ‘px; top:’ + (this.x0 * player.shipSide) + ‘px;’;
    Чёрт к левой координате добавляется положение корабля по вертикальной оси, а к верхней горизонтальное.

    • Вальдемар Денис Ответить 27 февраля 2020 в 13:37

      Здесь нет ошибки. Оси координат матрицы и экрана не совпадают (они противоположны), поэтому у вас и создалось впечатление, что игровое поле повернулось на 90 градусов.

  • Ещё раз, чтобы мозг встал на место. )
    1. Чтобы получить координату точки в окне браузера, мы сначала получаем её положение по горизонтали (ось X), потом по вертикали (ось Y).
    2. В двумерном массиве (матрице) для обращения к его элементу, сначала указывается строка (ось X), а потом столбец (ось Y).
    Такое обращение к матрице придумано не мной. Впервые матрицы упоминались ещё в древнем Китае, называясь тогда «волшебным квадратом». Также волшебные квадраты были известны чуть позднее у арабских математиков.
    Итог написанного: в браузере ось Х расположена по горизонтали, а ось Y — по вертикали. В матрице наоборот — ось Х вертикальная, а ось Y горизонтальная.
    Ещё раз внимательно изучите картинку из статьи, которая всё это объясняет:
    http://cleanjs.ru/wp-content/uploads/2017/09/convert_coordinates.png

    • Денис Вальдемар Ответить 28 февраля 2020 в 16:32

      Окэй. Нашёл другую залипательную штуку.
      Суть в следующем :> Если перед выстрелом клетку заблокировать и разблокировать, то независимо от того будет ли там корабль, будет промах. При снятии блока нет проверки, есть в этой клетке палуба корабля или нет.
      ~ 1058
      // если клетка, по которой кликнули, уже заштрихована, то
      if (isShaded) {
      // удаляем эту иконку
      el.parentNode.removeChild(el);
      // записываем в матрице игрового поля компьютера 0,
      // значение пустой клетки
      comp.matrix[coords.x][coords.y] = 0;
      }

  • заменяем на
    // записываем в матрице игрового поля компьютера 0 или 1
    // в зависимости от того море там или палуба
    comp.matrix[coords.x][coords.y] = self.isSea() ? 0 : 1;
    И ниже добавляем метод
    isSea: function() {
    let res = true;
    // перебор массива начнём с конца, для получения корректных значений
    // при возможном удалении его элементов
    for (var i = enemy.squadron.length — 1; i >= 0; i—) {
    var warship = enemy.squadron[i], // вся информация о корабле эскадры
    arrayDescks = warship.matrix; // массив с координатами палуб корабля

    // перебираем координаты палуб корабля
    for (var j = 0, length = arrayDescks.length; j < length; j++) {
    // если координаты одной из палуб корабля совпали с координатами выбраной точки
    if (arrayDescks[j][0] == coords.x && arrayDescks[j][1] == coords.y) {
    res = false;
    // выходим из цикла, т.к. палуба найдена
    break;
    }
    }
    }
    return res;
    },

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *