Игра Морской бой на JavaScript. Рандомная расстановка кораблей.

Вступление.

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

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

Как говорится — лучше один раз увидеть. На приведённом ниже скриншоте прекрасно видно, что представляет из себя игровое поле, сколько типов кораблей, в зависимости от количества палуб будет в эскадре, как выглядит промах, попадание и отметка, где корабля точно быть не может.

Игра Морской бой. Пример игрового поля во время игры.

Надеюсь всё всем понятно. Ну а теперь составим техническое задание, чтобы ничего не забыть и не пропустить.

Это очень важно.
При создании игры использовался JavaScript версии ES6/ES2015 и более поздних версий. Для понимания изложенного материала, необходимо предварительно изучить изменения и нововведения в версиях ES2015+.

Техническое задание на создание игры «Морской бой»

  • 1

    Количество и типы кораблей, их расположение.

    Как и в реальной игре, эскадра будет состоять из:
    — одного четырёхпалубного;
    — двух трёхпалубных;
    — трёх двухпалубных;
    — четырёх однопалубных кораблей.
    Корабли могут располагаться вертикально и горизонтально, но при этом между кораблями должна быть хотя бы одна пустая клетка, в том числе и по диагонали. Корабли не могут иметь Г-образную форму.

  • 2

    Расстановка кораблей и редактирование их положения.

    Корабли игрока могут расставляться по случайному закону или самим игроком, путём перетаскивания их на игровое поле. И в первом, и во втором случае можно редактировать направление положения палуб (вертикальное или горизонтальное), путём клика правой кнопкой мышки по первой палубе и расположение, путём перетаскивания. Первая палуба — при горизонтальном расположении корабля — самая левая, при вертикальном — самая верхняя.
    При перетаскивании или повороте корабля скрипт постоянно отслеживает корректность координат корабля. Если корабль будет соприкасаться с соседним или выходить за пределы игрового поля, то рамка корабля окрасится в красный цвет, а корабль, после отпускания кнопки мышки, вернётся в исходное положение. В противном случае, рамка корабля будет зелёная.
    Нет необходимости пытаться установить корабль точно по границам клеток — программа сама откорректирует его положение, установив в ближайшие валидные координаты.
    В момент начала игры, редактирование положения кораблей отключается.

  • 3

    Ведение морского боя

    Если сражение первое, рандомно определяем, кто стреляет первым. Далее, первым стреляет победитель.
    Координаты выстрела передаются путём клика по выбранной клетке — ввода координат с клавиатуры не будет.
    Попадание отмечается красным крестиком, промах — точкой, клетку, где корабля точно быть не может, отмечаем / снимаем отметку кликом правой кнопкой мышки.
    Сообщений «ранил», «убил / потопил» не будет — самостоятельно определяем тип корабля.
    Под игровыми полями выводятся сообщения о том, чей выстрел и результат выстрела.

  • 4

    Искусственный интеллект противника.

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

    На этих требования к ИИ мы пока и остановимся, хотя можно рассмотреть варианты размещения кораблей компьютером, при которых сильно уменьшается вероятность попадания в однопалубные корабли.

  • 5

    Окончание морского боя.

    Выводится сообщение о победителе. Если выиграл компьютер, то отображаются его не потопленные корабли.

HTML-вёрстка для игры «Морской бой».

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

  1. Основной родительский элемент, background которого похож на лист тетрадки в клетку.
  2. Два игровых поля — игрока, где мы будем расставлять свои корабли и контролировать выстрелы компьютера, и компьютера, где мы будут отмечаться наши попадания и промахи. Им задаётся background в виде рамки размером 10х10 клеток с буквенным обозначением строк и цифровым обозначением колонок.
  3. Контейнер с инструкцией по ручной расстановке кораблей и набором кораблей, которые необходимо перетащить на своё игровое поле.
  4. Два блока, верхний и нижний, для вывода информационных сообщений.
  5. Кнопка запуска игры.

Первоначально, при загрузке страницы, в контейнере инструкции отображаются только элементы для выбора способа размещения кораблей:
— рандомное размещение при помощи js-скрипта;
— самостоятельное размещение игроком.
Кнопка запуска игры изначально не видна. Она появится только после того, как игрок разместит свои корабли.

Исходный код разметки HTML:

Самый сложный блок, как вы заметили, это набор кораблей.

Таблица стилей для игры «Морской бой».

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

Начинаем писать Javascript для игры «Морской бой».

Весь наш код разместим в анонимной самозапускающейся функции для ограничения области видимости:

При написании js-скрипта мы будем использовать классы. Почему выбран такой стиль программирования?
У нас есть два игровых поля — игрока и компьютера. Эти поля совершенно идентичны. У них есть ряд одинаковых свойств и одинаковые методы, с помощью которых можно получить координаты кораблей и разместить их на этих полях. Кроме игровых полей, у нас есть множество объектов кораблей, которые также обладают рядом одинаковых свойств и методов.

Для создания выше перечисленных объектов мы будем использовать конструкторы классов, т. к. они хорошо подходят для создания экземпляров похожих объектов, имеющих одинаковую структуру, с общими именами свойств и методов.

Игра «Морской бой». Глобальные переменные и функции.

Прежде чем рассматривать классы и их функции-конструкторы, определим ряд переменных и функций, которые будут глобальными в пределах области видимости анонимной самозапускающейся функции.

Игра «Морской бой». Конструктор игровых полей.

Для создания игровых полей используем класс Field. Аргументом функции-конструктора класса будет объект игрового поля: или игрока, или компьютера. Других аргументов конструктору не требуется. В результате мы получим новые экземпляры игровых полей, дополненные новыми свойствами, которые будут использовать методы класса,+ отвечающие за расстановку кораблей.

Рассмотрим подробно ряд констант, используемых в классе, а также функцию-конструктор класса и её свойства, которые будут наследоваться созданными ею объектами human и computer:

На данном этапе, создаём только экземпляр поля игрока — human. Объект computer создадим с помощью конструктора позже, когда будем расставлять корабли компьютера.
Предварительно получим объекты ряда элементов, которые будут использоваться при подготовке к игре «Морской бой» и в процессе самой игры.

Запишем в самый конец анонимной самозапускающейся функции следующий код:

Игра «Морской бой». Рандомная расстановка кораблей в игре.

Игра «Морской бой». Алгоритм расстановки кораблей шаг за шагом.

Прежде, чем начать писать код, отвечающий за рандомную расстановку кораблей на игровом поле, предварительно рассмотрим алгоритм расстановки по шагам:

  • 1

    У нас есть два интерактивных элемента, клик по которым запускает js-скрипт расстановки кораблей — рандомную или самостоятельную. Чтобы не вешать два обработчика события, воспользуемся делегированием и с помощью функции addEventListener вешаем один обработчик на родителя этих элементов. Родительским элементом является:

    Запомните, это очень важно.
    Для управления элементами текущей страницы (показать / скрыть, изменить стиль, переместить, подгрузить и т.д.) должны использоваться элементы <span>, <button>, <div>, <li>. Именно на них вешаются обработчики событий.
    Не используйте для этой цели тег <a>. Этот тег должен использоваться только для формирования ссылок, ведущих на другие страницы сайта или другой интернет-ресурс.

    Используя метод делегирования событий определяем, по какому элементу был сделан клик и считываем значение его атрибута data-target. Если это значение равно random, то вызываем функцию randomLocationShips.

  • 2

    Функции randomLocationShips перебирает статичный объект SHIP_DATA с данными кораблей.

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

  • 3

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

    1. в полученных координатах не должны располагаться палубы ранее созданного корабля;
    2. в соседних клетках, включая диагональные, не должны находиться палубы ранее созданного корабля;
    3. корабль не должен выходить за пределы игрового поля.

    Если какое-то из этих условий не выполняется, снова вызываем функцию randomLocationShips.

  • 4

    Создаём экземпляр объекта корабля, используя конструктор кораблей класса Ships. Заносим координаты палуб корабля в двумерный массив объекта human и объект squadron, в котором содержаться данные по каждому кораблю эскадры.

Игра «Морской бой». Обработчик события запуска генерации начальных координат кораблей.

Ещё раз обратим внимание на следующую HTML-разметку:

Как было написано выше, обработчик события повесим на <div> с id="type_placement".

Игра «Морской бой». Очистка игрового поля от ранее расставленных кораблей.

Прежде чем начать размещение кораблей (неважно, каким способом), предварительно необходимо очистить игровое поле и ряд объектов и массивов. За эту операцию отвечает функция cleanField, являющаяся методом класса Field.
Данная функция выполняет следующие задачи:

  1. удаление ранее установленных кораблей с игрового поля;
  2. очистка массива squadron от записанных в него объектов кораблей;
  3. заполняем матрицу игрового поля нулями, что соотвествует пустому месту.

В дальнейшем, эта функция будет использоваться и для очистки игрового поля компьютера. Код функции:

Теперь, при попытке создать новую эскадру с более оптимальным расположением кораблей, конфликтов со старой эскадрой не возникнет.

Статичная функция createMatrix формирует двумерный массив и заполняет его нулями. В этот массив и будут записываться координаты кораблей, а в дальнейшем координаты попаданий, промахов и отметки заведомо пустых клеток. Код этой функции несложный и в комментариях не нуждается:

В дальнейшем, все статичные методы будем записывать сразу после функции-конструктора класса, а статичные свойства — перед конструктором.

Игра «Морской бой». Генерация начальных координат кораблей.

В данный момент, функция randomLocationShips вызывается, как метод экземпляра human. В дальнейшем, при расстановке кораблей компьютера, она будет вызываться, как метод экземпляра computer.
Как было сказано ранее, функция запускает перебор объекта SHIP_DATA с данными кораблей по каждому типу и создаёт экземпляр каждого корабля с заданными свойствами, используя для этого класс Ships.

Внутри этого цикла создадим ещё один, вложенный, цикл. Количество итераций вложенного цикла равно количеству кораблей текущего типа. Внутри этого цикла, при каждой итерации будем получать координаты первой палубы и направление расположения палуб с помощью функции getCoordinatesDecks. Аргументом данной функции будет количество палуб создаваемого корабля. Также сформируем уникального имя корабля, которое будет использоваться, как значение его атрибута id.

Теперь полный код функции randomLocationShips выглядит так:

Прежде чем приступать к функции getCoordinatesDecks, которая генерирует координаты первой палубы корабля и направление расположения его палуб, подумаем, какие ограничения мы должны наложить на диапазон создаваемых координат. Рассмотрим это на примере трёхпалубного корабля.

Запомните, это очень важно.
Данные о координатах корабля будут храниться в матрице (двумерном массиве), поэтому вместо буквенно-цифровых координат будут формироваться только цифровые. Отсчёт элементов массива начинается с 0. Строки матрицы — это координата ‘X’, а столбцы — координата ‘Y’.
Например, координата «Б3» в двумерном массиве будет иметь значение [1,2]. Как видно, координаты палубы корабля есть не что иное, как индексы элемента в двумерном массиве.
Игра Морской бой. Ограничения на координаты первой палубы.

Разобравшись, как накладываются ограничения на формирование координат, мы можем получить зависимость максимальной координаты от количества палуб корабля. Для координаты ‘Y’, при горизонтальном размещении корабля, это будет выглядеть так:
y = (9 - decks) + 1 или короче y = 10 - decks, где decks — количество палуб у корабля.
При этом, координата ‘X’ может принимать любое значение в диапазоне от 0 до 9.

Аналогичные ограничения накладываются и при вертикальном расположении корабля.

Так как координаты формируются рандомно, давайте напишем небольшую статичную функцию, которая будет генерировать случайные числа. С помощью аргумента, будем определять диапазон, в котором будут лежать полученные значения:

Рассмотрим по шагам, как работает функция getCoordinatesDecks:

  • 1

    Случайным образом определяем направление расположения корабля, присваивая значения специальным коэффициентам kx и ky. Если kx == 0 и ky == 1 — корабль расположен горизонтально, если kx == 1 и ky == 0, то вертикально.

  • 2

    Получаем начальные координаты корабля, не забывая про ограничения, рассмотренные ранее.

  • 3

    Вызываем функцию проверки полученных координат первой палубы. Если координаты не валидны, то через рекурсию опять вызываем функцию getCoordinatesDecks.

  • 4

    Возвращаем объект, в котором содержаться значения координат x и y, а также коэффициентов kx и ky.

Теперь представим функцию getCoordinatesDecks в её полном виде:

Данная функция также является методом класса Field.

Игра «Морской бой». Валидация координат всех палуб корабля.

Для проверки корректности координат палуб корабля используется функция checkLocationShip, аргументами которой являются начальные координаты корабля и направление расположения палуб.
Чтобы было более понятно, как работает данная функция, проиллюстрируем это:

Игра Морской бой. Отображение координат требующих проверки.

Как видно на приведённом рисунке, проверяется группа координат, совпадающая с координатами палуб корабля и прилегающих к ним клеток. Эта группа может быть меньше, если корабль примыкает к одной или сразу двум границам игрового поля. Значение проверяемых координат берётся из матрицы игрового поля, принадлежащей объекту, от которого в данный момент наследуется функция getCoordinatesDecks. Это может быть или поле игрока, или поле компьютера. Напоминаю, что в данный момент мы рассматриваем рандомную расстановку кораблей для игры «Морской бой» на поле игрока.
Полученное из матрицы значение сравнивается с 1, т. е. в проверяемой клетке есть палуба ранее установленного корабля.

Проверка реализована с помощью встроенной функции filter. Сначала из матрицы берётся массив координат по оси Х. Его границы определяются значениями fromX и toX. Далее этот массив ограничивается по оси Y — границы определяются значениями fromY и toY. Значение каждого элемента получившегося двумерного массива с помощью функции filter сравнивается с 1.
Если найдутся элементы содержащие 1, то функция checkLocationShip вернёт false, в противном случае — true.

Игра «Морской бой». Создаём экземпляр корабля и выводим его на экран.

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

Вернёмся к функции randomLocationShips. В конце функции есть такие строки:

Настало время рассмотреть их подробнее.

Игра «Морской бой». Конструктор кораблей.

В качестве конструктора кораблей используем функцию-конструктор класса Ships. В качестве аргументов конструктор принимает два параметра:

  1. this, указывает, для кого создаётся данный корабль — игрока или компьютера.
  2. options, это объект, имеющий свойства:
    x и y — координаты первой палубы;
    kx и ky — направлении расположения палуб;
    decks — количество палуб корабля;
    shipname — уникальное имя корабля, которое будет использоваться в качестве его идентификатора.

Игра «Морской бой». Создание корабля и сохранение информации о нём.

Для создания корабля используется функция createShip. Эта же функция используется и для создания кораблей компьютера. Информация, для кого создаётся корабль, находится в свойстве player — это экземпляр или human, или computer.

При создании корабля нам нужно в цикле записать координаты его палуб в двумерный массив matrix и массив arrDecks, который будет записан в объект squadron. В этом объекте храниться полная информация по каждому кораблю, что потребуется в дальнейшем при создании выстрела и оценки его результата.

После того, как корабль создан и координаты его палуб сохранены в соответствующих массивах, выводим корабль на экран с помощью функции showShip. Предварительно необходимо убедиться, что корабль принадлежит игроку, а корабли компьютера мы, естественно, отображать не будем.
Теперь рассмотрим полный код функции создания кораблей:

Игра «Морской бой». Вывод корабля на экран монитора.

Прежде чем рассматривать реализацию функции showShip, отвечающую за отображение корабля, нужно разобрать, каким образом осуществляется позиционирование корабля на игровом поле с учётом его координат в двумерном массиве.
Все корабли формируются элементами div с абсолютным позиционированием относительно своего родительского элемента:

Зная размер палубы корабля, он совпадает с размером клетки игрового поля и равен 33px, и зная координаты, можно преобразовать их в смещение в пикселях относительно родительского элемента.

Игра Морской бой. Преобразование координат.

Кроме смещения, элементу div нужно добавить класс по названию совпадающий с именем корабля. Этот класс определит стиль корабля. Если корабль размещён вертикально, то необходимо добавить ещё класс vertical.

Функция showShip является методом класса Ships и не связана с конкретными экземплярами класса (в ней отсутствует this). Поэтому объявим её статичной и разместим внутри класса, после функции-конструктора. Полный код функции:

В качестве результата работы функции showShip, мы видим выведенные на экран корабли эскадры в пределах игрового поля игрока. Корабли компьютера создаются теми же самыми функциями, только они на экран не выводятся.

Заключение

Итак, мы рассмотрели, как создать эскадру кораблей для игры в «Морской бой» и разместить её на игровом поле случайным образом. Код, который мы написали, используется не только для создания эскадры игрока, но и для создания кораблей компьютера.

В следующей статье будет рассказано, как с помощью JavaScript реализовать размещение кораблей игрока перетаскиванием на игровое поле, используя метод Drag’n’Drop.

Комментарии

Всего: 14 комментариев
Требования при посте комментариев:
  1. Комментарии должны содержать вопросы и дополнения по статье, ответы на вопросы других пользователей.
    Комментарии содержащие обсуждение политики, будут безжалостно удаляться.
  2. Для удобства чтения Вашего кода, не забываейте его форматировать. Вы его можете подсветить код с помощью тега <pre>:
    <pre class="lang:xhtml"> - HTML;
    <pre class="lang:css"> - CSS;
    <pre class="lang:javascript"> - JavaScript.
  3. Если что-то не понятно в статье, постарайтесь указать более конкретно, что именно не понятно.
  • Добрый день!
    Интересная статья . Вопрос — » вы можете скачать её и спрайт с картинками в архиве» ссылки на этот архив на сайте не нашел?
    Спасибо.

  • В ближайшие дни выложу архив игры «Морской бой» на сервер. Линк на него будет рядом с кнопкой «Посмотреть игру».

  • Выложил полный архив игры «Морской бой».
    В процессе написания статей, в js-код будут вносится изменения дополнения и т. д. Код в файле function.js не всегда будет соответствовать коду, приведённому в тексте статьи. Прошу это учесть.

  • Нет проверки на — какие корабли потоплены и стоит ли стрелять дальше в соседнею клетку. А значит ПК изначально уступает человеку.

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

  • Хорошо что поле квадратное, поэтому проблемы не видно. На самом деле допущена серёзная логическая ошибка в процессе кодирования. В какой-то момент автор запутался и поле повернулось на 90 градусов. Координаты x и y поменялись местами. Мне кажется, отсюда растёт проблема с расстановкой кораблей, которые в ручном режиме становится очень сложно правильно выстроить.
    Например: чтобы не быть голословным приведу пример >

    Ships.prototype.showShip = function () {
    ~263 стр …
    // через атрибут ‘style’ задаём позиционирование кораблю относительно
    // его родительского элемента
    // смещение вычисляется путём умножения координаты первой палубы на
    // размер клетки игрового поля, этот размер совпадает с размером палубы
    div.style.cssText = ‘left:’ + (this.y0 * player.shipSide) + ‘px; top:’ + (this.x0 * player.shipSide) + ‘px;’;
    Чёрт к левой координате добавляется положение корабля по вертикальной оси, а к верхней горизонтальное.

    • Вальдемар Денис Ответить 27 февраля 2020 в 13:37

      Здесь нет ошибки. Оси координат матрицы и экрана не совпадают (они противоположны), поэтому у вас и создалось впечатление, что игровое поле повернулось на 90 градусов.

  • Ещё раз, чтобы мозг встал на место. )
    1. Чтобы получить координату точки в окне браузера, мы сначала получаем её положение по горизонтали (ось X), потом по вертикали (ось Y).
    2. В двумерном массиве (матрице) для обращения к его элементу, сначала указывается строка (ось X), а потом столбец (ось Y).
    Такое обращение к матрице придумано не мной. Впервые матрицы упоминались ещё в древнем Китае, называясь тогда «волшебным квадратом». Также волшебные квадраты были известны чуть позднее у арабских математиков.
    Итог написанного: в браузере ось Х расположена по горизонтали, а ось Y — по вертикали. В матрице наоборот — ось Х вертикальная, а ось Y горизонтальная.
    Ещё раз внимательно изучите картинку из статьи, которая всё это объясняет:
    http://cleanjs.ru/wp-content/uploads/2017/09/convert_coordinates.png

    • Денис Вальдемар Ответить 28 февраля 2020 в 16:32

      Окэй. Нашёл другую залипательную штуку.
      Суть в следующем :> Если перед выстрелом клетку заблокировать и разблокировать, то независимо от того будет ли там корабль, будет промах. При снятии блока нет проверки, есть в этой клетке палуба корабля или нет.
      ~ 1058
      // если клетка, по которой кликнули, уже заштрихована, то
      if (isShaded) {
      // удаляем эту иконку
      el.parentNode.removeChild(el);
      // записываем в матрице игрового поля компьютера 0,
      // значение пустой клетки
      comp.matrix[coords.x][coords.y] = 0;
      }

  • заменяем на
    // записываем в матрице игрового поля компьютера 0 или 1
    // в зависимости от того море там или палуба
    comp.matrix[coords.x][coords.y] = self.isSea() ? 0 : 1;
    И ниже добавляем метод
    isSea: function() {
    let res = true;
    // перебор массива начнём с конца, для получения корректных значений
    // при возможном удалении его элементов
    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) {
    res = false;
    // выходим из цикла, т.к. палуба найдена
    break;
    }
    }
    }
    return res;
    },

  • Добрый день. Хочу поменять проверку в функции IsShipSunk. Хочу сравнивать хиты темпшипа не с самым большим оставшимся кол-вом палуб, а с его собственным кол-вом палуб. Т.е. если корабль на три палубы и хита три — все, утонул и срабатывает функция ЮзелессСеллс. Что-то не могу сообразить, как обратиться к опции decks корабля. Не подскажите?

    • Вальдемар Антон Ответить 16 марта 2021 в 10:33

      Добрый день.
      При первом попадании необходимо в объекте human.squadron найти корабль у которого координаты палубы совпадают с координатами попадания. Для этого необходимо перебрать объект human.squadron и при каждой итерации проверять массив arrDecks на наличие координат, равных координатам попадания. После совпадения координат, посмотреть кол-во палуб (decks) у данного корабля.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *