Сегодня мы будем делать морской бой. Морской бой будет консольным и не совсем оконченным.

Я долго думал, рассматривать или нет полный код. В итоге решил убрать кое-какие возможности компьютерного игрока для того чтобы упростить программу.

Создавать морской бой лучше в такой последовательности:

Вы, не читая дальше этот урок, открываете редактор и пишите код с нуля самостоятельно. Как только закончите морской бой, можете начинать читать следующий урок. Загляните только в упражнения к этому уроку (в конце).
Если по каким-то причинам у вас не получается самостоятельно написать код игры, то вы читаете этот урок до конца и снова пробуете написать весь код.
Если у вас всё ещё ничего не получается, тогда загляните в раздел листинги, там есть ссылка на страничку с исходным кодом. Исходный код представлен в двух вариантах. Первый - каркас приложения. Вот первый вариант вам как раз и нужен. В нём полностью написана функция main. Код прокомментирован. В этом варианте вам всего-лишь нужно заполнить функции.
Если и предыдущий вариант не помог, то тогда копируете код из первого варианта листинга в редактор, открываете второй вариант и, тщательно изучая что и как в нём происходит, заполняете функции из первого варианта. Во втором варианте кода представлена рабочая программа (с упрощениями). Она компилируется и работает.
Если вам не помог даже последний вариант, тогда вам придётся перечитать предыдущие выпуски.
Если непонятны какие-то моменты, то напишите мне на e-mail, не стесняйтесь. Постараюсь помочь.

Небольшое отступление
По умолчанию в редакторе используется 4 символа для знака табуляции. Код я пишу с отступами. Поэтому, если вы скопирует код с сайта рассылки в редактор, отступы могут быть слишком большие. На мой взгляд 2 символа для отступов/табуляции вполне достаточно. Количество символов для отступов можно поменять:

Пункт меню Tools -> Options (Сервис -> параметры).

Октроется окно Параметры. В левой части нужно выбрать: Text Editor -> C/C++ -> Tabs (Текстовый редактор -> C/C++ -> Отступы (возможно он как-то по другому называется, он там второй).

И в правой части окна поменять значения Tab size и Indent size на 2 - или на любое другое значение.

Ну чтож, приступим к разбору!

Глобальная область видимости программы Морской бой

Сначала мы включаем следующие файлы: clocale, conio.h, stdlib.h, iostream, ctime. Со всеми файлами мы уже работали кроме ctime. Данный файл предназначен для работы с системным временем. Для чего он нужен в нашей программе смотрите ниже.

Далее идёт перечисление:

enum direction{h,v};

Оно используется для задания ориентации корабля (direction - направление). h - корабль расположен горизонтально (от horizontal), v - корабль расположен вертикально (от vertical).

Далее идёт определение класса player (игрок). Для простоты я объявил все переменные и функции в блоке public. Объектами класса являются игрок и компьютер.

В классе содержатся следующие данные:

bool defeat_flag;
int hits[10][10];
int ships[10][10];

Одна переменная типа bool - defeat_flag (флаг поражения). В данной переменной хранится информация, а не проиграл ли игрок? 0 - игрок ещё не проиграл. 1 - игрок проиграл.

Двумерный массив целых чисел hits (попадания) размером 10x10. Ячейки массива могут принимать только два значения: 0 - данную клетку игрок ещё не называл и 1 - игрок уже назвал данную клетку. Например, вы называете клетку а-1 на вражеском поле. Соответственно в ваш массив hits в клетку [0][0] (отсчёт ведётся с нуля) заносится 1.

Данный массив позволяет компьютеру не называть одну и ту же клетку дважды. Когда компьютер выбирает клетку по которой он будет "стрелять" (клетка выбирается случайным образом), если он видит что в выбранной клетке в его массиве hits стоит единица, он выбирает другую клетку.

Двухмерный массив ships[10][10] (корабли). В данном массиве хранится поле с кораблями игрока.

Данный массив используется при размещении на поле кораблей (делается это случайно).

Ячейки массива могут принимать три значения: 1 - данным значением инициализируются все ячейки - это пустые клетки (море), 2 - в данной ячейке расположен корабль (или его часть), 3 - в данной ячейке расположен подбитый врагом корабль (или его часть).

Вот в общем-то и все данные которые хранятся в классе.

Методы класса player:

В конструкторе поле defeat_flag задаётся равным нулю.

void ships_init()
Инициализация массива ships, помещение в него кораблей. Обратите внимание, инициализацию массива ships можно было бы поместить и в конструктор.

void set(int deck)
Данная функция размещает на поле один корабль. Чтобы разместить на поле все корабли, данную функцию нужно вызвать десять раз. В функцию передаётся один параметр целого типа - deck (палуба). Он сообщает функции какой тип корабля нужно разместить на поле (однопалубный - 1, четырёхпалубный - 4).

void place_ship(int s, int c, direction dir, int deck)
Данная функция вызывается из set(). Она принимает четыре параметра: s,c - координаты (string - строка, column - столбец). Третий параметр - направление корабля. четвёртый параметр - сколько палуб на корабле.

Рассмотрим пример: если у вас есть корабль в клетке в-3, то нельзя размещать другой корабль в клетке в-4. Т.е. корабли не должны друг друга касаться. Функция place_ship проверяет, касается ли размещаемый корабль других.

И последние две функции:

void turn(player&, int character, int digit)
void turn(player&)

В первой фунции вы "стреляете" по полю компьютера. В функцию передаётся ссылка на объект класса player. При вызове, вы передаёте компьютерного игрока, чтобы получить доступ к его полю с кораблями. Два других параметра - координаты по которым вы будете бить: character (символ) - буква (горизонтальная координата), digit (цифра) - цифра (вертикальная координата).

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

С объявлением класса player пока всё.

Затем идёт инициализация двух констант: s (string - строка) - количество строк в массиве и c (column - столбец) - количество столбцов в массиве. Строк - 13: строка для "букв", две строки горизонтальных разделителей и десять строк под поля с кораблями. Столбцов - 29: 20 столбцов на два поля с кораблями, 4 столбца на вертикальные разделители, 2 столбца на столбики цифр, 2 столбца отведены на пустое пространсто между полями и 1 столбец на хранение символов конца строки.

Затем идёт инициализация всего поля. Здесь я приведу первые три строки:

код на языке c++
char map[s][c] = {
		"  0123456789     0123456789 ",
		" #----------#   #----------#",
		"0|          |  0|          |",

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

И ещё один момент: вместо строки букв я использовал строку цифр. Ввод тоже осуществляется цифрами. Например 1-1 - второй столбец, вторая строка. Связано это опять же с особенностями досовской консоли - трудности обработки русских символов. К тому же так легче осуществлять доступ к элементам массивов. Просто помните, что первая цифра при вводе отвечает за буквы.

Затем мы создаём два объекта класса player:

player human;
player computer;

Тут всё просто. Первый объект представляет живого игрока (human - человек). Второй - компьютерного игрока.

Затем идут прототипы функции:


void map_init(char map[s][c]);

Здесь происходит помещение в массив map информации о кораблях игрока, которая беретёся из массива human.ships.

void show (char map[s][c]);


Функция выводит массив map на экран (show - показать).

int input(char&, char&);
В данной функции осуществляется ввод координат пользователем (input - ввод).

void test();
Функция для тестирования. Подробности ниже.

int check_ending();
Данная функция проверяет - окончена ли игра? При этом проверяются флаги defeat_flag игроков.

Также я определил массив из десяти символов. В программе он используется в нескольких местах:

char numbers[10] = { '0','1','2','3','4','5','6','7','8','9'};

Морской бой - функция main

код на языке c++
int main()
{
  setlocale(LC_CTYPE, "Russian");
  int message = 0; // переменная хранит коды сообщений

  // установка начального значения генератора случайных чисел
  srand( static_cast(time(NULL)) );

  human.ships_init();
  computer.ships_init();
  map_init(map);

  while (message != 2)
  {
    int user_input = 0;
    system("cls");
    show(map);
    if (message == 1) // код сообщения 1 - введено неверное значение
      std::cout << "Вы ввели неверное значение!\n";
    message = 0;
    //test();
    char character, digit;

    user_input = input(character, digit);
    if (user_input == 1)
    {
      message = 1;
      continue;
    }

    human.turn(computer,character, digit);
    computer.turn(human);
    message = check_ending();
  }

  _getch();
  return 0;
}

Переменная message хранит коды сообщений:

0 - всё нормально.
1 - пользователь нажал неверную клавишу.
2 - кто-то победил, игра закончилась.
Далее задаём начальное значение генератора случайных чисел (подробности ниже).

Затем мы вызываем функции ответственные за инициализацию полей с кораблями.

Основной цикл программы Морской бой:

- Очистка экрана.
- Вывод всей карты на экран.
- Вывод сообщения если пользователь нажал неверную клавишу.
- Обнуление кода сообщений message.
- Вызов функции ввода пользователя.
- Проверка ввода пользователя!!! Если пользователь нажал неверную клавишу, то присваиваем message код 1 и начинаем выполнение цикла заново.

Вот здесь стоит остановиться подробнее. До этого момента мы ещё ничего не поменяли на полях с кораблями. Мы вывели карту на экран, дали пользователю возможность ввести координаты. Значения которые ввёл пользователь теперь хранятся в переменных character и digit.

И вот если пользователь ввёл неверные координаты (например ввёл букву), то тело цикла дальше не выполняется! Вместо этого мы начинаем выполнение тела цикла с первого оператора. Достигается это за счёт использования оператора continue.

А вот в трёх последних операторах происходят все изменения:

- Ход игрока.
- Ход компьютера.
- Проверка на окончание игры.

Теперь остановимся на каждой функции поподробнее.

Рассматривать функциии мы будем в том порядке в котором они вызываются из main():

void player::ships_init()
Здесь мы инициализируем массив ships объекта единицами, а массив hits нулями. Плюс к этому несколько раз вызываем функцию set(). По разу на каждый корабль. В качестве аргумента передаём количество палуб на корабле.

void player::set (int deck)
Самое интересное в функции - цикл. Он выполняется до тех пор пока не удастся подобрать свободное место в массиве ships.

Сначала случайным образом выбирается направление корабля (даже для однопалубного). Затем случайным образом выбираются координаты первой клетки. s - номер строки, c - номер столбца.

Дальше идёт ветвление switch. В котором мы проверяем ориентацию корабля. Рассмотрим ветку, в которой корабль будет расположен горизонтально:

код на языке c++
if (ships[s][c+deck-1] == 1)
{
  e = place_ship(s,c,dir,deck);
  if (e == 0)
  {
    for (int i = 0; i < deck; i++)
    {
      ships[s][c+i] = 2;
    }
    isset = 1;
  }
}

Допустим, случайным образом были выбраны две координаты: s = 3, c = 4. Допустим, нам нужно разместить четырёхпалубный корабль.

В if мы проверяем свободна ли клетка ships[3][4+deck-1] или ships[3][7]. Как это выглядит на поле (один - выбранная координата):

4567
#----
3|1 X

Если ships[3][7] = 1, то выполняется тело этого ветвления. То есть, на поле есть четыре свободных клетки по горизонтали и можно попробовать всунуть туда четырёхпалубный корабль.

Но надо проверить нет ли кораблей в смежный клетках.

Для этого мы вызываем функцию place_ship(). В неё передаём координаты корабля, направление и количество палуб.

int player::place_ship(int s, int c, direction dir, int deck)
Данная функция возвращает целое число e. Если e = 0, то всё нормально - смежные клетки свободны, корабль можно размещать, если e = 1, то корабль размещать нельзя.

Напомню, что у нас корабль расположен горизонтально. Поэтому мы выбираем ветку case h: в switch:

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

код на языке c++
if (ships[s-1][c-1] == 2)
{
	e = 1;
/*
  345678
 #-------
2|X      
3| 2222     
4|      
*/
}

Возвращаем информацию о том можно ли разместить в данном месте корабль в функцию set()

В set() проверяется возвращаемое значение place_ship() и если корабль можно разместить в данной координате,то мы его размещаем. Делается это следующим образом:

код на языке c++
for (int i = 0; i < deck; i++)
{
  ships[s][c+i] = 2;
}

Данный код позволяет разместить в координате [s][c] любой тип корабля. Заметьте, что это код для размещения корабля ориентированного горизонтально.

После размещения корабля устанавливаем переменную isset = 1, что позволяет выйти из цикла while и из функции set().

Возвращаемся в функцию ships_init, в которой ещё несколько раз вызывается функция set() с разными аргументами (количество палуб). После выполнения тела функции ships_init() происходит возврат в main().

Теперь в функции main() начинается основной цикл. Кратко опишу, какие действия здесь происходят:

- очистка экрана: system("cls");
- показ карты show(map). Нам не обязательно передавать в show() массив map, так как мы объявили его глобальным.

Вызов: void show(char map[s][c])
Здесь мы обрабатываем все элементы двумерных массивов размером [10][10].

Рассмотрим код для элемента [3][4].

Проверка условия (в массиве компьютера hits по данной клетке был сделан выстрел И в нашем поле ships в данной клетке стоял корабль).
На нашем поле в map ставим X - корабль подбит. Обратите внимание, что мы используем смещение для обращения к элементам массива map - map[3+2][4+2]. То есть делаем дополнительный отступ сверху и сбоку. Сверху - строка букв и строка горизонтальных разделителей, сбоку - столбец цифр и столбец вертикальных разделитей.

Если первое условие не выполнилось, мы проверяем (в массиве компьютера hits по данной клетке был сделан выстрел И в нашем массиве ships на этом месте не было корабля).
Помечаем соответствующую клетку - О. Т.е. по клетке был сделан выстрел, но корабля в ней не оказалось.

Затем идёт блок заполнения поля компьютера. Здесь тоже самое: X - в клетке был корабль, O - В клетке корабля не было.

Обратите внимание, что при заполнения поля компьютера в массиве map мы делаем отступ в 2 клетки по вертикали (строка с буквами и строка с горизонтальными разделителями) и в 17 клеток по горизнотали (смещение в 17 клеток нужно, чтобы покрыть поле игрока).

Возвращаемся в функцию main()

Вызов void test()
Я создал test() специально для тестирования ships и hits. Функция проста - вывод на экран массивов player.hits, computer.hits и player.ships, computer.ships. Благодаря этой функции можно увидеть что содержится в массивах. По умолчанию вызов данной функции закомментирован. Данная функция позволяет увидеть правильно ли заполнены соответствующие массивы.

Вызов: int input(char& character, char& digit)
В данной функции пользователь вводит координаты. Как я уже писал выше, координаты пользователь вводит в виде двух цифр x-x. Первая цифра - отвечает за столбцы, вторая за строки. Для простоты программирования и избежания путаницы я обозначил первую цифру как character (символ), а вторую как digit (цифра).

Рассмотрим ввод первой цифры.

Ввод осуществляется функцией _getch():

character = _getch();

В функии используется переменная match хранящая код ошибки. 0 - пользователь ввёл цифру от нуля до девяти. 1 - пользователь ввёл какую-то ерунду, надо выходить из функции.

Так как переменная character - символьного типа, то у нас тут небольшая проблема. Для её решения я создал массив символов numbers содержащий символы от нуля до девяти (объявлен в глобальной области видимости).

Именно с элементами этого массива сравнивается character:

код на языке c++
character = _getche();
int match = 0;

for (int i = 0; i < 10; i++)
{
  if (numbers == character)
  {
    match = 1;
    character = i;
  }
}

Здесь происходит преобразования character из символьного типа в тип int. Заодно устанавливаем флаг match, что пользователь ввёл корректный символ.

Затем проверяем match и если данная переменная равна 0, надо выходить из функции.

Остальная часть функции почти не отличается от того что мы рассмотрели. Возвращаемся в main().

Далее проверяется значение, которое было возвращено функцией input(). Если пользователь нажал неверную клавишу, мы устанавливаем код сообщения message = 1 и начинаем итерацию цикла заново.

Последняя часть main() состоит из вызова трёх функций.

[i]human.turn(computer,character, digit);
computer.turn(human);

Здесь вызываются две перегруженные функции turn().

В функции turn() происходит изменение значений массивов: своего hits и вражеского ships. Именно для того, чтобы изменить массив вражеского ships мы и передаём в функцию ссылку на объект player.

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

И вызов последней функции:

message = check_ending();

В данной функции мы проверяем массивы ships игроков и если у игрока не осталось ни одного корабля (в массиве нет двоек), устанавливем флажок, что этот игрок проиграл.

Установка srand

В функции main() встречается такая строка:

srand( static_cast(time(NULL)) );

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

Чтобы понять как работают генераторы случайных чисел, можете прочитать на сайте статью "Случайные числа". Там я подробно всё описал.

Обратите внимание, что в качестве начального значение берётся системное время.

Функция time() с параметром NULL, возвращает количестов секунд прошедшее с 1-ого января 1970-ого года. При каждом запуске программы, это значение будет различным.

Если вы будете работать с функциями времени, знайте, там определён свой тип. Так вот: никогда не используйте 32-ух битную версию этого типа (_time32), всегда используйте 64-ёх битную.

Потому как, если вы будете использовать 32-ух битную версию, то во всех ваших программах, где вы используете этот тип, после 18-ого января 2038-ого года могут возникнуть ошибки! Будьте осторожны!

Пространства имён

В программе я не стал использовать пространство имён std явно:

using namespace std;

Поэтому пространство имён пришлось указывать для каждого объекта. Например:

std::cout

Оптимизация

Программу я старался писать как можно проще. Кое-где есть ошибки (не проверяются все возможные варианты при размещении кораблей). Данная реализация не является идеальной. Тут много чего неправильного, но для того чтобы сделать код как можно понятнее и немножко его сократить, мне местами пришлось пожертвовать правилами хорошего кода.
Напоследок несколько слов:

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

Главное, если ничего (ну совсем ничего) не получается и ничего не понятно, не бросайте! Читайте предыдущие уроки, изучайте код программы в полном варианте и пытайтесь воссоздать его самостоятельно.

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

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

Вы можете улучшить игру. Самое простое: когда компьютер попадает по клетке с кораблём, пусть он отмечает все диагональные клетки.

Подумайте, что можно сделать, чтобы компьютер действовал так же как человек. Например, можно добавить две переменные (координату подбитого корабля и направление), чтобы научить компьютера добивать раненые корабли.

Ну и последнее. В следующем уроке мы начнём знакомиться с программами под Windows. Ура! Хотя мы ещё будем возвращаться к C++, чтобы рассмотреть более сложные темы.