Пишемо задачки на FBD. Квачі і Сімпсон

Доброго дня.

У цій статті буде показано, як на мові програмування FBD написати просту програму, яка, тим не менш, буде робити щось корисне. У нашому прикладі це буде гра у Квача.

Для початку нагадаю правила ігри: гра у «15», «Квачі», «Такен» — популярна головоломка, придумана в 1878 році Ноєм Чепмэном. Являє собою набір однакових квадратних кісточок з нанесеними числами, ув'язнених у квадратну коробку. Довжина сторони коробки в чотири рази більше довжини сторони кісточок для набору з 15 елементів, відповідно в коробці залишається незаповненим одне квадратне поле. Мета гри — переміщаючи кісточки по коробці, домогтися упорядкування їх за номерами, бажано зробивши якомога менше ходів.

Як ми бачимо, правила гранично прості. І реалізація буде простий і займе 15 хвилин без графічної частини і півгодини з усіма картинками. При цьому хочу звернути вашу увагу на те, що питання оптимізації алгоритмів і логіки залишаються за рамками цієї статті оскільки ці питання вже не такі прості і вимагають куди більше часу.

Ось що вийшло в результаті:


Опис програми, коментарі і картинки під катом.

Основа програми
Як видно з опису ігри, у нас є поле з 16 елементів. Таким чином основою програми буде елемент «Клітка пятнашек».
Сформулюємо вимоги до цього елемента:

— Наша «клітка пятнашек» повинна приймати зовнішні команди, коли гравець говорить, що хоче пересунути її на сусіднє порожнє місце.
— Клітка повинна знати про сусідів по вертикалі і горизонталі, щоб зрозуміти, чи є поряд порожня клітка, з якої можна помінятися місцями.
— Клітка має на старті гри встановити початкове значення.
— На вихід Клітина повинна передати значення, встановлене в ній на даний момент.
— Якщо гравець подав команду Клітці переміститися на сусіднє порожнє поле, то Клітина повинна передати команду сусідній порожній Клітці про те, що вони зараз будуть мінятися місцями.

Виходячи з цих вимог отримуємо набір вхідних і вихідних сигналів.

Входи:
  • УпрКоманда — команда управління від гравця, сусідніх клітин або початкової установки значення
  • Зверху — значення сусідньої зверху клітини
  • Знизу — значення сусідньої знизу клітини
  • Ліворуч — значення сусідньої клітини зліва
  • праворуч — значення сусідньої праворуч клітини
  • УстЗначение — значення, яке має бути встановлено в клітку при старті гри
Виходи:
  • Значення — значення клітини
  • Поїхали — команда сусідові, що ми хочемо «помінятися з ним місцями»
«Помінятися місцями» в лапках, тому що самі Клітини пятнашек у нас нікуди не будуть бігати, вони просто будуть заміняти своє значення.

В результаті виходить ось такий макрос:


Поставимо таких макросів в нашу задачу 16 штук і зв'яжемо їх між собою. В результаті отримуємо:

Все дуже просто. Поставили завдання шістнадцять макросів «Клітка п'ятнашки» і зв'язали кожну клітину з сусідами по вертикалі і горизонталі. Якщо якихось сусідів у клітини (наприклад в клетке_1_1 немає сусіда зверху і зліва), ставимо на відповідному вході "-1".

Набивання основи
На минулому кроці ми заклали основу програми. Але оскільки сам елемент «Клітка п'ятнашки» поки у нас порожній, то нічого хорошого наша програма робити не вміє. Пора це виправити, наповнивши логікою макрос «Клітка п'ятнашки».



Це готова реалізація нашого макросу. Коротко принцип роботи:

1. Отримуємо на вхід керуючу команду і розпаковуємо її. Виділяємо окремо сигнал тривалістю один цикл — ознака, що надійшла команда, і параметр команди. У нашому випадку параметр команди це набір цілих чисел від 1 до 6, де числа 1 — 4 це команда помінятися значеннями з одним із сусідів, число 5 означає команду встановити початкове значення в комірку, а число 6 — що гравець клікнув мишкою по даній клітині і хоче «пересунути» її на можливо знаходиться по сусідству порожнє поле.

2. Перевіряємо всіх сусідів на нульове значення (числом 0 у нас позначається порожнє поле).

3. Далі дивимося на поточне значення даної клітини і команду. Якщо значення клітини дорівнює нулю (клітка порожня) і одночасно прийшла команда помінятися значеннями з сусідом (команди 1-4), то перезаписуємо в клітку значення сусідньої клітини.

4. Якщо нам прийшла команда 5 (встановити початкове значення), то просто записуємо в пам'ять значення зі входу «УстЗначение».

5. Якщо нам прийшла команда 6 (гравець клікнув мишкою по даній клітині) і значення однієї з сусідніх клітин дорівнює нулю, то записуємо в дану клітку нульове значення і відправляємо команду «Поїхали» на сусідні клітини із зазначенням, з яким саме сусідом ми хочемо змінитися.

Ось власне і все. Єдиний тонкий момент у нашій реалізації, це алгоритми «Затримка». Але здогадатися, навіщо вони потрібні, легко просто представивши як проходить виконання програми:

— ми отримали команду від гравця «пересунути» цю клітку на порожнє місце.
— перевірили, чи дійсно поруч є пусте місце.
— записали в клітку «0» і передали сусідові, що хочемо помінятися з ним значеннями.
— сусід (при цьому потрібно розуміти, що відпрацьовує все той же макрос «Клітка п'ятнашки», просто інший екземпляр) бачить, що йому прийшла команда помінятися місцями з інший кліткою.
— сусід перевіряє що в ньому самому записаний «0» тобто він зараз порожня клітка і записує собі значення першої клітини, від якої прийшла команда помінятися місцями.

Але секундочку! Адже одночасно з тим, як відправити команду сусідові ми в нашу першу клітку записали «0», отже сусід теж запише собі нуль і у нас вийдуть дві порожні клітини, а одне значення (з ряду 1-15) пропаде. І так поки всі ігрове поле не стане порожнім. Щоб цього не сталося, затримуємо в першій клітці перезапис нулем. Таким чином порожня сусідня клітина встигне собі записати правильне значення.

Кілька слів про оптимізаціюЯк нескладно помітити, сам макрос «Клітка п'ятнашки» досить простий. Однак його можна ще спростити, викинувши звідти частина блоків. Ідея проста. Зараз ми перевіряємо команду і сусідні клітини перед виконанням перестановок. Однак, як було сказано вище, при будь перестановки відпрацьовує два примірники одного і того ж макросу. І в першому примірнику ми проводимо перевірку перед відправленням команди, а в другому примірнику проводимо ту ж саму перевірку при отриманні команди. Таким чином одну з перевірок можна сміливо викидати. Це перше, що відразу впадає в очі. Другий момент — зайва затримка на значенні. Адже значення зберігається в блоці «Пам'ять», куди перезаписується тільки по команді. Отже досить затримати тільки команду перезапису значення, а саме значення подавати як є.
Перемога чи поразка
Власне основа нашої програми готова і вже чудово працює. Можна грати. Всі подальші дії потрібні тільки для впровадження допоміжних функцій.

Почнемо з автоматичної перевірки перемоги або програшу гравця.

У грі «п'ятнашки» є цікавий момент полягає в тому, що з усіх можливих початкових розкладів плиток половина розкладів можна зібрати якщо плитки з номерами «14» і «15» стоять не в тому порядку.

Виходячи з цього сформуємо дві умови:
— умова 1: значення, записане в кожну клітину відповідає порядковому номеру. Тобто гравець переміг.
— умова 2: значення, записані в клітини відповідають їх порядковим номером за винятком клітин «14» і «15», які поміняні місцями. Тобто трохи не пощастило з розкладом.

Ставимо 18 алгоритмів порівняння, і збираємо по І два набори по шістнадцять рівностей. ЧИ формуємо ознака, що ми прийшли до одного з кінців розкладу.

18 алгоритмів тому, що ми перевіряємо відповідність 16 клітинок для першого випадку і додатково перестановку осередків «14» і «15» для другого випадку. До речі т. к. у нас масив даних заздалегідь визначений, то достатньо п'ятнадцяти перевірок для всього поля, оскільки остання клітинка перевіряється автоматом.



Початкова розстановка
Отже, програма у нас готова. Можна грати і при перемозі видається повідомлення. Все добре за винятком того, що на початку гри потрібно розставити плитки в полі випадковим чином. І виявилося, що в даному прикладі це найскладніше завдання. Але якщо не впиратися в швидкодію, оптимальність алгоритмів і т. п. то можна вирішити цю задачу «в лоб» малою кров'ю порівняно швидко.

Спочатку робимо генератор випадкових цілих чисел 0 — 15. Підемо старим перевіреним способом. Це цілком допустимий варіант т. к. отримана послідовність випадкових чисел залежить від моменту часу, коли була натиснута кнопка «Нова гра». А оскільки цей момент часу трапляється й ніколи не повторюється, то в результаті ми отримуємо досить простий і хороший генератор випадкових чисел.



На виході алгоритму Дійсне-в-Ціле (ДвЦ) отримуємо випадкове ціле значення 0 — 15.

Тепер стоїть завдання сформувати ряд неповторюваних значень, і записати кожне значення у свою комірку.

Як я вже сказав, конкретно для мови FBD більш або менш гарне рішення цієї задачі зробити складно. Чи може просто я не бачу очевидне для інших красиве і просте рішення. Звичайно завжди є варіант зробити вставку на ST, але мета зробити завдання цілком мовою FBD.

Рішення задачі «в лоб» полягає в наступному:

— ставимо керуючі елемент.
— при подачі команди забиваємо початковий масив "-1" (можна було і будь-якими числами поза діапазону [0..15]).
— обнуляем лічильник.
— далі генеруємо випадкове число від 0 до 15 і перевіряємо, чи є таке в нашому масиві. Якщо є — продовжуємо генерацію випадкових чисел.
— якщо такого числа немає — збільшуємо лічильник на одиницю і записуємо у відповідну комірку пам'яті наше значення.
— повторюємо ці дії ще 15 разів.
— перевіряємо, якщо ми дійшли до шістнадцятого кроку, то формуємо імпульс, за яким відправляємо команду на запис цих значень у всі комірки.

Головним недоліком цього алгоритму є багаторазовий перебір викинутих раніше значень. В теорії час генерації такої випадкової послідовності може бути нескінченним. В реальності при десятках прогонів програми воно не було більше ніж півсекунди. У будь-якому разі час генерації (раз воно не миттєве) ставимо блокування на дії гравця.

Ось що в результаті вийшло:



Додаємо графіком
Тепер, коли завдання у нас повністю готова, саме час приробити UI.

Для цього вибираємо в інтернеті будь сподобалися картинки з плямками. Одночасно вибираємо картинки для вдалого збору розкладу і невдалого. Я вирішив помістити туди Гомера Сімпсона.

Ставимо 16 картинок і додаємо до них анімацію щоб вони показували число, відповідне числу в комірці. А в комірці з нулем плитка повинна бути невидимою. Ставимо на кожну плитку формування команди «6». Знизу маємо кнопку «Нова гра» з якої будемо запускати алгоритм генерації випадкової початкової розстановки. Одночасно не забуваємо поставити блокування на всі плитки під час генерації. Поверх всього цього діла поміщаємо Гомера. Всі. Графіка готова, можна грати.

Намальований варіант. Можна помітити що спочатку всі плитки у нас з одиницею.


Початкова позиція.


Невдалий розклад.


І перемога!


Що залишилося нереалізованим
Насправді не так вже й багато:

— можна приробити лічильник ходів. Робиться це елементарно. В макросі «Клітка п'ятнашки» є команда «6» — команда гравця. Достатньо взяти від неї логічний сигнал тривалістю в один цикл. Вивести цей сигнал назовні. Зібрати всі шістнадцять сигналів, АБО і завести на алгоритм складання, охоплений зворотним зв'язком. Тобто всього три додатковий алгоритму і пара хвилин роботи.

— статистика. Можна зробити лічильник виграних і невдалих ігор. Додається два алгоритму додавання і пару алгоритмів обв'язки. Теж пару хвилин роботи.

— відбір розкладів. Як вже говорилося, половина розкладів не сходиться спочатку із-за неможливості поміняти місцями плитки «14» і «15». Можна при старті перевіряти «невдалий розклад» і відразу міняти ці дві плитки місцями.

Зробити це не складно, тому залишу таку можливість всім бажаючим.

Висновки
На цей раз за півгодини мовою FBD була реалізована простенька іграшка «П'ятнашки». При цьому всі використовувані алгоритми прості і зрозумілі. Вся логіка обробки сигналів очевидна. Розібратися в такій програмі не складе праці будь-якій людині, знайомому з азами логіки і основами мов програмування МЕК 61131.

Для перевірки накидав приблизно те ж саме на С. Ось що вийшло:
Текст програми на ЗТ. к. програмуванням на мові С/С++ я не володію, то швидше за все, програма жахлива. Більше того я відразу бачу кілька «сумнівних» місць. Однак вона працює і працює без помилок.

Головна ідея полягає в тому, що одна і та ж програма, написана на мові С і мовою FBD має різну складність розуміння людиною «зі сторони». І якщо з мовою функціональних блоків розібратися не представляє ніякої праці, то з реалізацією З доведеться повозитися.

І хоча програма на FBD складається з 153 блоків (і ми пам'ятаємо, що там ще є 16 макросів, кожен з яких складається з 32 блоків), але на її написання пішло значно менше часу, ніж на написання 50 рядків коду на С.

#include < iostream>
#include "cstdlib"
#include <locale.h>
#include < stdio.h>
#include < conio.h>
#include <math.h>
#include <memory.h>
#include < string.h>
#include <stdlib.h>
#include <time.h>
#include <cmath>

using namespace std;


int main(void)
{
int MyPyatn[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
int i = 0, x = 0, y = 0, MyRand = 0, MyBuffer = 0, MyButton = 0;
bool MyGameover = false, MyChange = false;

setlocale(LC_ALL, "ru-UA");
srand(time(NULL));

cout << "Натисни 'n' для нової гри або будь-яку іншу кнопку для виходу: ";

kbhit();
if(getch() != 110)
return 0;

std::system("cls");

cout << "Граємо в п'ятнашки: ";

for(i=0;i<16;i++)
{
MyRand = rand() %(16-i);
MyBuffer = MyPyatn[15-i];
MyPyatn[15-i] = MyPyatn[MyRand];
MyPyatn[MyRand] = MyBuffer;
}

for (i=0;i<16;i++)
{

if ((i %4) == 0)
{
cout << "\n";
cout << "\n";
}

if ((MyPyatn[i]<10) || (MyPyatn[i] == 16)) cout << " ";
else cout << " ";

if (MyPyatn[i] == 16) 
{
cout << "_";
x = i %4 + 1;
y = int (i/4) + 1;
}

else cout << MyPyatn[i];

cout << " ";


}

cout << "\n";
cout << "\n";
cout << "Управління стрілками вгору-вниз-вліво-вправо. Вихід - '0' \n";
cout << "Знак _ це пуста клітка";

do
{
MyChange = false;
MyGameover = true;
for(i=0;i<16;i++)
{
if (MyPyatn[i] != (i+1)) MyGameover = false; 
}

while(!kbhit ());

MyButton = getch();

if ((MyButton == 48) || (MyButton == 72) || (MyButton == 75) || (MyButton == 77) || (MyButton == 80))
{

if (MyButton == 48) return 0;

if ((MyButton == 72) && (y > 1))
{
MyBuffer = MyPyatn[(y-1)*4 + x - 1];
MyPyatn[(y-1)*4 + x - 1] = MyPyatn[(y-2)*4 + x - 1];
MyPyatn[(y-2)*4 + x - 1] = MyBuffer;
y = y - 1;
MyChange = true;
}

if ((MyButton == 80) && (y < 4))
{
MyBuffer = MyPyatn[(y-1)*4 + x - 1];
MyPyatn[(y-1)*4 + x - 1] = MyPyatn[y*4 + x - 1];
MyPyatn[y*4 + x - 1] = MyBuffer;
y = y + 1;
MyChange = true;
}

if ((MyButton == 75) && (x > 1))
{
MyBuffer = MyPyatn[(y-1)*4 + x - 1];
MyPyatn[(y-1)*4 + x - 1] = MyPyatn[(y-1)*4 + x - 2];
MyPyatn[(y-1)*4 + x - 2] = MyBuffer;
x = x - 1;
MyChange = true;
}

if ((MyButton == 77) && (x < 4))
{
MyBuffer = MyPyatn[(y-1)*4 + x - 1];
MyPyatn[(y-1)*4 + x - 1] = MyPyatn[(y-1)*4 + x];
MyPyatn[(y-1)*4 + x] = MyBuffer;
x = x + 1;
MyChange = true;
}
}

if (MyChange)
{
std::system("cls");

cout << "Граємо в п'ятнашки: ";

for (i=0;i<16;i++)
{

if ((i %4) == 0)
{
cout << "\n";
cout << "\n";
}

if ((MyPyatn[i]<10) || (MyPyatn[i] == 16)) cout << " ";
else cout << " ";

if (MyPyatn[i] == 16) cout << "_";
else cout << MyPyatn[i];

cout << " ";

}

cout << "\n";
cout << "\n";
cout << "Управління стрілками вгору-вниз-вліво-вправо. Вихід - '0' \n";
cout << "Знак _ це пуста клітка";
}

} while(!MyGameover);

std::system("cls");

cout << "ПЕРЕМОГА!!!";

getch();

return 0; 

}


Іншими словами мова FBD це простий і зрозуміла мова, призначений для далеких від програмування людей. Програмувати прості задачки на ньому легко. І розібратися в принципах роботи програми, написаної іншим людиною теж зазвичай не становить великої проблеми.

На цьому все. Сподіваюся було цікаво.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.