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

Логика игры
У нас будет очень простой «Морской бой» — с заранее предустановленными кораблями для каждой стороны, чтобы не усложнять алгоритм. В следующих частях добавим генерацию кораблей в случайных местах, а пока так.
Вот вкратце, что мы делаем сегодня:
- Готовим игровое поле: HTML и CSS — создадим разметку, сделаем сетку и добавим счётчики.
 - Рисуем корабли и работаем с двумерными массивами.
 - Оживляем поле — создаём класс для управления игрой в целом и разбираем, как отображать корабли, попадания и промахи.
 - Добавляем интерактивность — обрабатываем клики и работаем с CSS.
 - Реализуем логику игры — используем счётчики, считаем подбитые корабли и добавляем проверку условий победы и поражения.
 - Разбираем, как стреляет компьютер.
 
Полезный блок со скидкой
Если вам интересно разбираться со смартфонами, компьютерами и прочими гаджетами и вы хотите научиться создавать софт под них с нуля или тестировать то, что сделали другие, — держите промокод Практикума на любой платный курс: 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)— это специальный метод, который автоматически вызывается при создании нового объекта класса Fieldthis.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;
}
Разберём логику выстрела по шагам:
- Определяем цель — 
targetэто DOM-элемент клетки, в которую кликнул игрок. - Проверяем попадание — смотрим, есть ли у клетки класс 
sheep(корабль :-) - Обрабатываем попадание — добавляем класс 
broken(подбито) и увеличиваем счётчик. - Обрабатываем промах — добавляем класс 
missed(мимо). - Проверяем победу — если подбито 20 кораблей, игрок побеждает.
 - Ответный выстрел — компьютер стреляет в ответ.
 - Обновляем интерфейс — показываем актуальные счётчики.
 
Ура, теперь можно снова заниматься стилями в 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();
						