Игра Морской бой на JavaScript. Выстрел игрока.

Вступление.

Очередная статья из цикла «Игра Морской бой на чистом JavaScript». В этой статье мы рассмотрим:

  1. Визуальное отображение клеток, по которым стрелять нет смысла.
  2. Выстрел игрока.
  3. Обработка результата выстрела.
  4. Визуальное отображение попадания и промаха.

Прежде чем приступать к написанию алгоритма выстрела, необходимо вспомнить несколько массивов, о которых писалось в статье Игра Морской бой на JavaScript. Рандомная расстановка кораблей.

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

  • 0 — пустая клетка;
  • 1 — палуба корабля;
  • 2 — палубы корабля не может быть по условиям игры (только для экземпляра human);
  • 3 — промах;
  • 4 — попадание.

Двумерный массив matrix используется, в первую очередь, для визуального отображения хода игры.
Во время игры используются два экземпляра массива matrix:
human.matrix — соответствует игровому полю игрока;
computer.matrix — соответствует игровому полю компьютера.

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

Также хочу напомнить, что весь алгоритм игры «Морской бой», реализован в классе Controller. Подробнее о классе Controller и инициализации игры «Морской бой», читайте в статье Игра Морской бой на JavaScript. Редактирование положения корабля и начало игры.

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

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

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

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

Алгоритм по установке и снятию маркера на клетке игрового поля не сложен. Давайте рассмотрим его:

  • 1

    Используя функцию transformCoordsInMatrix, переводим координаты точки клика правой кнопкой мыши, в координаты матрицы.

    Запомните, это очень важно.
    Координаты точки клика отсчитываются относительно верхнего левого угла игрового поля противника.
  • 2

    Получаем коллекцию всех объектов маркеров заблокированных клеток.

  • 3

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

  • 4

    Если координаты ни одной иконки не совпадут, значит клетка, по которой был сделан клик, пустая. Устанавливаем на неё маркер пустой клетки.

  • 5

    Если координаты совпадут, значит этой клетке уже установлена какая-то иконка. В зависимости от типа иконки или удаляем её, или кратковременно подсвечиваем красным цветом.

Теперь напишем JS-код, реализующий данный алгоритм. Для этого мы создадим функцию setUselessCell и запишем её в конец класса Controller.

Как мы видим, функция setUselessCell использует три новые функции — transformCoordsInMatrix, checkUselessCell, showIcons. Эти функции будут использоваться и в дальнейшем, при формировании выстрела. Поэтому рассмотрим их подробнее.

Преобразование координат относительно документа в координаты матрицы.

Функция transformCoordsInMatrix принимает два параметра:

event
Событие сгенерированное пользователем. В список свойств данного события, входят pageX и pageY, которые содержат координаты клика в пикселях по оси 'X' и оси 'Y' относительно всего документа.
self
Экземпляр игрового поля, относительно которого будут преобразовываться координаты клика. Содержит координаты сторон игрового поля — fieldTop и fieldLeft.

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

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

Где:

e.pageX
координата точки клика по оси Х относительно документа;
fieldLeft
координата левой границы игрового поля по оси Х;
e.pageX — fieldLeft
координата точки клика по оси Х относительно левой границы игрового поля.

По оси ‘Y’ — аналогично.

Полученные координаты точки клика переводим в координаты двумерного массива (матрицы). Для этого разность координат необходимо поделить на ширину стороны клетки, которая равна размеру палубы корабля. Размер палубы является значением статического свойства (константы) SHIP_SIDE класса Field. Полученное значение округляем с недостатком или просто отбрасываем от него дробную часть, что одно и то же. Оставшаяся целая часть и есть координата двумерного массива.

Проверка наличия маркеров по вычисленным координатам.

Проверка наличия маркеров заведомо пустых клеток по координатам клика левой или правой кнопкой мыши осуществляется в функции checkUselessCell. Функция принимает единственный параметр — координаты клика.
Теперь рассмотрим особенность в работе функции.

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

К сожалению, выдаётся такая ошибка:

Uncaught TypeError: ‘caller’, ‘callee’, and ‘arguments’ properties may not be accessed on strict mode functions or the arguments objects for calls to them

Запомните, это очень важно.
В версии ES6/ES2015 и более поздних версиях режим use strict включается принудительно, независимо от того, прописано это в коде или нет.

Придётся идти более длинным и неочевидным путём, используя нестандартное свойство stack объекта Error. Это свойство возвращает трассировку стека вызываемых функций в порядке их выполнения. Парсим полученный результат и получаем имя функции, вызвавшей checkUselessCell.

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

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

Отображение иконок на игровом поле.

Напомню, что за отображение иконок отвечает функция showIcons, которая в качестве аргумента принимает три параметра:

  1. экземпляр игрового поля, на котором будет размещена иконка;
  2. координаты иконки в виде координат матрицы;
  3. класс CSS, определяющий тип иконки.

Алгоритм работы функции очень простой. С помощью встроенного метода createElement создаётся элемент SPAN, которому прописываются полученный класс CSS, а стилям left и top соответствующие координаты. После этого SPAN переносится на игровое поле. Иконки промаха и попадания выводятся с задержкой в 400 мс.

JS-код функции showIcons:

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

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

Аргументом функции makeShot является событие, генерируемое игроком, при клике по игровому полю компьютера. По наличию данного аргумента можно определить — кто стреляет. Если выстрел был компьютера, то e === undefined. На данном этапе, мы рассмотрим работу функции применительно к выстрелу игрока. Небольшие дополнения к JS-коду, относящиеся к выстрелу компьютера мы рассмотрим позже, в следующей статье.

Итак, e !== undefined, т. е. стреляет игрок. При этом необходимо убедиться, что клик сделан левой кнопкой мыши и не установлен флаг compShot, сигнализирующий о том, что в данный момент запущен алгоритм выстрела компьютера. Добавим для этого в код функции проверку следующего условия:

Если условие не выполнилось, то, используя ранее рассмотренную функцию transformCoordsInMatrix, преобразуем координаты клика в координаты (индексы) матрицы. Используя полученные координаты, проверяем, что в месте клика не установлен маркер однозначно пустой клетки или иконки промаха и попадания. Для этого вызовем функцию checkUselessCell. Если функция возвращает true, запускаем механизм визуализации взрыва по координатам выстрела.

Реализация взрыва достаточно простая и основана на css-анимации.
C помощью функции showIcons создаём иконку взрыва. Добавляем иконке класс active, который реализует css-анимацию. Через 430 ms удаляем иконку с игрового поля.

Весь этот алгоритм реализуем в функции showExplosion():

Добавим вызов функции makeShot. Теперь, на данном этапе функция makeShot выглядит следующим образом:

Игра «Морской бой». Обработка результатов выстрела.

В данном разделе мы с вами рассмотрим JS-код, который обрабатывает информацию, полученную из двумерного массива matrix и, в зависимости от результата выстрела (промах, попадание, повторный обстрел одних и тех же координат), отображает игровой процесс на экране монитора.
Этот JS-код обрабатывает как выстрел игрока, так и, с небольшими дополнениями, выстрел компьютера. Поэтому постараемся разобрать его как можно подробнее.

Для вызова тех или иных функции, в зависимости от полученного значения, будем использовать конструкцию switch. Аргументом для данной конструкции будет полученное из матрицы значение v. В конец функции makeShot запишем следующий JS-код:

Теперь рассмотрим подробнее JS-код обработки промаха и попадания.

Игра «Морской бой». Промах и продолжение игры.

Рассмотрим алгоритм обработки промаха:

  • 1

    Визуально отображаем промах.

  • 2

    Записываем в матрицу (двумерный массив) экземпляра computer по этим координатам значение 3.
    Напоминаю, что мы рассмотриваем выстрел игрока. В случае выстрела компьютера запись будет происходит в двумерный массив экземпляра human.

  • 3

    Выводим сообщение о промахе.

  • 4

    Определяем, чей выстрел будет следующим.

  • 5

    Если следующий выстрел делает компьютер, то устанавливаем флаг compShot в true, чтобы в это время игрок не мог производить никаких действий. В противном случае — устанавливаем флаг в состояние false.

Данный алгоритм реализован в функции miss. В качестве параметров функция принимает координаты выстрела преобразованные в координаты (индексы) двумерного массива matrix.

Игра «Морской бой». Попадание в корабль противника.

Алгоритм обработки результата попадания в корабль противника:

  • 1

    Визуально отображаем попадание и записываем в двумерный массив matrix экземпляра противника по этим координатам значение 4.

  • 2

    Выводим сообщение о попадании.

  • 3

    Перебираем объект эскадры squadron экземпляра противника, в котором храниться информация о каждом корабле. По координатам выстрела определяем корабль в который произошло попадание.

  • 4

    Увеличиваем на единицу счётчик попаданий по данному кораблю.

  • 5

    Если количество попаданий равно количеству палуб у корабля, то удаляем корабль из объекта эскадры.

  • 6

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

Данный алгоритм реализован в функции hits. В качестве параметров функция принимает координаты выстрела преобразованные в координаты (индексы) двумерного массива matrix.

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

Игра «Морской бой». Поиск корабля, в который произошло попадание.

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

Внимание.
Более подробно об объекте корабля изложено в описании конструктора кораблей — Ships в статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.»

На данном этапе нам понадобятся только три свойства объекта корабля:

  1. decks — количество палуб;
  2. hits — счётчик попаданий;
  3. arrDecks — массив с координатами каждой палубы корабля.

В процессе поиска, нужно перебрать массив arrDecks каждого корабля эскадры, сравнивая координаты палуб с координатами выстрела. В случае совпадения, увеличиваем счётчик попадания объекта найденного корабля на единицу и сравниваем значение счётчика с количеством палуб. Если эти значения равны, то удаляем корабль из объекта эскадры.
Допишем в конец функции hits следующий JS-код:

Чтобы избежать вложенных условных операторов if, в приведённом JS-коде использована директива continue.
Для того, чтобы исключить ненужные итерации внутреннего и внешнего цикла, используется оператор break с меткой outerloop. Такое использование оператора break позволяет выйти сразу и из внутреннего и из внешнего циклов.

Игра «Морской бой». Окончание игры.

Принимать решение о продолжении боя или его окончании мы будем на основании длины массива ключей объекта squadron — если его длина равна нулю, значит все корабли эскадры противника уничтожены. В противном случае, морской бой продолжается и игрок может совершить новый выстрел. Хочу сразу отметить, что большинство JS-кода относится к выстрелу компьютера. Подробно это будет рассмотрено в следующей статье.

Рассмотрим подробнее окончание игры при выстреле игрока. При окончании игры необходимо совершить всего два действия:

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

Напишем JS-код, который реализует описанный функционал. Разместим его в конце функции hits:

Обратите внимание на строку:

У объектов отсутствует встроенное свойство length, определяющее их размер. С помощью метода Object.keys получим массив ключей объекта, а далее вычислим длину этого массива.

Теперь полный JS-код функции hit выглядит следующим образом:

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

Если игрок выстрелил по клетке, которую обстрелял ранее и там зафиксировано попадание или промах, то игроку выдаётся предупреждение о повторном обстреле данных координат.

Напомню код конструкции switch в функции makeShot:

На этом мы закончим рассматривать выстрел игрока.

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

Комментарии

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

    • Вальдемар Владислав Ответить 17 мая 2018 в 11:28

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

  • Привет, я насчёт всего этого бурелома с new Error().stack….. А что, если функции checkUselessCell добавить ещё один параметр и назвать его, скажем, isShaded? И вызывать её из разных функций соответственно либо true, либо false? Вроде работает.

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

Ваш адрес email не будет опубликован.