Сегодня мы вместе напишем классический «Морской бой» на чистом 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();