Небольшая игрушка «Сапер» не в 30 строк
В прошлый раз я показал, как можно легко и быстро написать игру «змейка» на языке программирования FBD, загрузить программу в контроллер и наконец-то заставить аппаратуру, занимающуюся управлением и автоматическим регулированием производства электроэнергии и тепла для нас всех, заняться еще и чем-нибудь «полезным».
Однако от людей, не понаслышке знакомых с работой оперативного персонала на электростанциях, я получил важное замечание о том, что «змейка» категорически не подходит для АСУ ТП по объективным причинам. Во-первых, несмотря на то, что со всей автоматикой на станции управляется система АСУ ТП и она же занимается регулированием и защитами, реалии жизни таковы, что от оператора также требуется следить за технологическим процессом для оперативного вмешательства в него по мере необходимости. Поэтому игрушка должна быть такой, чтобы (в отличие от ранее представленной) не занимать полностью внимание оператора, позволять ему без ущерба для игры переключаться между приложениями и делать что-нибудь. А во-вторых, сама игрушка «змейка» весьма динамичная и требует быстрого (а на высоких уровнях вообще мгновенного и ювелирного) нажимания на кнопки, что может легко привести скажем к небольшой ошибке: например вместо управления игрушкой можно случайно поуправлять каким-нибудь важным технологическим оборудованием, которое при данном режиме работы трогать было ну никак нельзя. Разумеется ничего страшного не произойдет т.к. в любом случае отработает защита, но останов турбины или отключение котла защитой, это вещи, приводящие к солидным финансовым потерям и лишнему труду по запуску их обратно в работу.
Все это наводить на очевидную мысль, что реализовывать нужно спокойные игрушки на логику. Как вариант — всевозможные пасьянсы или всем известный «Сапер». Ввиду того, что пасьянсы всех видов требуют изображения карт, который не были сходу найдены в интернете (разумеется рисунки карт найти легко, но у меня были особые требования к размеру и качеству а так же оформлению карт) было решено реализовать игрушку «Сапер».
Помимо этой есть еще одна важная причина в реализации именно этой игры. А именно желание еще раз наглядно показать как легко и весело программировать на языке FBD.
Несколько вступительных словНо для начала несколько вступительных слов (для тех кто еще не слишком устал читать).
Наверное каждый, кто прочтет этот пост до конца, скажет (или подумает): «Фигня! Да я то же самое на С (С++, Delphi, JS, и т.д.) напишу в 30 строчек кода». И я с этим согласен. Но есть одно но. Прежде чем написать что нибудь на языке высокого уровня в 30 строчек кода нужно всего-навсего
Разумеется и для этого языка есть серьезные алгоритмы, борьба за ресурсы контроллера, оптимизацию выполнения техпрограммы, оптимизацию памяти и быстродействия в целом. Умные люди ломают головы над тем, как создать идеальные алгоритмы из простейших блоков. Но т.к. эта статья носит чисто ознакомительный характер, то мы опустим все эти тонкости и перейдем наконец непосредственно к самому программированию.
Но перед этим небольшой вопрос на сообразительность. Кто сам догадается — «респект и уважуха», для нетерпеливых же — ответ в спойлере ниже. Так вот — игра «Змейка» и игра «Сапер» это одна и та же игра с точки зрения программирования на FBD. Почему? И что у них общего, что позволяет сделать такое утверждение?
Начинаем программироватьИтак, начинаем программировать нашу задачу. Лично мое мнение заключается в том, что 80% программирования на языке FBD заключается в том, чтобы четко себе представить, а что же такое мы хотим получить в конечном итоге. И так только мы добиваемся такого понимания, то 80% задачи решены и остается буквально немного работы по наброске кода и его причесыванию. Сейчас попробую продемонстрировать этот принцип на практике.
Предположим что поле для «сапера» у нас будет по аналогии со «змейкой» 20х20 клеток. Следовательно нам понадобится 400 ячеек памяти, в каждой из которых будет обрабатываться каждая клетка поля. Чтобы структурировать программу — разобьем все ячейки на строки. Таким образом нам понадобится двадцать алгоритмов строк, каждый из которых будет состоять из 20 алгоритмов ячеек. Т.е. мы пришли к пониманию основы программы.
Алгоритм «Строка»Рассмотрим подробнее алгоритм строки. А точнее то, какие данные нам нужно иметь на входе и какие — на выходе этого алгоритма. Предлагаю начать с конца — т.е. с выходных данных.
Выходы алгоритма— Во-первых, нам понадобится выходная строка данных, определяющая состояние каждой ячейки для отрисовки ее в человеко-машинном интерфейсе (или проще говоря в операторской станции). — Во-вторых, нужен вектор, показывающий в каких ячейках находятся бомбы, а какие ячейки безопасны. — В-третьих, нужен признак того, что игрок «наступил» на поле с бомбой и игра проиграна, назовем этот выход «Big_Bada_Boom». — В-четвертых, для удобства в «сапере» заложена фишка, что при попадании на клетку не имеющую соседства с бомбой, автоматически открываются все пустые клетки, граничащие с ней. Т.е. нам понадобится выход, посылающий обратную команду «открыть клетку» самой программой. — В-пятых, для алгоритма установки бомб нам понадобится выход, показывающий, сколько бомб находится в данной строке. Разумеется это значение можно получить напрямую из вектора, описанного в пункте 2, достаточно просто сложить все ненулевые биты. Но для удобства внесем эту функцию внутрь макроса. — В-шестых, для ведения статистики нам понадобится знать, сколько еще закрытых клеток осталось в строке. Вот и все, что нам нужно для полного функционала игры «сапер».
Всего 6 выходов с данными.
Теперь подумаем, что же нам нужно иметь на входе, чтобы суметь сформировать требуемые нам выходы.
Входы алгоритма— Логично предположить, что главный вход алгоритма это команда от игрока «Открыть ячейку». Ведь это основной смысл игры. — Для наглядности программирования задействуем еще один вход — «Поставить на клетку флаг», помечающий что там бомба и не дающий случайно нажать на эту клетку и взорваться. — Для формирования статуса каждой ячейки (а мы то с вами помним, что если в ячейке нет бомбы, то она показывает сколько бомб находится в клетках, граничащих с ней) понадобится завести вектора со строки выше и строки ниже. Сделаем алгоритм универсальным и добавим три входа: вход для вектора с бомбами в строке выше текущей, вход для вектора с бомбами в текущей строке и вход для вектора с бомбами в строке ниже текущей. — Для запуска новой игры и переписывания значений ячеек добавим логический вход «Новая игра». — Как уже упоминалось ранее, если клетка пустая и рядом нет клеток с бомбами, то должны автоматически открываться соседние с ней клетки. Добавим для этого вход «Открыть программой». — Ну и разумеется какая же это игра без заминированного поля. Значит нам понадобится вход «Установить бомбы».
Всего получилось 8 входов.
Вот собственно и все, что нам нужно от главного алгоритма программы. Набиваем быстро макрос с нашими входами и выходами и получаем вот такой алгоритм: или если раскрыть его: Теперь наполним наш алгоритм смыслом. Как я уже говорил — основа наполнения нашего главного алгоритма это макрос «Ячейка памяти», которых будет 20 штук.
Алгоритм «Ячейка памяти»Попробуем продумать, что нам может понадобиться от каждой ячейки поля игры «сапер», и что мы должны для этого подавать на вход макроса. Начнем опять с конца, т.е. с выходов. А когда сформулируем все данные, что нам нужны, то станет понятно, что нужно иметь на входе, чтобы их получить.
Выходы алгоритма— Отдельно для подсчетов и прочей обработки вынесем логические выходы «Ячейка закрыта» и «В ячейке установлена бомба». — Как уже говорилось — если открыта пустая ячейка, то она должна автоматически открыть соседние с ней ячейки. соответственно нам нужнен еще логический признак «Открыть соседей». — Ну и в конце-концов если игрок ошибся и «наступил» на поле с бомбой, то нужно сформировать логический признак того, что игра проиграна. назовем его «Bada_Boom». Итого всего 5 выходов.
Входы алгоритмаПопробуем определиться, что же нам нужно для того, чтобы сформировать требуемые выходы. — Прежде всего алгоритм должен принимать команду от игрока «Открыть поле». — Для простоты отдельной командой заводим сюда еще и команду «Поставить флаг». — Как я уже говорил ранее — каждая клетка в поле должна знать, сколько заложено бомб в соседних клетках. заведем для этого отдельный вход «Соседи». — Для того, чтобы случайным образом установить вначале бомбы нужен логический признак «Здесь бомба» по которому клетка будет минироваться. — Ну и разумеется для начала новой игры должен быть признак сброса, который обнуляет всю информацию в клетках.
Набиваем и этот макрос получая следующий алгоритм:
Теперь запрограммируем сам алгоритм «Ячейка памяти». Он предельно простой:
— На алгоблок «БитДешифр1» приходит вектор, сформированный из состояния соседей, который распаковывается на этой алгоритме и далее на алгоблоке «Слож1» подсчитывается количество единичек (бомб) в векторе. Сразу возникает вопрос: почему здесь 9 бит, когда у клетки всего 8 соседей максимум. Отвечаю: для универсальности. Дело в том, что как видно будет позже, я написал еще один макрос для нахождения соседей у каждой клетки. И для того, чтобы макрос получился универсальный для всех случаев (а клетка может иметь не только 8 соседей, когда она находится в центре поля, но и всего 3, когда она стоит в углу, или 5, когда она стоит на краю поля) пришлось задействовать девять бит. — Триггер «RSтриг1» служит для установки и сброса «флажка» на клетке. Первая команда взводит триггер, а повторная команда сбрасывает. Как видно из схемы, взведенный триггер, блокирует прохождение команды «Открыть» на алгоблоке «И2» т.к. значение с выхода триггера заведено на вход алгоритма «И» с инверсией. Т.е. пока триггер взведен, на второй вход алгоритма «И» поступает «False» и выход алгоритма «И» тоже равен «False» независимо от значения на первом входе. Получается, что команда «Открыть» не может сбросить триггер «RS2» и мы застрахованы от случайного открытия клетки, которую не хотим открывать. — Триггер «RS2» служит для формирования статуса ячейки: закрыта она или открыта. Как видно из схемы триггер взводится командой «Сброс» (начало новой игры) и сбрасывается только по приходу команды «Открыть». — Триггер «RS1» служит для индикации наличия бомбы в этой ячейке. Он взводится при поступлении от программы команды «Здесь бомба», которая устанавливает бомбы в ячейку и сбрасывается с началом новой игры. — Алгоритм «И3» один из самых важных в этой схеме. на нем по И складывается значение триггера «RS1» (тут установлена бомба) и прихода команды «открыть» в эту ячейку. Если оба условия выполнились, то на выходе формируется «True», означающее, что игрок наступил на поле с бомбой. Это значение подается на выход «Bada_Boom», которые потом со всех ячеек собираются на выходе «Big_Bada_Boom» и означают проигрыш. — Три алгоритма «Выбор», расположенные каскадом, формируют статус ячейки. На алгоритме «Выбор1» формируется, что подавать на выход в случае открытия ячейки: числа «0-8», соответствующие количеству бомб в ячейках по соседству, или число «9» соответствующее заминированной клетке. Если поле у нас еще закрыто (Триггер «RS2» взведен), то на алгоритме «Выбор2» устанавливается значение "-1", которое, как мы договорились ранее, соответствует статусу закрытой ячейки. Если поле закрыто (Триггер «RS2» взведен) и одновременно (алгоритм «И4») взвелся триггер «RSтриг1», то на алгоритме «Выбор3» значение подменяется принудительно на «10», соответствующее установленному флагу. — Последний алгоритм сравнения служит для автоматического открытия соседних клеток. Если на выходе «Выбор3» мы получили ноль, значит игрок открыл клетку, рядом с которой нет бомб. Тогда отправляется команда на открытие соседних клеток.
Вот этот примитивный алгоритм и есть основа всей игры. остались только некоторые причесывания программы и допиливание функционала.
Теперь приступим к макросу «Строка», состоящему из 20 «Ячеек памяти».Идея очень проста. К нам приходит три вектора и нужно посчитать, сколько же мин окружают каждую клетку на конкретной позиции. Для этого я намеревался просто сдвинуть вектор вправо на (позицию — 2) и посчитать количество единичек в первых трех битах каждого вектора после сдвига. Но тут я наткнулся на забавную вещь. А что делать если позиция клетки первая? тогда по аналогии с остальными я должен сдвинуть вектор на отрицательную величину (т.е. не вправо, а влево). Разумеется все передвинутые слева биты будут нулевые, но зато общий принцип подсчета будет сохранен. Однако тонкости реализации алгоритма не позволили это сделать, поэтому пришлось ставить проверку на позицию клетки и для первой позиции клетки проводить отдельную обработку. Собственно говоря эта обработка заключается только в обработке второго бита в текущей строке и третьих битов во всех строках. Помните, я писал о том, что для стандартного макроса мне понадобилось формировать 9 бит соседей, а не 8, как того требует логика. Это связано именно с этой ситуацией.
Самые наблюдательные читатели уже заметили, что здесь выполняется ну явно лишняя операция. Я получаю биты, описывающие соседей, упаковываю их в вектор, а затем опять распаковываю и суммирую. Т.е. операции по упаковке-распаковке явно избыточны. Но я оставил такой вариант для отладки и наглядности.
Отдельно хотел упомянуть про еще один макрос. Перед стартом игры нам нужно случайным образом расположить в поле мины. Но что делать, если нет генератора случайных числе и команда «Random» недоступна. Разумеется в интернете есть множество алгоритмов формирования псевдослучайных последовательностей. Но они довольно сложные и их реализация сама по себе заслуживает отдельной статьи. Поэтому пришлось пойти привычным путем и сделать макрос «Генератор Random».
Соединяем все части вместе и добавляем красотыКак видно из рисунка — самый главный действующий алгоритм тут — «РучСелектор1» который выдает на выходе логическую единицу на один цикл и запускает новую игру. Запуск новой игры заключается в том, что сигнал подается на все макросы «Строка» на вход «Новая_игра» и по этому сигналу происходит установка триггера «SR2» в макросе «Ячейка_памяти» и сброс всех остальных триггеров, отвечающих за наличие бомб и флагов в ячейке. Одновременно этот же выход алгоритма «РучСелектор1» сбрасывает триггера «RS1» («Game_over» — этот триггер взводится в случае открытия игроком клетки, в которой пряталась бомба, и триггер «RS3» («Victory»), который взводится в случае победы игрока, т.е. обнаружении всех спрятанных в поле бомб. Одновременно тот же самый сигнал посылается на вход алгоритма «Память1» для записи в память количества устанавливаемых бомб в текущей игре (игрок моет менять количество бомб в поле в любой момент, но вот в программу эти настройки попадут только с началом новой игры). Далее с задержкой на цикл (алгоритм «Задержка1») (который нужен чтобы сбросить все триггера в ячейках памяти) взводится триггер «RS2» («Ставим_бомбы»), который включает процедуру установки бомб. Т.е. передачи наших случайных координат, сформированных на выходах макросов «Генератор_Random» на алгоритмы «Столбец» и «Строка», посылающие по заданным координатам команду на взвод триггера «RS1» в «Ячейке памяти». Контроль количества установленных бомб простой. Подсчитывается общее количество установленных бомб и как только оно сравнивается с числом, заданным на алгоритме «Память1», сбрасывается триггер «SR2» тем самым прекращая установку новых бомб.
Алгоритмы 36-40 служат для проверки корректности заданного числа бомб игроком. Здесь я для отладки ограничил минимальное и максимальное количество бомб в 0 и 400 штук соответственно. При задании числа, выходящего за этот диапазон, оно автоматически приравнивается к ближайшей границе.
Проверка на выполнение условий победы предельно простая. Считаются все закрытые клетки. Как только их количество станет равным заданному количеству бомб и триггер «RS1» («Game_over») не взведен, взводится триггер «RS3» («Victory»).
Нам осталось рассмотреть всего 2 алгоритма: Алгоритм «ИЛИ2». На нем по логическому ИЛИ собираются выходы всех трех триггеров. И по выходу алгоритма «ИЛИ2» (когда он равен единице) выставляется блокировка на действия игрока. Т.е. пока хоть один из триггеров взведен (Т.е. или игрок уже победил, или он уже проиграл, или идет установка мин для новой игры) игроку запрещаются любые нажатия в поле. Он не может ни открывать ячейки, ни ставить флажки. Алгоритм «Секундомер1» служит секундомером для отсчета времени, затраченного на игру. Подсчитываем количество закрытых ячеек. В начальный момент каждой игры оно равно 400. Как только число закрытых ячеек становится меньше чем 400, запускается таймер, который останавливается по взведению одного из трех триггеров.
Вот собственно и вся программа.
Прикручиваем графический интерфейс Подводим итогиТот, кто смог осилить этот пост целиком наверняка заметил, что мы написали пусть и простую, но все-таки не уровня «Hello, World!» программу, и при этом нам не потребовалось абсолютно никаких справочников, хелпов, сидений на форумах программистов, курения мануалов и т.п. Все что было нужно мы реализовали за пару часов (и то лично у меня большая часть времени ушла на красивую расстановку алгоблоков для картинок), при этом использовав понятные даже школьнику алгоритмы сложения, вычитания, умножения и алгоритмы логики «И», «ИЛИ». Плюс задействовали несколько простейших RS-триггеров. Самый сложный алгоритм, использованный в программе, это «Интегратор». И то мы с вами использовали его просто как сумматор с обратной связью и проверкой границ. Т.е. фактически можно заменить интегратор на алгоритм «Сложение» и два алгоритма «Сравнение». Другими словами, программировать на FBD просто и весело, и на начальном уровне не требуется вообще никаких знаний и навыков кроме понимания элементарной логики. Главным при этом является четкое представление себе конечного результата. Если оно имеется, то сама программа является его логическим довершением. Если же представить «А что же мы хотим в итоге получить» пока не получается, то возможно стоит четче сформулировать задачу. Сразу оговорюсь, что все вышесказанное применимо не ко всем проектам, реализованным на FBD, а к простым маленьким задачкам, вроде той, что приведена в нашем примере. Т.к. для огромных проектов на десятки тысяч сигналов соединить это все в голове наверное не получится ни у кого. Но и в этом случае огромный проект разбивается на небольшие подзадачи, где наш подход уже становится вполне применим (разумеется с учетом общих требований к проекту). Т.к. язык FBD универсальный и мы в программе задействовали простейшие алгоритмы, являющиеся базовыми для любых реализаций, то данную программу можно с минимальными усилиями воспроизвести на любом контроллере, как отечественной разработки, так и иностранных (например на контроллерах фирмы Siemens или ABB).
Из того, что осталось нереализованнымНереализованными остались три функции классической игры сапер. Первая, это возможность в настройках задавать размер поля. Делается это несложно. Собственно говоря просто алгоритмы «Ячейка памяти» копируются столько раз, сколько клеток должно быть в поле. Проблема тут одна: отсутствие динамической памяти, т.е. невозможности «на лету» добавлять или убирать какие либо данные. А из этого следует простой вывод — сделать изменяющиеся размеры поля очень просто — нужно сначала запрограммировать задачу под максимальный размер поля, а потом просто выставлять логический признак на незадействованные ячейки, которые по этому признаку не будут отображаться в графическом интерфейсе и не будут обрабатываться при игре. Вторая — это подмена ячейки при первом клике, если в ней изначально находилась бомба. Т.е. в нашей программе в отличие от «сапера» в Windows есть шанс проиграть на первом же клике. Это делается тоже легко, достаточно проверять пришедший сигнал на триггер «Game_over» и количество закрытых клеток. Если у нас число закрытых клеток 399 и пришел сигнал на взведение триггера «Game_over», то нужно заблокировать этот сигнал, сбросить ту ячейку, на которую был произведен клик и запустить алгоритм случайной установки еще одной мины, при этом заблокировав возможность установки мины в уже открытую ячейку. Ну и как видно из описания — не реализована функция сбора статистики. Т.е. не запоминается лучший результат, имя человека, поставившего рекорд. Соотношение побед и поражений и т.п. Но это все настолько тривиально, что делается буквально парой щелчков мыши, поэтому эту функцию оставляю на реализацию всем желающим.