Игра Морской бой на JavaScript. Редактирование положения корабля и начало игры.
Вступление.
Третья статья из цикла «Игра Морской бой на чистом JavaScript». В этой статье мы рассмотрим:
- Редактирование положение кораблей на игровом поле.
- Изменения направления расположения палуб путём поворота корабля на 90°.
- Начало игры.
- Инициализацию контроллера игры.
Прежде чем читать дальше, необходимо ознакомиться со статьёй «Игра Морской бой на JavaScript. Расстановка кораблей методом перетаскивания.», иначе вам не всё будет понятно в этой статье.
Игра «Морской бой». Редактирование положения корабля.
Код редактирования положения корабля на игровом поле практически ничем не отличается от кода, реализующего его перемещения. При этом используются те же самые обработчики событий onMouseDown
, onMouseMove
и onMouseUp
с небольшими изменениями и дополнениями.
Игра «Морской бой». Начало редактирования положения корабля.
Инициирование редактирования положения корабля так же, как и начало переноса, начинается с нажатия левой кнопки мыши на корабль. Отличие состоит в том, что в объект dragObject
, в котором запоминаются свойства переносимого элемента, записывается дополнительная информация — значения left
и top
, прописанные в атрибуте style
. Эта информация будет использоваться для возвращения корабля в исходное место при невалидных координатах его нового местоположения.
Корабль может быть расположен как горизонтально, так и вертикально. Поэтому необходимо получить направление расположения его палуб. Как вы помните, полная информация о каждом экземпляре корабля хранится в массиве squadron
и, зная имя корабля, эту информацию можно получить из этого массива.
Запишем в конец функции onMouseDown
следующий код:
1 2 3 4 5 6 7 8 9 10 |
// редактируем положение корабля на игровом поле // проверяем, что корабль находится на поле игрока if (el.parentElement === Placement.FIELD) { const name = Placement.getShipName(el); // запоминаем текущее направление расположения палуб this.dragObject.kx = human.squadron[name].kx; this.dragObject.ky = human.squadron[name].ky; } |
Теперь полный код функции onMouseDown
выглядит так:
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 |
onMouseDown(e) { // если нажата не левая кнопка мыши или игра уже запущена if (e.which != 1 || startGame) return; // проверяем, что нажатие произошло над кораблём const el = e.target.closest('.ship'); if(!el) return; this.pressed = true; // переносимый объект и его свойства this.dragObject = { el, parent: el.parentElement, next: el.nextElementSibling, // координаты, с которых начат перенос downX: e.pageX, downY: e.pageY, // координаты 'left' и 'top' используются при редактировании // положения корабля на игровом поле left: el.offsetLeft, top: el.offsetTop, // горизонтальное положение корабля kx: 0, ky: 1 }; // редактируем положение корабля на игровом поле // проверяем, что корабль находится на поле игрока if (el.parentElement === Placement.FIELD) { const name = Placement.getShipName(el); // запоминаем текущее направление расположения палуб this.dragObject.kx = human.squadron[name].kx; this.dragObject.ky = human.squadron[name].ky; } } |
Игра «Морской бой». Перемещение корабля при редактировании положения.
Перемещение корабля при редактировании его положения ничем не отличается от его первоначальной установки, поэтому используется тот же самый функционал с небольшим дополнением.
После создания клона, необходимо удалить всю информацию о сдвигаемом корабле из объекта squadron
и матрицы, где отмечено расположение палуб редактируемого корабля. После окончания редактирования, туда будет записана новая информация о положении корабля. Для удаления данных создадим функцию removeShipFromSquadron
и вызовем её сразу после создания клона. Теперь JS-код создания клона в функции onMouseMove
будет выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// если клона ещё не существует, создаём его if (!this.clone) { // получаем количество палуб у перемещаемого корабля this.decks = Placement.getCloneDecks(this.dragObject.el); // создаём клон, используя ранее полученные координаты его сторон this.clone = this.creatClone({left, right, top, bottom}) || null; // если по каким-то причинам клон создать не удалось, выходим из функции if (!this.clone) return; // вычисляем сдвиг курсора по координатам X и Y this.shiftX = this.dragObject.downX - left; this.shiftY = this.dragObject.downY - top; // z-index нужен для позиционирования клона над всеми элементами DOM this.clone.style.zIndex = '1000'; // перемещаем клон в BODY document.body.appendChild(this.clone); // удаляем устаревший экземпляр корабля, если он существует // используется при редактировании положения корабля this.removeShipFromSquadron(this.clone); } |
Рассмотрим функцию removeShipFromSquadron
. Её код достаточнопростой и будет достаточно комментариев внутри функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
removeShipFromSquadron(el) { // имя редактируемого корабля const name = Placement.getShipName(el); // если корабля с таким именем не существует, // прекращаем работу функции if (!human.squadron[name]) return; // получаем массив с координатами палуб корабля и // записываем в него нули, что означает - пустое место const arr = human.squadron[name].arrDecks; for (let coords of arr) { const [x, y] = coords; human.matrix[x][y] = 0; } // удаляем всю информацию о корабле из массива эскадры delete human.squadron[name]; } |
Игра «Морской бой». Поворот корабля на 90°.
Поворот корабля на 90° будем осуществлять кликом по нему правой кнопкой мыши, при котором возникает событие contextmenu
. Чтобы не вешать обработчик события на каждый корабль, воспользуемся делегированием и с помощью метода addEventListener
вешаем всего лишь один обработчик на родительский элемент — humanfield
. Данный обработчик при срабатывании будет вызывать функцию rotationShip
.
В код, где у нас регистрируются обработчики события, добавим ещё одну строчку:
1 2 3 4 5 6 7 8 9 10 |
setObserver() { if (isHandlerPlacement) return; document.addEventListener('mousedown', this.onMouseDown.bind(this)); document.addEventListener('mousemove', this.onMouseMove.bind(this)); document.addEventListener('mouseup', this.onMouseUp.bind(this)); humanfield.addEventListener('contextmenu', this.rotationShip.bind(this)); isHandlerPlacement = true; } |
Рассмотрим алгоритм работы функции rotationShip
, являющейся методом класса Placement
, по шагам:
-
1
Т. к. для поворота корабля используется клик по правой кнопки мыши, необходимо отменить действие браузера по умолчанию — запретить появление контекстного меню.
-
2
Получаем
id
корабля, по которому был сделан клик, т. к.id
совпадает с именем корабля. -
3
Полная информация о каждом корабле хранится в объекте
human.squadron
. Обходим данный объект в поисках корабля с именем равным полученномуid
. -
4
Меняем у найденного корабля значения специальных коэффициентов
kx
иky
на противоположные. Еслиkx == 0
иky == 1
— корабль расположен горизонтально, еслиkx == 1
иky == 0
, то вертикально. -
5
Использую функцию
removeShipFromSquadron
, удаляем устаревшую информацию о расположении палуб корабля из объекта эскадры и двумерного массиваmatrix
. -
6
При помощи функции
checkLocationShip
проверяем валидность координат при новом направлении расположения палуб. Если координаты окажутся невалидны, то возвращаем старые значения коэффициентовkx
иky
. -
7
Используя конструктор
Ships
, создаём новый экземпляр корабля. -
8
Подсветим на 0.75 сек. красным цветом контур корабля, если в результате проверки валидности было получено
false
— так мы визуально покажем, что поворот корабля невозможен.
Более подробно об объекте
squadron
, функции checkLocationShip
, создании экземпляра корабля и конструкторе Ships
изложено в статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.»
Теперь, когда разобрались с алгоритмом поворота корабля на 90°, напишем функционал реализующий этот алгоритм. Полный код функции:
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 |
rotationShip(e) { // запрещаем появление контекстного меню e.preventDefault(); if (e.which != 3 || startGame) return; const el = e.target.closest('.ship'); const name = Placement.getShipName(el); // нет смысла вращать однопалубный корабль if (human.squadron[name].decks == 1) return; // объект с текущими коэффициентами и координатами корабля const obj = { kx: (human.squadron[name].kx == 0) ? 1 : 0, ky: (human.squadron[name].ky == 0) ? 1 : 0, x: human.squadron[name].x, y: human.squadron[name].y }; // очищаем данные о редактируемом корабле const decks = human.squadron[name].arrDecks.length; this.removeShipFromSquadron(el); human.field.removeChild(el); // проверяем валидность координат после поворота // если координаты не валидны, возвращаем старые коэффициенты // направления положения корабля const result = human.checkLocationShip(obj, decks); if(!result) { obj.kx = (obj.kx == 0) ? 1 : 0; obj.ky = (obj.ky == 0) ? 1 : 0; } // добавляем в объект свойства нового корабля obj.shipname = name; obj.decks = decks; // создаём экземпляр нового корабля const ship = new Ships(human, obj); ship.createShip(); // кратковременно подсвечиваем рамку корабля красным цветом if (!result) { const el = getElement(name); el.classList.add('unsuccess'); setTimeout(() => { el.classList.remove('unsuccess') }, 750); } } |
Игра «Морской бой». Запуск игры.
Игра запускается при нажатии на кнопку «Play». Изначально она не отображается, пока все десять кораблей эскадры игрока не будут расставлены на игровом поле. Соответственно, эта кнопка видна при редактировании положения кораблей эскадры.
При клике на кнопку «Play» происходит следующее:
- Убирается поле с инструкцией и контейнер
shipsCollection
. - Выводится игровое поле компьютера и на нём рандомно расставляются корабли. Естественно, визуально они не отображаются.
- Скрывается кнопка «Play» и выводится сообщение, информирующее о начале игры.
-
Удаляются обработчики событий привязанные к игровому полю игрока:
— редактирование положения кораблей;
— поворот корабля на 90°. - Создаётся экземпляр класса
Controller
управляющего игрой.
JavaScript, реализующий данный функционал, не сложный. Особое внимание хочется обратить на расстановку кораблей компьютера. Для этой цели используются таже самая функция, что и при рандомной расстановке кораблей игрока — randomLocationShips
, только перед этим создаётся экземпляр computer
при помощи конструктора Field
и очищается игровое поле компьютера от ранее расставленных кораблей. Это актуально при перезапуске игры.
1 2 3 4 5 6 7 |
// создаём экземпляр игрового поля компьютера computer = new Field(computerfield); // очищаем поле от ранее установленных кораблей computer.cleanField(); computer.randomLocationShips(); |
Более подробно о конструкторе
Field
и работе функции randomLocationShips
изложено в статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.»
Полный код обработчика события запуска игры:
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 |
// кнопка начала игры const buttonPlay = getElement('play'); buttonPlay.addEventListener('click', function(e) { // скрываем не нужные для игры элементы buttonPlay.dataset.hidden = true; instruction.hidden = true; // показываем игровое поле компьютера computerfield.parentElement.hidden = false; toptext.innerHTML = 'Морской бой между эскадрами'; // создаём экземпляр игрового поля компьютера computer = new Field(computerfield); // очищаем поле от ранее установленных кораблей computer.cleanField(); computer.randomLocationShips(); // устанавливаем флаг запуска игры startGame = true; // создаём экземпляр контроллера, управляющего игрой if (!control) control = new Controller(); // запускаем игру control.init(); }); |
Флаг startGame
нужен для блокировки изменения расстановки кораблей игроком во время игры.
Игра «Морской бой». Контроллер управления игрой «Морской бой».
Весь геймплей игры «Морской бой», реализован в классе Controller
. Рассмотрим, какие задачи выполняет данный класс:
-
1
Подготовка к игре «Морской бой»: регистрация ряда массивов, для координат выстрелов компьютера для различных игровых ситуаций.
-
2
Инициализация игры «Морской бой»:
— рандомно определяется, кто стреляет первым и выводится сообщение об этом;
— генерируются координаты выстрелов компьютера, на основе оптимального порядка обстрела игрового поля противника;
— устанавливаются обработчики событий (нажатие левой и правой кнопок мыши) для игрока; -
3
Формирование выстрела и его визуальное отображение.
-
4
Обработка результата выстрела и установка иконки, соответствующей результату. Вывод текстовой информации о результате и о том, чей следующий выстрел.
-
5
Вычисление координат, где не может быть корабля по правилам игры и их визуальное отображение.
-
6
Управление ведением боя компьютером:
— анализ игровой ситуации;
— выбор оптимальных координат для выстрела;
— обстрел «раненого» корабля с учётом предыдущих попаданий и промахов. -
7
Подсчёт количества потопленных кораблей в эскадрах игрока и компьютера и определение победителя.
-
8
Перезапуск игры после окончания морского боя.
В этой статье мы рассмотрим подготовку к игре и её инициализацию. Хочу сразу предупредить, что все функции, которые будут разобраны ниже, являются методами класса Controller
и расположены в пределах его области видимости.
Игра «Морской бой». Подготовка к игре.
Прежде всего рассмотрим конструктор класса Controller
. Конструктор инициализирует ряд переменных, в которых, на момент выстрела, содержится информация о стреляющем и его противнике. Также инициализируются три массива, в которых будут храниться координаты выстрелов компьютера для различных игровых ситуаций:
- coordsFixedHit
- массив с заранее вычисленными координатами выстрелов для реализации оптимальной тактики игры;
- coordsRandomHit
- массив с координатами, оставшихся после использования всех координат массива coordsFixed;
- coordsAroundHit
- массив с координатами вокруг клетки с попаданием.
Кроме инициализации переменных и массивов, конструктор вызывает функцию resetTempShip
. Функция очищает временный объект корабля, куда будем заносить координаты попаданий, расположение корабля, количество попаданий. Это необходимо при повторном запуске игры, когда все данные сбрасываются в исходное состояние.
JS-код конструктора:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
constructor() { this.player = ''; this.opponent = ''; this.text = ''; // массив с координатами выстрелов при рандомном выборе this.coordsRandomHit = []; // массив с заранее вычисленными координатами выстрелов this.coordsFixedHit = []; // массив с координатами вокруг клетки с попаданием this.coordsAroundHit = []; // временный объект корабля, куда будем заносить координаты // попаданий, расположение корабля, количество попаданий this.resetTempShip(); } |
JS-код функции resetTempShip
:
1 2 3 4 5 6 7 8 9 10 |
resetTempShip() { this.tempShip = { hits: 0, firstHit: [], kx: 0, ky: 0 }; } |
Рассмотрим ещё ряд статических переменных (констант) и функций, которые являются свойствами и методами класса Controller
. Статические методы не связаны с экземпляром класса и используются для повторяющихся вспомогательных вычислений.
- START_POINTS
- массив базовых координат для формирования массива coordsFixedHit;
- SERVICE_TEXT
- блок, в который выводятся информационные сообщения в процессе игры;
- showServiceText
- вывод информационных сообщений под игровыми полями;
- getCoordsIcon
- преобразование абсолютных координат иконок в координаты матрицы;
- removeElementArray
- удаление ненужных координат из массива.
JS-код класса Controller
на данном этапе:
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 |
class Controller { // массив базовых координат для формирования coordsFixedHit static START_POINTS = [ [ [6,0], [2,0], [0,2], [0,6] ], [ [3,0], [7,0], [9,2], [9,6] ] ]; // Блок, в который выводятся информационные сообщения по ходу игры static SERVICE_TEXT = getElement('service_text'); constructor() { this.player = ''; this.opponent = ''; this.text = ''; // массив с координатами выстрелов при рандомном выборе this.coordsRandomHit = []; // массив с заранее вычисленными координатами выстрелов this.coordsFixedHit = []; // массив с координатами вокруг клетки с попаданием this.coordsAroundHit = []; // временный объект корабля, куда будем заносить координаты // попаданий, расположение корабля, количество попаданий this.resetTempShip(); } // вывод информационных сообщений static showServiceText = text => { Controller.SERVICE_TEXT.innerHTML = text; } // преобразование абсолютных координат иконок в координаты матрицы static getCoordsIcon = el => { const x = el.style.top.slice(0, -2) / Field.SHIP_SIDE; const y = el.style.left.slice(0, -2) / Field.SHIP_SIDE; return [x, y]; } // удаление ненужных координат из массива static removeElementArray = (arr, [x, y]) => { return arr.filter(item => item[0] != x || item[1] != y); } } |
Игра «Морской бой». Инициализация игры.
Инициализация игры происходит в функции init
, являющейся методом класса Controller
. Так как функция вызывается из обработчика события запуска игры, т. е. за пределами области видимости класса, для её вызова используется созданный экземпляр класса control
.
1 2 3 4 |
// запускаем игру control.init(); |
Что делает функция init
:
- Рандомно определяет, кто стреляет первым. Исходя из этого задаёт значения свойствам
player
иopponent
. -
Устанавливает для игрока обработчики событий:
— нажатие на левую кнопку мыши (выстрел);
— нажатие на правую кнопку мыши (маркирование пустой клетки). - Выводит под игровыми полями сообщение о том, кто стреляет первым.
JS-код функции init
:
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 |
init() { // Рандомно выбираем игрока и его противника const random = Field.getRandom(1); this.player = (random == 0) ? human : computer; this.opponent = (this.player === human) ? computer : human; // обработчики события для игрока if (!isHandlerController) { //выстрел игрока computerfield.addEventListener('click', this.makeShot.bind(this)); // устанавливаем маркер на заведомо пустую клетку computerfield.addEventListener('contextmenu', this.setUselessCell.bind(this)); isHandlerController = true; } if (this.player === human) { compShot = false; this.text = 'Вы стреляете первым'; } else { // выстрел компьютера ... } Controller.showServiceText(this.text); } |
На данном этапе подготовка к игре закончена полностью. В следующей статье мы рассмотрим выстрел игрока.
Комментарии
-
Комментарии должны содержать вопросы и дополнения по статье, ответы на вопросы других пользователей.
Комментарии содержащие обсуждение политики, будут безжалостно удаляться. -
Для удобства чтения Вашего кода, не забываейте его форматировать. Вы его можете подсветить код с помощью тега
<pre>
:
—<pre class="lang:xhtml">
- HTML;
—<pre class="lang:css">
- CSS;
—<pre class="lang:javascript">
- JavaScript. - Если что-то не понятно в статье, постарайтесь указать более конкретно, что именно не понятно.
-
В данный момент пишу статью. Надеюсь, через неделю будет готова.
Хотел узнать, когда сможете выложить следующую четвертую статью «Морской бой»? Очень интересный ход работы над игрой.