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

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

После такого обстрела, добить оставшиеся корабли игрока не составит труда, т. к. необстрелянных клеток, где может располагаться корабль, практически не останется.
Игра «Морской бой». Инициализация ведения морского боя компьютером.
Для формирования и хранения координат выстрела компьютера нам понадобится несколько несколько массивов и объектов, являющимися свойствами экземпляра comp
конструктора Field
. Рассмотрим подробно их название и назначение:
comp.shootMatrixAI
— массив с заранее сгенерированными координатами выстрелов.comp.shootMatrix
— данный массив координат будет использован для рандомного обстрела, после того, как будут использованы все координаты из массиваcomp.shootMatrixAI
. Изначально в данном массиве содержаться координаты всех клеток игрового поля. При каждом выстреле компьютера, координата выстрела будет удаляться из данного массива. Так же будут удаляться и координаты гарантированно пустых клеток. В результате, после того, как будут использованы все значения массиваcomp.shootMatrixAI
, в массивеcomp.shootMatrix
останутся лишь те координаты, где реально может быть расположен корабль игрока. Вот эти оставшиеся координаты и будут использоваться для следующего выстрела.comp.shootMatrixAround
— в данный массив будут записаны координаты клеток, расположенных вокруг попадания.comp.startPoints
— служебный массив, в котором находятся координаты начала диагоналей. Эти данные используется для формирования координат, которые будут записываться в массивcomp.shootMatrixAI
.comp.tempShip
— временный объект для хранения первого и второго попадания (данная информация необходима для определения положения корабля), координаты первой палубы и количество попаданий.
Теперь инициализируем эти массивы и объекты. Делать это будем в свойстве init
объекта battle
, который находится в модуле Controller
.
Более подробно о модуле
Controller
и его объекте battle
изложено в статье «Игра Морской бой на JavaScript. Редактирование положения корабля и начало игры.»
Добавим следующий 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 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 |
// инициализация игры init: function() { self = this; // рандомно определяем кто будет стрелять первым: человек или компьютер var rnd = getRandom(1); player = (rnd == 0) ? user : comp; // определяем, кто будет противником, т.е. чей выстрел следующий enemy = (player === user) ? comp : user; // массив с координатами выстрелов при рандомном выборе comp.shootMatrix = []; // массив с координатами выстрелов для ИИ comp.shootMatrixAI = []; // массив с координатами вокруг клетки с попаданием comp.shootMatrixAround = []; // массив координат начала циклов comp.startPoints = [ // начальные координаты "красных" диагоналей [ [6,0], [2,0], [0,2], [0,6] ], // начальные координаты "синих" диагоналей [ [3,0], [7,0], [9,2], [9,6] ] ]; // объект для хранения информация по обстреливаемому кораблю // в момент инициализации вся информации обнулена comp.tempShip = { // количество попаданий в корабль totalHits: 0, // объекты для хранения координат первого и второго попадания // необходимы для вычисления положения корабля firstHit: {}, nextHit: {}, // значения коэффициентов зависит от положения корабля // данные значения используются для вычисления координат // обстрела "раненого" корабля kx: 0, ky: 0 }; // генерируем координаты выстрелов компьютера в соответствии // с рассмотренной стратегией и заносим их в массивы // shootMatrix и shootMatrixAI self.setShootMatrix(); // первым стреляет человек if (player === user) { // устанавливаем на игровое поле компьютера обработчики событий // регистрируем обработчик выстрела compfield.addEventListener('click', self.shoot); // регистрируем обработчик визуальной отметки клеток, в которых // однозначно не может быть кораблей противника compfield.addEventListener('contextmenu', self.setEmptyCell); // выводим сообщение о том, что первый выстрел за пользователем self.showServiseText('Вы стреляете первым.'); } } |
Игра «Морской бой». Формирование массивов с координатами выстрела компьютера.
Для заполнения массивов shootMatrixAI
и shootMatrix
координатами выстрелов запускается функция setShootMatrix
, являющаяся методом объекта battle
и использующая в своей работе данные массива startPoints
. Прежде чем рассматривать работу данной функции, давайте сначала более подробно разберём содержание массива startPoints
.
Массив startPoints
содержит в себе всего лишь два элемента, каждый из которых, в свою очередь, так же является массивом. Первый массив содержит координаты начальных точек четырёх диагоналей направленных вправо-вниз. На рисунке они изображены красным цветом. Второй — координаты начальных точек четырёх диагоналей направленных вправо-вверх. На рисунке они отображены синим цветом.
Добавим в конец объекта battle
следующий 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 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 |
setShootMatrix: function() { // заполняем массив shootMatrix координатами каждой из // 100 клеток игрового поля for (var i = 0; i < 10; i++) { for(var j = 0; j < 10; j++) { comp.shootMatrix.push([i, j]); } } // заполняем массив shootMatrixAI for (var i = 0, length = comp.startPoints.length; i < length; i++) { // получаем массив координат начальных точек диагоналей var arr = comp.startPoints[i]; for (var j = 0, lh = arr.length; j < lh; j++) { // координаты текущей начальной точки диагонали var x = arr[j][0], y = arr[j][1]; // в зависимости от направления диагонали используется разный алгоритм // получения и проверки следующей координаты клетки расположенной на // диагонали switch(i) { // получаем координаты клеток находящиеся на диагоналях направленных // вправо-вниз, при этом и координата 'X', и координата 'Y' увеличиваются // на единицу при каждом цикле, пока не достигнут своих предельных // значений case 0: while(x <= 9 && y <= 9) { comp.shootMatrixAI.push([x,y]); x = (x <= 9) ? x : 9; y = (y <= 9) ? y : 9; x++; y++; }; break; // получаем координаты клеток находящиеся на диагоналях направленных // вправо-вверх, при этом у координты 'X' при каждом цикле значение // уменьшается на единицу, а у координаты 'Y' - увеличивается, // пока не достигнут своих предельных значений case 1: while(x >= 0 && x <= 9 && y <= 9) { comp.shootMatrixAI.push([x,y]); x = (x >= 0 && x <= 9) ? x : (x < 0) ? 0 : 9; y = (y <= 9) ? y : 9; x--; y++; }; break; } } } // перемешиваем полученные массивы shootMatrix и shootMatrixAi, // чтобы игрок не мог определить тактику компьютера function compareRandom(a, b) { return Math.random() - 0.5; } comp.shootMatrix.sort(compareRandom); comp.shootMatrixAI.sort(compareRandom); } |
Игра «Морской бой». Получение координат для выстрела компьютера.
Добавим в конец свойства init
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 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 |
// инициализация игры init: function() { self = this; // рандомно определяем кто будет стрелять первым: человек или компьютер var rnd = getRandom(1); player = (rnd == 0) ? user : comp; // определяем, кто будет противником, т.е. чей выстрел следующий enemy = (player === user) ? comp : user; // массив с координатами выстрелов при рандомном выборе comp.shootMatrix = []; // массив с координатами выстрелов для AI comp.shootMatrixAI = []; // массив с координатами вокруг клетки с попаданием comp.shootMatrixAround = []; // массив координат начала циклов comp.startPoints = [ // начальные координаты "красных" диагоналей [ [6,0], [2,0], [0,2], [0,6] ], // начальные координаты "синих" диагоналей [ [3,0], [7,0], [9,2], [9,6] ] ]; // объект для хранения информация по обстреливаемому кораблю // в момент инициализации вся информации обнулена comp.tempShip = { // количество попаданий в корабль totalHits: 0, // объекты для хранения координат первого и второго попадания // необходимы для вычисления положения корабля firstHit: {}, nextHit: {}, // значения коэффициентов зависит от положения корабля // данные значения используются для вычисления координат // обстрела "раненого" корабля kx: 0, ky: 0 }; // генерируем координаты выстрелов компьютера в соответствии // с рассмотренной стратегией и заносим их в массивы // shootMatrix и shootMatrixAI self.setShootMatrix(); // первым стреляет человек if (player === user) { // устанавливаем на игровое поле компьютера обработчики событий // регистрируем обработчик выстрела compfield.addEventListener('click', self.shoot); // регистрируем обработчик визуальной отметки клеток, в которых // однозначно не может быть кораблей противника compfield.addEventListener('contextmenu', self.setEmptyCell); // выводим сообщение о том, что первый выстрел за пользователем self.showServiseText('Вы стреляете первым.'); } else { // выводим сообщение о том, что первым стреляет компьютер self.showServiseText('Первым стреляет компьютер.'); // через одну секунду вызываем функцию выстрела setTimeout(function() { return self.shoot(); }, 1000); } } |
Координаты очередного выстрела компьютера будем получать, в зависимости от игровой обстановки, в одном из трёх массивов: shootMatrixAround
, shootMatrixAI
или shootMatrix
.
В-первую очередь обращаемся к массиву shootMatrixAround
в котором хранятся координаты выстрелов для обстрела клетки с попаданием, получаем его элемент и считываем из него координаты выстрела.
Если попадания ещё не было или координаты, хранившиеся в данном массиве, уже использованы для предыдущих выстрелов, т. е. массив shootMatrixAround
в данный момент пустой, то обращаемся к массиву shootMatrixAI
и получаем элемент с координатами из него.
Если и массив shootMatrixAI
пустой (все диагонали обстреляны), то координаты выстрела берём из массива shootMatrix
.
Данные из массивов получаем используя метод pop
. Данный метод удаляет последний элемент из массива и возвращает его.
В предыдущей статье мы рассматривали работу функции shoot
применительно к выстрелу игрока. Теперь рассмотрим JS-код, который относится к выстрелу компьютера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
shoot: function(e) { // e !== undefined - значит выстрел производит игрок if (e !== undefined) { // если клик сделан не левой кнопкой мыши, прекращаем работу функции if (e.which != 1) return false; // преобразуем координаты выстрела в координаты матрицы coords = self.transformCoordinates(e, enemy); } else { // получаем координаты выстрела компьютера в формате объекта // с двумя свойствами: X и Y coords = self.getCoordinatesShot(); } // значение матрицы по полученным координатам var val = enemy.matrix[coords.x][coords.y]; ... } |
Все рассматриваемые ниже функции, являются методами объекта
battle
и будут записываться в конец объекта в формате простой литеральной нотации.
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 |
getCoordinatesShot: function() { // в первую очередь, обращаемся к массиву shootMatrixAround и получаем // координаты для обстрела попадания, если данный массив пустой, то // если ещё есть координаты выстрелов для реализации оптимальной // тактики, получаем их, в противном случае // берём координаты очередного выстрела из массива shootMatrix coords = (comp.shootMatrixAround.length > 0) : comp.shootMatrixAround.pop() ? (comp.shootMatrixAI.length > 0) ? comp.shootMatrixAI.pop() : comp.shootMatrix.pop(); // заносим полученные координаты в объект var obj = { x: coords[0], y: coords[1] }; // удаляем выбранные координаты из массивов shootMatrixAI и shootMatrix // для исключения повторной стрельбы по этим координатам в дальнейшем if (comp.shootMatrixAI.length != 0) { self.deleteElementMatrix(comp.shootMatrixAI, obj); } self.deleteElementMatrix(comp.shootMatrix, obj); return obj; } |
Как видно из приведённого кода, у нас появилась ещё одна новая функция — deleteElementMatrix
, с помощью которой можно удалить элемент массива содержащий определённые координаты. Данная функции имеет два аргумента:
— массив, из которого нужно удалить элемент;
— объект с координатами выстрела.
Рассмотрим подробно код функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
deleteElementMatrix: function(array, obj) { // перебираем массив for (var i = 0, lh = array.length; i < lh; i++) { // находим ячейку массива, в которой содержатся координаты равные // переданным во втором аргументе и удаляем эту ячейку if (array[i][0] == obj.x && array[i][1] == obj.y) { array.splice(i, 1); // нужный элемент массива удалён, поэтому перебирать массив // далее нет смысла, поэтому прерываем цикл break; } } } |
Итак, координаты для выстрела получены. Теперь рассмотрим сам выстрел и оценку его результатов.
Игра «Морской бой». Выстрел компьютера и оценка его результата.
Ещё раз настоятельно рекомендую ознакомится со статьёй «Игра Морской бой на JavaScript. Выстрел игрока.», в частности, с разделом «Игра «Морской бой». Обработка результатов выстрела.»
Выстрел представляет из себя извлечение значения двумерного массива игрового поля противника (user.matrix
) по координатам, взятым из одного из трёх массивов: shootMatrixAround
, shootMatrixAI
или shootMatrix
. Для обработки полученного значения будем использовать конструкцию switch
, аргументом которой является переменная val
.
Игра «Морской бой». Промах.
Промах подробно я рассматривать не буду, т. к. JS-код в этом случае является практически общим, как для игрока, так и для компьютера. Всё отличие в устанавливаемых и удаляемых обработчиках событий. Всё это описано в предыдущей статье. Единственное, что я сделаю — это добавлю несколько строчек кода, которые сбрасывают свойства объекта comp.tempShip
в исходное состояние, если массив shootMatrixAround
пустой.
Этот сброс имеет значение в том случае, если происходит обстрел вокруг попадания и, если все координаты из массива использованы, значит корабль потоплен.
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 |
case 0: // устанавливаем иконку промаха и записываем промах в матрицу self.showIcons(enemy, coords, 'dot'); enemy.matrix[coords.x][coords.y] = 3; // выводим сообщение о промахе в нижней части экрана text = (player === user) ? 'Вы промахнулись. Стреляет компьютер.' : 'Компьютер промахнулся. Ваш выстрел.'; self.showServiseText(text); // определяем, чей выстрел следующий player = (player === user) ? comp : user; enemy = (player === user) ? comp : user; // следующий выстрел компьютера if (player == comp) { // снимаем обработчики событий для пользователя compfield.removeEventListener('click', self.shoot); compfield.removeEventListener('contextmenu', self.setEmptyCell); // проверяем, есть ли ещё координаты для обстрела вокруг попадания if (comp.shootMatrixAround.length == 0) { // если массив пустой, считаем обстреливаемый корабль потопленным // сбрасываем свойства объекта tempShip в исходное состояние self.resetTempShip(); } // запускаем функцию shoot для выстрела компьютера setTimeout(function() { return self.shoot(); }, 1000); // следующий выстрел игрока } else { // устанавливаем обработчики событий для пользователя compfield.addEventListener('click', self.shoot); compfield.addEventListener('contextmenu', self.setEmptyCell); } break; |
У нас появилась новая функция — resetTempShip
, которая приводит все свойства объекта tempShip
в исходные значения, которые были установлены при инициализации морского боя.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
resetTempShip: function() { // обнуляем массив с координатами обстрела клеток // вокруг попадания comp.shootMatrixAround = []; comp.tempShip = { // количество попаданий в корабль totalHits: 0, // объекты для хранения координат первого и второго попадания // необходимы для вычисления положения корабля firstHit: {}, nextHit: {}, // значения коэффициентов зависит от положения корабля // данные значения используются для вычисления координат // обстрела "раненого" корабля kx: 0, ky: 0 }; } |
Если вы обратили внимание, то код этой функции, кроме одной строчки, совпадает с инициализацией обекта tempShip
в функции init
. Логично будет заменить код инициализации, вызовом функции resetTempShip
.
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 |
// инициализация игры init: function() { self = this; // рандомно определяем кто будет стрелять первым: человек или компьютер var rnd = getRandom(1); player = (rnd == 0) ? user : comp; // определяем, кто будет противником, т.е. чей выстрел следующий enemy = (player === user) ? comp : user; // массив с координатами выстрелов при рандомном выборе comp.shootMatrix = []; // массив с координатами выстрелов для AI comp.shootMatrixAI = []; // массив с координатами вокруг клетки с попаданием comp.shootMatrixAround = []; // массив координат начала циклов comp.startPoints = [ // начальные координаты "красных" диагоналей [ [6,0], [2,0], [0,2], [0,6] ], // начальные координаты "синих" диагоналей [ [3,0], [7,0], [9,2], [9,6] ] ]; // объект для хранения информация по обстреливаемому кораблю // в момент инициализации вся информации обнулена self.resetTempShip(); // генерируем координаты выстрелов компьютера в соответствии // с рассмотренной стратегией и заносим их в массивы // shootMatrix и shootMatrixAI self.setShootMatrix(); // первым стреляет человек if (player === user) { // устанавливаем на игровое поле компьютера обработчики событий // регистрируем обработчик выстрела compfield.addEventListener('click', self.shoot); // регистрируем обработчик визуальной отметки клеток, в которых // однозначно не может быть кораблей противника compfield.addEventListener('contextmenu', self.setEmptyCell); // выводим сообщение о том, что первый выстрел за пользователем self.showServiseText('Вы стреляете первым.'); } else { // выводим сообщение о том, что первым стреляет компьютер self.showServiseText('Первым стреляет компьютер.'); // через одну секунду вызываем функцию выстрела setTimeout(function() { return self.shoot(); }, 1000); } } |
Игра «Морской бой». Попадание в корабль игрока.
JS-код обработки попадания в корабль игрока во многом совпадает с JS-кодом обработки попадания в корабль компьютера, но есть существенные дополнения, касающиеся ИИ компьютера.
Алгоритм обработка результата попадания в корабль игрока:
-
1
Визуально отображаем попадание и записываем в матрицу игрового поля игрока по этим координатам значение
4
. Выводим сообщение о попадании. -
2
Перебираем массив эскадры игрока —
user.squadron
, в котором храниться информация о каждом корабле, и определяем корабль в который произошло попадание. Увеличиваем счётчик попадания на 1. Если количество попаданий в корабль становится равным количеству палуб, считаем этот корабль уничтоженным и удаляем его из эскадры, но перед этим сохраняем координаты первой палубы удаляемого корабля в объектcomp.tempShip
. Эти координаты понадобятся для отметки клеток по краям корабля. -
3
Проверяем, все ли корабли игрока потоплены. Если эскадра игрока полностью уничтожена, выводим оставшиеся корабли компьютера на экран и заканчиваем игру. В противном случае, переходим к следующему пункту нашего алгоритма.
-
4
Увеличиваем счётчик попаданий, являющийся свойством объекта
comp.tempShip
и отмечаем гарантированно пустые клетки вокруг попадания. -
5
Проверяем, остались ли в эскадре игрока корабли, количество палуб которых больше, чем значение сётчика попаданий. Если таких кораблей не найдено, то:
- считаем, что обстреливаемый корабль потоплен;
- помечаем клетки вокруг корабля, как гарантированно пустые;
- сбрасываем значения свойств объекта
comp.tempShip
в исходное состояние; - переходим к следующему выстрелу по игровому полю игрока.
-
6
Если найдены корабли, у которых количество палуб больше, чем количество попаданий, то продолжаем уничтожение текущего корабля путём обстрела вокруг палубы, в которую было сделано попадание.
Игра «Морской бой». Сохраняем координаты первой палубы потопленного корабля.
На первом и втором пунктах приведённого алгоритма я останавливаться не буду, т. к. JS-код используется тот же самый, что и при выстреле игрока. Рассмотрим только сохранение координат первой палубы корабля в объект comp.tempShip
. Напомню, что каждый элемент массива squadron
представляет из себя объект (ассоциативный массив), в котором хранится вся информация по кораблю. В частности, координаты первой палубы находятся в свойствах x0
и y0
.
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 |
case 1: // записываем в матрицу значение '4', которое соответствует попаданию enemy.matrix[coords.x][coords.y] = 4; // отображаем иконку попадания self.showIcons(enemy, coords, 'red-cross'); // выводим сообщение о попадании в нижней части экрана text = (player === user) ? 'Поздравляем! Вы попали. Ваш выстрел.' : 'Компьютер попал в ваш корабль. Выстрел компьютера'; self.showServiseText(text); // необходимо найти корабль, в который попали // перебор массива начнём с конца, для получения корректных значений // при возможном удалении его элементов 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) { // если координаты одной из палуб совпали с координатами выстрела // увеличиваем счётчик попаданий warship.hits++; // если кол-во попаданий в корабль становится равным кол-ву палуб // считаем этот корабль уничтоженным и удаляем его из эскадры, // но перед этим сохраняем координаты первой палубы удаляемого корабля // понадобятся для отметки клеток по краям корабля if (warship.hits == warship.decks) { if (player === comp) { // сохраняем координаты первой палубы comp.tempShip.x0 = warship.x0; comp.tempShip.y0 = warship.y0; } enemy.squadron.splice(i, 1); } // выходим из цикла, т.к. палуба найдена break; } } } // все корабли эскадры противника уничтожены, игра закончена if (enemy.squadron.length == 0) { text = (player === user) ? 'Поздравляем! Вы выиграли.' : 'К сожалению, вы проиграли.'; srvText.innerHTML = text; // победа игрока if (player == user) { // удаляем обработчики событий для пользователя compfield.removeEventListener('click', self.shoot); compfield.removeEventListener('contextmenu', self.setEmptyCell); // победа компьютера } else { // если выиграл комп., показываем оставшиеся корабли компьютера // подробно рассмотрим ниже ..... } // бой продолжается } else { // следующий выстрел компьютера if (player === comp) { // подготовка к выстрелу и сам выстрел // подробно рассмотрим ниже ..... } } break; |
Игра «Морской бой». Выводим корабли компьютера на экран.
Для лучшего понимания ниже изложенного материала, рекомендую ещё раз прочитать раздел «Игра Морской бой. Вывод корабля на экран монитора» в статье «Игра Морской бой на JavaScript. Рандомная расстановка кораблей.»
Для вывода оставшихся кораблей компьютера, в случае его победы, необходимо перебрать в цикле массив comp.squadron
. При каждой итерации будет создаваться элемент DIV
, к которому присвоим класс и применим стили на основе информации, находящейся в объекте, являющемся элементом массива comp.squadron
.
Обновлённый и дополненный 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 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 |
case 1: // записываем в матрицу значение '4', которое соответствует попаданию enemy.matrix[coords.x][coords.y] = 4; // отображаем иконку попадания self.showIcons(enemy, coords, 'red-cross'); // выводим сообщение о попадании в нижней части экрана text = (player === user) ? 'Поздравляем! Вы попали. Ваш выстрел.' : 'Компьютер попал в ваш корабль. Выстрел компьютера'; self.showServiseText(text); // необходимо найти корабль, в который попали // перебор массива начнём с конца, для получения корректных значений // при возможном удалении его элементов 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) { // если координаты одной из палуб совпали с координатами выстрела // увеличиваем счётчик попаданий warship.hits++; // если кол-во попаданий в корабль становится равным кол-ву палуб // считаем этот корабль уничтоженным и удаляем его из эскадры, // но перед этим сохраняем координаты первой палубы удаляемого корабля // понадобятся для отметки клеток по краям корабля if (warship.hits == warship.decks) { if (player === comp) { // сохраняем координаты первой палубы comp.tempShip.x0 = warship.x0; comp.tempShip.y0 = warship.y0; } enemy.squadron.splice(i, 1); } // выходим из цикла, т.к. палуба найдена break; } } } // все корабли эскадры противника уничтожены, игра закончена if (enemy.squadron.length == 0) { text = (player === user) ? 'Поздравляем! Вы выиграли.' : 'К сожалению, вы проиграли.'; srvText.innerHTML = text; // победа игрока if (player == user) { // удаляем обработчики событий для пользователя compfield.removeEventListener('click', self.shoot); compfield.removeEventListener('contextmenu', self.setEmptyCell); // победа компьютера } else { // если выиграл комп., показываем оставшиеся корабли компьютера // перебираем массив эскадры компьютера for (var i = 0, length = comp.squadron.length; i < length; i++) { // при каждой итерации создаём новый элемент с указанным тегом var div = document.createElement('div'), // присваиваем имя класса в зависимости от направления расположения корабля dir = (comp.squadron[i].kx == 1) ? ' vertical' : '', classname = comp.squadron[i].shipname.slice(0, -1); // собираем в одну строку все классы div.className = 'ship ' + classname + dir; // через атрибут 'style' задаём позиционирование кораблю относительно // его родительского элемента // смещение вычисляется путём умножения координаты первой палубы на // размер клетки игрового поля, этот размер совпадает с размером палубы div.style.cssText = 'left:' + (comp.squadron[i].y0 * comp.shipSide) + 'px; top:' + (comp.squadron[i].x0 * comp.shipSide) + 'px;'; // вставляем созданный элемент корабля в 'document' comp.field.appendChild(div); } } // бой продолжается } else { // следующий выстрел компьютера if (player === comp) { // подготовка к выстрелу и сам выстрел // подробно рассмотрим ниже ..... } } break; |
Игра «Морской бой». Отмечаем гарантированно пустые клетки вокруг попадания.
После каждого удачного выстрела ИИ компьютера определяет координаты клеток, находящихся по диагонали от клетки с попаданием. Таких клеток может быть от одной до четырёх, в зависимости от координат попадания — в центре, на краю или в углу игрового поля.

Полученные координаты передаются в качестве аргумента в функцию markEmptyCell
.
Обновлённый и дополненный 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 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 |
case 1: // записываем в матрицу значение '4', которое соответствует попаданию enemy.matrix[coords.x][coords.y] = 4; // отображаем иконку попадания self.showIcons(enemy, coords, 'red-cross'); // выводим сообщение о попадании в нижней части экрана text = (player === user) ? 'Поздравляем! Вы попали. Ваш выстрел.' : 'Компьютер попал в ваш корабль. Выстрел компьютера'; self.showServiseText(text); // необходимо найти корабль, в который попали // перебор массива начнём с конца, для получения корректных значений // при возможном удалении его элементов 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) { // если координаты одной из палуб совпали с координатами выстрела // увеличиваем счётчик попаданий warship.hits++; // если кол-во попаданий в корабль становится равным кол-ву палуб // считаем этот корабль уничтоженным и удаляем его из эскадры, // но перед этим сохраняем координаты первой палубы удаляемого корабля // понадобятся для отметки клеток по краям корабля if (warship.hits == warship.decks) { if (player === comp) { // сохраняем координаты первой палубы comp.tempShip.x0 = warship.x0; comp.tempShip.y0 = warship.y0; } enemy.squadron.splice(i, 1); } // выходим из цикла, т.к. палуба найдена break; } } } // все корабли эскадры противника уничтожены, игра закончена if (enemy.squadron.length == 0) { text = (player === user) ? 'Поздравляем! Вы выиграли.' : 'К сожалению, вы проиграли.'; srvText.innerHTML = text; // победа игрока if (player == user) { // удаляем обработчики событий для пользователя compfield.removeEventListener('click', self.shoot); compfield.removeEventListener('contextmenu', self.setEmptyCell); // победа компьютера } else { // если выиграл комп., показываем оставшиеся корабли компьютера // перебираем массив эскадры компьютера for (var i = 0, length = comp.squadron.length; i < length; i++) { // при каждой итерации создаём новый элемент с указанным тегом var div = document.createElement('div'), // присваиваем имя класса в зависимости от направления расположения корабля dir = (comp.squadron[i].kx == 1) ? ' vertical' : '', classname = comp.squadron[i].shipname.slice(0, -1); // собираем в одну строку все классы div.className = 'ship ' + classname + dir; // через атрибут 'style' задаём позиционирование кораблю относительно // его родительского элемента // смещение вычисляется путём умножения координаты первой палубы на // размер клетки игрового поля, этот размер совпадает с размером палубы div.style.cssText = 'left:' + (comp.squadron[i].y0 * comp.shipSide) + 'px; top:' + (comp.squadron[i].x0 * comp.shipSide) + 'px;'; // вставляем созданный элемент корабля в 'document' comp.field.appendChild(div); } } // бой продолжается } else { // следующий выстрел компьютера if (player === comp) { // увеличиваем счётчик попаданий, равный кол-ву уничтоженных палуб comp.tempShip.totalHits++; // отмечаем клетки, где точно не может стоять корабль var points = [ [coords.x - 1, coords.y - 1], [coords.x - 1, coords.y + 1], [coords.x + 1, coords.y - 1], [coords.x + 1, coords.y + 1] ]; self.markEmptyCell(points); ..... } } break; |
Как вы обратили внимание, у нас появилась новая функция — markEmptyCell
, которая выполняет следующие задачи:
- Перебирает полученный массив и удаляет из него координаты по которым уже установлены ранее отметки пустых клеток, попаданий или промахов, а так же, если эти координаты оказываются за пределами игрового поля игрока.
- Записывает по этим координатам в двумерный массив игрового поля игрока значение ‘2’, что соответствует отметке пустой клетки.
- Вызывает функцию
showIcons
, для визуального отображения гарантированно пустых клеток на игровом поле игрока. - Удаляет эти координаты из массивов
shootMatrixAround
,shootMatrixAI
иshootMatrix
, чтобы исключить в дальнейшем стрельбу по однозначно пустым клеткам.
Ниже представлен полный JS-код функции markEmptyCell
:
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 |
markEmptyCell: function(points) { var obj; // перебираем массив с координатами for (var i = 0, lh = points.length ; i < lh ; i++) { // записываем координаты в объект obj = { x: points[i][0], y: points[i][1] }; // если выполняется хотя бы одно из условий, значит координата находится // за пределами игрового поля и она нам не нужна // такое возможно, если клетка с попаданием расположена в углу // или на краю игрового поля // прерываем текущую итерацию и переходим к следующей if (obj.x < 0 || obj.x > 9 || obj.y < 0 || obj.y > 9) continue; // если по данным координатам прописано уже какое-то значение отличное // от нуля, значит в этом месте уже стоит отметка или промаха, или попадания, // или ранее поставленной пустой клетки // прерываем текущую итерацию и переходим к следующей if (user.matrix[obj.x][obj.y] != 0) continue; // отображаем по данным координатам иконку гарантированно пустой клетки self.showIcons(enemy, obj, 'shaded-cell'); // записываем в двумерный массив игрового поля игрока по данным координатам // значение '2', соответствующее пустой клетке user.matrix[obj.x][obj.y] = 2; // удаляем из массивов выстрелов данные координаты, чтобы исключить // в дальнейшем их обстрел self.deleteElementMatrix(comp.shootMatrix, obj); if (comp.shootMatrixAround.length != 0) { self.deleteElementMatrix(comp.shootMatrixAround, obj); } if (comp.shootMatrixAI.length != 0) { self.deleteElementMatrix(comp.shootMatrixAI, obj); } self.deleteElementMatrix(comp.shootMatrix, obj); } } |
Игра «Морской бой». Проверяем, потоплен ли корабль.
На первый взгляд, реализация этой проверки не должна вызвать трудностей. Достаточно сравнить в массиве user.squadron
значения свойств decks
(количество палуб) и hits
(количество попаданий) обстреливаемого корабля. Если эти значения равны, то корабль считается потопленным. К сожалению, при таком подходе мы поставим игрока в заведомо неравное положение, ведь он обстреливает корабль до тех пор, пока явно не будет видно, что корабль потоплен. Откажемся от этого способа и научим компьютер самостоятельно определять необходимость дальнейшего обстрела раненого корабля — потоплен корабль или нет.
Вариант с использованием ИИ компьютера тоже достаточно прост. Для его реализации достаточно:
- Найти, из оставшихся кораблей в эскадре игрока, корабль с максимальным количеством палуб;
- Сравнить полученное значение с количеством попаданий, которое хранится свойстве
comp.tempShip.totalHit
.
ИИ должен анализировать результаты после каждого попадания, чтобы не делать лишних выстрелов. Например, если уничтожены две палубы, необходимо проверить, остались ли не потопленными трёхпалубные и четырёх палубный (если трёхпалубные все потоплены) корабли
Для удобства чтения, будем писать код проверки отдельно, а потом уже целиком вставим его в ветвь case 1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// находим максимально количество палуб из оставшихся кораблей var max = self.checkMaxDecks(); if (comp.tempShip.totalHits >= max) { // корабль потоплен // помечаем клетки вокруг корабля, как гарантированно пустые self.markEmptyCell(points); // сбрасываем значения свойств объекта comp.tempShip в исходное состояние; self.resetTempShip(); } else { // продолжаем обстрел клеток вокруг попадания self.setShootMatrixAround(); } |
Рассмотрим код функции checkMaxDecks
, возвращающей максимальное количество палуб:
1 2 3 4 5 6 7 8 9 10 11 12 |
checkMaxDecks: function() { var arr = []; // перебираем массив оставшихся кораблей эскадры игрока for (var i = 0, length = user.squadron.length; i < length; i++) { // записываем в массив кол-во палуб у оставшихся кораблей arr.push(user.squadron[i].decks); } // возвращаем max значение return Math.max.apply(null, arr); } |
Теперь нужно сформировать массив points
, являющийся аргументом функции markEmptyCell
. Для этого нам понадобятся данные записанные в объект comp.tempShip
:
- количество попаданий в корабль;
- координаты первой палубы обстреливаемого корабля;
- направление расположения палуб (горизонтальное или вертикальное).
Количество и положение клеток, которые необходимо пометить, зависит от количества палуб у потопленного корабля.

Варианты расположения гарантированно пустых клеток
в зависимости от положения потопленного корабля.
Как видно из приведённого рисунка, для однопалубного корабля, помечаются максимум четыре клетки, расположенные с четырёх сторон.
Для многопалубных кораблей требуются максимум две клетки, расположенные с торцов.
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 |
// находим максимально количество палуб из оставшихся кораблей var max = self.checkMaxDecks(); if (comp.tempShip.totalHits >= max) { // корабль потоплен // помечаем клетки вокруг корабля, как гарантированно пустые // однопалубный корабль if (comp.tempShip.totalHits == 1) { points = [ // верхняя [comp.tempShip.x0 - 1, comp.tempShip.y0], // нижняя [comp.tempShip.x0 + 1, comp.tempShip.y0], // левая [comp.tempShip.x0, comp.tempShip.y0 - 1], // правая [comp.tempShip.x0, comp.tempShip.y0 + 1], ]; // многопалубный корабль } else { // получаем координаты левой или верхней клетки var x1 = comp.tempShip.x0 - comp.tempShip.kx, y1 = comp.tempShip.y0 - comp.tempShip.ky, // получаем координаты правой или нижней клетки // для этого к координате первой палубы прибавляем количество палуб // умноженное на коэффициент, определяющий направление расположения // палуб корабля // kx == 1 и ky == 0 - вертикально, // kx == 0 и ky == 1 - горизонтально x2 = comp.tempShip.x0 + comp.tempShip.kx * comp.tempShip.totalHits, y2 = comp.tempShip.y0 + comp.tempShip.ky * comp.tempShip.totalHits; points = [ [x1, y1], [x2, y2] ]; } self.markEmptyCell(points); // сбрасываем значения свойств объекта comp.tempShip в исходное состояние; self.resetTempShip(); } else { // формируем координаты выстрелов вокруг попадания self.setShootMatrixAround(); } // производим новый выстрел setTimeout(function() { return self.shoot(); }, 1000); |
Добавим полученный JS-код в раздел обработки попадания case 1
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 |
case 1: // записываем в матрицу значение '4', которое соответствует попаданию enemy.matrix[coords.x][coords.y] = 4; // отображаем иконку попадания self.showIcons(enemy, coords, 'red-cross'); // выводим сообщение о попадании в нижней части экрана text = (player === user) ? 'Поздравляем! Вы попали. Ваш выстрел.' : 'Компьютер попал в ваш корабль. Выстрел компьютера'; self.showServiseText(text); // необходимо найти корабль, в который попали // перебор массива начнём с конца, для получения корректных значений // при возможном удалении его элементов 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) { // если координаты одной из палуб совпали с координатами выстрела // увеличиваем счётчик попаданий warship.hits++; // если кол-во попаданий в корабль становится равным кол-ву палуб // считаем этот корабль уничтоженным и удаляем его из эскадры, // но перед этим сохраняем координаты первой палубы удаляемого корабля // понадобятся для отметки клеток по краям корабля if (warship.hits == warship.decks) { if (player === comp) { // сохраняем координаты первой палубы comp.tempShip.x0 = warship.x0; comp.tempShip.y0 = warship.y0; } enemy.squadron.splice(i, 1); } // выходим из цикла, т.к. палуба найдена break; } } } // все корабли эскадры противника уничтожены, игра закончена if (enemy.squadron.length == 0) { text = (player === user) ? 'Поздравляем! Вы выиграли.' : 'К сожалению, вы проиграли.'; srvText.innerHTML = text; // победа игрока if (player == user) { // удаляем обработчики событий для пользователя compfield.removeEventListener('click', self.shoot); compfield.removeEventListener('contextmenu', self.setEmptyCell); // победа компьютера } else { // если выиграл комп., показываем оставшиеся корабли компьютера // перебираем массив эскадры компьютера for (var i = 0, length = comp.squadron.length; i < length; i++) { // при каждой итерации создаём новый элемент с указанным тегом var div = document.createElement('div'), // присваиваем имя класса в зависимости от направления расположения корабля dir = (comp.squadron[i].kx == 1) ? ' vertical' : '', classname = comp.squadron[i].shipname.slice(0, -1); // собираем в одну строку все классы div.className = 'ship ' + classname + dir; // через атрибут 'style' задаём позиционирование кораблю относительно // его родительского элемента // смещение вычисляется путём умножения координаты первой палубы на // размер клетки игрового поля, этот размер совпадает с размером палубы div.style.cssText = 'left:' + (comp.squadron[i].y0 * comp.shipSide) + 'px; top:' + (comp.squadron[i].x0 * comp.shipSide) + 'px;'; // вставляем созданный элемент корабля в 'document' comp.field.appendChild(div); } } // бой продолжается } else { // следующий выстрел компьютера if (player === comp) { // увеличиваем счётчик попаданий, равный кол-ву уничтоженных палуб comp.tempShip.totalHits++; // отмечаем клетки, где точно не может стоять корабль var points = [ [coords.x - 1, coords.y - 1], [coords.x - 1, coords.y + 1], [coords.x + 1, coords.y - 1], [coords.x + 1, coords.y + 1] ]; self.markEmptyCell(points); // находим максимально количество палуб из оставшихся кораблей var max = self.checkMaxDecks(); if (comp.tempShip.totalHits >= max) { // корабль потоплен // помечаем клетки вокруг корабля, как гарантированно пустые // однопалубный корабль if (comp.tempShip.totalHits == 1) { points = [ // верхняя [comp.tempShip.x0 - 1, comp.tempShip.y0], // нижняя [comp.tempShip.x0 + 1, comp.tempShip.y0], // левая [comp.tempShip.x0, comp.tempShip.y0 - 1], // правая [comp.tempShip.x0, comp.tempShip.y0 + 1], ]; // многопалубный корабль } else { // получаем координаты левой или верхней клетки var x1 = comp.tempShip.x0 - comp.tempShip.kx, y1 = comp.tempShip.y0 - comp.tempShip.ky, // получаем координаты правой или нижней клетки // для этого к координате первой палубы прибавляем количество палуб // умноженное на коэффициент, определяющий направление расположения // палуб корабля // kx == 1 и ky == 0 - вертикально, // kx == 0 и ky == 1 - горизонтально x2 = comp.tempShip.x0 + comp.tempShip.kx * comp.tempShip.totalHits, y2 = comp.tempShip.y0 + comp.tempShip.ky * comp.tempShip.totalHits; points = [ [x1, y1], [x2, y2] ]; } self.markEmptyCell(points); // сбрасываем значения свойств объекта comp.tempShip в исходное состояние; self.resetTempShip(); } else { // формируем координаты выстрелов вокруг попадания self.setShootMatrixAround(); } // производим новый выстрел setTimeout(function() { return self.shoot(); }, 1000); } } break; |
Игра «Морской бой». Обстрел игрового поля вокруг попадания.
Если корабль, в который было попадание, не потоплен, компьютер должен продолжить его обстрел, а именно, обстрелять клетку вокруг попадания. Для этого используется функция setShootMatrixAround
.
Функция setShootMatrixAround
является основной частью ИИ компьютера и способна менять алгоритм формирования координат в зависимости от результатов обстрела «раненого» корабля. Кроме этого, функция способна определить, когда следует прекратить обстрел «раненного» корабля, посчитав его уничтоженным.
В своей работе функция использует информацию хранящуюся в объектах coords
и comp.tempShip
. Эти объекты являются глобальными в пределах области видимости модуля Controller
, поэтому нет необходимости передавать их явно, в качестве аргументов. Все возможные координаты обстрела клеток вокруг попадания записываются в массив shootMatrixAround
.
После первого попадания в корабль, ИИ не может вычислить, как расположен этот корабль — горизонтально или вертикально. Поэтому ему необходимо обстрелять клетку с попаданием со всех сторон: сверху, снизу, слева и справа. Количество точек обстрела может варьироваться от двух до четырёх, в зависимости от координат попадания.

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

В данном случае, точками обозначены возможные координаты выстрелов.
Начнём с того, что разберёмся, как ИИ вычисляет направление расположения палуб, используя координаты первого и второго попадания.
Данные вычисления запускаются, если значения коэффициентов kx
и ky
, хранящихся в объекте comp.tempShip
равны 0.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// если положение корабля не определено, то вычисляем его используя // координаты первого и второго попадания if (comp.tempShip.kx == 0 && comp.tempShip.ky == 0) { // проверяем, есть ли в объекте 'tempShip.firstHit' координаты, если нет // то будем считать, что это первое попадание и запишем // в этот объект координаты первого попадания if (Object.keys(comp.tempShip.firstHit).length === 0) { comp.tempShip.firstHit = coords; } else { // запишем координаты второго попадания в объект 'nextHit' comp.tempShip.nextHit = coords; // вычисляем коэффициенты определяющие положения корабля // разность между соответствующими координатами первого и второго // попадания не может быть больше 1, в противном случае будем // считать, что второе попадание было по другому кораблю comp.tempShip.kx = (Math.abs(comp.tempShip.firstHit.x - comp.tempShip.nextHit.x) == 1) ? 1 : 0; comp.tempShip.ky = (Math.abs(comp.tempShip.firstHit.y - comp.tempShip.nextHit.y) == 1) ? 1 : 0; } } |
Ещё раз напомню значение комбинаций коэффициентов:
comp.tempShip.kx == 1 && comp.tempShip.ky == 0
— корабль расположен вертикально;comp.tempShip.kx == 0 && comp.tempShip.ky == 1
— корабль расположен горизонтально.
Рассмотрим JS-код, отвечающий за вычисление координат выстрелов и использующий при этом значения коэффициентов kx и ky:
1 2 3 4 5 6 7 8 |
// корабль расположен вертикально if (coords.x > 0 && comp.tempShip.ky == 0) comp.shootMatrixAround.push([coords.x - 1, coords.y]); if (coords.x < 9 && comp.tempShip.ky == 0) comp.shootMatrixAround.push([coords.x + 1, coords.y]); // корабль расположен горизонтально if (coords.y > 0 && comp.tempShip.kx == 0) comp.shootMatrixAround.push([coords.x, coords.y - 1]); if (coords.y < 9 && comp.tempShip.kx == 0) comp.shootMatrixAround.push([coords.x, coords.y + 1]); |
В условии коэффициенты kx и ky сравниваются с 0, а не с 1. Это сделано для универсальности вычислений как для первого попадания, так и для последующих, т. к. при первом попадании оба коэффициента равны 0.
Получив координаты обстрела попадания, необходимо проверить их валидность. Координата валидна, если значение двумерного массива игрового поля игрока не равно или 2 (гарантированно пустая клетка), или 3 (промах), или 4 (попадание).
Перебирать массив shootMatrixAround
будем с конца, чтобы после удаления элемента массива не нарушился порядок дальнейшего перебора.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// получив координаты обстрела вокруг попадания, необходимо проверить их валидность // координата валидна, если значение массива не равно или 2 (гарантированно пустая // клетка), или 3 (промах), или 4 (попадание) for (var i = comp.shootMatrixAround.length - 1; i >= 0; i--) { // получаем координаты X и Y возможного выстрела var x = comp.shootMatrixAround[i][0], y = comp.shootMatrixAround[i][1]; // проверяем валидность этих координат и если они не валидны - удаляем их из массива // координат выстрелов вокруг клетки с попаданием if (user.matrix[x][y] !== 0 && user.matrix[x][y] !== 1) { comp.shootMatrixAround.splice(i,1); self.deleteElementMatrix(comp.shootMatrix, coords); } } |
Теперь необходимо проверить, остались после валидации элементы в массиве shootMatrixAround
или нет. Если массив пустой — это значит, что обстреливать корабль далее нет смысла и он считается потопленным.
1 2 3 4 5 6 7 |
if (comp.shootMatrixAround.length == 0) { // считаем корабль потопленным, сбрасываем свойства объекта tempShip // в исходные состояния self.resetTempShip(); } |
Соберём весь 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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
setShootMatrixAround: function() { // если положение корабля не определено, то вычисляем его используя // координаты первого и второго попадания if (comp.tempShip.kx == 0 && comp.tempShip.ky == 0) { // проверяем, есть ли в объекте 'tempShip.firstHit' координаты, если нет // то будем считать, что это первое попадание и запишем // в этот объект координаты первого попадания if (Object.keys(comp.tempShip.firstHit).length === 0) { comp.tempShip.firstHit = coords; } else { // запишем координаты второго попадания в объект 'nextHit' comp.tempShip.nextHit = coords; // вычисляем коэффициенты определяющие положения корабля // разность между соответствующими координатами первого и второго // попадания не может быть больше 1, в противном случае будем // считать, что второе попадание было по другому кораблю comp.tempShip.kx = (Math.abs(comp.tempShip.firstHit.x - comp.tempShip.nextHit.x) == 1) ? 1 : 0; comp.tempShip.ky = (Math.abs(comp.tempShip.firstHit.y - comp.tempShip.nextHit.y) == 1) ? 1 : 0; } } // корабль расположен вертикально if (coords.x > 0 && comp.tempShip.ky == 0) comp.shootMatrixAround.push([coords.x - 1, coords.y]); if (coords.x < 9 && comp.tempShip.ky == 0) comp.shootMatrixAround.push([coords.x + 1, coords.y]); // корабль расположен горизонтально if (coords.y > 0 && comp.tempShip.kx == 0) comp.shootMatrixAround.push([coords.x, coords.y - 1]); if (coords.y < 9 && comp.tempShip.kx == 0) comp.shootMatrixAround.push([coords.x, coords.y + 1]); // получив координаты обстрела попадания, необходимо проверить их валидность // координата валидна, если значение массива не равно или 2 (гарантированно пустая // клетка), или 3 (промах), или 4 (попадание) for (var i = comp.shootMatrixAround.length - 1; i >= 0; i--) { // получаем координаты X и Y возможного выстрела var x = comp.shootMatrixAround[i][0], y = comp.shootMatrixAround[i][1]; // проверяем валидность этих координат и если они не валидны - удаляем их из массива // координат выстрелов вокруг клетки с попаданием if (user.matrix[x][y] !== 0 && user.matrix[x][y] !== 1) { comp.shootMatrixAround.splice(i,1); self.deleteElementMatrix(comp.shootMatrix, coords); // if (comp.shootMatrixAI.length != 0) { // self.deleteElementMatrix(comp.shootMatrixAI, coords); // } } } if (comp.shootMatrixAround.length == 0) { // считаем корабль потопленным, сбрасываем свойства объекта tempShip // в исходные состояния self.resetTempShip(); } return; } |
Итак, на этом я на этом заканчиваю рассмотрение алгоритма выстрела компьютера.
Заключение.
В пяти статьях, посвящённых игре «Морской бой», я постарался подробно и доступно рассказать, как написать эту популярную игру на JavaScript. Я не претендую на то, что написанный мною код идеален и полностью оптимизирован. Возможно вы сможете предложить свои варианты каких-либо участков кода, другие пути решения. С удовольствием обсудим их в комментариях.
Можно сделать ряд существенных изменений и дополнений к существующему коду, которые позволят сделать игру «Морской бой» более интересной и сложной:
-
Добавить выбор сложности игры «Морской бой», изменив ИИ компьютера в части касающейся, как расстановки кораблей, так и тактики обстрела. Например, многопалубные корабли можно размещать не просто рандомно, а вдоль границ игрового поля или ближе к какой-то одной границе, что освободит больше места для размещения однопалубных кораблей. Такая расстановка значительно уменьшит вероятность попадания в однопалубник и при этом повысит шансы компьютера на победу.
-
Набор кораблей, которые игроку необходимо перетащить на игровое поле, создавать «налету», по событию:
123getElement('type_placement').addEventListener('click', function(e) { ... } -
Координаты выстрелов, попаданий, промахов и т. д. передавать не в объекте
obj
, а в числовом массиве, что, возможно, улучшит оптимизацию кода.
Если у вас возникли вопросы или что-то не совсем понятно написано — пишите в комментарии, постараюсь объяснить более доступно и, при необходимости, перепишу или дополню статьи.
Комментарии
-
Комментарии должны содержать вопросы и дополнения по статье, ответы на вопросы других пользователей.
Комментарии содержащие обсуждение политики, будут безжалостно удаляться. -
Для удобства чтения Вашего кода, не забываейте его форматировать. Вы его можете подсветить код с помощью тега
<pre>
:
—<pre class="lang:xhtml">
- HTML;
—<pre class="lang:css">
- CSS;
—<pre class="lang:javascript">
- JavaScript. - Если что-то не понятно в статье, постарайтесь указать более конкретно, что именно не понятно.
-
-
Спасибо, поправил.
-
-
В это игре есть один существенный баг. Если в расположить коробли близко друг к други и попытаться повернуть один корабль так, чтобы он попал в зону нахождения второго корабля, то корабль изчезает. Можно начать игру и затащить на изи. Но корабли перед использование этой уязвимости должны ВСЕ находиться на поле.
-
Ошибку исправил. Это не баг логики, а косяк при работе с редактором. В скрипте примера, в функции Instance.prototype.rotationShip, случайно были поменяны местами несколько строк кода.
Код приведённый в статье в разделе «Поворот корабля на 90°» — верный, там править ничего не надо.
Спасибо за указанную ошибку.
-
-
Статьи пока не читал, добавил в закладки, почитаю на свежую голову.
Поиграл немного, нашел много разных багов:
1. Корабли можно перетаскивать даже во время боя.
2. Хотелось бы, чтобы убитые корабли автоматически окружались промахами по периметру.
3. Отсюда вытекает следующий пункт: никогда не знаешь точно убит ли корабль полностью. Соперник добивает корабль и следующий выстрел всегда делает в следующую клетку, как если бы там была еще одна палуба.-
По первому пункту, действительно, баг присутствует — разберусь. А по п.п. 2 и 3 — так и задумывалось.
-
Так это же идёт вразрез с правилами игры. Когда корабль убит полностью сопернику сообщают об этом, и он не должен делать бессмысленный выстрел рядом с полностью убитым кораблём
-
-
-
Еще бывает так, что в ячейке стоит промах, а на самом деле там палуба корабля
-
Вы не правы на счет того, что компьютер с рандомным вариантом стрельбы без тактики не соперник для человека, поиграйте в мою версию игры, там 5 уровней сложности компьютера и на всех пяти уровнях компьютер стреляет рандомно, но я вас уверяю, победить там задача не из легких. Вот скриншот ( https://drive.google.com/file/d/1q12eQyAXhttDgoJa5LWuDx6sSmYW00jY/view?usp=sharing ), счет в его пользу 25:71.
Плюс, по Вашей игре есть замечания, убитый корабль должен как-то обозначаться, и компьютер не должен обстреливать уже потопленный корабль, это противоречит правилам игры.
Добавьте больше интерактива в игру, звуки, кнопки, возможность начать игру заново после раунда.
Вот ссылка на мою версию игры (не работает на Internet Explorer и Microsoft Eagle):
https://eugene-kul.github.io/seaBattle/index.html-
Процитируйте, плиз, где написано «компьютер с рандомным вариантом стрельбы без тактики не соперник для человека». А вообще, по тактике игры в Морской бой есть множество статей с математическими вкладками по теории вероятности, картинками и т.д., где доказывается преимущество тактики перед рандомом.
Уничтоженный корабль видно по попаданиям, а повторных обстрелов быть не может, т.к. из массива возможных выстрелов удаляются координаты, по которым стреляли. Хотя может есть какие-то не выявленные баги, на 100% не буду утверждать.
В игру можно добавить много чего, но тогда статья удлинится в несколько раз и, вряд ли кто-то будет читать её до конца. Я не ставлю перед собой целью распространить (навязать) готовый плагин, скрипт, программу и т.д. Смысл этого блога показать направления и пути решения часто возникающих задач. А окончательное «вылизование» — это на усмотрение читателей. Возможно мои решения не всегда оптимальны и реализованы не лучшим способом, для этого есть блок комментариев — давайте это обсуждать.
А так ваш коммент больше похож на пиар вашей программы.-
Цитата из пункта Введение:
«Для того, чтобы компьютер мог, как минимум, играть на равных с человеком, он (компьютер) должен иметь некую тактику игры. Причём, данная тактика должна быть оптимальна применительно к игре «Морской бой» и обеспечивать высокие шансы на победу над человеком.
Исходя из этого, сразу же откажемся от рандомного обстрела поля игрока в начальной стадии игры. В рандомном формировании координат выстрела компьютера в полной мере присутствует элемент случайности, а нам, для победы компьютера над человеком, нужно свести этот элемент к минимуму.»
Поэтому я Вам показал свой вариант игры, где реализована тактика игры компьютера на уровне человека с рандомным обстрелом поля. Я свое приложение не пиарю, это первое что я сделал на javaScript, и я знаю, что там в коде миллион ошибок. Я только учусь.
По поводу обстрела убитых кораблей компьютером, ссылка на запись: ( https://drive.google.com/file/d/1UPLGqovxXbvAswLvOuAjMnqPBlD6A_Ei/view?usp=sharing ). И я не вижу никаких выделений уничтоженных кораблей и я не могу понять, убил я корабль или нет, и начинаю бессмысленно простреливать вокруг потопленного корабля. Чисто мое мнение, если ты начал что-то делать, то это что-то должно быть похоже на законченную вещь. Я не критикую Вашу игру, Вы молодец, что все это делаете, я просто опровергаю слова сказанные в статье, вот и все, не стоит на меня обижаться)-
Обид никаких нет, для того комменты и прикручены, чтобы можно было обсуждать, указывать на ошибки и предлагать свои варианты реализации.
По поводу обстрела корабля вокруг — именно так я планировал, так сложнее код в выстреле компьютера. Кстати, компьютер тоже обстреливает все возможные клетки рядом с кораблём, так что игрок и комп в этом случае находятся в равном положении.
А с косяком буду разбираться, всё-равно хотел переписать на ES6
-
-
-
-
Только учусь фронтэнд разработке, в 28 лет пришло понимание, что занимаюсь не тем! Сейчас прохожу курсы, верстку осилил, приступил к изучению java-scripta. Отличный у Вас сайт!!! Приятно здесь находиться, очень много полезного! Создать игру такую как морской бой для меня сейчас просто фантастическая задача но статейки читаю!) Исключительно с точки зрения юзера по нраву больше игра Вальдемара, оформление такое тетрадное как в детстве. Я все детство играл в эту игру в таком варианте, что не нужно было говорить — тебя ранили или убили. Просто попал! Ваш вариант очень близок! Конечно не хватает, аудио. В игре Евгения большой минус как по мне это невозможность расставить корабли самостоятельно! А вообще вы виртуозы, это очень круто!!!! Надеюсь доросту до такого…
-
Было бы неплохо если бы не только код в архиве был переписан по новым стандартам, но и само обучение)Нет в планах пункта улучшить сам текст обучалки?)
-
Статьи с описанием создания игры в стандарте ES6+ практически готовы. Осталось дописать последнюю — «Выстрел компьютера». Если есть острая необходимость, то могу выложить первые четыре статьи и новый архив. В процессе переписывания статей в код вносились изменения связанные с оптимизацией.
-
Спасибо за ответ и за труды,очень жду обновление этой интереснейшей статьи.Если не затруднит,то можете выкладывать первые части.Я думаю,что это разумно)
Пока люди проработают первые пару частей у Вас будет время дописать оставшуюся.
-
-
-
Еще раз доброго времени суток)
Все ждемс обновленную игру детства.Как там успехи?Скоро ли увидим обновление?)
В статье присутствует ошибка синтаксиса в коде, в функции getCoordinatesShot (восьмая строка в вашем блоке кода). Поправьте