Давно хотел написать полезную в хозяйстве программу на Си. Этот язык привлекал своей чистотой и высокими требованиями к программисту. Здесь нет такого разгильдяйства, как в PHP или JS, где можно особенно не думать про типы данных и писать код, как придётся. Либо ты делаешь всё, как надо, либо ничего не работает.

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

Кстати, скачать и поиграть можно здесь.

Код на Гитхаб посмотреть здесь. Дальше опишу, как проходила разработка и какие решения применялись. Сразу отмечу, что статья в большей степени может заинтересовать начинающих программистов, профи вряд ли найдут для себя что-то новое.

Стек технологий

Применялась среда разработки CodeBlocks, бесплатная, но для простых задач вполне достаточная. Мне она нравится, поскольку даёт хорошие возможности для дебаггинга, позволяет делать точки останова и следить за состоянием стека вызова. Ещё один плюс – уже встроены нужные компиляторы. Её родной сайт.

Для создания графического интерфейса применял кроссплатформенную библиотеку GTK+, которая предоставляет набор форм и элементов управления. Поскольку конструировать кнопки и области отображения в голове задача довольно трудная, использовал графический редактор Glade. В нём можно нарисовать готовую форму, сгенерировать файл в формате xml и подключить в коде. Хорошую русскую документацию по GTK можно почитать здесь, по Glade — здесь.

Наконец, последний инструмент – графическая библиотека Cairo для оживления игрушки. Подробные доки здесь.

Общая концепция

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

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

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

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

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

Рисуем интерфейс

Поскольку чукча не UX/UI дизайнер, то интерфейс был выбран простенький. Вот основные элементы:

1 Меню. В нём можно запустить программу на 1 из 5 уровней, выбрать одну из двух цветовых схем, включить подсказку и активировать окошечко «Об игре».

2 Три области рисования: для игрового поля, для лица персонажа, для звёздочек – подсказок.

3 Таблица кнопок от 1 до 9 для ввода цифр.

Ещё была строка состояния, в которой по идее, программа должна была текстом говорить о происходящем. Она была нарисована, но руки до её логики так и не дошли.

Вот так выглядит поле.

А так дерево элементов, сгенерированное Glade.

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

Также на этом этапе были установлены события для нужных элементов, которые здесь называются «сигналами». Для кнопок – on_clicked, для элементов меню – on_activated, для областей отрисовки – on_expose_event.

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

К сожалению, Glade не слишком порадовал. Накидать элементы на форму, конечно, просто. Но программа опенсорсная, кроссплатформенная, а значит, глючная. По крайней мере, в 10Win она постоянно вылетала при редактировании меню, так что приходилось сохранять каждый шаг. Во-вторых, в Glade нет обратной совместимости форматов. Когда я, помучившись, решил попробовать более свежую версию, выяснилось, что проект, созданный в старой версии, открываться не хочет.

Впрочем, работать с ним в Си оказалось достаточно просто. Для этого создаётся элемент типа GtkBuilder, потом он загружается из файла функцией gtk_builder_add_from_file. Затем отдельные виджеты GTK извлекаются из билдера функцией gtk_builder_get_object. После этого с ними можно работать, уже используя все возможности GTK – менять свойства, вешать обработчики.

Графические ресурсы

Их не так много.

1 Поле для судоку из клеточек 9 на 9, в котором внутри выделены квадратики 3 на 3.

2 Иконка программы.

3 Картинка с 5 звёздочками для подсказок. Там отрисованы ряды от 0 из 5 до 5 из 5. По мере надобностей в поле игры выбирается и отображается нужный фрагмент картинки. Обычный приём для десктопных игрушек.

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

Архитектура приложения

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

main.c – точка входа, обработчики событий кнопок, меню, зон графики, инициализация графических ресурсов, функция старта игры.

view.c – набор функций для работы с графикой: отображение пустого поля игры, вывод символов на поле, вывод окошек с информацией, вывод лица, реагирующего на события игры, вывод информации о подсказках.

model.c – логика игры. Здесь реализованы два главных алгоритма: инициализация поля и проверка победителя. Также сюда добавилось несколько вспомогательных функций: генерация подсказки, инициализация вспомогательных массивов, обмен местами двух колонок и обмен местами двух рядов.

В общем, с некоторой натяжкой получилась всем привычная структура MVC-проекта, с контроллером в main.

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

Итак, к каждому программному файлу был написал одноимённый заголовочный, в котором содержались сигнатуры функций и используемые переменные. Заголовочные файлы взаимно подключались друг к другу. main.h содержал инклуды на остальные два файла, а каждый из них подключал main.h.

Представление данных

Поскольку объектов в чистом Си нет, пришлось использовать массивы. По мере работы их количество разрослось до трёх.

int Board[ROWS][ROWS] – массив, c игровым полем. Он весь заполнен исходными данными, нулей в нём нет.

int Request[ROWS][ROWS] – это актуальное состояние игры. Нули – пустые поля, цифры – то, что отображается на экране.

int Status[ROWS][ROWS] – массив со статусом ячеек. 0 – пустая, 1 – исходное значение, 2 – значение, которое ввёл пользователь.

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

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

Другие глобальные переменные:

int helpItem – подсказки.

int diffLevel – уровень сложности.

int pointerX – указатель на выбранную ячейку.

int pointerY – указатель на выбранную ячейку.

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

int gameStatus – 0/1 игры нет/игра идёт. Чтобы отключить часть функционала после окончания игры и до её начала.

int colorProfile – цветовая схема. Пока может принимать только два значения: 0 – ч/б 1 – цветная.

Алгоритмы

Интересных алгоритмов в игре всего два.

Во-первых, генерация исходного поля.

Здесь всё просто. Изначально массив заполняется подходящей комбинацией, после чего рандомно несколько раз вызываются функции перемены местами двух столбцов и двух строк. Перемешивать можно строки, которые входят в малую группу. То есть, например, строки 1-2-3 спокойно можно менять местами, точно так же строки 4-5-6 и 7-8-9. Аналогично поступаем со столбцами.

Во-вторых, проверка на выигрыш.

Здесь сначала убеждаемся, что все клетки заняты. Потом запускаем цикл по цифрам от 1 до 9. Внутри него сначала пробегаемся по всем строкам. Убеждаемся что в каждой строке есть только 1 из проверяемых цифр. Если их две или такой цифры нет – прерываемся и выходим. Аналогично проверяем столбцы и маленькие квадратики 3 на 3.

Графика

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

В программе она решает две задачи.

1 Выводим текст в зону рисования. Это цифра либо знак вопроса.

Для этого создаёт структуру типа cairo_t с помощью команды gdk_cairo_create – это поверхность рисования, которая привязана к конкретному виджету. Устанавливаем тип, размер и цвет шрифта (команды cairo_select_font_face, cairo_set_font_size и cairo_set_source_rgb). Затем устанавливаем смещение области рисования (cairo_move_to) и выводим символ (cairo_show_text). После чего поверхность уничтожаем командой cairo_destroy.

2 Выводим фрагмент картинки.

Аналогично создаётся поверхность рисования, командной cairo_set_source_surface добавляется источник – требуемая картинка со смещением. Затем командой cairo_rectangle указывается область, которая командой cairo_fill.

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

Сюрпризы

О том, что Glade неприятно удивил отсутствие обратной совместимости и вообще плохим отношением к Windows, я уже упоминал.

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

Третье, и, пожалуй, самое неприятное – современные антивирусы не любят всё, самостоятельно скомпилированное. Поэтому итоговую программу было проблематично переслать через соцсети и запустить на компьютерах с Avast. На других машинах не проверял. Решить проблему можно с помощью онлайн-сервисов, которые проверят эвристику и внесут программу в белые листы антивирусных баз. Но это долго, да и пока не очень требуется.

Итоги

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

С другой стороны, поработать на низком уровне всегда интересно. В перспективе это не только простые логические игрушки. С помощью функционала GTK и Glade удобно загружать данные из файла, быстро обработать их средствами Си, сохранять и визуализировать. Так можно писать, например, простые конвертеры, быстрые обработчики данных, технические калькуляторы, выполняющие расчёты по ГОСТам и СНиПам. В таком случае этот стек технологий смотрится перспективно.