Игра Змейка с автопилотом
1C-admin
15.05.2019
Игра Змейка с автопилотом реализована в парадигме автоматного программирования.
Игра реализована в парадигме автоматного программирования. Для ознакомления с этой парадигмой и вообще с конечными автоматами крайне рекомендую книгу Поликарпова Н.И., Шалыто А.А. Автоматное программирование — http://is.ifmo.ru/books/_book.pdf
Дополнительно рекомендую сайты:
http://is.ifmo.ru/automata/
http://softcraft.ru/auto/
ПС: игра делалась по следующему алгоритму — сначала была написана спецификация, то есть спроектированы конечные автоматы, а уже потом всё было закодировано.
Разработка велась на платформе 8.3.10.2561.
Спецификация
А0. Головной автомат
Смешанный автомат событийного типа. Описывает общую логику программного продукта. Запускается только по событию.
Схема связей
Граф перехода (диаграмма состояний)
&НаКлиенте
Процедура А0_ГоловнойАвтомат(е)
у = Состояния.у0;
ИмяА= "А0_ГоловнойАвтомат";
ЛогНачалоА(ИмяА, у, е);
Если у = "1_Игра" Тогда
А3_ОбработкаСобытийИгры(е);
Если е = "е7_НажатаКнопкаЗавершитьИгру" Или Состояния.у2 = "2_ТелоЗмейки" Или Состояния.у2 = "3_Препятствие" Тогда
у = "0_Стоп";
ИначеЕсли Состояния.у2 = "1_Яблоко" И х4_СледующийУровень() Тогда
у = "3_СледующийУровень";
ИначеЕсли е = "е5_НажатаКнопкаИграть" Тогда
у = "2_Пауза";
Иначе
z7_Обработчик_Ожидания(1);
КонецЕсли;
ИначеЕсли у = "0_Стоп" Тогда
Если е = "е5_НажатаКнопкаИграть" И Не х5_ЭтоПервыйУровень() Тогда
z11_ДиалогОСледующемУровне(1);
ИначеЕсли е = "е5_НажатаКнопкаИграть" Или е = "е9_ПродолжитьТекущийУровень" Тогда
у = "1_Игра";
z9_УстановитьДоступностьКнопкиЗавершить(1);
z10_Игра(2);
ИначеЕсли е = "е10_НачатьСначала" Тогда
у = "1_Игра";
z9_УстановитьДоступностьКнопкиЗавершить(1);
z10_Игра(0);
z10_Игра(2);
ИначеЕсли е = "е0_Инициализировать" Тогда
z10_Игра(0);
КонецЕсли;
ИначеЕсли у = "2_Пауза" Тогда
Если е = "е7_НажатаКнопкаЗавершитьИгру" Или Состояния.у2 = "2_ТелоЗмейки" Или Состояния.у2 = "3_Препятствие" Тогда
у = "0_Стоп";
ИначеЕсли е = "е5_НажатаКнопкаИграть" Тогда
у = "1_Игра";
КонецЕсли;
ИначеЕсли у = "3_СледующийУровень" Тогда
Если е = "е8_НачатьСледующийУровень" Тогда
у = "1_Игра";
z10_Игра(1);
z10_Игра(2);
Иначе
у = "0_Стоп";
КонецЕсли;
Иначе
Лог(СтрШаблон("Ошибка в автомате %1: неизвестный номер состояния! - %2", ИмяА, у));
КонецЕсли;
Если у <> Состояния.у0 Тогда ЛогПереходА(ИмяА, Состояния.у0, у);
Состояния.у0 = у;
Если у = "1_Игра" Тогда
А3_ОбработкаСобытийИгры("е0_Инициализировать");
z8_ОтбразитьНадписьНаКнопкеИграть(2);
z7_Обработчик_Ожидания(1);
ИначеЕсли у = "0_Стоп" Тогда
z7_Обработчик_Ожидания(0);
z8_ОтбразитьНадписьНаКнопкеИграть(1);
z9_УстановитьДоступностьКнопкиЗавершить(0);
ИначеЕсли у = "2_Пауза" Тогда
z7_Обработчик_Ожидания(0);
z8_ОтбразитьНадписьНаКнопкеИграть(3);
ИначеЕсли у = "3_СледующийУровень" Тогда
z7_Обработчик_Ожидания(0);
z11_ДиалогОСледующемУровне(0);
Иначе
Лог(СтрШаблон("Ошибка в автомате %1: неизвестный номер состояния! - %2", ИмяА, у));
КонецЕсли;
КонецЕсли;
ЛогКонецА(ИмяА, е, у);
КонецПроцедуры
А3. Обработка событий игры
Автомат Мили событийного типа.
Схема связей
Граф перехода (диаграмма состояний)
&НаКлиенте
Процедура А3_ОбработкаСобытийИгры(е)
у = Состояния.у3;
ИмяА= "А3_ОбработкаСобытийИгры";
ЛогНачалоА(ИмяА, у, е);
Если у = "0_РучноеУправление" Тогда
Если х6_УстановленАвтопилот() Тогда
у = "1_Автопилот";
z13_ПостроитьМаршрутДоЯблока();
z14_ОбработатьНаправлениеПоМаршруту(е);
Иначе
z15_ОбработатьНаправлениеПоСобытию(е);
КонецЕсли;
ИначеЕсли у = "1_Автопилот" Тогда
Если Не х6_УстановленАвтопилот() Тогда
у = "0_РучноеУправление";
z15_ОбработатьНаправлениеПоСобытию(е);
ИначеЕсли е = "е0_Инициализировать" Или Состояния.у2 = "1_Яблоко" Тогда
z13_ПостроитьМаршрутДоЯблока();
z14_ОбработатьНаправлениеПоМаршруту(е);
Иначе
z14_ОбработатьНаправлениеПоМаршруту(е);
КонецЕсли;
Иначе
Лог(СтрШаблон("Ошибка в автомате %1: неизвестный номер состояния! - %2", ИмяА, у));
КонецЕсли;
Если у <> Состояния.у3 Тогда ЛогПереходА(ИмяА, Состояния.у1, у);
Состояния.у3 = у;
КонецЕсли;
ЛогКонецА(ИмяА, е, у);
КонецПроцедуры
А1. Направление змейки
Смешанный автомат событийного типа.
Схема связей
Граф перехода (диаграмма состояний)
&НаКлиенте
Процедура А1_НаправлениеЗмейки(е)
у = Состояния.у1;
ИмяА= "А1_Движение";
ЛогНачалоА(ИмяА, у, е);
Если у = "1_Верх" Тогда
Если е = "е1_НажатаКнопкаВерх" Или е = "е6_СработалОбработчикОжидания" Тогда
z1_СдвинутьКоординатыГоловы(1, у);
z6_СделатьШаг();
ИначеЕсли е = "е2_НажатаКнопкаПраво" Тогда у = "2_Право";
ИначеЕсли е = "е3_НажатаКнопкаНиз" Тогда
ИначеЕсли е = "е4_НажатаКнопкаЛево" Тогда у = "4_Лево";
КонецЕсли;
ИначеЕсли у = "2_Право" Тогда
Если е = "е1_НажатаКнопкаВерх" Тогда у = "1_Верх";
ИначеЕсли е = "е2_НажатаКнопкаПраво" Или е = "е6_СработалОбработчикОжидания" Тогда
z1_СдвинутьКоординатыГоловы(2, у);
z6_СделатьШаг();
ИначеЕсли е = "е3_НажатаКнопкаНиз" Тогда у = "3_Низ";
ИначеЕсли е = "е4_НажатаКнопкаЛево" Тогда
КонецЕсли;
ИначеЕсли у = "3_Низ" Тогда
Если е = "е1_НажатаКнопкаВерх" Тогда
ИначеЕсли е = "е2_НажатаКнопкаПраво" Тогда у = "2_Право";
ИначеЕсли е = "е3_НажатаКнопкаНиз" Или е = "е6_СработалОбработчикОжидания" Тогда
z1_СдвинутьКоординатыГоловы(3, у);
z6_СделатьШаг();
ИначеЕсли е = "е4_НажатаКнопкаЛево" Тогда у = "4_Лево";
КонецЕсли;
ИначеЕсли у = "4_Лево" Тогда
Если е = "е1_НажатаКнопкаВерх" Тогда у = "1_Верх";
ИначеЕсли е = "е2_НажатаКнопкаПраво" Тогда
ИначеЕсли е = "е3_НажатаКнопкаНиз" Тогда у = "3_Низ";
ИначеЕсли е = "е4_НажатаКнопкаЛево" Или е = "е6_СработалОбработчикОжидания" Тогда
z1_СдвинутьКоординатыГоловы(4, у);
z6_СделатьШаг();
КонецЕсли;
Иначе
Лог(СтрШаблон("Ошибка в автомате %1: неизвестный номер состояния! - %2", ИмяА, у));
КонецЕсли;
Если у <> Состояния.у1 Тогда ЛогПереходА(ИмяА, Состояния.у1, у);
Состояния.у1 = у;
Если у = "1_Верх" Тогда
z1_СдвинутьКоординатыГоловы(1, у);
z5_УстановитьВидГоловы(1, "A");
z6_СделатьШаг();
ИначеЕсли у = "2_Право" Тогда
z1_СдвинутьКоординатыГоловы(2, у);
z5_УстановитьВидГоловы(2, ">");
z6_СделатьШаг();
ИначеЕсли у = "3_Низ" Тогда
z1_СдвинутьКоординатыГоловы(3, у);
z5_УстановитьВидГоловы(3, "V");
z6_СделатьШаг();
ИначеЕсли у = "4_Лево" Тогда
z1_СдвинутьКоординатыГоловы(4, у);
z5_УстановитьВидГоловы(4, "<");
z6_СделатьШаг();
Иначе
Лог(СтрШаблон("Ошибка в автомате %1: неизвестный номер состояния! - %2", ИмяА, у));
КонецЕсли;
КонецЕсли;
ЛогКонецА(ИмяА, е, у);
КонецПроцедуры
А2. Сделать шаг
Автомат Мура.
Схема связей
Граф перехода (диаграмма состояний)
&НаКлиенте
Процедура А2_СделатьШаг()
у = Состояния.у2;
ИмяА= "А2_СделатьШаг";
ЛогНачалоА(ИмяА, у);
// Определяем текущее состояние.
Если у = "0_Пусто" Тогда
Если х1_ЭтоЯблоко() Тогда у = "1_Яблоко";
ИначеЕсли х2_ЭтоТелоЗмейки() Тогда у = "2_ТелоЗмейки";
ИначеЕсли х3_ЭтоПрепятствие() Тогда у = "3_Препятствие";
КонецЕсли;
ИначеЕсли у = "1_Яблоко" Тогда
Если х1_ЭтоЯблоко() Тогда
ИначеЕсли х2_ЭтоТелоЗмейки() Тогда у = "2_ТелоЗмейки";
ИначеЕсли х3_ЭтоПрепятствие() Тогда у = "3_Препятствие";
Иначе у = "0_Пусто";
КонецЕсли;
ИначеЕсли у = "2_ТелоЗмейки" Тогда у = "0_Пусто";
ИначеЕсли у = "3_Препятствие" Тогда у = "0_Пусто";
Иначе
Лог(СтрШаблон("Ошибка в автомате %1: неизвестный номер состояния! - %2", ИмяА, у));
КонецЕсли;
Если у <> Состояния.у2 Тогда ЛогПереходА(ИмяА, Состояния.у2, у);
Состояния.у2 = у;
КонецЕсли;
// Выполняем действия в текущем состоянии.
Если у = "0_Пусто" Тогда
z2_УдалитьХвост();
z3_ВставитьГолову();
ИначеЕсли у = "1_Яблоко" Тогда
z3_ВставитьГолову();
z4_ДобавитьЯблоко();
z12_ОбновитьОстатокЯблокДоСледующегоУровня();
ИначеЕсли у = "2_ТелоЗмейки" Тогда
z5_УстановитьВидГоловы(5, "Х");
z3_ВставитьГолову();
ПоказатьПредупреждение(, "Змейка воткнулась в свое же тельце :(");
ИначеЕсли у = "3_Препятствие" Тогда
z5_УстановитьВидГоловы(5, "Х");
z3_ВставитьГолову();
ПоказатьПредупреждение(, "Походу змейка убилась об стену. На этом конец :(");
Иначе
Лог(СтрШаблон("Ошибка в автомате %1: неизвестный переход в номер состояния! - %2", ИмяА, у));
КонецЕсли;
ЛогКонецА(ИмяА, , у);
КонецПроцедуры
+1. спасибо за ссылки!
Прикольною Только бессмысленно
(3) В самой игре смысла нет, для меня было важно попробовать подход автоматного программирования. А поскольку на инфостарте об этом почти ничего нет, то решил поделиться своим опытом, так сказать.
(0) отлично сделано
если я правильно понял входной алфавит включает управляющие события (e) и условия на переходе (x , y) ?
Х и Y по какому принципу вы их различаете ?
(5) Спасибо за отзыв и вопрос.
Конечный автомат здесь рассматривается не как распознаватель языка, а как устройство управления и соответственно оперирует немного другими терминами — входные воздействия (делятся на события (Е) и входные переменные (Х)), выходные воздействия (Z) и управляющие состояния (Y).
Не всегда автомат имеет события (Е). Если же автомат событийный, то он как правило, запускается по событию, а входные переменные (Х) могут быть опрошены в любой момент.
Поиск управляющих состояний (Y) в сущности со сложным поведением часто является сложной творческой задачей.
Авторы книги про автоматное программирование разделяют состояния на управляющие и вычислительные. Первые можно условно назвать качественными, а вторые количественные. По вычислительным состояниям граф построить невозможно, либо возможно, но был бы бесполезен.
Поэтому «в процессе выделения управляющих состояний приходится внимательно исследовать описание сущности со сложным поведением в поисках набора «ситуаций», в которых поведение сущности имеет качественные особенности».
(6) интересно
управляющие состояния (Y) это те случаи когда при наступлении события Y управление передается вложенному КА ?
(7) Да, при наступлении события управление передается КА.
В игре «змейка» несколько КА и для них сделана следующая иерархия: А0 -> А3 -> А1 -> А2. Таким образом внешнее событие, например «е1_НажатаКнопкаВерх» всегда передает управление А0. Дальше если А0 в состоянии Игра, то управление передается А3 с тем же событием. В свою очередь А3 передает управление А1, НО в зависимости от своего состояния может передать другое событие (взять из очереди, которую сгеренировал автопилот). И в завершении А1 пределает управление А2. Это похоже на поиск в глубину на графе.
Когда автоматов несколько то приходится думать о схеме взаимодействия, иначе может получиться рекурсия. Поэтому я и сделал передачу событий сверху вниз.
Я посмотрел, у меня на схеме связи А3 написаны события (е1 и др.), на самом деле их правильно перенести в схему А0.
(7) Номерация КА делалась по времени разработки. Сначала я сделал А0, А1 и А2. Это все работало и пару месяцев лежало. После я захотел сделать новую функцию — автопилот и для этого пришлось сделать еще один КА с порядновым номером А3. Таким образом для себя я проверил насколько всё это дело масштабируемо. Оказалось всё очень хорошо. Спустя пару месяцев, глядя на спецификацию я понял, что для автопилота необходимо еще один КА, сначала спроектировал его «на бумаге» и потом закодил. На удивление ничего не поломалось. Тестирование тоже заняло мало времени, требовалось лишь проверить правильность поведения во всех состояних нового автомата и переходах.
(9)
как вы их тестируете ? подаете некоторые события на вход и делаете трассировку?
Делаете проверку на зацикливание ?
кстати у Шалыто есть книга про формальную верификацию КА я ее полистал по диагонали , очень много математики , но суть (как мне кажется) сводится к тому , что можно верифицировать некоторые атомарные утверждения на графе переходов преобразованом в модель Крипке
оставлю здесь
обработка(шутка) написана в автоматном стиле
(10) Я читал, что формальных тестов около 32 штук чтоли, но их я не использую.
Да, я делаю трассировку интерактивно. Для этого каждый автомат в режиме предприятия при запуске пишет всё в протокол (текстовый документ на форме).
Я же смотрю граф перехода и инициирую события, потом проверяю по протоколу поведение. Ошибки можно заметить сразу. Обычно это поведение не то, которое я ожидаю, тогда в протокле я почти сразу нахожу место которое, например, не соответствует графу перехода, либо понимаю, что поведение входных либо выходных воздействий не правильное. Опять же по протоколу я знаю какие переменные и действия проверить, потому как в протоколе пишутся например, х1_ЭтоВходнаяПеременная или z1_Действие.
Чтобы быстро находить места, в коде так же именую процедуры и функции как в графе. Поскольку код автомата у меня не генерируется, а пишется в ручную, то и ошибки могут возникать в кодировании графа перехода. Такие ошибки по протоколу легко находятся, в будущем думаю найти способ для генерации кода, тогда и ошибок такого рода не будет (у меня эти ошибки редкие), да и не буду тратить время на кодирование графа.
Следующие типы ошибок — ошибки во входных переменных, либо действиях. Их либо отлаживаю в конфигурации (ставлю точку останова) либо можно обнаружить ошибку прочитав код.
Но в основном всё работает сразу, и если честно тестирование со чтением логов я провожу для того, чтобы выявить скрытые ошибки и проанализировать поведение сложных моментов.
Зацикливание тоже можно увидеть в протоколе. В общем как-то так.
(11) Вложил свои 5 копеек коментарием в вашей публикацииСборка автомата (с примерами) 🙂
(0) можно пошучу ?
«На нижнем слайде вы видите как устроен помощник закрытия месяца. Как говорится, без поллитры не разобраться…» 🙂
(0) интересные статьи у вас
(14) 🙂 и чтобы окончательно «убить» бухгалтера по кнопке «Подробнее» будет открываться схема связей.
(15) Спасибо.