Создаём простой «Морской бой» на JavaScript: пошаговый разбор для начинающих

Будет чем занять детей на досуге

Создаём простой «Морской бой» на JavaScript: пошаговый разбор для начинающих

Сегодня мы вместе напишем классический «Морской бой» на чистом JavaScript. Это отличный проект для начинающих: здесь есть и работа с DOM, и логика игры, и даже простейший искусственный интеллект для компьютера.

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

Логика игры

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

Вот вкратце, что мы делаем сегодня:

  1. Готовим игровое поле: HTML и CSS — создадим разметку, сделаем сетку и добавим счётчики.
  2. Рисуем корабли и работаем с двумерными массивами.
  3. Оживляем поле — создаём класс для управления игрой в целом и разбираем, как отображать корабли, попадания и промахи.
  4. Добавляем интерактивность — обрабатываем клики и работаем с CSS.
  5. Реализуем логику игры — используем счётчики, считаем подбитые корабли и добавляем проверку условий победы и поражения.
  6. Разбираем, как стреляет компьютер.

Полезный блок со скидкой

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

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

Подготовка игрового поля: HTML и CSS

Сразу создадим HTML-файл и добавим в него сразу всё, что нам нужно для игры:

<!DOCTYPE html>
<html lang="ru">  <!-- Указываем язык страницы -->
<head>
    <meta charset="UTF-8">  <!-- Кодировка для поддержки русских букв -->
    <title>Морской бой</title>  <!-- Заголовок вкладки браузера -->
    <link rel="stylesheet" href="style.css">  <!-- Подключаем файл со стилями -->
</head>
<body>
    <div class="block">  <!-- Блок для поля компьютера -->
        <h2>Твоих попаданий:  <!-- Заголовок счётчика -->
            <span id="user-hint">0</span>/20</h2>  <!-- Счётчик попаданий игрока -->
        <div id="field-comp" class="field"></div>  <!-- Контейнер для поля компьютера -->
    </div>
    <div class="block">  <!-- Блок для поля игрока -->
        <h2>Осталось кораблей:  <!-- Заголовок счётчика -->
            <span id="comp-hint">20</span>/20</h2>  <!-- Счётчик оставшихся кораблей -->
        <div id="field-user" class="field"></div>  <!-- Контейнер для поля игрока -->
    </div>
    <script src="script.js"></script>  <!-- Подключаем JavaScript-код -->
</body>
</html>

Мы сразу подключили файл со скриптом, хотя у нас его ещё нет — это некритично, дальше мы его добавим, а пока пусть так. Теперь займёмся стилями — создадим файл style.css и заполним его базовыми настройками:

/* Сбрасываем стандартные отступы и включаем границы в общий размер */
html, body {
    margin: 0; 
    padding: 0;
    box-sizing: border-box;
}

/* Задаём высоту и фон всей страницы */
html, body {
    height: 100%;
}

/* Основные стили для тела страницы */
body {
    padding: 64px;  /* Отступы от краёв окна */
    height: 100%;
    background: #fafafa;  /* Светло-серый фон */
    display: flex;  /* Включаем flex-раскладку */
    flex-wrap: wrap;  /* Разрешаем перенос блоков */
    justify-content: center;  /* Выравниваем по центру */
}

/* Стили для блоков с игровыми полями */
.block {
    padding: 20px;  /* Внутренние отступы */
}

/* Стили для игрового поля */
.field {
    width: 242px;  /* Ширина поля (10 клеток × 24px + границы) */
    margin: 24px;  /* Внешние отступы */
    border: 1px solid #bbdefb;  /* Голубая рамка */
}

/* Очистка float для контейнера поля */
.field::before, .field::after {
    content: '';
    display: table;
    clear: both;
}

/* Стили для каждой клетки поля */
.field > div {
    display: inline-block;  /* Блочно-строчное отображение */
    float: left;  /* Обтекание слева */
    height: 24px;  /* Высота клетки */
    width: 24px;  /* Ширина клетки */
    border: 1px solid #bbdefb;  /* Граница клетки */
    background-color: #e3f2fd;  /* Светло-голубой фон */
}

Что у нас получилось:

  • Два игровых поля — для компьютера и игрока (которые пока не видны :) 
  • Счётчики — отслеживают прогресс игры.
  • Сетка 10×10 — каждая клетка имеет размер 24×24 пикселя.
  • Адаптивная вёрстка — поля центрируются и переносятся на мобильных устройствах.

Правда, выглядит всё пока не очень, но мы это пофиксим дальше.

Обратите внимание на свойство box-sizing: border-box — благодаря ему границы клеток не увеличивают их размер, а включаются в указанные 24 пикселя.

Рисуем корабли: работа с двумерными массивами

Теперь, когда у нас есть красивые игровые поля, нужно расставить на них корабли. В классическом «Морском бое» у каждого игрока должно быть:

  • 1 корабль — 4 клетки;
  • 2 корабля — 3 клетки ; 
  • 3 корабля — 2 клетки;
  • 4 корабля — 1 клетка.

У нас всё это тоже будет, но в очень (пока) упрощённом виде — мы заранее заполним поля у игрока и компьютера в двумерном массиве, чтобы не усложнять алгоритм.

Для этого расчехляем JavaScript и создаём файл script.js — работать будем пока с ним:

// Создаём игровое поле 10×10 для пользователя
const userField = [
    ['.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],  // Строка 1
    ['.', '.', '.', '+', '+', '+', '.', '.', '+', '+'],  // Строка 2
    ['.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],  // Строка 3
    ['.', '.', '.', '.', '.', '+', '.', '.', '.', '.'],  // Строка 4
    ['.', '.', '.', '.', '.', '.', '.', '+', '+', '+'],  // Строка 5
    ['.', '.', '.', '.', '+', '.', '.', '.', '.', '.'],  // Строка 6
    ['+', '.', '.', '.', '+', '.', '+', '+', '.', '.'],  // Строка 7
    ['.', '.', '.', '.', '+', '.', '.', '.', '.', '.'],  // Строка 8
    ['.', '+', '+', '.', '+', '.', '+', '.', '+', '.'],  // Строка 9
    ['.', '.', '.', '.', '.', '.', '.', '.', '.', '.']   // Строка 10
];

Разбираем обозначения:

‘.’ — пустая клетка (вода)

‘+’ — клетка с кораблём

А вот поле компьютера:

const compField = [
    ['.', '.', '+', '.', '+', '.', '+', '.', '.', '.'],  // Строка 1
    ['.', '.', '+', '.', '+', '.', '+', '.', '+', '.'],  // Строка 2
    ['.', '.', '+', '.', '+', '.', '+', '.', '.', '.'],  // Строка 3
    ['+', '.', '.', '.', '+', '.', '.', '.', '.', '.'],  // Строка 4
    ['.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],  // Строка 5
    ['.', '.', '.', '.', '.', '.', '.', '.', '+', '.'],  // Строка 6
    ['.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],  // Строка 7
    ['.', '+', '.', '.', '.', '.', '.', '.', '+', '.'],  // Строка 8
    ['.', '+', '.', '.', '.', '.', '.', '.', '.', '.'],  // Строка 9
    ['.', '.', '.', '+', '+', '.', '+', '+', '.', '.']   // Строка 10
];

😁 Мы знаем, что корабль по-английски — это ship. Но так как у нас очень простой проект, будем использовать sheep — звучит почти так же и добавляет нужный градус странности. 

Визуально ничего у нас не изменилось, поэтому пока без скриншотов тут.

Оживляем поле: класс Field и его конструктор

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

Продолжаем работать в файле со скриптом и добавляем класс в самое начало:

// Создаём класс для управления игровым полем
class Field {
    constructor(field, role) {  // Конструктор вызывается при создании нового поля
        this.field = field;     // Сохраняем массив с кораблями
        this.role = role;       // Сохраняем роль ('user' или 'comp')
        
        // Создаём счётчики для отслеживания игры
        var count = 0,          // Счётчик для логики ответных выстрелов
            userCount = 0,      // Количество попаданий игрока
            compCount = 20;     // Количество оставшихся кораблей компьютера
        
        // Находим элементы для отображения счётчиков
        var userHint = document.getElementById('user-hint'),
            compHint = document.getElementById('comp-hint');

        // Устанавливаем начальные значения счётчиков
        userHint.innerText = userCount;
        compHint.innerText = compCount;
    }
}

Разберём конструктор подробнее:

  • constructor(field, role) — это специальный метод, который автоматически вызывается при создании нового объекта класса Field
  • this.field — сохраняет переданный массив с расстановкой кораблей
  • this.role — определяет, чьё это поле: игрока (‘user’) или компьютера (‘comp’)

А вот как мы создаём игровые поля на основе нашего класса:

// Создаём поле для пользователя
var gameu = new Field(userField, 'user');  // new — ключевое слово для создания объекта
gameu.render();  // Вызываем метод render() для отображения поля

// Создаём поле для компьютера  
var gamec = new Field(compField, 'comp');  // Второй параметр — роль поля
gamec.render();  // Отображаем поле компьютера

Теперь добавим в наш класс метод render(), который будет рисовать игровое поле на странице:

render() {
    // Находим контейнер для этого поля на странице
    var fieldBlock = document.getElementById('field-' + this.role);
    
    // Проходим по всем строкам игрового поля
    for (let i = 0; i < this.field.length; i++) {
        // Проходим по всем клеткам в текущей строке
        for (let j = 0; j < this.field[i].length; j++) {
            // Создаём div-элемент для клетки
            var block = document.createElement('div');
            
            // Если в массиве стоит '+', добавляем класс корабля
            if (this.field[i][j] === '+') {
                block.classList.add('sheep');  // sheep вместо ship (особенность кода)
            };
            
            // Только для поля компьютера добавляем обработчик кликов
            if (this.role === 'comp') {
                block.addEventListener('click', (event) => this.fire(event.target));
            };
            
            // Добавляем клетку в контейнер поля
            fieldBlock.appendChild(block);
        }
    }
}

Обратим ваше внимание на стрелочную функцию (event) => this.fire(event.target). Она сохраняет контекст this, чтобы мы могли вызывать методы класса.

В итоге класс Field инкапсулирует всю логику работы с полем. Это хороший пример объектно-ориентированного программирования — мы создаём независимые объекты, которые сами управляют своим состоянием и поведением. В этом и состоит мощь ООП.

Снова без скриншотов — мы пока занимаемся логикой, поэтому визуально ничего на странице не меняется.

Добавляем интерактивность: обработка кликов и логика выстрелов

Теперь настало время сделать нашу игру интерактивной Мы добавим возможность стрелять по кораблям компьютера и обрабатывать попадания. 

Сначала разберём метод fire() — основу боевой системы. Добавим в наш класс Field метод, который будет обрабатывать каждый выстрел игрока:

// Создаём метод для обработки выстрелов
this.fire = (target) => {    // target — клетка, в которую стреляем
    // Считаем текущее количество подбитых кораблей компьютера
    userCount = document.querySelectorAll('field-comp .broken').length;
    
    // Проверяем, есть ли в целевой клетке корабль
    if (target.classList.contains('sheep')) {
        target.classList.add('broken');    // Добавляем класс «подбито»
        userCount += 1;                    // Увеличиваем счётчик попаданий

    } else {
        // Если промах — помечаем клетку как «мимо»
        target.classList.add('missed');
        count += 1;  // Увеличиваем счётчик для логики ответного выстрела
    }
    
    // Проверяем условие победы (все 20 кораблей подбиты)
    if(userCount == 20) {
        alert('You WIN!!!!')  // Показываем сообщение о победе
    }
    
    // Компьютер делает ответный выстрел
    this.backFire();
    
    // Обновляем счётчики на экране
    userHint.innerText = userCount;
    compHint.innerText = compCount;
}

Разберём логику выстрела по шагам:

  1. Определяем цель — target это DOM-элемент клетки, в которую кликнул игрок.
  2. Проверяем попадание — смотрим, есть ли у клетки класс sheep (корабль :-)
  3. Обрабатываем попадание — добавляем класс broken (подбито) и увеличиваем счётчик.
  4. Обрабатываем промах — добавляем класс missed (мимо).
  5. Проверяем победу — если подбито 20 кораблей, игрок побеждает.
  6. Ответный выстрел — компьютер стреляет в ответ.
  7. Обновляем интерфейс — показываем актуальные счётчики.

Ура, теперь можно снова заниматься стилями в CSS:

/* Стили для поля компьютера — корабли скрыты */
.field-comp .sheep {
    background-color: #e3f2fd;  /* Невидимые корабли — такой же цвет, как вода */
}

/* Подбитые корабли на обоих полях */
.field .broken,
.field-comp .broken {
    background-color: #ff6d00;  /* Ярко-оранжевый для попаданий */
}

/* Промахи */
.field .missed {
    background-color: #bbdefb;  /* Голубой для промахов */
}

Обратите внимание на хитрость в CSS: для поля компьютера (field-comp) корабли имеют такой же цвет фона, как и вода. Но когда мы попадаем в корабль, срабатывает правило .broken, которое переопределяет цвет на оранжевый.

Важный момент: мы используем classList.contains() для проверки наличия корабля. Это потому, что после рендеринга мы «забываем» исходный массив и работаем только с DOM-элементами.

Что видит игрок:

  • При попадании — клетка становится оранжевой.
  • При промахе — клетка становится голубой.  
  • Счётчик обновляется. 
  • Победа определяется, когда все корабли подбиты.

Реализуем логику игры: счётчики и проверка победы

Теперь давайте разберёмся, как игра отслеживает прогресс и определяет победителя. В конструкторе класса Field создадим  несколько переменных-счётчиков:

var count = 0,          // Счётчик для логики ответных выстрелов компьютера

userCount = 0,      // Количество попаданий игрока по компьютеру

compCount = 20;     // Количество оставшихся кораблей у игрока

var userHint = document.getElementById(‘user-hint’),    // Элемент для отображения попаданий игрока

compHint = document.getElementById(‘comp-hint’);    // Элемент для отображения оставшихся кораблей

Как работают счётчики:

  • userCount — увеличивается на 1 при каждом попадании игрока в корабль компьютера.
  • compCount — уменьшается при каждом попадании компьютера в корабль игрока.  
  • count — специальный счётчик для управления частотой ответных выстрелов компьютера.

После каждого выстрела мы обновляем текстовые подсказки:

userHint.innerText = userCount;    // Показываем текущее количество попаданий

compHint.innerText = compCount;    // Показываем оставшиеся корабли игрока

В методе fire() после каждого попадания проверяем, не достиг ли игрок победы:

if(userCount == 20) {
    alert('Вы выиграли!')  // Игрок подбил все 20 кораблей компьютера!
}

А вот проверка поражения происходит в методе backFire():

if (sheeps.length === 0) {
    alert('Вы проиграли :-(')  // У игрока не осталось кораблей
}

Для подсчёта кораблей алгоритм использует querySelectorAll():

// Считаем все корабли игрока, которые ещё не подбиты

var sheeps = document.querySelectorAll(‘field-user .sheep’);

Метод querySelectorAll — это мощный инструмент:

  • field-user .sheep — находит все элементы с классом sheep внутри блока field-user
  • .sheep — неподбитые корабли (без класса broken).
  • .sheep.broken — подбитые корабли (оба класса одновременно).

После каждого хода компьютер пересчитывает, что поменялось на поле:

// После ответного выстрела компьютера
compCount = sheeps.length - document.querySelectorAll('field-user .broken').length;

Мы не храним отдельную переменную для кораблей компьютера. Вместо этого каждый раз пересчитываем document.querySelectorAll(‘field-comp .broken’).length — это менее эффективно, но проще для понимания.

Как компьютер отвечает на выстрелы

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

Вот код, который заставляет компьютер отвечать на наши выстрелы — добавляем его всё в тот же класс:

this.backFire = () => {
    // Находим все клетки поля игрока и все его корабли
    var targets = document.querySelectorAll('field-user div');  // Все клетки поля игрока
    var sheeps = document.querySelectorAll('field-user .sheep');  // Все корабли игрока
    
    // Компьютер стреляет только при определённых условиях
    if (count == 1 && sheeps.length > 0) {
        // Выбираем случайную клетку для выстрела
        let firedItemIndex = Math.floor(Math.random()  targets.length);
        
        // Вызываем метод fire для выбранной клетки
        this.fire(targets[firedItemIndex]);
        
        // Пересчитываем оставшиеся корабли игрока
        compCount = sheeps.length - document.querySelectorAll('field-user .broken').length;
        count = 0;  // Сбрасываем счётчик
    }
    
    // Проверяем условие поражения — у игрока не осталось кораблей
    if (sheeps.length === 0) {
        alert('You LOST')  // Игрок проиграл
    }
}

В нашем коде компьютер использует тот же метод fire(), что и игрок. Это пример полиморфизма в ООП — один метод работает в разных контекстах.

Запускаем игру

Для запуска у нас уже всё готово, поэтому просто добавляем код рендера в конец JS-файла:

var gameu = new Field(userField, 'user')
gameu.render();

var gamec = new Field(compField, 'comp')
gamec.render();

Бонус для читателей

Если вам интересно погрузиться в мир ИТ и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, поможет с льготной ипотекой и даст безлимит на маркетплейсах. Ладно, окей, это просто скидка, без остального, но хорошая. 

Вам слово

Приходите к нам в соцсети поделиться своим мнением об игре и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте — шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <title>Морской бой</title>
    <link rel="stylesheet" href="https://public.codepenassets.com/css/normalize-5.0.0.min.css">
<link rel="stylesheet" href="./style.css">

  </head>
    
  <body>
  <div class="block">
        <h2>Твоих попаданий:
            <span id="user-hint"></span>/20</h2>
        <div id="field-comp" class="field"></div>
    </div>
    <div class="block">
        <h2>Осталось кораблей:
            <span id="comp-hint">20</span>/20</h2>
        <div id="field-user" class="field"></div>

    </div>
    <script  src="./script.js"></script>

  </body>
  
</html>

html, body, *{
  margin: 0; 
  padding: 0;
  box-sizing: border-box
}
html, body {
  height: 100%;

}
body {
  padding: 64px;
  height: 100%;
  background: #fafafa;
  display: flex;
  flex-wrap:wrap;
  justify-content: center;
}
.block {
  padding: 20px;
}
.field {
  width: 242px;
  margin: 24px;
  border: 1px solid #bbdefb;
}
.field::before, .field::after {
  content: '';
  display: table;
  clear: both;
}
.field > div {
  display: inline-block;
  float: left;
  height: 24px;
  width: 24px;
  border: 1px solid #bbdefb; 
  background-color: #e3f2fd;
}
#field-comp .sheep {
  background-color: #e3f2fd;
}

.field .sheep {
  background-color: #81c784;
}
.field .broken,
#field-comp .broken {
  background-color: #ff6d00;
}
.field .missed {
  background-color:  #bbdefb;
}

class Field {
    constructor(field, role) {
        this.field = field;
        this.role = role;

        var count = 0,
            userCount = 0,
            compCount = 20;
        var userHint = document.getElementById('user-hint'),
            compHint = document.getElementById('comp-hint');

        userHint.innerText = userCount;
        compHint.innerText = compCount;

        this.fire = (target) => {
            userCount = document.querySelectorAll('#field-comp .broken').length;
            if (target.classList.contains('sheep')) {
                target.classList.add('broken');
                userCount += 1;

            } else {
                target.classList.add('missed');
                count += 1;
            }
            if(userCount == 20) {
                alert('You WIN!!!!')
            }
            this.backFire();
            userHint.innerText = userCount;
            compHint.innerText = compCount;
        }

        this.backFire = () => {
            // функция устанавливает значение на поле юзера
            var targets = document.querySelectorAll('#field-user div');
            var sheeps = document.querySelectorAll('#field-user .sheep');
            if (count == 1 && sheeps.length > 0) {
                let firedItemIndex = Math.floor(Math.random() * targets.length);
                this.fire(targets[firedItemIndex]);
                compCount = sheeps.length - document.querySelectorAll('#field-user .broken').length;
                count = 0;
            }
            if (sheeps.length === 0) {
                alert('You LOST')
            }
        }
    }

    render() {
        var fieldBlock = document.getElementById('field-' + this.role)
        for (let i = 0; i < this.field.length; i++) {
            for (let j = 0; j < this.field[i].length; j++) {
                var block = document.createElement('div');
                if (this.field[i][j] === '+') {
                    block.classList.add('sheep');
                };
                if (this.role === 'comp') {
                    block.addEventListener('click', (event) => this.fire(event.target));
                };
                fieldBlock.appendChild(block)
            }
        }
    }
}

const userField = [
    ['.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
    ['.', '.', '.', '+', '+', '+', '.', '.', '+', '+'],
    ['.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
    ['.', '.', '.', '.', '.', '+', '.', '.', '.', '.'],
    ['.', '.', '.', '.', '.', '.', '.', '+', '+', '+'],
    ['.', '.', '.', '.', '+', '.', '.', '.', '.', '.'],
    ['+', '.', '.', '.', '+', '.', '+', '+', '.', '.'],
    ['.', '.', '.', '.', '+', '.', '.', '.', '.', '.'],
    ['.', '+', '+', '.', '+', '.', '+', '.', '+', '.'],
    ['.', '.', '.', '.', '.', '.', '.', '.', '.', '.']
]
const compField = [
    ['.', '.', '+', '.', '+', '.', '+', '.', '.', '.'],
    ['.', '.', '+', '.', '+', '.', '+', '.', '+', '.'],
    ['.', '.', '+', '.', '+', '.', '+', '.', '.', '.'],
    ['+', '.', '.', '.', '+', '.', '.', '.', '.', '.'],
    ['.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
    ['.', '.', '.', '.', '.', '.', '.', '.', '+', '.'],
    ['.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
    ['.', '+', '.', '.', '.', '.', '.', '.', '+', '.'],
    ['.', '+', '.', '.', '.', '.', '.', '.', '.', '.'],
    ['.', '.', '.', '+', '+', '.', '+', '+', '.', '.']
]

var gameu = new Field(userField, 'user')
gameu.render();

var gamec = new Field(compField, 'comp')
gamec.render();

Обложка:

Алексей Сухов

Корректор:

Александр Зубов

Вёрстка:

Егор Степанов

Соцсети:

Юлия Зубарева

Вам может быть интересно
easy
[anycomment]
Exit mobile version