“П’ятнашки” – популярна головоломка, придумана у 1878 році Ноєм Чепменом. Складається з 15 однакових квадратних пластинок з нанесеними числами від 1 до 15 або частинами одного малюнка. Пластинки поміщаються в ігрове поле (квадрат), довжина сторони якого в чотири рази більша довжини сторони пластинок, відповідно залишається незаповненим одне квадратне поле. Мета гри – переміщаючи пластинки по коробці добитися впорядковування їх по номерах, бажано зробивши якомога менше переміщень.
Дана тематика обрана для виконання проетку, тому що результати абстрагування об’єктів у цій предметній галузі дозволяють застосувати всі вивчені принципи та методи об’єктно-орієнтованого програмування для створення ПЗ, зокрема шаблони проектування.
Метою роботи є розроблення програмного забезпечення для гри “П’ятнашки” з використанням шаблонів проектування.
Реалізовані шаблони проектування: Factory Method, Template Method, Prototype, Proxy (as Authenticate Proxy), Command, State, Observer, Facade, Singleton.
Розроблене ПЗ може бути використане з розважальною метою та для розвитку розумових здібностей. Також, як і будь-яка гра, яка змушує гравця міркувати логічно, шукати алгоритм для перемоги, дана гра може бути використана для навчання нейронних мереж.
До функціональних можливостей програми належать: вибір складності гри: 3х3 чи 4х4, відтворення звукових ефектів, вибір типу ігрового поля, можливість зупинити гру в будь-який час, можливість перезапуску гри в будь-який час, різні способи переміщення кнопок, підрахунок часу та кількості перемикань під час гри, збереження результату гри в таблицю рейтингу.
Модуль інтерфейсу користувача – модуль відповідає за інтерфейс гри. Відображає ігрове поле, різні роду меню та кнопки з якими може взаємодіяти користувач. Модуль посилає запити до модуля зв’язку інтерфейсу та програми, необхідно оновити стан гри, змінити налаштування, тощо.
Для реалізації використовується мова розмітки FXML, таблиці стилів CSS та платформа JavaFX.
Модуль зв’язку інтерфейсу та програми – модуль відповідає за взаємодію між модулем інтерфейсу користувача та іншими модулями, які містять в собі основну логіку гри: основний модуль гри, модуль роботи з таблицею результатів та модуль керування налаштуваннями гри. Він відповідальний за своєчасне оновлення інтерфейсу та об’єктів гри при взаємодії користувача з програмою.
Основний модуль гри – модуль Game Model (Модель гри), який містить у собі основні сутності та реалізує 4 стани, в яких може перебувати гра (свого роду скінченний автомат). Встановлює взаємодію з модулем роботи з ігровим полем та модулем допоміжних утиліт.
Модуль роботи з ігровим полем – модуль, який напряму взаємодіє з основним модулем гри, контролює ігрове поле, відповідальний за правильну зміну положень кнопок, спосіб їх переміщення та інший функціонал системи. На основі стану ігрового поля може повідомити основний модуль про завершення гри. Виконує свого роду роль “розуму” в основному модулі гри.
Модуль допоміжних утиліт – модуль, який загалом ніяк не впливає на стан гри. Надає іншим модулям системи об’єкти та функціонал для виконання певних операцій, щоб спростити роботу іншим модулям. Наприклад, дає можливість завантаження та обробки зображень, відтворення звукових ефектів, роботи з таймером.
Модуль керування налаштуваннями гри – модуль, відповідальний за те, яким чином буде відпрацьовувати основний модуль гри. Гравець при зміні налаштувань гри працює саме з даним модулем (звичайно через модуль зв’язку інтерфейсу та програми) і він, в свою чергу, впливає (видозмінює поведінку) на модуль гри. Модуль підтримує зберігання та відтворення свого стану при вимкненні – старту гри.
Модуль роботи з таблицею результатів – модуль, відповідальний за збереження результатів гри та формування рейтингу. Модуль підтримує зберігання та відтворення свого стану при вимкненні – старту гри.
Взаємодія з грою розпочинається з ігрового поля (відтвореного за попередніми налаштуваннями). В даний момент гра очікує на початок. Як тільки гравець почне взаємодіяти з кнопками на ігровому полі, відразу запускається відлік часу. Кожне переміщення кнопки рахується, а лічильник зображується на екрані гри.
Гравець в будь-який момент гри може поставити паузу – лічильник часу зупиниться, а ігрове поле буде сховане. Після вимкнення паузи – гра продовжиться з того ж моменту. Коли гра активна, а гравець відчиняє меню налаштувань чи таблицю рекордів – гра автоматично вмикає паузу.
У будь-який момент гравець може натиснути клавішу “Shuffle”, що призведе до наступних змін:
- якщо гра була почата, то всі результати будуть скинуті, ігрове поле перемішане, гра перейде в стан очікування;
- якщо гра в стані очікування, то поле просто буде перемішане. Причому якщо ігрове поле містить зображення – воно буде змінене на інше.
Під час гри гравець може відмінити всі зроблені ним переміщення, чи повернутися в останнє положення (якщо гравець вже зробив відміну). Звичайно ці дії будуть розцінюватися як звичайні переміщення і збільшувати лічильник ходів.
Гравець може переглянути таблицю рекордів, а також занести в неї власний результат, якщо закінчить гру. Також гравець може змінити налаштування гри:
- тип ігрового поля;
- складність гри;
- спосіб перемикання кнопок;
- чи потрібно підсвічувати кнопки на правильних позиціях;
- чи потрібно відтворювати звукові ефекти;
- режим “Hardmode”.
Якщо налаштування були змінені – гра перейде в стан очікування та змінить свій функціонал згідно з новими налаштуваннями.
- Алгоритм перемішування кнопок ігрового поля (реалізований у класі GameBoard).
Випадкове розміщення кнопок на ігровому полі не є правильним, адже існує такі варіанти розміщення, коли зібрати “П’ятнашки” не є можливим.
Розглянемо математичне підґрунтя цієї проблеми в її класичному варіанті 4х4:
Можна показати, що рівно половину з усіх можливих 20 922 789 888 000 (=16!) початкових положень п’ятнашок неможливо привести до зібраного стану: нехай квадратик з числом i розташований до (якщо вважати зліва направо і зверху вниз) k квадратиків з числами меншими i. Будемо вважати ni = k, тобто якщо після квадрату з i-м числом немає чисел, менших i, то k = 0. Число ni показує кількість інверсій для клітинки з номером і. Також введемо число e - номер ряду порожньої клітинки (рахуючи з 1).
Якщо сума є непарною, то рішення головоломки не існує.
Отже, необхідно написати алгоритм, який рахує цю суму N для конкретного ігрового поля. Він виглядає наступним чином:
private boolean isSolvable() {
int inv = 0;
for (int i = 0; i < difficulty.getNumberOfCells(); i++)
if (buttons.get(i) != buttonLocked) {
for (int j = 0; j < i; j++)
if (buttons.get(j).getGameIndex() > buttons.get(i).getGameIndex() &&
buttons.get(j).getGameIndex() != difficulty.getNumberOfCells() - 1) {
++inv;
}
}
if (difficulty == Difficulty.Hard)
inv += 1 + this.getIndexOfRow(this.buttonLocked);
return inv % 2 == 0;
}
Це універсальний варіант алгоритму, який враховує той факт, що для парного значення клітинок необхідно додавати номер строки пустої клітинки, а для непарного – це не потрібно.
Отже, тепер можна перевірити, чи існує рішення для конкретного стану головоломки. Але що робити у випадку, якщо його не існує. Для того, щоб постійно не перемішувати дошку, доки рішення не знайдеться пропонується наступний алгоритм:
public void shuffleGameBoard() {
Collections.shuffle(this.buttons);
if (!isSolvable()) {
if (getIndexOf(buttonLocked) <= 1) {
swapButtons(difficulty.getNumberOfCells() - 2, difficulty.getNumberOfCells() - 1);
} else {
swapButtons(0, 1);
}
}
}
Пояснення: спочатку просто перемішуємо ігрове поле, використовуючи стандартний метод в мові програмування java для колекцій. Далі перевіряємо чи існує рішення для головоломки:
- якщо рішення існує – мета досягнута;
- якщо рішення не існує – потрібно поміняти місцями два квадрати поруч у одному рядку, але при цьому обов’язково врахувати той факт, що обидва з цих квадратів не є пустою клітинкою. При такому обміні сума N стане праною і головоломку буде мати рішення.
Загальна діаграма класів:
1. Factory Method
Породжуючий шаблон. Визначає інтерфейс для створення об'єктів, але рішення про те, який саме об’єкт створювати, залишає за підкласами.
Структура. Реалізований у двох варіантах:
-
Для створення різних типі ігрового поля: клас GameBoard виступає в ролі «Product», а класи, що від нього наслідуються ImageBoard та TextBoard в ролі «ConcreteProduct». Клас GameBoardCreator -«ConcreteCreator», він містить у собі один метод, який приймає тип ігрового поля та складність і на основі цих параметрів повертає створене ігрове поле.
-
Для створення команд перемикання кнопок: клас Command виступає в ролі «Product», а класи, що від нього наслідуються SingleSwitch та MultiSwitch в ролі «ConcreteProduct». Клас CommandCreator -«ConcreteCreator», він містить у собі один метод, який повертає створену команду, клас якої залежить від параметрів, переданих у метод..
Обидва варінти є реалізаціями параметризованого Фабричного метода.
Обґрунтування використання даного шаблону.
Після створення системи класів, яка представляє різновид наявних у програмі команд, та ігрових полів треба було зручним чином організувати створення цих об’єктів за допомогою якихось сутностей. Так як кожна з команд має абстрактний клас Command у якості базового класу (фундаменту) (а ігрове поле у якості базового класу має абстрактний клас GameBoard), то було прийняте рішення про використання структури шаблону параметризований “Factory Method” за основу у проектуванні цих частин системи.
2. Template Method
Поведінковий шаблон. Визначає функціональність конкретних методів в рамках лише абстрактних сутностей. Визначає основу алгоритму та дозволяє підкласам перевизначити деякі кроки алгоритму, не змінюючи структуру в цілому.
Структура. Клас GameBoard виступає в ролі абстрактного класу, який в шаблонному методі init локалізує поведінку, що є загально для всіх підкласів (ImageBoard та TextBoard) і виносить в абстрактний метод createBtn поведінку, яка повинна бути реалізована для кожного класу самостійно.
Обґрунтування використання даного шаблону. При написанні ПЗ завжди слід уникати дублювання коду розділення. Використання даного шаблону надає хороший приклад техніки “винесення за лапки з метою узагальнення”. При чому результат виконання шаблонного метода залежить від класу нащадку, в той час як основний алгоритм залишається незмінним.
3. Prototype
Породжуючий шаблон. Задає види створюваних об’єктів за допомогою екземпляра-прототипу і створює нові об’єкти шляхом копіювання цього прототипу. Це єдиний шаблон з серії Породжуючих, котрий для створення нових об’єктів використовує не явне інстанціювання (ключове слово new), а клонування.
Структура. Реалізований для зручного отримання нових об’єктів кнопок, без потреби постійно надавати їм одні й ті ж самі властивості. Клас ButtonPrototype виступає в ролі «Prototype», а класи-нащадки ImageButton та TextButton в ролі «ConcretePrototype». ButtonCache ініціалізує нові об’єкти кнопок, надає їм певні властивості, загальні для різних видів кнопок та зберігає їх в кеш. Надалі, витягаючи з кешу, та клонуючи ці об’єкти можна отримувати кнопки з потрібними властивостями.
Обґрунтування використання даного шаблону. Даний шаблон дає змогу абстрагуватися від способу створення та реалізації об’єктів, які входять в конкретну систему. А зберігання об’єктів в певному стані, та подальше їх копіювання для отримання нових з тими ж властивостями дозволяє зручно отримати необхідні об’єкти в будь-який частині системи.
4. Proxy (as Authenticate Proxy)
Структурний шаблон. Забезпечує створення заступника об’єкта для контролю доступу до останнього через перехоплення всіх викликів. Надає об’єкт-заступник (surrogate) або об’єкт-замінник (placeholder). Обгортаючи доступ до реального компонента, «Заступник» зменшує складність роботи з ним.
Структура. Інтерфейс IHiglight виступає в ролі «Subject», класи StylesManager та GameController, що реалізують даний інтерфейс відповідно відіграють ролі «RealSubject» та «Proxy». GameController вміщає у собі об’єкт класу StylesManager. Коли клієнт хоче, щоб об’єкт класу StylesManager, виконав операцію, визначену інтерфейсом IHiglight, запит на виконання методу спочатку переадресовується до GameController. Він виконує додаткову перевірку умов доступу до об’єкту основного класу та в разі їх виконання – переадресовує запит до StylesManager.
Обґрунтування використання даного шаблону. Для того, щоб реалізувати підсвічення кнопки, яка стала в правильну позицію, необхідно перед її підсвіченням перевіряти позицію кнопки. Шаблон Заступник, а саме його варіант – Заступник – захисник, надає необхідну структуру для реалізації такого функціоналу. В такому випадку StylesManager не цікавиться за яких умов було підсвічено кнопку, а просто видозмінює її, а GameController виконує свою основну функцію – контролює підсвічення кнопок.
5. Command
Поведінковий шаблон. Перетворює операцію в об'єкт. Це дозволяє передавати операції як аргументи при викликах методів, ставити операції в чергу, логувати їх, а також підтримувати можливість скасування операцій.
Структура. Даний шаблон використовується, щоб інкапсулювати дію перемикання кнопок у формі об’єкту та додати можливість відміняти перемикання (в обидві сторони). Абстрактний клас Command виступає в ролі «Command», а класи нащадки SingleSwitch та MultiSwitch в ролі «ConcreteCommand». Кожна команда при створенні запам’ятовує посилання на ігрове поле, власне з яким і виконує операцію переміщення кнопок. В такому разі клас GameBoard виступає в ролі «Receiver» для даного шаблону. Клас CommandManger при вдалому виконанні команди, запам’ятовує її, щоб в майбутньому за необхідності використати її для відміни перемикання, тоді CommandManger виконує роль «Invoker».
Обґрунтування використання даного шаблону. Головне завдання даного шаблону в даному ПЗ – підтримання відміни операцій перемикання кнопок. А винесення операцій в окремі класи дає змогу інкапсулювати логіку перемикання в окремому об’єкті, та при необхідності швидко та безболісно змінювати її.
6. State
Поведінковий шаблон. Дозволяє об'єкту варіювати свою поведінку залежно від внутрішнього стану (динамічна Strategy). Передбачає реалізацію поведінки, асоційованої з певним станом об'єкту, а також забезпечення зміни поведінки відповідно до зміни внутрішнього стану.
Структура. Реалізований у двох варіантах:
- для класу ButtonPrototype: В даній реалізації ButtonPrototype виконує роль «Сontext», абстрактний клас ButtonState – «State», а класи-нащадки ActiveState та DisabledState –«ConcreteState». Оскільки наявно лише два стани кнопки, то відповідно при зміні стану – вони заміняють один одного. Кнопка в стані ActiveState – це кнопка, з якою може взаємодіяти гравець, в стані DisabledState – кнопка неактивна.
- для класу GameModel: В даній реалізації GameModel виконує роль «Сontext», абстрактний клас GameState – «State», а класи-нащадки GameIsWaiting, GameIsStarted, GameIsSuspend та GameIsOver – «ConcreteState». Вся логіка гри зосереджена в різних станах, які реалізують одні операції та забороняють інші. Переходи між станами відбуваються в залежності від дій користувача.
Обґрунтування використання даного шаблону. Шаблон був використаний для спрощення виконання операцій в залежності від внутрішнього стану об’єкта, а поведінка моделі гри залежить від того, в якому стані вона знаходиться у даний момент. Він дозволяє позбутися багатьох умовних операторів в коді.
7. Observer
Поведінковий шаблон. Створює механізм підписки, за допомогою якого одні об'єкти можуть підписуватися на оновлення, що відбуваються в інших об'єктах.
Структура. Інтерфейс IObserver виступає в ролі «Observer», абстрактний клас Subject в ролі «Subject», класи-наслідники GameModel та SettingsModel в ролі «ConcreteSubject». Клас MainController і воодночас GameModel реалізують інтерфейс IObserver та виступають в ролі «ConcreteObserver».
MainController підписується на оновлення GameModel, а модель гри в свою чергу повідомляє спостерігачів, коли гра була закінчена, коли гра була перезавантажена чи відбулася подія відміни перемикання кнопок.
Клас GameModel, який виступає «Subject» для MainController водночас є «Observer» для SettingsModel та слідкує за змінами в моделі налаштувань і в разі чого – перезавантажує гру, та повідомляє MainController про зміни в GameModel.
Обґрунтування використання даного шаблону. При виконанні логіки, зв’язаної зі змінами налаштувань гри, відбуваються події, про які слід сповістити модель гри. Вона в свою чергу після перезавантаження з новими налаштуваннями повідомляє контролер ігрового поля програми, щоб той оновив інтерфейс. Даний шаблон дозволяє реалізувати таку можливість сповіщень.
8. Facade
Структурний шаблон. Надає єдиний простий інтерфейс до безлічі інших інтерфейсів в деякій складній підсистемі.
Структура. В ролі «Facade» виступає клас GameModel. Він включає в себе об’єкти інших класів, які незалежно один від одного не мають можливості реалізувати певний функціонал системи. GameModel комутує об’єкти класів підсистем таким чином, щоб сформувати єдиний, простий інтерфейс, що представляє собою логіку гри та дозволяє керувати нею через цей інтерфейс.
Обґрунтування використання даного шаблону. Реалізований для того, щоб мати один об’єкт, через який можна безпечно та зручно взаємодіяти зі всією системою.
При запуску гри відразу відображається ігрове поле, та кнопки керування. Гра знаходиться в режимі очікування.
Як тільки гравець почне взаємодіяти з ігровим полем, запуститься відлік часу та підрахунок кількості переходів.
Якщо гра в процесі, то гравець може в будь-який момент призупинити її. В такому випадку ігрове поле буде сховане, відлік часу також зупиниться. Гра ставиться на паузу також, коли гравець починає взаємодіяти з іншими вікнами програми.
У випадку, коли гравець успішно закінчує гру з’являється діалог, якій пропонує ввести ім’я гравця, та зберегти результат до таблиці рекордів.
У меню гри гравець може відкрити вікно налаштувань, талицю рекордів, оновити картинку (якщо ігрове поле – картинка) та закрити гру.
У вікні налаштувань гравець може змінити налаштування гри. Якщо він після змін натисне кнопку «ОК» - поле гри буде оновлене.
У будь-який момент часу гравець може переглянути таблицю рекордів та сортувати її за 4 параметрами. Також гравець може скинути всі результати.
Приклад гри в режимі 4х4 та ігровим полем з числами.