Игра Морской бой на JavaScript. Рандомная расстановка кораблей.
Вступление.
Игру «Морской бой», думаю, представлять не надо. Вряд ли найдётся хоть один человек, который хотя бы раз не играл в неё. Давайте и мы создадим полноценный «Морской бой», используя HTML-вёрстку для отображения структуры игры на экране монитора, визуально оформим её с помощью таблицы стилей, а с помощью чистого JavaScript напишем «движок», определяющий поведение игры.
Начнём с того, что словами опишем, как будет выглядеть наша игра, сколько и каких будет в ней кораблей, как будут расставляться корабли, каким образом будет происходить управление выстрелом и искусственный интеллект компьютера по ведению морского боя в качестве нашего противника.
Как говорится — лучше один раз увидеть. На приведённом ниже скриншоте прекрасно видно, что представляет из себя игровое поле, сколько типов кораблей, в зависимости от количества палуб будет в эскадре, как выглядит промах, попадание и отметка, где корабля точно быть не может.
Надеюсь всё всем понятно. Ну а теперь составим техническое задание, чтобы ничего не забыть и не пропустить.
При создании игры использовался JavaScript версии ES6/ES2015 и более поздних версий. Для понимания изложенного материала, необходимо предварительно изучить изменения и нововведения в версиях ES2015+.
Техническое задание на создание игры «Морской бой»
-
1
Количество и типы кораблей, их расположение.
Как и в реальной игре, эскадра будет состоять из:
— одного четырёхпалубного;
— двух трёхпалубных;
— трёх двухпалубных;
— четырёх однопалубных кораблей.
Корабли могут располагаться вертикально и горизонтально, но при этом между кораблями должна быть хотя бы одна пустая клетка, в том числе и по диагонали. Корабли не могут иметь Г-образную форму. -
2
Расстановка кораблей и редактирование их положения.
Корабли игрока могут расставляться по случайному закону или самим игроком, путём перетаскивания их на игровое поле. И в первом, и во втором случае можно редактировать направление положения палуб (вертикальное или горизонтальное), путём клика правой кнопкой мышки по первой палубе и расположение, путём перетаскивания. Первая палуба — при горизонтальном расположении корабля — самая левая, при вертикальном — самая верхняя.
При перетаскивании или повороте корабля скрипт постоянно отслеживает корректность координат корабля. Если корабль будет соприкасаться с соседним или выходить за пределы игрового поля, то рамка корабля окрасится в красный цвет, а корабль, после отпускания кнопки мышки, вернётся в исходное положение. В противном случае, рамка корабля будет зелёная.
Нет необходимости пытаться установить корабль точно по границам клеток — программа сама откорректирует его положение, установив в ближайшие валидные координаты.
В момент начала игры, редактирование положения кораблей отключается. -
3
Ведение морского боя
Если сражение первое, рандомно определяем, кто стреляет первым. Далее, первым стреляет победитель.
Координаты выстрела передаются путём клика по выбранной клетке — ввода координат с клавиатуры не будет.
Попадание отмечается красным крестиком, промах — точкой, клетку, где корабля точно быть не может, отмечаем / снимаем отметку кликом правой кнопкой мышки.
Сообщений «ранил», «убил / потопил» не будет — самостоятельно определяем тип корабля.
Под игровыми полями выводятся сообщения о том, чей выстрел и результат выстрела. -
4
Искусственный интеллект противника.
Для ведения морского боя, компьютер должен обладать хотя бы примитивным искусственным интеллектом. Он должен уметь:
— формировать координаты выстрела, согласно заложенного алгоритма ведения морского боя.
— при попадании в палубу корабля, продолжить его обстрел, пока корабль не будет потоплен.
— отмечать клетки, куда стрелять не имеет смысла и исключать координаты этих клеток из матрицы возможных выстрелов.На этих требования к ИИ мы пока и остановимся, хотя можно рассмотреть варианты размещения кораблей компьютером, при которых сильно уменьшается вероятность попадания в однопалубные корабли.
-
5
Окончание морского боя.
Выводится сообщение о победителе. Если выиграл компьютер, то отображаются его не потопленные корабли.
HTML-вёрстка для игры «Морской бой».
При создании HTML страницы, наша главная задача — воспроизвести внешний вид игрового поля морского боя максимально похожий на тот, к которому вы привыкли, играя на тетрадочных листах. Разметка HTML не сложная и будет включать в себя следующие основные блоки:
- Основной родительский элемент, background которого похож на лист тетрадки в клетку.
- Два игровых поля — игрока, где мы будем расставлять свои корабли и контролировать выстрелы компьютера, и компьютера, где мы будут отмечаться наши попадания и промахи. Им задаётся background в виде рамки размером 10х10 клеток с буквенным обозначением строк и цифровым обозначением колонок.
- Контейнер с инструкцией по ручной расстановке кораблей и набором кораблей, которые необходимо перетащить на своё игровое поле.
- Два блока, верхний и нижний, для вывода информационных сообщений.
- Кнопка запуска игры.
Первоначально, при загрузке страницы, в контейнере инструкции отображаются только элементы для выбора способа размещения кораблей:
— рандомное размещение при помощи js-скрипта;
— самостоятельное размещение игроком.
Кнопка запуска игры изначально не видна. Она появится только после того, как игрок разместит свои корабли.
Исходный код разметки HTML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
<div class="wrap"> <div class="battlefield"> <div id="text_top" class="flex text-top">Расстановка кораблей</div> <div class="flex outer"> <!-- playing field human --> <div class="field field-human"> <div id="field_human" class="ships"></div> </div> <!-- /playing field human --> <!-- playing field computer --> <div class="field field-computer" hidden> <div id="field_computer" class="ships"></div> </div> <!-- /playing field computer --> <!-- instruction --> <div id="instruction" class="instruction"> <div id="type_placement" class="type-placement-box"> 1. <span class="link" data-target="random">Случайным образом</span><br> 2. <span class="link" data-target="manually">Методом перетаскивания.</span> </div> <div id="ships_collection" class="ships-collection" hidden> <p>Перетащите мышкой корабли на игровое поле. Для установки корабля по вертикали, кликните по нему правой кнопкой мышки.</p> </div> </div> <!-- /instruction --> </div> <div class="service-row"> <div id="service_text" class="service-text"></div> <button id="play" type="button" class="btn-play" hidden>Играть</button></button> <button id="newgame" type="button" class="btn-play btn-newgame" hidden>Продолжить</button> </div> </div> </div> <ul class="initial-ships" hidden> <li> <div id="fourdeck1" class="ship fourdeck"></div> <div id="tripledeck1" class="ship tripledeck tripledeck1"></div> <div id="tripledeck2" class="ship tripledeck tripledeck2"></div> </li> <li> <div id="doubledeck1" class="ship doubledeck"></div> <div id="doubledeck2" class="ship doubledeck doubledeck2"></div> <div id="doubledeck3" class="ship doubledeck doubledeck3"></div> </li> <li> <div id="singledeck1" class="ship singledeck"></div> <div id="singledeck2" class="ship singledeck singledeck2"></div> <div id="singledeck3" class="ship singledeck singledeck3"></div> <div id="singledeck4" class="ship singledeck singledeck4"></div> </li> </ul> |
Самый сложный блок, как вы заметили, это набор кораблей.
Таблица стилей для игры «Морской бой».
Полностью всю таблицу стилей приводить здесь не буду, вы можете скачать её и спрайт с картинками в архиве. Представлю только стили, непосредственно определяющие стилевое оформление игры «Морской бой»:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
/* спрайт с контурами кораблей, маркерами промахов и попаданий, кнопкой запуска игры */ .ship, .icon-field, .btn-play { background: url('img/sprite.png') no-repeat; } /* игровое поле находится на середине страницы и имеет фон тетрадного листа в клеточку */ .battlefield { width: 860px; height: 530px; position: relative; margin: 0 auto; background: url('img/grid.png') repeat; } .outer { margin-bottom: 21px; } /* Фоном данного блока является рамка игрового поля с буквами, обозначающими строки и цифрами, обозначающими столбцы. Блок позиционирован так, что рамка совпадает с клетками тетрадного листа. */ .field { width: 366px; height: 363px; position: relative; background: url('img/bg_play_field.png') no-repeat; } .field-computer { margin-left: 31px; } /* в этом блоке с помощью абсолютного позиционирование размещаются корабли эскадры, координаты их первых палуб будут отсчитываться от его левого верхнего угла блок позиционирован таким образом, чтобы точно совпадать с клетками фона */ .ships { width: 330px; height: 330px; position: relative; left: 29px; top: 27px; } /* блоки для вывода сообщений */ .text-top { height: 66px; font-size: 22px; line-height: 66px; text-align: center; margin-bottom: 7px; } .text-btm { color: #c00; text-align: center; padding-top: 10px; } /* кнопки запуска и продолжения игры */ .btn-play { width: 144px; height: 45px; font-family: SegoePrint; font-size: 24px; line-height: 40px; color: #4530af; text-align: center; background-position: 0 -150px; cursor: pointer; } .btn-newgame { font-size: 19px; margin-top: 10px; } /* визуальное отображение кораблей на игровом поле */ .ship { height: 35px; position: absolute; } .fourdeck { width: 134px; background-position: 0 0; /* синий */ } .fourdeck.success { background-position: 0 -50px; /* зелёный */ } .fourdeck.unsuccess { background-position: 0 -100px; /* красный */ } .tripledeck { width: 101px; background-position: -150px 0; } .tripledeck.success { background-position: -150px -50px; } .tripledeck.unsuccess { background-position: -150px -100px; } .doubledeck { width: 68px; background-position: -270px 0; } .doubledeck.success { background-position: -270px -50px; } .doubledeck.unsuccess { background-position: -270px -100px; } .singledeck { width: 35px; background-position: -360px 0; } .singledeck.success { background-position: -360px -50px; } .singledeck.unsuccess { background-position: -360px -100px; } /* отображение и позиционирование набора кораблей для самостоятельной расстановки */ .initial-ships li { height: 35px; position: relative; overflow: hidden; margin-top: 31px; } .initial-ships .ship { left: 0; top: 0; float: left; cursor: move; } .initial-ships .tripledeck1 { left: 164px; } .initial-ships .tripledeck2 { left: 297px; } .initial-ships .doubledeck2 { left: 99px; } .initial-ships .doubledeck3 { left: 197px; } .initial-ships .singledeck2 { left: 65px; } .initial-ships .singledeck3 { left: 131px; } .initial-ships .singledeck4 { left: 197px; } /* стиль для вертикального положения корабля */ .vertical { transform: rotate(90deg); transform-origin: 17.5px 17.5px; } /* оформление блока инструкции */ .instruction { margin-left: 430px; -webkit-user-select: none; user-select: none; } .type-placement-box { line-height: 34px; padding-bottom: 18px; } .type-placement-box .link { font-size: 20px; border-bottom: dashed 2px #4530af; cursor: pointer; } /* маркеры */ .icon-field { width: 33px; height: 33px; position: absolute; z-index: 5; } /* маркер промаха */ .dot { background-position: -410px 0; } /* маркер попадания */ .red-cross { background-position: -410px -50px; } /* маркер клетки, где корабля быть не может */ .shaded-cell { background-position: -410px -100px; } /* красный маркер клетки, где корабля быть не может */ .shaded-cell_red { background-position: -410px -140px; } /* иконка взрыва */ .explosion { background-position: -150px -150px; z-index: 6; opacity: 0; transform: scale(.2); } .explosion.active { animation-name: Explosion; animation-duration: 0.4s; transform: scale(1); } @keyframes Explosion { 0% { opacity: 0; transform: scale(.2); } 50% { opacity: 1; transform: scale(1); } 100% { opacity: 0; } } |
Начинаем писать Javascript для игры «Морской бой».
Весь наш код разместим в анонимной самозапускающейся функции для ограничения области видимости:
1 2 3 4 5 6 |
;(function() { 'use strict'; })(); |
При написании js-скрипта мы будем использовать классы. Почему выбран такой стиль программирования?
У нас есть два игровых поля — игрока и компьютера. Эти поля совершенно идентичны. У них есть ряд одинаковых свойств и одинаковые методы, с помощью которых можно получить координаты кораблей и разместить их на этих полях. Кроме игровых полей, у нас есть множество объектов кораблей, которые также обладают рядом одинаковых свойств и методов.
Для создания выше перечисленных объектов мы будем использовать конструкторы классов, т. к. они хорошо подходят для создания экземпляров похожих объектов, имеющих одинаковую структуру, с общими именами свойств и методов.
Игра «Морской бой». Глобальные переменные и функции.
Прежде чем рассматривать классы и их функции-конструкторы, определим ряд переменных и функций, которые будут глобальными в пределах области видимости анонимной самозапускающейся функции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// флаг начала игры, устанавливается после нажатия кнопки 'Play' и запрещает // редактирование положения кораблей let startGame = false; // флаг установки обработчиков событий ручного размещения кораблей и // редактирование их положения let isHandlerPlacement = false; // флаг установки обработчиков событий ведения морского боя let isHandlerController = false; // флаг, блокирующий действия игрока во время выстрела компьютера let compShot = false; // получаем объект элемента DOM по его ID const getElement = id => document.getElementById(id); // пересчитаем координаты относительно документа, для этого // добавим величину прокрутки документа по вертикали и горизонтали // Если вы расположили игровые поля в верхней части страницы и уверенны, // что для их отображения прокручивать страницу не потребуется, то // полученные координаты можно не преобразовывать const getCoordinates = el => { const coords = el.getBoundingClientRect(); return { left: coords.left + window.pageXOffset, right: coords.right + window.pageXOffset, top: coords.top + window.pageYOffset, bottom: coords.bottom + window.pageYOffset }; }; // игровое поле игрока const humanfield = getElement('field_human'); // игровое поле компьютера const computerfield = getElement('field_computer'); |
Игра «Морской бой». Конструктор игровых полей.
Для создания игровых полей используем класс Field
. Аргументом функции-конструктора класса будет объект игрового поля: или игрока, или компьютера. Других аргументов конструктору не требуется. В результате мы получим новые экземпляры игровых полей, дополненные новыми свойствами, которые будут использовать методы класса,+ отвечающие за расстановку кораблей.
Рассмотрим подробно ряд констант, используемых в классе, а также функцию-конструктор класса и её свойства, которые будут наследоваться созданными ею объектами human
и computer
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class Field { // размер стороны игрового поля в px static FIELD_SIDE = 330; // размер палубы корабля в px static SHIP_SIDE = 33; // объект с данными кораблей // ключом будет являться тип корабля, а значением - массив, // первый элемент которого указывает кол-во кораблей данного типа, // второй элемент указывает кол-во палуб у корабля данного типа static SHIP_DATA = { fourdeck: [1, 4], tripledeck: [2, 3], doubledeck: [3, 2], singledeck: [4, 1] }; constructor(field) { // объект игрового поля, полученный в качестве аргумента this.field = field; // создаём пустой объект, куда будем заносить данные по каждому созданному кораблю // эскадры, подробно эти данные рассмотрим при создании объектов кораблей this.squadron = {}; // двумерный массив, в который заносятся координаты кораблей, а в ходе морского // боя, координаты попаданий, промахов и заведомо пустых клеток this.matrix = []; // получаем координаты всех четырёх сторон рамки игрового поля относительно начала // document, с учётом возможной прокрутки по вертикали let { left, right, top, bottom } = getCoordinates(this.field); this.fieldLeft = left; this.fieldRight = right; this.fieldTop = top; this.fieldBottom = bottom; } } |
На данном этапе, создаём только экземпляр поля игрока — human
. Объект computer
создадим с помощью конструктора позже, когда будем расставлять корабли компьютера.
Предварительно получим объекты ряда элементов, которые будут использоваться при подготовке к игре «Морской бой» и в процессе самой игры.
Запишем в самый конец анонимной самозапускающейся функции следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// родительский контейнер с инструкцией const instruction = getElement('instruction'); // контейнер, в котором будут размещаться корабли, предназначенные для перетаскивания // на игровое поле const shipsCollection = getElement('ships_collection'); // контейнер с набором кораблей, предназначенных для перетаскивания // на игровое поле const initialShips = document.querySelector('.wrap + .initial-ships'); // контейнер с заголовком const toptext = getElement('text_top'); // кнопка начала игры const buttonPlay = getElement('play'); // кнопка перезапуска игры const buttonNewGame = getElement('newgame'); // получаем экземпляр игрового поля игрока const humanfield = getElement('field_human'); const human = new Field(humanfield); // экземпляр игрового поля компьютера только регистрируем const computerfield = getElement('field_computer'); let computer = {}; |
Игра «Морской бой». Рандомная расстановка кораблей в игре.
Игра «Морской бой». Алгоритм расстановки кораблей шаг за шагом.
Прежде, чем начать писать код, отвечающий за рандомную расстановку кораблей на игровом поле, предварительно рассмотрим алгоритм расстановки по шагам:
-
1
У нас есть два интерактивных элемента, клик по которым запускает js-скрипт расстановки кораблей — рандомную или самостоятельную. Чтобы не вешать два обработчика события, воспользуемся делегированием и с помощью функции
addEventListener
вешаем один обработчик на родителя этих элементов. Родительским элементом является:123<div id="type_placement" class="type-placement-box"> ... </div>Запомните, это очень важно.
Для управления элементами текущей страницы (показать / скрыть, изменить стиль, переместить, подгрузить и т.д.) должны использоваться элементы<span>
,<button>
,<div>
,<li>
. Именно на них вешаются обработчики событий.
Не используйте для этой цели тег<a>
. Этот тег должен использоваться только для формирования ссылок, ведущих на другие страницы сайта или другой интернет-ресурс.Используя метод делегирования событий определяем, по какому элементу был сделан клик и считываем значение его атрибута
data-target
. Если это значение равноrandom
, то вызываем функциюrandomLocationShips
. -
2
Функции
randomLocationShips
перебирает статичный объектSHIP_DATA
с данными кораблей.При каждой итерации создаются координаты первой палубы корабля и направление расположений палуб. Зная начальные координаты и ориентацию корабля, в дальнейшем мы сможем сгенерировать координаты остальных палуб, которые будут находиться в соседних столбцах при горизонтальном расположении или соседних строках — при вертикальном.
-
3
Проверяем полученные координаты с помощью функции
checkLocationShip
. Зная координаты первой палубы и направление расположения палуб (горизонтальное или вертикальное), получаем координаты остальных палуб. При этом проверяем три условия:- в полученных координатах не должны располагаться палубы ранее созданного корабля;
- в соседних клетках, включая диагональные, не должны находиться палубы ранее созданного корабля;
- корабль не должен выходить за пределы игрового поля.
Если какое-то из этих условий не выполняется, снова вызываем функцию
randomLocationShips
. -
4
Создаём экземпляр объекта корабля, используя конструктор кораблей класса
Ships
. Заносим координаты палуб корабля в двумерный массив объектаhuman
и объектsquadron
, в котором содержаться данные по каждому кораблю эскадры.
Игра «Морской бой». Обработчик события запуска генерации начальных координат кораблей.
Ещё раз обратим внимание на следующую HTML-разметку:
1 2 3 4 5 6 |
<div id="type_placement" class="type-placement-box"> 1. <span class="link" data-target="random">Случайным образом</span><br> 2. <span class="link" data-target="manually">Самостоятельно с чистого листа.</span> </div> |
Как было написано выше, обработчик события повесим на <div>
с id="type_placement"
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
getElement('type_placement').addEventListener('click', function(e) { // используем делегирование основанное на всплытии событий if (e.target.tagName != 'SPAN') return; // если мы уже создали эскадру ранее, то видна кнопка начала игры // скроем её на время повторной расстановки кораблей buttonPlay.hidden = true; // очищаем игровое поле игрока, если уже была попытка расставить корабли human.cleanField(); // способ расстановки кораблей на игровом поле const type = e.target.dataset.target; // создаём литеральный объект typeGeneration // каждому свойству литерального объекта соответствует функция // в которой вызывается рандомная или ручная расстановка кораблей const typeGeneration = { random() { // скрываем контейнер с кораблями, предназначенными для перетаскивания // на игровое поле shipsCollection.hidden = true; // вызов ф-ии рандомно расставляющей корабли для экземпляра игрока human.randomLocationShips(); }, manually() { // этот код мы рассмотрим, когда будем реализовывать // расстановку кораблей перетаскиванием на игровое поле ... } }; // вызов функции литерального объекта в зависимости // от способа расстановки кораблей typeGeneration[type](); }); |
Игра «Морской бой». Очистка игрового поля от ранее расставленных кораблей.
Прежде чем начать размещение кораблей (неважно, каким способом), предварительно необходимо очистить игровое поле и ряд объектов и массивов. За эту операцию отвечает функция cleanField
, являющаяся методом класса Field
.
Данная функция выполняет следующие задачи:
- удаление ранее установленных кораблей с игрового поля;
- очистка массива
squadron
от записанных в него объектов кораблей; - заполняем матрицу игрового поля нулями, что соотвествует пустому месту.
В дальнейшем, эта функция будет использоваться и для очистки игрового поля компьютера. Код функции:
1 2 3 4 5 6 7 8 9 10 11 12 |
cleanField() { // удаляем все объекты с игрового поля while (this.field.firstChild) { this.field.removeChild(this.field.firstChild); } // удаляем всё элементы объекта эскадры this.squadron = {}; // заполняем матрицу игрового поля нулями this.matrix = Field.createMatrix(); } |
Теперь, при попытке создать новую эскадру с более оптимальным расположением кораблей, конфликтов со старой эскадрой не возникнет.
Статичная функция createMatrix
формирует двумерный массив и заполняет его нулями. В этот массив и будут записываться координаты кораблей, а в дальнейшем координаты попаданий, промахов и отметки заведомо пустых клеток. Код этой функции несложный и в комментариях не нуждается:
1 2 3 4 5 |
static createMatrix() { return [...Array(10)].map(() => Array(10).fill(0)); } |
В дальнейшем, все статичные методы будем записывать сразу после функции-конструктора класса, а статичные свойства — перед конструктором.
Игра «Морской бой». Генерация начальных координат кораблей.
В данный момент, функция randomLocationShips
вызывается, как метод экземпляра human
. В дальнейшем, при расстановке кораблей компьютера, она будет вызываться, как метод экземпляра computer
.
Как было сказано ранее, функция запускает перебор объекта SHIP_DATA
с данными кораблей по каждому типу и создаёт экземпляр каждого корабля с заданными свойствами, используя для этого класс Ships
.
1 2 3 4 5 6 7 8 9 10 |
randomLocationShips() { for (let type in Field.SHIP_DATA) { // кол-во кораблей данного типа let count = Field.SHIP_DATA[type][0]; // кол-во палуб у корабля данного типа let decks = Field.SHIP_DATA[type][1]; } } |
Внутри этого цикла создадим ещё один, вложенный, цикл. Количество итераций вложенного цикла равно количеству кораблей текущего типа. Внутри этого цикла, при каждой итерации будем получать координаты первой палубы и направление расположения палуб с помощью функции getCoordinatesDecks
. Аргументом данной функции будет количество палуб создаваемого корабля. Также сформируем уникального имя корабля, которое будет использоваться, как значение его атрибута id
.
Теперь полный код функции randomLocationShips
выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
randomLocationShips() { for (let type in Field.SHIP_DATA) { // кол-во кораблей данного типа let count = Field.SHIP_DATA[type][0]; // кол-во палуб у корабля данного типа let decks = Field.SHIP_DATA[type][1]; // прокручиваем кол-во кораблей for (let i = 0; i < count; i++) { // получаем координаты первой палубы и направление расположения палуб let options = this.getCoordsDecks(decks); // кол-во палуб options.decks = decks; // имя корабля, понадобится в дальнейшем для его идентификации options.shipname = type + String(i + 1); // создаём экземпляр корабля со свойствами, указанными в // объекте options const ship = new Ships(this, options); ship.createShip(); } } } |
Прежде чем приступать к функции getCoordinatesDecks
, которая генерирует координаты первой палубы корабля и направление расположения его палуб, подумаем, какие ограничения мы должны наложить на диапазон создаваемых координат. Рассмотрим это на примере трёхпалубного корабля.
Данные о координатах корабля будут храниться в матрице (двумерном массиве), поэтому вместо буквенно-цифровых координат будут формироваться только цифровые. Отсчёт элементов массива начинается с 0. Строки матрицы — это координата ‘X’, а столбцы — координата ‘Y’.
Например, координата «Б3» в двумерном массиве будет иметь значение
[1,2]
. Как видно, координаты палубы корабля есть не что иное, как индексы элемента в двумерном массиве.
Разобравшись, как накладываются ограничения на формирование координат, мы можем получить зависимость максимальной координаты от количества палуб корабля. Для координаты ‘Y’, при горизонтальном размещении корабля, это будет выглядеть так:
y = (9 - decks) + 1
или короче y = 10 - decks
, где decks
— количество палуб у корабля.
При этом, координата ‘X’ может принимать любое значение в диапазоне от 0 до 9.
Аналогичные ограничения накладываются и при вертикальном расположении корабля.
Так как координаты формируются рандомно, давайте напишем небольшую статичную функцию, которая будет генерировать случайные числа. С помощью аргумента, будем определять диапазон, в котором будут лежать полученные значения:
1 2 3 4 |
// n - максимальное значение, которое хотим получить static getRandom = n => Math.floor(Math.random() * (n + 1)); |
Рассмотрим по шагам, как работает функция getCoordinatesDecks
:
-
1
Случайным образом определяем направление расположения корабля, присваивая значения специальным коэффициентам
kx
иky
. Еслиkx == 0
иky == 1
— корабль расположен горизонтально, еслиkx == 1
иky == 0
, то вертикально. -
2
Получаем начальные координаты корабля, не забывая про ограничения, рассмотренные ранее.
-
3
Вызываем функцию проверки полученных координат первой палубы. Если координаты не валидны, то через рекурсию опять вызываем функцию
getCoordinatesDecks
. -
4
Возвращаем объект, в котором содержаться значения координат
x
иy
, а также коэффициентовkx
иky
.
Теперь представим функцию getCoordinatesDecks
в её полном виде:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
getCoordsDecks(decks) { // получаем коэффициенты определяющие направление расположения корабля // kx == 0 и ky == 1 — корабль расположен горизонтально, // kx == 1 и ky == 0 - вертикально. let kx = Field.getRandom(1), ky = (kx == 0) ? 1 : 0, x, y; // в зависимости от направления расположения, генерируем // начальные координаты if (kx == 0) { x = Field.getRandom(9); y = Field.getRandom(10 - decks); } else { x = Field.getRandom(10 - decks); y = Field.getRandom(9); } const obj = {x, y, kx, ky} // проверяем валидность координат всех палуб корабля const result = this.checkLocationShip(obj, decks); // если координаты невалидны, снова запускаем функцию if (!result) return this.getCoordsDecks(decks); return obj; } |
Данная функция также является методом класса Field
.
Игра «Морской бой». Валидация координат всех палуб корабля.
Для проверки корректности координат палуб корабля используется функция checkLocationShip
, аргументами которой являются начальные координаты корабля и направление расположения палуб.
Чтобы было более понятно, как работает данная функция, проиллюстрируем это:
Как видно на приведённом рисунке, проверяется группа координат, совпадающая с координатами палуб корабля и прилегающих к ним клеток. Эта группа может быть меньше, если корабль примыкает к одной или сразу двум границам игрового поля. Значение проверяемых координат берётся из матрицы игрового поля, принадлежащей объекту, от которого в данный момент наследуется функция getCoordinatesDecks
. Это может быть или поле игрока, или поле компьютера. Напоминаю, что в данный момент мы рассматриваем рандомную расстановку кораблей для игры «Морской бой» на поле игрока.
Полученное из матрицы значение сравнивается с 1, т. е. в проверяемой клетке есть палуба ранее установленного корабля.
Проверка реализована с помощью встроенной функции filter
. Сначала из матрицы берётся массив координат по оси Х. Его границы определяются значениями fromX
и toX
. Далее этот массив ограничивается по оси Y — границы определяются значениями fromY
и toY
. Значение каждого элемента получившегося двумерного массива с помощью функции filter
сравнивается с 1.
Если найдутся элементы содержащие 1, то функция checkLocationShip
вернёт false
, в противном случае — true
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
checkLocationShip(obj, decks) { let { x, y, kx, ky, fromX, toX, fromY, toY } = obj; // формируем индексы, ограничивающие двумерный массив по оси X (строки) // если координата 'x' равна нулю, то это значит, что палуба расположена в самой // верхней строке, т. е. примыкает к верхней границе и началом цикла будет строка // с индексом 0, в противном случае, нужно начать проверку со строки с индексом // на единицу меньшим, чем у исходной, т.е. находящейся выше исходной строки fromX = (x == 0) ? x : x - 1; // если условие истинно - это значит, что корабль расположен вертикально и его // последняя палуба примыкает к нижней границе игрового поля // поэтому координата 'x' последней палубы будет индексом конца цикла if (x + kx * decks == 10 && kx == 1) toX = x + kx * decks; // корабль расположен вертикально и между ним и нижней границей игрового поля // есть, как минимум, ещё одна строка, координата этой строки и будет // индексом конца цикла else if (x + kx * decks < 10 && kx == 1) toX = x + kx * decks + 1; // корабль расположен горизонтально вдоль нижней границы игрового поля else if (x == 9 && kx == 0) toX = x + 1; // корабль расположен горизонтально где-то по середине игрового поля else if (x < 9 && kx == 0) toX = x + 2; // формируем индексы начала и конца выборки по столбцам // принцип такой же, как и для строк fromY = (y == 0) ? y : y - 1; if (y + ky * decks == 10 && ky == 1) toY = y + ky * decks; else if (y + ky * decks < 10 && ky == 1) toY = y + ky * decks + 1; else if (y == 9 && ky == 0) toY = y + 1; else if (y < 9 && ky == 0) toY = y + 2; if (toX === undefined || toY === undefined) return false; // отфильтровываем ячейки, получившегося двумерного массива, // содержащие 1, если такие ячейки существуют - возвращаем false if (this.matrix.slice(fromX, toX) .filter(arr => arr.slice(fromY, toY).includes(1)) .length > 0) return false; return true; } |
Игра «Морской бой». Создаём экземпляр корабля и выводим его на экран.
Итак, мы получили начальные координаты корабля и направление расположения его палуб. Теперь нам нужно создать корабль и вывести его на экран.
Вернёмся к функции randomLocationShips
. В конце функции есть такие строки:
1 2 3 4 5 6 |
// создаём экземпляр корабля со свойствами, указанными в // объекте options с помощью класса Ship const ship = new Ships(this, options); ship.createShip(); |
Настало время рассмотреть их подробнее.
Игра «Морской бой». Конструктор кораблей.
В качестве конструктора кораблей используем функцию-конструктор класса Ships
. В качестве аргументов конструктор принимает два параметра:
this
, указывает, для кого создаётся данный корабль — игрока или компьютера.options
, это объект, имеющий свойства:
—x
иy
— координаты первой палубы;
—kx
иky
— направлении расположения палуб;
—decks
— количество палуб корабля;
—shipname
— уникальное имя корабля, которое будет использоваться в качестве его идентификатора.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class Ships { constructor(self, { x, y, kx, ky, decks, shipname }) { // с каким экземпляром работаем this.player = (self === human) ? human : computer; // this.player = self; // на каком поле создаётся данный корабль this.field = self.field; // уникальное имя корабля this.shipname = shipname; //количество палуб this.decks = decks; // координата X первой палубы this.x = x; // координата Y первой палубы this.y = y; // направлении расположения палуб this.kx = kx; this.ky = ky; // счётчик попаданий this.hits = 0; // массив с координатами палуб корабля, является элементом squadron this.arrDecks = []; } } |
Игра «Морской бой». Создание корабля и сохранение информации о нём.
Для создания корабля используется функция createShip
. Эта же функция используется и для создания кораблей компьютера. Информация, для кого создаётся корабль, находится в свойстве player
— это экземпляр или human
, или computer
.
При создании корабля нам нужно в цикле записать координаты его палуб в двумерный массив matrix
и массив arrDecks
, который будет записан в объект squadron
. В этом объекте храниться полная информация по каждому кораблю, что потребуется в дальнейшем при создании выстрела и оценки его результата.
После того, как корабль создан и координаты его палуб сохранены в соответствующих массивах, выводим корабль на экран с помощью функции showShip
. Предварительно необходимо убедиться, что корабль принадлежит игроку, а корабли компьютера мы, естественно, отображать не будем.
Теперь рассмотрим полный код функции создания кораблей:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
createShip() { let { player, field, shipname, decks, x, y, kx, ky, hits, arrDecks, k = 0 } = this; while (k < decks) { // записываем координаты корабля в двумерный массив игрового поля // теперь наглядно должно быть видно, зачем мы создавали два // коэффициента направления палуб // если коэффициент равен 1, то соответствующая координата будет // увеличиваться при каждой итерации // если равен нулю, то координата будет оставаться неизменной // таким способом мы очень сократили и унифицировали код let i = x + k * kx, j = y + k * ky; // значение 1, записанное в ячейку двумерного массива, говорит о том, что // по данным координатам находится палуба некого корабля player.matrix[i][j] = 1; // записываем координаты палубы arrDecks.push([i, j]); k++; } // заносим информацию о созданном корабле в объект эскадры player.squadron[shipname] = {arrDecks, hits, x, y, kx, ky}; // если корабль создан для игрока, выводим его на экран if (player === human) { Ships.showShip(human, shipname, x, y, kx); // когда количество кораблей в эскадре достигнет 10, т.е. все корабли // сгенерированны, то можно показать кнопку запуска игры if (Object.keys(player.squadron).length == 10) { buttonPlay.hidden = false; } } } |
Игра «Морской бой». Вывод корабля на экран монитора.
Прежде чем рассматривать реализацию функции showShip
, отвечающую за отображение корабля, нужно разобрать, каким образом осуществляется позиционирование корабля на игровом поле с учётом его координат в двумерном массиве.
Все корабли формируются элементами div
с абсолютным позиционированием относительно своего родительского элемента:
1 2 3 4 5 6 |
<!-- родительский элемент для кораблей игрока --> <div id="field_user" class="ships"> ... </div> <!-- родительский элемент для кораблей компьютера --> <div id="field_comp" class="ships"> ... </div> |
Зная размер палубы корабля, он совпадает с размером клетки игрового поля и равен 33px, и зная координаты, можно преобразовать их в смещение в пикселях относительно родительского элемента.
Кроме смещения, элементу div
нужно добавить класс по названию совпадающий с именем корабля. Этот класс определит стиль корабля. Если корабль размещён вертикально, то необходимо добавить ещё класс vertical
.
Функция showShip
является методом класса Ships
и не связана с конкретными экземплярами класса (в ней отсутствует this). Поэтому объявим её статичной и разместим внутри класса, после функции-конструктора. Полный код функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
static showShip(self, shipname, x, y, kx) { // создаём новый элемент с указанным тегом const div = document.createElement('div'); // из имени корабля убираем цифры и получаем имя класса const classname = shipname.slice(0, -1); // получаем имя класса в зависимости от направления расположения корабля const dir = (kx == 1) ? ' vertical' : ''; // устанавливаем уникальный идентификатор для корабля div.setAttribute('id', shipname); // собираем в одну строку все классы div.className = `ship {classname}${dir}`; // через атрибут 'style' задаём позиционирование кораблю относительно // его родительского элемента // смещение вычисляется путём умножения координаты первой палубы на // размер клетки игрового поля, этот размер совпадает с размером палубы div.style.cssText = `left:${y * Field.SHIP_SIDE}px; top:${x * Field.SHIP_SIDE}px;`; self.field.appendChild(div); } |
В качестве результата работы функции showShip
, мы видим выведенные на экран корабли эскадры в пределах игрового поля игрока. Корабли компьютера создаются теми же самыми функциями, только они на экран не выводятся.
Заключение
Итак, мы рассмотрели, как создать эскадру кораблей для игры в «Морской бой» и разместить её на игровом поле случайным образом. Код, который мы написали, используется не только для создания эскадры игрока, но и для создания кораблей компьютера.
В следующей статье будет рассказано, как с помощью JavaScript реализовать размещение кораблей игрока перетаскиванием на игровое поле, используя метод Drag’n’Drop.
Комментарии
-
Комментарии должны содержать вопросы и дополнения по статье, ответы на вопросы других пользователей.
Комментарии содержащие обсуждение политики, будут безжалостно удаляться. -
Для удобства чтения Вашего кода, не забываейте его форматировать. Вы его можете подсветить код с помощью тега
<pre>
:
—<pre class="lang:xhtml">
- HTML;
—<pre class="lang:css">
- CSS;
—<pre class="lang:javascript">
- JavaScript. - Если что-то не понятно в статье, постарайтесь указать более конкретно, что именно не понятно.
-
В ближайшие дни выложу архив игры «Морской бой» на сервер. Линк на него будет рядом с кнопкой «Посмотреть игру».
-
Выложил полный архив игры «Морской бой».
В процессе написания статей, в 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;’;
Чёрт к левой координате добавляется положение корабля по вертикальной оси, а к верхней горизонтальное.-
Здесь нет ошибки. Оси координат матрицы и экрана не совпадают (они противоположны), поэтому у вас и создалось впечатление, что игровое поле повернулось на 90 градусов.
-
У меня мозг вывихнуло — в чём заключается рациональность такого подхода?
-
-
-
Ещё раз, чтобы мозг встал на место. )
1. Чтобы получить координату точки в окне браузера, мы сначала получаем её положение по горизонтали (ось X), потом по вертикали (ось Y).
2. В двумерном массиве (матрице) для обращения к его элементу, сначала указывается строка (ось X), а потом столбец (ось Y).
Такое обращение к матрице придумано не мной. Впервые матрицы упоминались ещё в древнем Китае, называясь тогда «волшебным квадратом». Также волшебные квадраты были известны чуть позднее у арабских математиков.
Итог написанного: в браузере ось Х расположена по горизонтали, а ось Y — по вертикали. В матрице наоборот — ось Х вертикальная, а ось Y горизонтальная.
Ещё раз внимательно изучите картинку из статьи, которая всё это объясняет:
http://cleanjs.ru/wp-content/uploads/2017/09/convert_coordinates.png-
Окэй. Нашёл другую залипательную штуку.
Суть в следующем :> Если перед выстрелом клетку заблокировать и разблокировать, то независимо от того будет ли там корабль, будет промах. При снятии блока нет проверки, есть в этой клетке палуба корабля или нет.
~ 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;
}, -
Добрый день. Хочу поменять проверку в функции IsShipSunk. Хочу сравнивать хиты темпшипа не с самым большим оставшимся кол-вом палуб, а с его собственным кол-вом палуб. Т.е. если корабль на три палубы и хита три — все, утонул и срабатывает функция ЮзелессСеллс. Что-то не могу сообразить, как обратиться к опции decks корабля. Не подскажите?
-
Добрый день.
При первом попадании необходимо в объекте human.squadron найти корабль у которого координаты палубы совпадают с координатами попадания. Для этого необходимо перебрать объект human.squadron и при каждой итерации проверять массив arrDecks на наличие координат, равных координатам попадания. После совпадения координат, посмотреть кол-во палуб (decks) у данного корабля.
-
-
Как запустить данный скаченный код?
-
Спасибо.
-
Здравствуйте. Не работает отметка, где корабля точно быть не может, на поле противника. Как ето поправить?
-
Если изменить размер окна, то выстрел делается не в ту клетку, в которой находится курсор мыши.
-
Игра не предусматривалась, как адаптивная. При инициализации игры отсчёт координат игровых полей ведётся исходя из текущих координат и размеров окна. Если после изменений размеров окна обновить страницу — координаты выстрела будут корректными.
Можно установить обработчик события «resize», по которому пересчитывать исходные координаты игровых полей.
-
Добрый день!
Интересная статья . Вопрос — » вы можете скачать её и спрайт с картинками в архиве» ссылки на этот архив на сайте не нашел?
Спасибо.