WPF версия игры "Пятнашки" на платформе.NET. Домашнее задание


Итак, на протяжении семи уроков мы с вами овладели минимальным набором знаний и умений для написания простейших игр на C++. Однако "простейших" не значит "скучных"! Инструментов, описанных в предыдущих уроках вполне хватит, чтобы написать игру под названием "пятнашки". На ее примере я покажу, как происходит создание программы на C++, о чем вы должны думать при работе над программой, чем обусловлен выбор того или иного инструмента языка и как вообще из отдельных элементов собрать цельную программу. В книгах и учебниках по программированию почему-то эти вопросы часто игнорируются, внимание уделяется только отдельным элементам языка, поэтому новичку в программировании приходится самому изобретать велосипед, пытаясь понять, как из этого, этого и вот этого создать работающую программу. Поэтому данный урок будет посвящен скорее общим идеям программирования, в частности - программирования на C++.
Быть может, кое-что из нижесказанного покажется вам описанным излишне подробно, а что-то вы и вовсе сочтете излишним, однако я рекомендую вам очень внимательно отнестись к моим рассуждениям и советам - они содержат действительно нужные и полезные сведения, которые пригодятся вам при написании и более серьезных программ.
Итак, приступим!

Шаг первый: постановка задачи
Самый первый вопрос, который вы должны себе задать, звучит примерно так: "Что должна делать эта программа? Какие функции она должна выполнять? Какими особенностями должна она обладать? Как должна выглядеть работа программы?" Ответы на эти вопросы составят приблизительный список требований, предъявляемый к программе и помогут нам яснее представить себе, что именно нам предстоит сделать.
В нашем случае я представил себе ответы на эти вопросы (и, соответственно, реализацию игры) так: "Программа должна реализовывать игру "Пятнашки". Первоначально поле должно быть перемешано произвольным образом, а игрок должен перемещать костяшки до тех пор, пока поле не будет собрана должным образом. При этом задача должна быть обязательно разрешимой. Рисование будет производиться с помощью псевдографики, а управление должно осуществляться с помощью клавиатуры." Пока это все, что нам нужно. Исходя из такой постановки задачи можно уже представить себе примерную структуру программы.

Шаг второй: проектирование структуры программы
Следующий не менее вопрос, который вам предстоит себе задать, после того, как вы определились с конкретизацией своей цели: "Какова должна быть структура программы? Из каких частей она должна состоять? Как эти части должны взаимодействовать друг с другом?" Поверьте мне, не стоит лепить код, если вы даже близко себе не представляете, как будет организована ваша программа. В лучшем случае наделаете кучу лишнего, в худшем - просто не сможете увязать друг с другом отдельные обрывки кода. При увеличении объема проекта важность проектирования возрастает в геометрической (приблизительно!;)) прогрессии!
На этом этапе нам уже необходимо обратиться к особенностям конкретного языка программирования (вообще говоря, выбор средств и инструментов, таких как конкретный язык программирования предшествует данному этапу, но для нас такой вопрос не стоит, поэтому мы его опустим). Во-первых, как театр начинается с вешалки, так и выполнение программы, написанной на языке C++, начинается с функции main . Она должна стать центром, в котором происходит обращение ко всем прочим частям и элементам программы. Кроме того, функция main должна во многом отражать структуру программы. Чаще всего в главной функции, и в главном модуле программы содержится меньшая часть кода по сравнению со всеми прочими частями. Итак, в функцию main мы сведем все прочие элементы программы, придав им четкую и ясную структуру. Но сначала определимся с этими самыми элементами.
Работая на языке C++ мы для разделения кода данной конкретной игры применим функции, каждая из которых будет выполнять строго определенные действия, реализуя вполне определенные части поставленной задачи. Итак, как должна выглядеть игра "Пятнашки", и какими данными должна оперировать программа?
Разумеется, что центральным объектом в данной игре является игровое поле размером 4 на 4 клетки. Естественным образом будет представить его в виде массива (краткий ликбез по массивам вы найдете чуть ниже по тексту, а более полный рассказ - в следующих уроках) целых чисел - каждый элемент массива будет содержать в себе номер костяшки. Представляется, что других фундаментальных объектов нам не понадобится.
Теперь представим себе, какие действия и в каком порядке должна будет выполнять программа. Во-первых, нам нужно будет сгенерировать первоначальное расположение костяшек (помним, что мы требуем от этого начального расположения разрешимость всей задачи!). Во-вторых, нам, несомненно, потребуется рисовать поле. В-третьих, игра должна завершиться как только поле будет собрано правильно, поэтому нам понадобится функция, выполняющая проверку правильности текущего положения костяшек. Сам же процесс игры будет происходить следующим образом. Сначала программа генерирует поле с учетом всех требований. Затем вплоть до завершения сборки поля происходит игровой цикл, включающий в себя: считывание с клавиатуры нажатия клавиши, перемещение костяшек в соответствии с нажатой клавишей, перерисовку поля. И наконец (после выхода из игрового цикла, т.е. после сборки поля) программа поздравляет победителя и завершается.
И здесь мы обнаруживаем, что забыли про функцию, перемещающую костяшки! Это естественно, т.к. прикидывая в уме список функций еще до определения четкой структуры программы мы далеко не всегда можем себе представить, что нам действительно понадобится, а что - нет. К счастью, наша забывчивость не смертельна, и ошибка была обнаружена еще на раннем этапе проектирования, и ничего не мешает нам добавить в список четвертую функцию. Впрочем, в больших проектах подобные (а также более серьезные) ошибки могут произойти позже, что в итоге может привести к кардинальному пересмотру всей структуры программы. Именно поэтому так важно заранее спроектировать программу, чтобы заранее предусмотреть все возможные сложные места и потенциальные ошибки.

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

Шаг третий: объявляем переменные и пишем функции
Надеюсь, вам еще не наскучило столь продолжительное словоблудие?;) Мужайтесь, мы переходим непосредственно к программированию! Но не забывайте также, что все, что было сказано выше - действительно важно и представляет вам вполне подходящий образец рассуждений, помогающих писать понятные и эффективные программы!
Итак, начинаем оформлять все эти мысли в виде кода, понятного нашему компилятору. Первым делом мы объявим объект, который будет представлением игрового поля. Как я уже говорил выше, мы представим его в виде массива, причем двумерного, целых чисел. Для каждого типа T существует тип T[N] - массив элементов типа T. Массив представляет собой набор из N элементов (N должна быть константой) данного типа, пронумерованных от 0 до N - 1. Массив объявляется следующим образом:
Теперь, однако, мы можем вполне четко представить себе структуру программы:

К i-му элементу массива Type Array мы обратимся с помощью конструкции Array[i] :

double series;
double sum = 0;
for (int i = 0; i {
series[i] = 1 / i;
sum += series[i];
}

Многомерные массивы представляются как массивы массивов: int Matrix - массив элементов типа массив целых чисел, по сути - двумерный массив (матрица) целых чисел. Пожалуй, это все, что нам пока надо знать о массивах для написания этой программы.
Итак, мы объявим двумерный массив целых чисел, причем сделаем это в самом начале кода программы, вне блоков, заключенных в фигурные скобки, в том числе - и вне каких-либо функций. Т.е. мы объявим его в глобальной области видимости , сделав этот массив глобальной переменной . Это облегчит нам жизнь, упростив доступ к нему функций - его не надо будет передавать им в качестве аргумента. Встречайте, первая строка кода нашей программы:

Теперь же перейдем к функциям. Самой первой мы напишем функцию генерации поля, коль скоро она у нас первой и вызывается. Определимся сначала с алгоритмом, который мы будем использовать. Как известно, среди всех возможных комбинаций расположения костяшек не всякое является разрешимым. Мы же изначально требовали, чтобы при генерации было получено поле, которое можно привести к правильному виду. Зачастую эту проблему решают не слишком изящно, просто устанавливая сначала поле в правильное положение, а потом перемешивая костяшки по правилам игры. Чтобы получить пристойный результат, надо совершить достаточно большое количество случайных перемещений костяшек, да и алгоритм получается не самым простым.
Но - открою вам секрет! - на самом деле так мучиться не надо! Существует гораздо более простой и рациональный способ определить разрешимое начальное положение костяшек. Для этого мы прибегнем к помощи математической теории игры "пятнашки". Лично я необходимые для этого знания почерпнул давно - после того, как в детском саду стал обладателем книжек "Занимательные задачи для маленьких" и "Смекалка для малышей" (о том, что детям полезно читать правильные книжки даже в наш цифровой век - как-нибудь в другой раз!;)). Именно, первая из них, составленная на основе книг Я. И. Перельмана, рассказывала об истории этой игры, разрешимых и неразрешимых задачах в ней, а также о способе, позволяющем узнать, можно ли данное расположение костяшек привести к исходному. Суть его заключается в следующем. Пусть у нас имеется некоторое расположение костяшек на поле, причем в правом нижнем углу костяшек нет. Беспорядком называется такое положение костяшки, при котором она стоит ранее другой костяшки, имеющей меньший номер. Сколько костяшек с меньшими номерами стоят после данной - таково количество беспорядков для нее. Если общее число беспорядков на всем поле - четное, то его можно привести в правильное расположение, если же нечетное - то этого никак нельзя сделать согласна правилам игры.
Вооружившись этой ценной информацией, приступим теперь к написанию кода функции. Первым делом мы просто-напросто сгенерируем абсолютно случайное расположение костяшек.

void CreateField()
{
bool NumIsFree; //NumIsFree[i] показывает, определили ли мы уже позицию i-й костяшки
int Nums; //Nums[i] содержит номер костяшки, находящейся в i-й позиции
for (int i = 0; i NumIsFree[i] = true;
randomize(); //randomize позволяет при каждом прогоне программы получать разные последовательности псевдослучайных чисел
bool Ok; //Флаг, определяющий корректность выбора костяшки для данной позиции
int RandNum; //Номер костяшки, генерируемый в дальнейшем случайным образом
for (int i = 0; i {
Ok = false; //Каждый раз сбрасываем значение флага
while (!Ok) //Продолжаем случайным образом определять номер костяшки, пока он не окажется корректным
{
RandNum = random(15) + 1; //random(n) генерирует псевдослучайное число от 0 до n - 1, а нам нужно от 1 до 15
if (NumIsFree) //Если костяшка с таким номером еще свободна (помним, что массивы нумеруются начиная с нуля)
Ok = true; //то мы определили ее номер корректно
}
Nums[i] = RandNum; //Записываем этот корректный номер в i-ю позицию
NumIsFree = false; //Костяшка с этим номером теперь занята
}
}

Затем нам нужно будет посчитать общее число беспорядков на поле, и если их окажется нечетное число, то мы поменяем местами костяшки на 14-й и 15-й позиции - при этом число беспорядков изменится на единицу и станет четным. В код функции добавится следующее:

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

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

++++++++++++++++
+ 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++ ее нет). Второй способ менее прозрачный, но использует только стандартные функции. Итак, приведем пример обоих реализаций функции:

Соответственно, здесь мы не рисуем ничего в клетке, содержащей 0, т.е. в которой нет костяшки. С помощью функции cout.width(2) мы указываем программе, что при следующем выводе потока cout (ко всем остальным это уже не будет относиться) ширина поля будет не менее двух символов; если для вывода потребуется всего один символ, то перед ним будет выведен пробел, если потребуется больше двух символов, то будут выведены все они (в нашем случае этого не произойдет).

Теперь напишем функцию, осуществляющую перемещение костяшек. Именно для этой функции нам будут полезны координаты свободной клетки игрового поля - EmptyX и EmptyY: во-первых, не надо будет каждый раз просматривать все поле в поисках этой клетки, во-вторых, даже если это делать, ее координаты все равно понадобятся - для самого перемещения соседней костяшки и для проверки корректности попытки перемещения.
Сделаем небольшое уточнение по работе функции. Будем считать, что при нажатии игроком клавиш со стрелками мы перемещаем одну из костяшек на свободную клетку в направлении, соответствующем этой стрелке. Т.е. если игрок нажал клавишу "влево", то на свободное поле (влево) перемещается костяшка, находившаяся справа от него, если только это можно сделать (свободная клетка не была в правом столбце).
Логично было бы при нажатии клавиши в функции main просто вызывать функцию перемещения, передавая ей информацию о том, какая из клавиш со стрелками была нажата, т.е. куда следует передвинуть одну из костяшек, если это возможно. Поэтому функция перемещения должна иметь аргумент, обозначающий направление. Конечно, можно для этой цели использовать, например, целые значения, но лучше всего определить для понятия "направление" особый тип-перечисление и передавать в качестве аргумента переменную этого типа. Это сделать очень просто: в самом начале кода программы, непосредственно перед объявлением наших глобальных переменных напишем:

enum Direction {LEFT, UP, RIGHT, DOWN};

И теперь мы можем передавать LEFT , UP , RIGHT и DOWN (прямо так, без всяких кавычек или еще чего-то) в качестве аргументов и вообще использовать как любые другие объекты любого другого встроенного типа. Реализация же функции тогда будет простой и прозрачной:

void Move(Direction dir)
{
switch (dir)
{
case LEFT:
{
if (EmptyX {
Field = Field;
Field = 0;
EmptyX++;
}
} break;
case UP:
{
if (EmptyY {
Field = Field;
Field = 0;
EmptyY++;
}
} break;
case RIGHT:
{
if (EmptyX > 0)
{
Field = Field;
Field = 0;
EmptyX--;
}
} break;
case DOWN:
{
if (EmptyY > 0)
{
Field = Field;
Field = 0;
EmptyY--;
}
} break;
}
}

И наконец, напишем функцию, проверяющую, является ли текущее положение костяшек на поле правильным. В отличие от предыдущих, эта функция возвращает значение, именно - значение логического типа bool . Мы организуем ее код с помощью простого цикла, проверяющего первые пятнадцать позиций поля. Если во время этой проверки обнаруживается хотя бы одна костяшка, находящаяся не на месте, то функция тут же возвращает значение false , не проверяя уже последующие позиции. И только если за всю проверку не будет найдено ни одного несоответствия, и, стало быть, причин преждевременно выйти из функции, будет возвращено значение true:

Теперь мы написали все функции, необходимые для работы программы и можем приступать к написанию функции main .

Шаг четвертый: функция main и заголовочные файлы
Для того, чтобы наши разрозненные функции обрели вид законченной программы, осталось сделать совсем немного - написать функцию main , а также объявить заголовочные файлы, содержащие объявления используемых нами функций. Сначала напишем нашу самую главную функцию - в самом низу кода, под всеми объявлениями и определениями. По сути под программой можно понимать именно ее - все остальные функции будут вызываться внутри нее, сама же функция main определяет логику работы программы. Поскольку мы уже в самом начале определили структуру нашей программы, то сейчас мы запишем то же самое, только в виде кода C++:

И теперь нам осталось только подключить нужные нам заголовочные файлы, добавив в самое начало кода (перед чем бы то ни было) следующие строки:

Хотя чаще всего подключение заголовочных файлов происходит по мере написания кода программы. Например, если я знаю, что в программе мне обязательно придется что-то выводить или считывать с помощью стандартных потоков ввода/вывода, то первой строкой ее кода обычно будет подключение заголовочного файла iostream . В дальнейшем если я использую в коде какую-то функцию из одного из заголовочных файлов, то одновременно с ее первым появлением в коде подключается необходимый файл. Ну а если я забываю это сделать, то я подключаю его после того, как компилятор заругается на незнакомую ему функцию!)

Заключение
Итак, наша первая программа, игра "Пятнашки" наконец готова! Рассказывая вам о ее написании, я постарался как можно более подробно описать вам логику создания программ, показав, о чем нужно думать, чтобы точно и корректно превратить изначальную задумку программы в готовый и работоспособный код, достаточно простой для понимания и эффективный. Возможно, некоторые из моих рассуждений покажутся вам чересчур подробными и занудными, однако эта подробность призвана наиболее полно ответить на вопросы, встающие перед начинающим программистом, не имеющим опыта создания программ на каком бы то ни было языке программирования. Скорее всего, в дальнейшем большую часть этих рассуждений вы будете проделывать "в фоновом режиме", не разделяя их на отдельные пункты, однако, чтобы довести что-то до автоматизма, необходимо сперва понять, как точно это должно делаться.
Следующий мой урок будет посвящен правильной организации кода - я расскажу о правилах, которыми нужно руководствоваться, чтобы хорошо писать программы, делая их более читаемыми и более логичными по структуре. Не пропустите!

Когда то давно, обучаясь программированию, я написал свою версию игры Пятнашки на C++ Builder. Обшаривая интернет, я нашел разные алгоритмы "случайной" расстановки кнопок на поле. Попадались разные, в том числе и достаточно экзотичные, но наиболее популярным является алгоритм многократного (в некоторых случаях до 200 раз) цикличного вызова функции random . В общем бе-е-е... Тогда я придумал свой алгоритм с использованием двух контейнеров (в случае С++ это был std::vector), с нахождением случайного значения индекса элемента в контейнере, а не самого элементы. Недавно для знакомства с технологий WPF я переписал этот алгоритм на платформе.NET и вот что получилось.



using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Fifteens { public class FButton: Button { public int X; public int Y; } /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow: Window { private int _x; private int _y; private Dictionary _buttons = new Dictionary(16); public MainWindow() { InitializeComponent(); gameItem.Click += (s, e) => Random(); int i = 1; foreach (var obj in grid.Children) if (obj is FButton) { var btn = (FButton)obj; btn.X = Grid.GetRow(btn); btn.Y = Grid.GetColumn(btn); btn.Padding = new Thickness(10); btn.Click += OnFButtonClick; _buttons.Add(i++, btn); } _buttons.Add(0, null); Random(); } protected void OnFButtonClick(object sender, RoutedEventArgs e) { var button = (FButton)sender; int x = Grid.GetRow(button); int y = Grid.GetColumn(button); // При нажатии на левый Ctrl можно и по диагонали! var down = Keyboard.IsKeyDown(Key.LeftCtrl); if ((down && (Math.Abs(_x - x) == 1 || Math.Abs(_y - y) == 1)) || ((Math.Abs(_x - x) == 1 && _y == y) || (Math.Abs(_y - y) == 1 && _x == x))) { Grid.SetRow(button, _x); Grid.SetColumn(button, _y); _x = x; _y = y; } else return; if (!_new) return; bool ok = _buttons.Values .Where(b => b != null) .All(b => b.X == Grid.GetRow(b) && b.Y == Grid.GetColumn(b)); if (!ok) return; MessageBox.Show("Игра закончена!"); _new = false; } private bool _new; private void Random() { _new = true; var r = new Random(); var a = new List(16); var v = new List(_buttons.Keys); int k = 0, n = 0; for (var x = 0; x < 4; x++) for (var y = 0; y < 4; y++) { do { k = r.Next(0, v.Count); } while (a.Any(o => o == v[k])); a.Add(v[k]); v.RemoveAt(k); var button = _buttons]; if (button == null) { _x = x; _y = y; } else { Grid.SetRow(button, x); Grid.SetColumn(button, y); } n++; } } } }

Данная программа создана для демонстрации работы с Windows Forms.

Using System; using System.Drawing; using System.Windows.Forms; namespace CSharpApplication.WindowsApplicationExample { // Игра "Пятнашки" class Game: Form { // Размер стороны поля, если изменить игра станет более веселой:) const int Side = 4; // Номер "пустышки" const int Void = Side * Side; // Начальные координаты пустышки int Voidx = Side - 1, Voidy = Side - 1; // Массив кнопок Button [,] Field; // Массив значений кнопок int [,] Numbers; // Количество проделанных ходов int Moves; // Индикатор запуска игры bool IsGameRun; // Надпись для отображения прошедшего времени Label clock = new Label(); // Объект таймера Timer timer = new Timer(); // Инициализация генератора случайных чисел Random Randomize = new Random(); // Время игры TimeSpan time; static void Main() { // Запуск приложения Application.Run(new Game()); } // Конструктор - инициализация игры Game() { // Заголовок формы Text = "Пятнашки"; // Стиль рамки для формы FormBorderStyle = FormBorderStyle.Fixed3D; // Выключение кнопки для развертывания окна MaximizeBox = false; // Вычисление размера клиентской области окна ClientSize = new Size(Side * 50 + 20, Side * 50 + 50); // Цвет фона BackColor = Color.Silver; // Массив кнопок Field = new Button; // Массив чисел Numbers = new int; /*************************************************************/ /* Добавление пунктов меню /*************************************************************/ MenuItem miNewGame = new MenuItem("Новая игра", new EventHandler(OnMenuStart), Shortcut.F2); MenuItem miSeparator = new MenuItem("-"); MenuItem miExit = new MenuItem("Выход", new EventHandler(OnMenuExit), Shortcut.CtrlX); MenuItem miGame = new MenuItem("&Игра", new MenuItem {miNewGame, miSeparator, miExit}); // Создание меню и его привязка к форме Menu = new MainMenu(new MenuItem {miGame}); // Игра не запущена IsGameRun = false; // Таймер будет срабатывать каждую секунду timer.Interval = 1000; // Подключение обработчика таймера timer.Tick += new EventHandler(OnTimer); // Размещение надписи clock.Location = new Point(10, 10); // Ширина надписи clock.Width = Side * 50; // Высота надписи clock.Height = 20; // Родитель надписи (форма) clock.Parent = this; // Тонкая рамка clock.BorderStyle = BorderStyle.FixedSingle; // Цвет фона clock.BackColor = Color.DarkGray; // Текст выравнивается по центру надписи clock.TextAlign = ContentAlignment.MiddleCenter; // Шифт надписи clock.Font = new Font("Century", 14, FontStyle.Bold); // Текст надписи clock.Text = "00:00:00"; int i, j; // Инициализация поля for(i = 0; i < Side; i++) { for(j = 0; j < Side; j++) { // Создание новой кнопки Field = new Button(); // Указываем родителя для кнопки (форма) Field.Parent = this; // Задаем очередное число Numbers = i * Side + j + 1; // Если не "пустышка" if(Numbers != Void) // Отображаем число на кнопке Field.Text = Convert.ToString(Numbers); // Вычисляем координаты очередной кнопки Field.Left = 10 + j * 50; Field.Top = 40 + i * 50; Field.Width = 50; Field.Height = 50; // Шрифт кнопки Field.Font = new Font("Century", 12, FontStyle.Bold); // Ассоциируем с кнопкой ее координаты в массиве Field.Tag = new Point(i, j); // Добавляем обработчик нажатия на кнопку Field.Click += new EventHandler(OnCellClick); // Цвет текста Field.ForeColor = Color.Yellow; // Цвет фона Field.BackColor = Color.Gray; } } // Отображаем форму по центру экрана CenterToScreen(); } // Обработчик пункта меню "Выход" void OnMenuExit(object obj, EventArgs ea) { // Закрываем форму Close(); } // Обработчик пункта меню "Новая игра" void OnMenuStart(object obj, EventArgs ea) { int i, j, k; int direction; /************************/ /* Перемешивание поля /************************/ int Times = Side * 100; for(k = 0; k < Times; k++) { // Направление движения direction = Randomize.Next(4); if(direction == 0) // Вверх { // Кнопка сверху существует if(Voidx - 1 >= 0) { Numbers = Numbers; Voidx--; } else { for(i = 0; i < Side - 1; i++) { Numbers = Numbers; } Voidx = Side - 1; } } else if(direction == 1) // Вниз { // Кнопка снизу существует if(Voidx + 1 < Side) { Numbers = Numbers; Voidx++; } else { for(i = Side - 1; i > 0; i--) { Numbers = Numbers; } Voidx = 0; } } else if(direction == 2) // Влево { // Кнопка слева существует if(Voidy - 1 >= 0) { Numbers = Numbers; Voidy--; } else { for(j = 0; j < Side - 1; j++) { Numbers = Numbers; } Voidy = Side - 1; } } else // Вправо { // Кнопка справа существует if(Voidy + 1 < Side) { Numbers = Numbers; Voidy++; } else { for(j = Side - 1; j > 0; j--) { Numbers = Numbers; } Voidy = 0; } } // Новая позиция "пустышки" Numbers = Void; } // Отображение перемешанных чисел на кнопках for(i = 0; i < Side; i++) { for(j = 0; j < Side; j++) { if(Numbers != Void) { Field.Text = Convert.ToString(Numbers); } else { Field.Text = ""; } } } Moves = 0; // Игра запущена IsGameRun = true; // Начальное время time = new TimeSpan(0, 0, 0); clock.Text = "00:00:00"; // Запуск таймера timer.Start(); } // Обработчик нажатия кнопки (ход) void OnCellClick(object obj, EventArgs ea) { // Если игра не запущена if(IsGameRun == false) return; // Вынимаем "нажатый" объект Button btn = (Button)obj; // Определяем его месторасположение в массиве // по ассоциированным координатам int i = ((Point)btn.Tag).X; int j = ((Point)btn.Tag).Y; // Если нажатая кнопка расположена // слева, или снизу, или справа, или сверху от "пустышки" if(Math.Abs(i - Voidx) + Math.Abs(j - Voidy) == 1) { // Ход Numbers = Numbers; Field.Text = Field.Text; // Новые координаты "пустышки" Voidx = i; Voidy = j; Numbers = Void; Field.Text = ""; // Ход сделан Moves++; } // Если "пустышка" в нижнем правом углу if(Voidx == Side - 1 && Voidy == Side - 1) { // Если победа if(IsWinner() == true) { // Остановка таймера timer.Stop(); string msg = "Поздравляем!!!\nВы достигли успеха за "; msg += Moves; if(Moves % 10 > 1 && Moves % 10 < 5 && Moves % 100 / 10 != 1) msg += " хода."; else if(Moves % 10 == 1 && Moves % 100 / 10 != 1) msg += " ход."; else msg += " ходов."; // Остановка игры IsGameRun = false; // Отображение информационного окна MessageBox.Show(msg, "Победа!!!", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } } } // Определение выигрышной позиции bool IsWinner() { int i, j, k = 1; for(i = 0; i < Side; i++) { for(j = 0; j < Side; j++) { // Если очередное число не совпадает с порядковым if(Numbers != k) return false; k++; } } // Выигрыш return true; } // Обработчик событий таймера void OnTimer(object obj, EventArgs ea) { // Увеличиваем время на секунду time += new TimeSpan(0, 0, 1); // Отображаем полученное время clock.Text = time.ToString(); } // Обработчик активизации формы (получение фокуса приложением) protected override void OnActivated(EventArgs ea) { // Вызов базового обработчика base.OnActivated(ea); // Если игра запущена if(IsGameRun == true) // Запуск таймера timer.Start(); } // Обработчик деактивизации формы (потеря фокуса приложением) protected override void OnDeactivate(EventArgs ea) { // Вызов базового обработчика base.OnDeactivate(ea); // Если игра запущена if(IsGameRun == true) // Остановка таймера timer.Stop(); } } }