++++++++++++++++ +
1++ 2++ 3++ 4+
++++++++++++++++
++++++++++++++++ +
5++ 6++ 7++ 8+
++++++++++++++++
++++++++++++++++ +
9++10++11++12+
++++++++++++++++
++++++++++++ +
13++14++15+
++++++++++++
Реализовать функцию можно двумя способами: либо рисовать по отдельности каждую костяшку с использованием функции gotoxy, либо рисовать по очереди все 12 рядов символов, представляющих изображение поля с использованием просто перевода строки. Первый способ более логичный и наглядный, однако, плохо переносимый, т.к. функция gotoxy, устанавливающая курсор в точке с указанными координатами, есть не во всех средах разработки (например, в IDE от фирмы Borland - Borland C++ и Borland C++ Builder она есть, а в Microsoft Visual C++ ее нет). Второй способ менее прозрачный, но использует только стандартные функции. Итак, приведем пример обоих реализаций функции:
void DrawField()
//С помощью gotoxy { clrscr();
//Функция, очищающая экран for (int i = 0;
i < 4;
i++) for (int j = 0;
j < 4;
j++) if (Field[i][j])
//Если в этой позиции есть костяшка с ненулевым номером { gotoxy(i *
4 + 1, j * 3 + 1);
//Рисуем первый ряд костяшки std::cout << "++++";
//верхнюю рамку gotoxy(i * 4 + 1, j * 3 + 2);
//Второй ряд std::cout << "+"; std::cout.width(2);
//Следующий вывод через cout будет шириной в 2 символа std::cout <<
Field[i][j] << "+";
//Номер костяшки и рамка gotoxy(i * 4 + 1, j * 3 + 3);
//Третий ряд std::cout << "++++"; } }
Соответственно, здесь мы не рисуем ничего в клетке, содержащей 0, т.е. в которой нет костяшки. С помощью функции cout.width (2) мы указываем программе, что при следующем выводе потока cout (ко всем остальным это уже не будет относиться) ширина поля будет не менее двух символов; если для вывода потребуется всего один символ, то перед ним будет выведен пробел, если потребуется больше двух символов, то будут выведены все они (в нашем случае этого не произойдет).
void DrawField()
//Вариант, подходящий для всех реализаций { clrscr();
//Очищаем экран for (int j = 0; j < 4; j++)
//Рисуем построчно четыре горизонтальных ряда костяшек
{ for (int i = 0;
i < 4; i++)
//Рисуем верхнюю часть ряда { if (Field[i][j]) std::cout << "++++";
else std::cout << " ";
} std::cout << '\n';
for (int i = 0;
i < 4; i++)
//Рисуем среднюю часть ряда (с номерами)
{ if (Field[i][j]) { std::cout << "+";
std::cout.width(2); std::cout << Field[i][j] << "+";
} else std::cout << " ";
} std::cout << '\n';
for (int i = 0; i < 4;
i++)
//Рисуем нижнюю часть ряда { if (Field[i][j]) std::cout << "++++";
else std::cout << " " ;
} std::cout << '\n'; } }
Теперь напишем функцию, осуществляющую перемещение костяшек. Именно для этой функции нам будут полезны координаты свободной клетки игрового поля - EmptyX и EmptyY: во-первых, не надо будет каждый раз просматривать все поле в поисках этой клетки, во-вторых, даже если это делать, ее координаты все равно понадобятся - для самого перемещения соседней костяшки и для проверки корректности попытки перемещения.
Сделаем небольшое уточнение по работе функции.
Будем считать, что при нажатии игроком клавиш со стрелками мы перемещаем одну из костяшек на свободную клетку в направлении, соответствующем этой стрелке. Т.е. если игрок нажал клавишу "влево", то на свободное поле (влево) перемещается костяшка, находившаяся справа от него, если только это можно сделать (свободная клетка не была в правом столбце). Логично было бы при нажатии клавиши в функции main просто вызывать функцию перемещения, передавая ей информацию о том, какая из клавиш со стрелками была нажата, т.е. куда следует передвинуть одну из костяшек, если это возможно. Поэтому функция перемещения должна иметь аргумент, обозначающий направление. Конечно, можно для этой цели использовать, например, целые значения, но лучше всего определить для понятия "направление" особый тип-перечисление и передавать в качестве аргумента переменную этого типа. Это сделать очень просто: в самом начале кода программы, непосредственно перед объявлением наших глобальных переменных напишем:
enum Direction {LEFT, UP, RIGHT, DOWN};
И теперь мы можем передавать LEFT, UP, RIGHT и DOWN (прямо так, без всяких кавычек или еще чего-то) в качестве аргументов и вообще использовать как любые другие объекты любого другого встроенного типа. Реализация же функции тогда будет простой и прозрачной:
void Move(Direction dir) { switch (dir) { case LEFT:
{ if (EmptyX < 3)
{ Field[EmptyX][EmptyY] = Field[EmptyX + 1][EmptyY];
Field[EmptyX + 1][EmptyY] = 0; EmptyX++;
} } break;
case UP: { if (EmptyY < 3)
{ Field[EmptyX][EmptyY] = ield[EmptyX][EmptyY + 1];
Field[EmptyX][EmptyY + 1] = 0;
EmptyY++;
} } break;
case RIGHT:
{ if (EmptyX > 0)
{ Field[EmptyX][EmptyY] = Field[EmptyX - 1][EmptyY];
Field[EmptyX - 1][EmptyY] = 0;
EmptyX--; } } break;
case DOWN: { if (EmptyY > 0)
{ Field[EmptyX][EmptyY] = Field[EmptyX][EmptyY - 1];
Field[EmptyX][EmptyY - 1] = 0;
EmptyY--; } } break; } }
И наконец, напишем функцию, проверяющую, является ли текущее положение костяшек на поле правильным. В отличие от предыдущих, эта функция возвращает значение, именно - значение логического типа bool. Мы организуем ее код с помощью простого цикла, проверяющего первые пятнадцать позиций поля. Если во время этой проверки обнаруживается хотя бы одна костяшка, находящаяся не на месте, то функция тут же возвращает значение false, не проверяя уже последующие позиции. И только если за всю проверку не будет найдено ни одного несоответствия, и, стало быть, причин преждевременно выйти из функции, будет возвращено значение true:
bool FieldIsCorrect() { for (int i = 0;
i < 15; i++) if (Field[i % 4][i / 4] != i + 1) return false;
//При первом же нахождении несоответствия выходим и возвращаем
false return true;
//Если не найдено ни одного несоответствия - поле собрано верно }
Теперь мы написали все функции, необходимые для работы программы и можем приступать к написанию функции main.
Шаг четвертый: функция main и заголовочные файлы
Для того, чтобы наши разрозненные функции обрели вид законченной программы, осталось сделать совсем немного - написать функцию main, а также объявить заголовочные файлы, содержащие объявления используемых нами функций. Сначала напишем нашу самую главную функцию - в самом низу кода, под всеми объявлениями и определениями. По сути под программой можно понимать именно ее - все остальные функции будут вызываться внутри нее, сама же функция main определяет логику работы программы. Поскольку мы уже в самом начале определили структуру нашей программы, то сейчас мы запишем то же самое, только в виде кода C++:
int main() { CreateField();
//Генерация поля DrawField();
//Первоначальное рисование поля char c;
//В эту переменную мы считываем символ нажатой клавиши while
(!FieldIsCorrect())
//Игровой цикл (выполняется пока поле не будет собрано) { c = getch();
//Считываем нажатие клавиши switch(c)
//В зависимости от нажатой клавиши (от ее целочисленного кода)
двигаем костяшки { case 75: Move(LEFT);
break;
//Нажата клавиша "Влево" case 72: Move(UP);
break;
//Нажата клавиша "Вверх" case 77: Move(RIGHT);
break;
//Нажата клавиша "Вправо" case 80: Move(DOWN);
break;
//Нажата клавиша "Вниз" case 27: return 0;
//Нажата клавиша "Escape" } DrawField();
//Рисование поля } std::cout << "\n\nCongratulations! Press Enter to exit!";
//Поздравление игрока std::cin.get();
//Ждем ввода строки и нажатия Enter перед выходом }
И теперь нам осталось только подключить нужные нам заголовочные файлы, добавив в самое начало кода (перед чем бы то ни было) следующие строки:
#include <iostream>
//Содержит потоки ввода/вывода std::cin и std::cout и все для работы с
ними #include <stdlib.h>
//Содержит функции randomize и random #include <conio.h>
//Содержит функции clrscr и getch
Хотя чаще всего подключение заголовочных файлов происходит по мере написания кода программы. Например, если я знаю, что в программе мне обязательно придется что-то выводить или считывать с помощью стандартных потоков ввода/вывода, то первой строкой ее кода обычно будет подключение заголовочного файла iostream. В дальнейшем если я использую в коде какую-то функцию из одного из заголовочных файлов, то одновременно с ее первым появлением в коде подключается необходимый файл. Ну а если я забываю это сделать, то я подключаю его после того, как компилятор заругается на незнакомую ему функцию!)
Заключение
Итак, наша первая программа, игра "Пятнашки" наконец готова! Рассказывая вам о ее написании, я постарался как можно более подробно описать вам логику создания программ, показав, о чем нужно думать, чтобы точно и корректно превратить изначальную задумку программы в готовый и работоспособный код, достаточно простой для понимания и эффективный. Возможно, некоторые из моих рассуждений покажутся вам чересчур подробными и занудными, однако эта подробность призвана наиболее полно ответить на вопросы, встающие перед начинающим программистом, не имеющим опыта создания программ на каком бы то ни было языке программирования. Скорее всего, в дальнейшем большую часть этих рассуждений вы будете проделывать "в фоновом режиме", не разделяя их на отдельные пункты, однако, чтобы довести что-то до автоматизма, необходимо сперва понять, как точно это должно делаться. Следующий мой урок будет посвящен правильной организации кода - я расскажу о правилах, которыми нужно руководствоваться, чтобы хорошо писать программы, делая их более читаемыми и более логичными по структуре. Не пропустите!