среда, 13 мая 2009 г.

Создаём компьютерную игру. Выпуск 13.




Морской бой

Доброго времени суток. Вы читаете тринадцатый выпуск рассылки "Создаём компьютерную игру".

Для тех, кто только присоединился:
Узнать чем мы здесь занимаемся, Вы можете, изучив архив. В общих чертах узнать о целях рассылки можно в первом выпуске или в разделе "О сайте" на сайте рассылки.

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

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

На сайте представлено два варианта кода: каркас приложения и полный код рассматриваемый в данном выпуске.

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

  • Вы, не читая дальше этот выпуск, открываете редактор и пишите код с нуля самостоятельно. Как только закончите морской бой, можете начинать ждать следующий выпуск. Загляните только в упражнения к этому выпуску (в конце).
  • Если по каким-то причинам у вас не получается самостоятельно написать код игры, то вы читаете этот выпуск до конца и снова пробуете написать весь код.
  • Если у вас всё ещё ничего не получается, тогда загляните на сайт в разделе листинги есть ссылка на страничку с исходным кодом. Исходный код представлен в двух вариантах. Первый - каркас приложения. Вот первый вариант вам как раз и нужен. В нём полностью написана функция 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 столбец на хранение символов конца строки.

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

 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 на экран.

int input(char&, char&);

В данной функции осуществляется ввод координат пользователем.

void test();

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

int check_ending();

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

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

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

Функция 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. В котором мы проверяем ориентацию корабля. Рассмотрим ветку, в которой корабль будет расположен горизонтально:

 if (ships[s][c+deck-1] == 1) {   e = place_ship(s,c,dir,deck);   if (e == 0)   {     for (int i = 0; i 

Допустим случайным образом были выбраны две координаты: 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, двойками отмечено место, где предположительно должен будет находиться корабль). Выглядит это приблизительно так:

 if (ships[s-1][c-1] == 2) { 	e = 1; /*   345678  #------- 2|X       3| 2222      4|       */ }

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

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

 for (int i = 0; i 

Данный код позволяет разместить в координате [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:

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

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

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

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

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

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

 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++ через выпуск-два.

На сегодня всё.

Упражнения:

  1. Создать морской бой!
  2. Создать линейный конгруэнтный генератор случайных чисел и использовать его вместо функции rand. Что это за фигня можете узнать на сайте.
    Инициализацию начального значения сделайте с помощью функции time().

Всего хорошего! Не сомневаюсь, что выполнение упражнений принесёт в вашу жизнь множество незабываемых минут!

Ведущий - Роман.
Если у вас есть вопросы или что-то непонятно по данному выпуску, пишите на e-mail: el_rey2007@bk.ru

P.S.: Если вы читаете данный выпуск через месяц или более после выхода, пожалуйста, загляните на сайт рассылки, там вы найдёте исправленную и дополненную информацию из выпуска.




Сообщить о нарушении данной рассылкой правил Сервиса
Отписаться : Нажмите и отправьте это письмо

Комментариев нет:

Отправить комментарий