Merge OOP coursework
1
2/coursework/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
9
2/coursework/LICENSE
Normal file
@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Anton Bilous <anton.bilous@nure.ua>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
7
2/coursework/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# OOP coursework
|
||||
|
||||
> Rewrite in Rust.
|
||||
|
||||
Program source code is in the [src](src/) directory.
|
||||
|
||||
Paper is written in **LaTeX** via [Overleaf](https://www.overleaf.com/), using [this template](https://github.com/rintaro129/kursova-latex).
|
BIN
2/coursework/main.pdf
Normal file
478
2/coursework/main.tex
Normal file
@ -0,0 +1,478 @@
|
||||
\documentclass[a4paper, 14pt]{article}
|
||||
\usepackage{fontspec, extsizes, geometry, setspace, titlesec, fancyhdr, graphicx, float, setspace, caption, array, tabularx, ulem, indentfirst, ragged2e, caption, appendix, url}
|
||||
\usepackage[main=ukrainian, english]{babel} % підтримка мов
|
||||
\usepackage[dotinlabels]{titletoc}
|
||||
\setmainfont[Ligatures=TeX]{Times New Roman}
|
||||
\geometry{a4paper,left=3cm,top=2cm,bottom=2cm,right=1.5cm} % cтавить береги та формат а4
|
||||
\def\changemargin#1#2{\list{}{\rightmargin#2\leftmargin#1}\item[]}\let\endchangemargin=\endlist % зручна команда для виставлення відступів
|
||||
\makeatletter\newcommand\Dotfill{\leavevmode\leaders\hb@xt@0.5em{\hss.\hss}\hfill}\makeatother % команда для ставлення точок
|
||||
\let\stdsection\section\renewcommand\section{\newpage\stdsection} % Новая секція -> нова сторінка
|
||||
\addto\captionsukrainian{\renewcommand{\figurename}{Рисунок}} % підпис картинок не Рис. а Рисунок
|
||||
\titleformat{\section}{\filcenter}{\bfseries{\thesection} }{0pt}{\bfseries\MakeUppercase} % Форматування заголовків всіх розділів
|
||||
% \renewcommand{\thesubsection}{\arabic{section}.\arabic{subsection}} % Чиним номер подраздела
|
||||
\titleformat{\subsection}{}{{\thesubsection} }{0pt}{} % нумерація підрозділів
|
||||
\titlespacing*{\subsection}{\parindent}{1em}{1em} % рядок перед та пілся підрозділу та підрозділ з абзацу
|
||||
\titleformat{\subsubsection}{}{{\thesubsubsection} }{0pt}{} % нумерація підпідрозділів
|
||||
\titlespacing*{\subsubsection}{\parindent}{1em}{1em} % рядок перед та пілся підпідрозділу та підпідрозділ з абзацу
|
||||
\titlespacing*{\section}{0pt}{1em}{1em} % рядок перед та пілся розділу
|
||||
\captionsetup{labelsep=endash} % "Рис. 1 - замість Рис. 1:"
|
||||
\fancyhf{}\renewcommand{\headrulewidth}{0pt}\newcommand{\changefont}{\fontsize{14}{14}\selectfont}\fancyhead[R]{\changefont \thepage}\fancypagestyle{plain}{\fancyhf{}\fancyhead[R]{\changefont \thepage}\renewcommand{\headrulewidth}{0pt}\renewcommand{\footrulewidth}{0pt}}\pagestyle{fancy} %номер страницы справа сверху на всех страничках [это ужас]
|
||||
\linespread{1.43} % Інтервал абзацу полуторний
|
||||
% \renewcommand{\contentsname}{ЗМІСТ} %изменяем название странички с содержанием
|
||||
\def\numberline#1{#1. } % Фикс чтобы названия не налезали друг на друга в содержании
|
||||
\titlecontents{section}[0pt]{\normalfont}{{\thecontentslabel} }{}{\Dotfill \contentspage} % оформление разделов, точек в содержании
|
||||
\titlecontents{subsection}[15pt]{\normalfont}{{\thecontentslabel} }{}{\Dotfill \contentspage} % оформление подразделов, точек в содержании
|
||||
\titlecontents{subsubsection}[30pt]{\normalfont}{{\thecontentslabel} }{}{\Dotfill \contentspage} % оформление подподразделов, точек в содержании
|
||||
\counterwithin{figure}{section} % нумерация картинок с номером раздела
|
||||
\counterwithin{table}{section} % нумерация таблиц с номером раздела
|
||||
\usepackage[none]{hyphenat} % щоб слова переносились закоментуйте цей та наступний рядок
|
||||
\justifying\sloppy % щоб текст розтягувався
|
||||
\setlength{\parindent}{0.5in} % виставляємо відступ абзацу
|
||||
\captionsetup[table]{format=hang,margin=0.5in, justification=raggedright, singlelinecheck=false} % підпис таблиць не по центру а з абзацу
|
||||
|
||||
% для форматування картинок та таблиць:
|
||||
\setlength{\intextsep}{0pt}
|
||||
\captionsetup[figure]{belowskip=-1em}
|
||||
\newlength{\magicspace}\setlength{\magicspace}{2\baselineskip}
|
||||
% щоб був рядок до і після картинки:
|
||||
\let\oldfigure\figure
|
||||
\let\endoldfigure\endfigure
|
||||
\renewenvironment{figure}[1][]{\begin{oldfigure}[#1]\vspace{1em}}{\vspace{1em}\end{oldfigure}}
|
||||
% щоб був рядок до і після таблиці: зроблено експериментально :)
|
||||
\let\oldtable\table
|
||||
\let\endoldtable\endtable
|
||||
\renewenvironment{table}[1][]{\begin{oldtable}[#1]\vspace{2em}}{\vspace{1em}\end{oldtable}}
|
||||
% це капець.....
|
||||
|
||||
\addto\extrasukrainian{\renewcommand\refname{Перелік джерел посилання}} % змінити назву переліка посилань з Література на Перелік джерел посилання
|
||||
% змінюємо нумерацію в переліку посилань з [1] на 1. :
|
||||
\makeatletter
|
||||
\renewcommand*\@biblabel[1]{#1.}
|
||||
\makeatother
|
||||
|
||||
\usepackage{totcount}
|
||||
% каунтер для референсів:
|
||||
\newtotcounter{citnum}
|
||||
\def\oldbibitem{} \let\oldbibitem=\bibitem
|
||||
\def\bibitem{\stepcounter{citnum}\oldbibitem}
|
||||
|
||||
% каунтер для рисунків та таблиць:
|
||||
\usepackage[figure,table]{totalcount}
|
||||
|
||||
% каунтер для додатків: (хз як це працює :) )
|
||||
\AtBeginDocument{
|
||||
%% register a counter on the basis of the last chapter in totcounter
|
||||
\regtotcounter{section}
|
||||
}
|
||||
|
||||
\usepackage{lastpage} % для каунтера сторінок
|
||||
|
||||
\usepackage{enumitem} % для прибирання зайвих відступів поміж ітемів списків
|
||||
\setlist[]{noitemsep, nolistsep, left=\parindent}
|
||||
\setlist[itemize]{label=--} % також заміняємо точку на тире
|
||||
\setlist[enumerate]{label=\arabic*)} % ставимо дужку
|
||||
% ------------------------------------------- Преамбула закінчилась -------------------------------------------
|
||||
|
||||
% ------------------------------------------- Титулка ---------------------------------------------------------
|
||||
\begin{document}
|
||||
% Титулка
|
||||
\thispagestyle{empty}
|
||||
\begin{center}
|
||||
Міністерство освіти і науки України\\
|
||||
Харківський національний університет радіоелектроніки \par
|
||||
\null\par
|
||||
Кафедра програмної інженерії \par
|
||||
\null\par\null\par\null\par
|
||||
КУРСОВА РОБОТА\\
|
||||
ПОЯСНЮВАЛЬНА ЗАПИСКА\\
|
||||
з дисципліни ``Об'єктно-орієнтоване програмування''\\
|
||||
Ігровий застосунок "Boulder Dash"
|
||||
\end{center}
|
||||
\par\null\par\null
|
||||
\begin{changemargin}{-0.25cm}{0cm}
|
||||
\begin{tabular}{ p{20em} p{11em} }
|
||||
Керівник , ст. викл. & Ляпота В. М. \\
|
||||
Студент гр. ПЗПІ-23-3 & Білоус А. А. \\
|
||||
\end{tabular}
|
||||
\par\null\par\null
|
||||
\begin{tabular}{ l }
|
||||
Комісія: \\
|
||||
\end{tabular}
|
||||
\end{changemargin}
|
||||
\begin{changemargin}{1cm}{0cm}
|
||||
\begin{tabular}{ p{12em} p{7em} p{8em} }
|
||||
Проф. & \underline{\makebox[7em][c]{}} & Бондарєв В. М. \\
|
||||
Ст. викл. & \underline{\makebox[7em][c]{}} & Черепанова Ю. Ю. \\
|
||||
Ст. викл. & \underline{\makebox[7em][c]{}} & Ляпота В. М. \\
|
||||
\end{tabular}
|
||||
\vspace*{\fill}\end{changemargin}
|
||||
\begin{center}
|
||||
Харків -- \the\year{}
|
||||
\end{center}
|
||||
\newpage
|
||||
% ------------------------------------------- Титулка закінчилась ---------------------------------------------
|
||||
|
||||
% ------------------------------------------- Аркуш завдання --------------------------------------------------
|
||||
\begin{center}
|
||||
ХАРКІВСЬКИЙ НАЦІОНАЛЬНИЙ УНІВЕРСИТЕТ РАДІОЕЛЕКТРОНІКИ
|
||||
\begin{changemargin}{1cm}{0cm}
|
||||
\begin{tabular}{ l l }
|
||||
Кафедра & \textit{програмної інженерії} \\
|
||||
Рівень вищої освіти & \textit{перший (бакалаврський)} \\
|
||||
Дисципліна & \textit{Об’єктно-орієнтоване програмування} \\
|
||||
Спеціальність & 1\textit{21 Інженерія програмного забезпечення} \\
|
||||
Освітня програма & \textit{Програмна інженерія} \\
|
||||
\end{tabular}
|
||||
\end{changemargin}
|
||||
\begin{tabularx}{\textwidth} {
|
||||
>{\raggedright\arraybackslash}X
|
||||
>{\centering\arraybackslash}X
|
||||
>{\raggedleft\arraybackslash}X }
|
||||
Курс \underline{\makebox[5em][c]{\textit{1}}} & Група \underline{\makebox[5em][c]{\textit{ПЗПІ-23-3}}} & Семестр \underline{\makebox[5em][c]{\textit{2}}}\\
|
||||
\end{tabularx}
|
||||
\null\par\null
|
||||
\textit{\textbf{ЗАВДАННЯ \\
|
||||
на текстовий проєкт студента}} \par
|
||||
\underline{\makebox[\textwidth][c]{Білоуса Антона Андрійовича}} \\
|
||||
\scriptsize{(Прізвище, Ім'я, По батькові)} \\
|
||||
\end{center}
|
||||
1 Тема проєкту: \\
|
||||
\uline{\makebox[\textwidth][c]{Ігровий застосунок "Boulder Dash" }} \\
|
||||
2 Термін здачі студентом закінченого проекту: \textbf{\textit{``\underline{08}'' - червня - 2024 р.}} \\
|
||||
3 Вихідні дані до проекту: \par
|
||||
\uline{Методичні вказівки до виконання курсової роботи} \par \null \par \noindent
|
||||
Зміст розрахунково-пояснювальної записки: \par
|
||||
\uline{Вступ, опис вимог, проектування програми, інструкція користувача, висновки} \\
|
||||
\newpage
|
||||
% ------------------------------------------- Аркуш завдання закінчився ---------------------------------------
|
||||
|
||||
% ------------------------------------------- Календарний план ------------------------------------------------
|
||||
\noindent
|
||||
\begin{center}
|
||||
КАЛЕНДАРНИЙ ПЛАН \par \null \par
|
||||
\end{center}
|
||||
\begin{tabular}{|p{1em} | p{17em} | p{11em}|}
|
||||
\hline
|
||||
\multicolumn{1}{|c|}{\textit{№}} & \multicolumn{1}{c}{\textit{Назва етапу}} & \multicolumn{1}{|c|}{\textit{Термін виконання}} \\ \hline
|
||||
1 & Видача теми, узгодження і затвердження теми & 13.02.2024 - 15.03.2024 р. \\ \hline
|
||||
2 & Формулювання вимог до програми & 15.02.2024 — 20.02.2024 р. \\ \hline
|
||||
3 & Розробка головної частини гри & 20.02.2024 — 4.03.2024 р. \\ \hline
|
||||
4 & Розробка парсера аргументів командної строки & 4.03.2024 — 14.03.2024 р. \\ \hline
|
||||
5 & Розробка основної логіки гри & 14.03.2024 — 3.04.2024 р. \\ \hline
|
||||
6 & Розробка відображення гри у терміналі та у вікні & 3.04.2024 — 23.04.2024 р. \\ \hline
|
||||
7 & Створення пояснювальної записки та документації до застосунку & 23.04.2024 — 10.05.2024 р. \\ \hline
|
||||
8 & Захист & \hspace{0.01em}03.\hspace{0.01em}06.2024 — \hspace{0.01em}08.\hspace{0.01em}06.2024 р. \\ \hline
|
||||
\end{tabular}
|
||||
|
||||
\par \null \par \null \par \null \par \null \par \null \par \noindent
|
||||
\begin{tabularx}{\textwidth} {
|
||||
>{\raggedright\arraybackslash}X
|
||||
>{\raggedleft\arraybackslash}X }
|
||||
Cтудент \underline{\hspace{10em}} \\
|
||||
\\
|
||||
Керівник \underline{\hspace{10em}} & \underline{\hspace{10em}} \\
|
||||
& \scriptsize{(Прізвище, Ім'я, По батькові)} \\
|
||||
<< 21 >> \underline{\makebox[5em][l]{ лютого}} 2024 р.
|
||||
\end{tabularx}
|
||||
% ------------------------------------------- Календарний план закінчився -------------------------------------
|
||||
|
||||
% ------------------------------------------- Реферат ---------------------------------------------------------
|
||||
\section*{РЕФЕРАТ}
|
||||
Пояснювальна записка до курсової роботи на тему "Ігровий застосунок «Boulder Dash»".
|
||||
|
||||
Метою роботи є розробка клону гри «Boulder Dash», яка буде мати базовий геймплей
|
||||
схожий на оригінальний, а також мінімальний редактор рівнів який дозволить створювати нові карти для гри.
|
||||
|
||||
Результатом є програма, котра може отимати список назв рівнів та опціональні параметри режиму малювання (графічний, псевдографічинй та консольний), програму яку треба запустити (гра або редактор рівнів), а також швидкість ігрового процесу в мілісекундах.
|
||||
|
||||
В процесі розробки використано текстовий редактор Neovim, графічну бібліотеку Rust-SDL2\cite{Rust-SDL2} та біблеотеку абстакції терміналу console, мова програмування Rust\cite{rust} 2021 року видання.
|
||||
% ------------------------------------------- Реферат закінчився ----------------------------------------------
|
||||
|
||||
% ------------------------------------------- Зміст -----------------------------------------------------------
|
||||
\tableofcontents %генерація змісту
|
||||
% ------------------------------------------- Зміст завершився ------------------------------------------------
|
||||
|
||||
% ------------------------------------------- Вступ -----------------------------------------------------------
|
||||
\section*{Вступ}
|
||||
\addcontentsline{toc}{section}{Вступ} %додаємо сторінку Вступу до змісту
|
||||
Залежність від дофаміну є великою проблемаю сучасності, тому темою цієї курсової роботи є створення мінімального клону Boulder Dash який допоможе людству не померти від нудьги, а автору відточити свої навички програмування.
|
||||
|
||||
Boulder Dash — це 2D відеогра-лабіринт-головоломка, випущена в 1984 році компанією First Star Software для 8-бітних комп'ютерів Atari. Її створили канадські розробники Пітер Ліепа та Кріс Грей. Гравець керує персонажем, який збирає скарби, уникаючи падаючого на нього каміння та інших небезпек.
|
||||
|
||||
Для реалізації цього проекту була обрана мова програмування Rust. Вона є безсумнівно найкращою мовою сьогодення, яка є неймовінро швидкою та ефективною по викорисанню пам'яті, і не витрачає час виконня і ресурси процесора збирачом сміття. Rust може працювати з критично важливими до продуктивності сервісами, запускатися на вбудованих пристроях та легко інтегруватися з іншими мовами. Розширена математична система типів та модель власності цієї дивовижної мови гарантуює безпеку пам'яті та надзвичайно просте використання мультипоточності, що дозволяє усунути майже всі помилки ще на етапі компіляції.
|
||||
|
||||
Ця курсова робота була розроблена для Linux сумісних систем через зручність та наявність нормальних інструментів для програмування і користування застосунком.
|
||||
% ------------------------------------------- Вступ закінчився ------------------------------------------------
|
||||
|
||||
% ------------------------------------------- Ну і далі сама ваша курсова -------------------------------------
|
||||
\section{Опис вимог}
|
||||
\label{sec:requirements} % можна позначати також розділи
|
||||
\subsection{Основний задум гри}
|
||||
Повинен бути створений мінімальний клон аркадної гри Boulder Dash. Застосунок отримує список рівнів які треба пройти і дає змогу це зробити. Рівні це лабінт-печера, в ньому потрібно знайти спосіб зібрати всі кристалики оминаюче падаюче каміння, штовхаючи його вбік та оминати блокуючих ситуацій. Гравець з'являється на першому рівні який є печерою, він має пройти лабіринт що б попасти на наступний, або, якщо наступного рівня немає, закінчити гру перемогою.
|
||||
|
||||
\subsection{Ігрові об'єкти}
|
||||
В оригінальній версії гри досить багато об'єктів. Через те що у цій роботі робиться лише клон, то мають бути реалізовані тільки ключові елементи. Застосунок має обов'язково містити в собі такі об'єкти:
|
||||
\subsubsection{Гравець}
|
||||
Гравець — об'єкт, яким користувач керує з клавіатури. Може переміщуватись у всіх чотирьох напрямках, ламати пісок та збирати алмази, а також штовхати каміння. Після руху гравець залишає порожнечу позаду себе.
|
||||
|
||||
Гравець має мати свій особистий клас який відповідає за поведінку, властивості та відображення цього об'єкту. Об'єкт має мати шляхи для взаємодії з усіма частинами застосунку. Реалізація класу повинна бути в окремому файлі.
|
||||
\subsubsection{Кристал}
|
||||
Кристал — статичний нерухомий об'єкт, який збільшує лічильник очок при знищені, та дає змогу завершити гру перемогою коли лічильник доходить до максимально значення, тобто було зібранне все каміння.
|
||||
|
||||
Кристал має мати свій особистий клас який відповідає за поведінку, властивості та відображення цього об'єкту. Об'єкт має мати шляхи для взаємодії з усіма частинами застосунку. Реалізація класу повинна бути в окремому файлі.
|
||||
\subsubsection{Каміння}
|
||||
Каміння — об'єкт який падає вниз якщо під ним порожнеча або по діагоналі якщо він находиться на краю, тобто клітина збоку та клітину знизу цього боку є порожніми. Коли під цим об'єктом знаходиться Гравець, та Гравець не зламав об'єкт який стримував каміння від падіння (Гравець може встигнути пробіжати під камінням, якщо воно не падало), то гра закінчується програшом. Гравець може рухати камень в бік якщо за цим каменем порожнеча.
|
||||
|
||||
Каміння має мати свій особистий клас який відповідає за поведінку, властивості та відображення цього об'єкту. Об'єкт має мати шляхи для взаємодії з усіма частинами застосунку. Реалізація класу повинна бути в окремому файлі.
|
||||
\subsubsection{Стіна}
|
||||
Стіна — статичний нерухомий та незламний об'єкт котрий обмежує рух об'єктів таких як гравець та камень. Цей об'єкт використовується для створення каркасу ріня і самого лабіринту цієї гри, тому вона має робити рамку навколо рівня.
|
||||
|
||||
Стіна має мати свій особистий клас який відповідає за поведінку, властивості та відображення цього об'єкту. Об'єкт має мати шляхи для взаємодії з усіма частинами застосунку. Реалізація класу повинна бути в окремому файлі.
|
||||
\subsubsection{Пісок}
|
||||
Пісок — статичний нерухомий об'єкт, який може бути зламаний гравцем. Цей об'єкт слугує для утримання каміння від падіння з ціллю формування більш цікавив та динамічних рівняв, в котрих існуть пастки.
|
||||
|
||||
Пісок має мати свій особистий клас який відповідає за поведінку, властивості та відображення цього об'єкту. Об'єкт має мати шляхи для взаємодії з усіма частинами застосунку. Реалізація класу повинна бути в окремому файлі.
|
||||
|
||||
|
||||
\subsection{Режими відображення}
|
||||
Гра має мати два режими відображення: консольний та графічний. Обидва режими мають мати спільну кодову базу, точніше мають бути незалежними від внутрішньої логіки гри і використовувати інтерфейси для взаємодії з нею, створюючи невеличке API для відображення логічних об'єктів на екран.
|
||||
|
||||
\subsubsection{Консольний режим відображення}
|
||||
Консольний режим має мати такий же самий вигляд як і текствоий вигляд рівня у файлі. Цей файл має бути завантажений у пам'ять застосунку, та кожен символ має бути перетворений на відповідний об'єкт у матриці рівня.
|
||||
|
||||
Консльний режим має відображати весь інтерфейс гри символами в консолі, а також оброблювати натискання клавіатури та корректно реагувати на сполуки клавіш такі як Ctrl+C для виходу з гри.
|
||||
|
||||
\subsubsection{Графічний режим відображення}
|
||||
Графічний режим має мати свою теку з текстурами для кожного ігрового об'єкту. За ім'ям кожного об'єкту цей режим завантажує в пам'ять застосунку відповідний файл в теці, піля чого при відображенні у графічному вікні загружена інформація перетворюється в текстуру.
|
||||
|
||||
Графічний режим має створити нове вікно де буде відображатись рівень та поточний статус застосунку. Вікно має автоматично змінювати свій розмір та коректно реагувати на події закриття, спроби зміни розміру вікна та натискання клавіш та їх сполук на клавіатурі.
|
||||
|
||||
\subsection{Редактор рівнів}
|
||||
Застосунок має містити в собі редактор рінів. Цей режим застосунку дозволяє створювати нові рівні гри та мати змогу їх зберегти для проходження в режимі гри пізніше. Редактор має мати інтуітивно зрозумілий інтерфейс та бути зручним для використання. Розмір рівня який редагується має мати змогу динамічно змінюватись.
|
||||
|
||||
Редактор має коректно реагувати на невідомі об'єкти, коректно зберігати рівні у файл та підтримувати всі режими відрисовки. Для цього мають бути реалізовані відповідні інтерфейси та робота з файлами.
|
||||
|
||||
\section{Проєктування програми}
|
||||
|
||||
\subsection{Вимоги}
|
||||
В ході реалізації проекту крім вимог наведених у першому пункті було також реалізовано наступні пункти:
|
||||
\begin{itemize}
|
||||
\item незалежний від інших компонентів застосунку парсер командної строки;
|
||||
\item третій режим відображення який знаходиться між консольним та графічним, — псевдографічний;
|
||||
\item запам'товування налаштувань гри між різними рівнями;
|
||||
\item можливість перепройти програний рівень;
|
||||
\item режим паузи у котрому каміння падає тільки поки гравець рухається;
|
||||
\item регулювання швидкості гри яке дозволяє прискорити падіння каміння та рух гравця.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Проектування}
|
||||
При проектування застосунку було вирішено використовувати принципи SOLID. На протязі розробки ці принципи були кілька разів порушені через нові вимоги які раптово з'являлись, а також через відсутність повноцінного об'єктно орієнтованого програмування в Rust та його специфічну парадигму яку прийшлося пристосовувати до ООП. Після рефакторингу та змін в структурі застосунку, принципи SOLID були дотримні. Поліморфізм та наявність інтерфейсів зробили кодову базу набагато зрозуміліше та менше, даючи можливість дуже просто імплементувати нові речі, такі як об'єкти, режими взаємодії (консольний, псевдографічний та графічний) та режими застосунку( гра та редактор рівів).
|
||||
|
||||
\subsection{Етап запуску застосунку}
|
||||
Застосунок запускається командою консолі, в якій він отримає аргументи для запуску, після чого ці аргументи передаються до парсерсу. Якщо якогось агрументу не має, то використовується параметр за замовчуванням. Коли було дано помилкове значення, то застосунок не запуститься, але напише який агрумент має некоректне значення. Отримати список всіх аргументів та їх параметрів можна за допомогою аргрумента "\texttt{-}h", або "\texttt{-{}-}help".
|
||||
|
||||
Реалізація парсеру аргументів повністю незалежна від інших компонетів гри, що відповідає принципам SOLID. Стуктура яку збирає цей парсер використовується по застосунку тільки для того, щоб компоненти могли ініціалізуватися дивлячись на конфігурацію задану користувачем. Вона передається у функцію run, яка сворює нові об'єкти режиму взаємодії та режиму гри. Після цього фукнція run передає контроль у створені об'єкти які можуть повернути помилку, в цьому випадку функція run поверне цю помилку у функцію main, де ця помилка буде виведена в консольний канал для помилок.
|
||||
|
||||
\subsection{Інтерфейси застосунку}
|
||||
Цей застосунок має в собі п'ять інтерфейсів, які є самою головною частиною програми та значно полегшують розробку нового функціоналу. Можна виокремити дві групи: інтерфейси взаємодії та інтерфейси ігрових об'єктів.
|
||||
|
||||
Інтерфейси взаємодії дозволяють різним режимам взаємодії відображати різні режими застосунку, їх перелік:
|
||||
\begin{itemize}
|
||||
\item "Drawable" — інтерфейс для режимів застосунку;
|
||||
\item "Interaction" — інтерфейс для режимів взаємодії.
|
||||
\end{itemize}
|
||||
|
||||
Кожний ігровий об'єкт імплементує три інтерфейси:
|
||||
\begin{enumerate}
|
||||
\item "Labels" — інтерфейс для позначення об'єкту;
|
||||
\item "Properties" — інтерфейс для отримання логічних властивостей об'єкту;
|
||||
\item "Behaviour" — інтерфейс для реалізації логіки об'єктів.
|
||||
\end{enumerate}
|
||||
|
||||
\subsubsection{Інтерфейс малювання Drawable}
|
||||
Цей інтерфейс має в собі функції для отримання графічних даних режима застосунка режимом взаємодії, а саме:
|
||||
\begin{itemize}
|
||||
\item get\_cursor — повертає позицію курсову. Використовується режимом редактору рівнів;
|
||||
\item get\_width — повертає найбільшу ширишу рівня враховуючи ширину тексту статусу. Використовується в графічному режимі відображення;
|
||||
\item get\_height — повертає висоту відображаємого вмісту режиму застосунка;
|
||||
\item get\_status — повертає строку стану застосунку. Використовується для отримання актуальної інформації;
|
||||
\item get\_damaged — повертає набір точок які були змінені і котрі треба перемалювати. Використовується консольним та графічним режимами взаємодії;
|
||||
\item get\_objects — повертає матрицю з усіма ігровими об'єктами режима застосунку. Використовується всіма режимами взаємодії;
|
||||
\item get\_object — отримує позицію та повертає об'єкт котрий лежить у тій позиції. Використовується у сполуці з функцією get\_damaged.
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection{Інтерфейс взаємодії Interaction}
|
||||
Цей інтерфейс має в собі функції для взаємодії режимів застосунку з зовнішнім світом, а саме:
|
||||
\begin{itemize}
|
||||
\item get\_input — функція імлементація якої повинна зчитати ввід з клавіатури та повернути уніфіцирований тип застосунку для вводу Input;
|
||||
\item draw — функція імплементація якої повинна отримати об'єкт імплементуючий інтферфейс малювання Drawable, та відобразити його на екрані.
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection{Інтерфейс позначення Labels}
|
||||
Цей інтерфейс має в собі функції для ідентифікації об'єкту, а саме:
|
||||
\begin{itemize}
|
||||
\item char — повертає символ який використовується при збереженні об'єкта у файл, а також у консольному режиму взаємодії;
|
||||
\item emoji — повертає емоджи яке використовується у псевдографічному режимі взаємодії;
|
||||
\item name — повертає ім'я об'єкту яке використовується для отримання назви файлу з картинкою даного об'єкту для його відображення у графічному режиму взаємодії.
|
||||
\end{itemize}
|
||||
\subsubsection{Інтерфейс властивостей Properties}
|
||||
Цей інтерфейс має в собі функції для отримання логічних властивостей об'єкту, а саме:
|
||||
\begin{itemize}
|
||||
\item placeholder — об'єкти які просто займають місце у матриці рівня;
|
||||
\item can\_be\_moved — об'єкти які можуть бути подвинуті гравцем;
|
||||
\item player — об'єкт котрий являється гравцем;
|
||||
\item can\_be\_broken — об'єкт котрий може бути зламаний гравцем.
|
||||
\end{itemize}
|
||||
\subsubsection{Інтерфейс поведінки Behaviour}
|
||||
Цей інтерфейс має в собі функції які імплементують ігрову логіку об'єкту, а саме:
|
||||
\begin{itemize}
|
||||
\item init — функція яка викликається при створені об'єкту і відправляє запроси до рівня гри;
|
||||
\item on\_broken — функція яка викликається при знищенні об'єкту і відправляє запроси до рівня гри;
|
||||
\item tick — функція яка викликається коли оновлюється рівень, і відправляє запроси до рівня гри.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Об'єкти взаємодії}
|
||||
Ці об'єкти імплементують інтерфейс взаємодії Interaction та відображають об'єкти які імплементають інтерфейс відображення Drawabale. Це дозволяє ефективне використання поліморфізму для розширення застосунку. Інтерфейс взаємодії Interaction дозволяє режимам застосунку запросити дані з клавіатури, а інтерфейс взаємодії Drawable дає змогу відобразити зміни на екран.
|
||||
|
||||
\subsubsection{Графічний режим (Gui)}
|
||||
Об'єкт взаємодії Gui створює нове вікно за допомогою Rust-SDL2\cite{Rust-SDL2} та обролює його події перетворюючи ввід з клавіатури в уніфіцирований тип вводу Input який використовуєтья по самому застосунку. Gui загружає всі файли спрайтів у бінарне дерево, за допомогою которого можна швидко знайти потрібну картинку об'єтку та перетворити її у текстуру.
|
||||
|
||||
\subsubsection{Псевдографічний режим (Tui)}
|
||||
Об'єкт взаємодії Tui не свторює вікна, він отримує ввід з клавіатури та рисує графічні елементи у консолі за допомогою емоджи. Через те, що емоджи мають відмінну ширину від клітини консолі, реалізація відстеження пошкоджень не була імплементована, вона тільки додасть складності і зробить цей режим менш продуктивним. Щоб зробити відстеження пошкодженнь був створений консольний режим Cli.
|
||||
|
||||
\subsubsection{Консольний режим (Cli)}
|
||||
Об'єкт взаємодії Cli має дуже зхожу кодову базу до Tui, він навіть використовує функцію для отримання вводу з клавіатури від нього, але функція відображення графіки відрізняється. У всіх режимах ця функція отримує об'єкт який імплементує інтерфейс для рисовки, він має в собі функцію для отримання змінених клітин. При відображенні інтерфейсу Cli не очищає екран, натомість він малює символ з нових клітин поверх старих, які повертає ця фунція. Ефективніть цього режиму дозволить дуже швидко малювати неймовірно великі рівні.
|
||||
|
||||
\subsection{Режими застосунку}
|
||||
Режими застосунку дозволяють мати кілька програм в одній. Вони імплементують інтерфейс для графічного відображення Drawable. Цей інтерфейс дозволяє об'єктам взаємодії отримати необхідну їм інформацію від режиму застосунку для того, щоб його відобразити.
|
||||
|
||||
\subsubsection{Режим гри}
|
||||
Коли запускається режим гри, створюється новий об'єкт гри який створює об'єкти рівней, і для нього запускаться функція run. Після об'єкт гри починає запускати рівні по черзі. Він викликає функції об'єкту взаємодії для обробки подій клавіатури та рисування рівня з статусом. Рівні повертають стан на якому гра була закінчена, якщо була повернена поразка, гра закінчується з повідомленням поразки, в іншому випадку гра продовжується поки не буде пройдений останній рівень і виведе повідомлення перемоги.
|
||||
|
||||
\subsubsection{Режим редактору рівнів}
|
||||
Цей режим отримує назву файла, та відкриває його. Зміст файлу записується в внутрішні данні редактору, після чого можна можна побачити рівень на екрані, та редагувати його. Цей режим дуже схожий на гру, тільки він не так тісно взаємодіє з об'єктами. Після виходу з цього режиму рівень зберігається, а програма завершує своє виконання.
|
||||
|
||||
\subsection{Ігрові об'єкти}
|
||||
У кожного об'єкту є власний файл де лежить його імплементація. Вони мають свою власну теку, і їх імпортує модуль який має рівну область видимості для всіх елементів застосунку. Ігрові об'єкти не належать лише до області режиму гри, бо вони активно використовуються іншими компонетами застосунку через інтерфейси.
|
||||
|
||||
\subsubsection{Гравець (Player)}
|
||||
Цей об'єкт має параметр гравця. Йому передається ввід з клавіатури, після чого він рухається і ламає інші об'єкти. Також об'єкт гравця перевіряє камінь зверху щоб визначити чи може він впасти і закінчити гру програшом.
|
||||
|
||||
Згідно з умовами гравець має свій особистий клас який відповідає за поведінку об'єкту, його властивості та відображення. Також цей об'єкт за допомогою своїх інтерфейсів може взаємодіяти з ішними об'єктами використовуючи механізми ігрового рівня.
|
||||
|
||||
\subsubsection{Кристал (Gem)}
|
||||
Кристал має параметр знищення. При ініціалізації він додає один бал до максимального значення лічильнику, а при знищенні збільшує лічильник. Якщо при знищенні лічильник досягне максимального значення, то рівень гри закінчується перемогою.
|
||||
|
||||
Згідно з умовами кристал має свій особистий клас який відповідає за поведінку об'єкту, його властивості та відображення. Також цей об'єкт за допомогою своїх інтерфейсів може взаємодіяти з ішними об'єктами використовуючи механізми ігрового рівня.
|
||||
|
||||
\subsubsection{Каміння (Rock)}
|
||||
Каміння має параметр руху. Йому передається рівень та його поточні кординати. Камінь дивиться на стан об'єкту знизу, і якщо там порожнеча, то він падає вниз. Якщо камінь не зміг впасти в низ, то він перевіряє диагоналі, якщо вони вільні, то падіння відбудеться по диагоналі.
|
||||
|
||||
Згідно з умовами каміння має свій особистий клас який відповідає за поведінку об'єкту, його властивості та відображення. Також цей об'єкт за допомогою своїх інтерфейсів може взаємодіяти з ішними об'єктами використовуючи механізми ігрового рівня.
|
||||
|
||||
\subsubsection{Пісок (Dirt)}
|
||||
Пісок має параметр знищення. В нього немає поведінки і він слугує тільки для втримування каміння на місці.
|
||||
|
||||
Згідно з умовами пісок має свій особистий клас який відповідає за поведінку об'єкту, його властивості та відображення. Також цей об'єкт за допомогою своїх інтерфейсів може взаємодіяти з ішними об'єктами використовуючи механізми ігрового рівня.
|
||||
|
||||
\subsubsection{Стіна (Wall)}
|
||||
Стіна не має ані власних властивостей, ані поведінки. Від піску цей об'єкт відрізняє тільки те, що його не можна зламати.
|
||||
|
||||
Згідно з умовами стіна має свій особистий клас який відповідає за поведінку об'єкту, його властивості та відображення. Також цей об'єкт за допомогою своїх інтерфейсів може взаємодіяти з ішними об'єктами використовуючи механізми ігрового рівня.
|
||||
|
||||
\subsubsection{Пороженча (Void)}
|
||||
Порожнеча має властивість заповнювача і не має поведінки. Він свторений просто для заповнювання порожніх клітин рівня, і не впливає на рух або існування об'єктів.
|
||||
|
||||
Порожнечі не було в умовах, але цей об'єкт теж має свій особистий клас який відповідає за поведінку об'єкту, його властивості та відображення. За допомогою своїх інтерфейсів він може взаємодіяти з ішними об'єктами використовуючи механізми ігрового рівня.
|
||||
|
||||
\subsubsection{Невідомий об'єкт (Unknown)}
|
||||
Невідомий об'єкт має взалстивість заповнювача і не має поведінки. Від пороженчі його відрізняє тільки імплементація інтерфейсу позначення Labels, тобто цей елемент малюється по іншому. Він був створений при написанні режиму редактору рівнів для того що б позначати помилкові об'єкти, або об'єкти для яких не існує текстури.
|
||||
|
||||
|
||||
\section{Інструкція користувача}
|
||||
\subsection{Аргументи консолі}
|
||||
Застосунок має такі аргументи командної строки:
|
||||
\begin{itemize}
|
||||
\item для отримання списку всіх аргументів застосунку "\texttt{-{}-}h" або "\texttt{-{}-}help";
|
||||
\item обрати режим інтеракції можна за допомогою опції"\texttt{-{}-}m" або "\texttt{-{}-}mode" і написати "gui", "tui" або "cli" через пробіл після неї;
|
||||
\item обрати підпрограму можна за допомогою опції "\texttt{-{}-}r" або "\texttt{-{}-}run" і написав "game" або "editor" через пробіл після неї;
|
||||
\item поставити паузу в грі при запуску можна за допомогою опції "\texttt{-{}-}p" або "\texttt{-{}-}pause";
|
||||
\item обрати розмір елементів у пікселях в графічному режимі можна за допомогою опції "\texttt{-{}-}s" або "\texttt{-{}-}size" та число через пробіл;
|
||||
\item обрати затримку між кадрами в мілісекундах для гри можна за допомогою опції "\texttt{-{}-}d" або "\texttt{-{}-}delay" та число через пробіл.
|
||||
\end{itemize}
|
||||
|
||||
Всі інші аргументи сприймаються як назви файлів рівнів.
|
||||
|
||||
\subsection{Керування застосунком}
|
||||
Керування застосунком здійснюється з клавіатури виключно. Таке рішення було прийнято через багато факторів пов'язаних з легкістю імлементації та використання застосунка.
|
||||
|
||||
\subsubsection{Керування грою}
|
||||
\begin{itemize}
|
||||
\item рух здійснюється за допомогою стрілочек клавіатури;
|
||||
\item перезапустити рівень можна за допомогою клавіші R;
|
||||
\item пауза або її зняття виконоуються натисканням пробіла;
|
||||
\item зменшення та збільшення затримки між кадрами здійснюється комою і крапою відповідно;
|
||||
\item вийти з гри можна за допомогою клавіши Q, або натиснув Ctrl+C.
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection{Керування редактором рівнів}
|
||||
\begin{itemize}
|
||||
\item рух здійснюється за допомогою стрілочек клавіатури;
|
||||
\item перезагрузити рівень з файлу можна за допомогою клавіші R;
|
||||
\item опустити або підняти перо виконоується натисканням пробіла;
|
||||
\item обирати об'єкт можна комою та крапкою, вони обераютья попередній і наступний об'єкт відповідно;
|
||||
\item вийти та зберегти рівень можна за допомогою клавіши Q, або натиснув сполуку Ctrl+C.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Позначення об'єктів}
|
||||
В консольному режимі взаємодії та у файлі рівня ігрові об'єкти позначаються однаковими симоволами. Ось позначення кожного ігрового об'єкту в дужках:
|
||||
\begin{itemize}
|
||||
\item гравець: "p";
|
||||
\item кристал: "+";
|
||||
\item каміння: "O";
|
||||
\item пісок: "*";
|
||||
\item стіна: "\#";
|
||||
\item порожнеча: "\ ";
|
||||
\item невідомий об'єкт: "?".
|
||||
\end{itemize}
|
||||
|
||||
Якщо в рівні видно невідомий об'єкт, значить в файлі рівня є символи які гра не може інтепретувати. Вирішити це можна використав режим редактору, або, якщо рівень вже запущений в грі, власноруч відредагувати файл у текствому редакторі та натиснути клавішу R для перезагрузки рівня.
|
||||
|
||||
|
||||
% ------------------------------------------- Курсова завершилась ---------------------------------------------
|
||||
|
||||
% ------------------------------------------- Висновки --------------------------------------------------------
|
||||
\section*{Висновки}
|
||||
\addcontentsline{toc}{section}{Висновки} %додаємо сторінку Вступу до змісту
|
||||
Було створено застосунок який має три режими вводу/виводу, гру та редактор рівнів. За допомогою інтерфейсів та поліморфізму можна дуже легко додати ще один режим вводу/виводу або розширити програму як це було зроблено з редактором рівнів. Застосунок дуже швидко працює та має такі оптимізації як відстеження пошкоджених клітинок та буферизацію графічних елементів.
|
||||
|
||||
В ході виконання цієї роботи я отримав багато практики, знань і навичок програмування на Rust, і став комфортно працювати з великою кількістю файлів, системою контролю версій git і пакетним менеджером cargo.
|
||||
% ------------------------------------------- Висновки закінчилися --------------------------------------------
|
||||
|
||||
% ------------------------------------------- Перелік джерел посилання ----------------------------------------
|
||||
\raggedright % увімкнути вирівнювання вліво
|
||||
\begin{thebibliography}{99} % починаємо перелік та вставляємо туди наші посилання
|
||||
\addcontentsline{toc}{section}{Перелік джерел} % додаємо перелік до змісту
|
||||
\bibitem{dstu} Grafiati: Оформити списки використаних джерел онлайн. Grafiati: Оформити списки використаних джерел онлайн. URL: https://www.grafiati.com/uk/ (дата звернення: 09.05.2024).
|
||||
\bibitem{Rust-SDL2} GitHub - Rust-SDL2/rust-sdl2: SDL2 bindings for Rust. GitHub. URL: https://github.com/Rust-SDL2/rust-sdl2 (дата звернення: 2.06.2024).
|
||||
\bibitem{rust} Rust Programming Language. Rust Programming Language. URL: https://www.rust-lang.org/ (дата звернення: 10.05.2024).
|
||||
\end{thebibliography}
|
||||
|
||||
% ------------------------------------------- Перелік джерел посилання закінчився -----------------------------
|
||||
|
||||
% ------------------------------------------- Перелік джерел посилання закінчився -----------------------------
|
||||
|
||||
% ------------------------------------------- Додатки (якщо треба) --------------------------------------------
|
||||
\titleformat{\section}[display]{\filcenter}{\MakeUppercase{\bfseries{Додаток \thesection}}}{0pt}{} % зміна форматування заголовків у додатках
|
||||
\titlecontents{section}[0pt]{\normalfont}{Додаток {\thecontentslabel} }{}{\Dotfill \contentspage} % зміна форматування додатків у змісті
|
||||
\appendix
|
||||
\setlength{\parindent}{0.5in}
|
||||
% додавайте додатки: додатки додаються так само секціями
|
||||
|
||||
\section{Код програми}
|
||||
Актуальну версію коду можна занйти за посиланням \url{https://github.com/oxid8/nure/tree/main/2/coursework/src}
|
||||
|
||||
|
||||
% ------------------------------------------- Додатки (якщо треба) закінчилися -------------------------------
|
||||
|
||||
\end{document}
|
216
2/coursework/src/Cargo.lock
generated
Normal file
@ -0,0 +1,216 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "boulder_dash"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console",
|
||||
"enum_dispatch",
|
||||
"sdl2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"unicode-width",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||
|
||||
[[package]]
|
||||
name = "enum_dispatch"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sdl2"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8356b2697d1ead5a34f40bcc3c5d3620205fe0c7be0a14656223bfeec0258891"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"sdl2-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sdl2-sys"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26bcacfdd45d539fb5785049feb0038a63931aa896c7763a2a12e125ec58bd29"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
11
2/coursework/src/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "boulder_dash"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
console = "0.15.8"
|
||||
enum_dispatch = "0.3.13"
|
||||
sdl2 = { version = "0.36.0", features = ["image", "ttf"] }
|
43
2/coursework/src/README.md
Normal file
@ -0,0 +1,43 @@
|
||||
```sh
|
||||
cargo r -- -h
|
||||
```
|
||||
```
|
||||
FLAGS:
|
||||
-h, --help
|
||||
Show this message.
|
||||
-p, --pause
|
||||
Launch paused.
|
||||
OPTIONS:
|
||||
-l, --level <string>
|
||||
Required.
|
||||
Specify a level to run.
|
||||
Can be used multiple times.
|
||||
-m, --mode <string>
|
||||
* gui
|
||||
* tui (default)
|
||||
* cli
|
||||
Select the interaction mode.
|
||||
-r, --run <string>
|
||||
* g / b / game (default)
|
||||
* e / editor
|
||||
Select the program mode.
|
||||
-s, --size <integer>
|
||||
Object size for GUI. (default 30 pixels).
|
||||
-d, --delay <integer>
|
||||
Delay between frames. (default: 1000 ms)\
|
||||
```
|
||||
|
||||
```sh
|
||||
cargo r -- -l assets/levels/level -m tui
|
||||
```
|
||||

|
||||
|
||||
```sh
|
||||
cargo r -- -l assets/levels/level -m cli -r e
|
||||
```
|
||||

|
||||
|
||||
```sh
|
||||
cargo r -- -l assets/levels/level -m gui -r e
|
||||
```
|
||||

|
BIN
2/coursework/src/assets/font.ttf
Normal file
BIN
2/coursework/src/assets/img/cli_editor.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
2/coursework/src/assets/img/gui_editor.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
2/coursework/src/assets/img/tui_game.png
Normal file
After Width: | Height: | Size: 45 KiB |
17
2/coursework/src/assets/levels/level
Normal file
@ -0,0 +1,17 @@
|
||||
#################
|
||||
#+OOO#**O #*O#O+#
|
||||
#******#OO*+****#
|
||||
#*######## ######
|
||||
#*O* O O O O O O#
|
||||
#+# OOO OOO OOO #
|
||||
#O# * O OO+ O #
|
||||
#* O OOO #
|
||||
# * OO #
|
||||
#OO* + O #
|
||||
#** * #
|
||||
# O #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #+******p#
|
||||
#################
|
4
2/coursework/src/assets/levels/test
Normal file
@ -0,0 +1,4 @@
|
||||
######
|
||||
#+ Op#
|
||||
##**##
|
||||
######
|
0
2/coursework/src/assets/levels/test2
Normal file
BIN
2/coursework/src/assets/sprites/dirt
Normal file
After Width: | Height: | Size: 159 B |
BIN
2/coursework/src/assets/sprites/gem
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
2/coursework/src/assets/sprites/player
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
2/coursework/src/assets/sprites/rock
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
2/coursework/src/assets/sprites/unknown
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
2/coursework/src/assets/sprites/void
Normal file
After Width: | Height: | Size: 87 B |
BIN
2/coursework/src/assets/sprites/wall
Normal file
After Width: | Height: | Size: 1.3 KiB |
130
2/coursework/src/src/args.rs
Normal file
@ -0,0 +1,130 @@
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
const HELP_MSG: &str = "\
|
||||
FLAGS:
|
||||
-h, --help
|
||||
Show this message.
|
||||
-p, --pause
|
||||
Launch paused.
|
||||
OPTIONS:
|
||||
-l, --level <string>
|
||||
Required.
|
||||
Specify a level to run.
|
||||
Can be used multiple times.
|
||||
-m, --mode <string>
|
||||
* gui
|
||||
* tui (default)
|
||||
* cli
|
||||
Select the interaction mode.
|
||||
-r, --run <string>
|
||||
* g / b / game (default)
|
||||
* e / editor
|
||||
Select the program mode.
|
||||
-s, --size <integer>
|
||||
Object size for GUI. (default 30 pixels).
|
||||
-d, --delay <integer>
|
||||
Delay between frames. (default: 1000 ms)\
|
||||
";
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum InteractionMode {
|
||||
Gui,
|
||||
Tui,
|
||||
Cli,
|
||||
}
|
||||
|
||||
impl FromStr for InteractionMode {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"gui" => Ok(Self::Gui),
|
||||
"tui" => Ok(Self::Tui),
|
||||
"cli" => Ok(Self::Cli),
|
||||
_ => Err(format!("Can't parse `{s}` as a valid display mode!")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum ProgramMode {
|
||||
Game,
|
||||
Editor,
|
||||
}
|
||||
|
||||
impl FromStr for ProgramMode {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"g" | "b" | "game" => Ok(Self::Game),
|
||||
"e" | "editor" => Ok(Self::Editor),
|
||||
_ => Err(format!("Can't parse `{s}` as a valid program mode!")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub size: u32,
|
||||
pub pause: bool,
|
||||
pub delay: Duration,
|
||||
pub level_paths: Vec<String>,
|
||||
pub program_mode: ProgramMode,
|
||||
pub interaction_mode: InteractionMode,
|
||||
}
|
||||
|
||||
impl Default for Arguments {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
size: 30,
|
||||
pause: false,
|
||||
delay: Duration::from_millis(1000),
|
||||
level_paths: vec![],
|
||||
program_mode: ProgramMode::Game,
|
||||
interaction_mode: InteractionMode::Tui,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_arg<T, E>(arg_opt: Option<String>, arg_name: &str) -> Result<T, String>
|
||||
where
|
||||
T: FromStr<Err = E>,
|
||||
E: ToString,
|
||||
{
|
||||
match arg_opt {
|
||||
Some(arg) => arg.parse().map_err(|e: E| e.to_string()),
|
||||
None => Err(format!("Missing value for `{arg_name}`!")),
|
||||
}
|
||||
}
|
||||
|
||||
impl Arguments {
|
||||
pub fn parse(mut args: impl Iterator<Item = String>) -> Result<Self, String> {
|
||||
let mut config = Self::default();
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"-h" | "--help" => {
|
||||
println!("{HELP_MSG}");
|
||||
std::process::exit(0);
|
||||
}
|
||||
"-p" | "--pause" => config.pause = true,
|
||||
|
||||
"-s" | "--size" => config.size = parse_arg(args.next(), arg.as_str())?,
|
||||
"-d" | "--delay" => {
|
||||
config.delay = Duration::from_millis(parse_arg(args.next(), arg.as_str())?);
|
||||
}
|
||||
"-l" | "--level" => config
|
||||
.level_paths
|
||||
.push(parse_arg(args.next(), arg.as_str())?),
|
||||
"-r" | "--run" => config.program_mode = parse_arg(args.next(), arg.as_str())?,
|
||||
"-m" | "--mode" => config.interaction_mode = parse_arg(args.next(), arg.as_str())?,
|
||||
|
||||
_ => return Err(format!("Unrecognized option `{arg}`!")),
|
||||
}
|
||||
}
|
||||
|
||||
match config.level_paths.first() {
|
||||
Some(_) => Ok(config),
|
||||
None => Err("Specify a level path with `-l some/path`!".into()),
|
||||
}
|
||||
}
|
||||
}
|
40
2/coursework/src/src/direction.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use crate::interaction::Input;
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl TryFrom<Input> for Direction {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(input: Input) -> Result<Self, Self::Error> {
|
||||
match input {
|
||||
Input::Up | Input::W => Ok(Self::Up),
|
||||
Input::Down | Input::S => Ok(Self::Down),
|
||||
Input::Left | Input::A => Ok(Self::Left),
|
||||
Input::Right | Input::D => Ok(Self::Right),
|
||||
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Direction {
|
||||
pub const fn apply_to(&self, point: &(usize, usize)) -> (usize, usize) {
|
||||
let (x, y) = match self {
|
||||
Self::Up => (0, -1),
|
||||
Self::Down => (0, 1),
|
||||
Self::Left => (-1, 0),
|
||||
Self::Right => (1, 0),
|
||||
};
|
||||
|
||||
(
|
||||
point.0.saturating_add_signed(x),
|
||||
point.1.saturating_add_signed(y),
|
||||
)
|
||||
}
|
||||
}
|
183
2/coursework/src/src/editor.rs
Normal file
@ -0,0 +1,183 @@
|
||||
use crate::{
|
||||
args::Arguments,
|
||||
direction::Direction,
|
||||
interaction::{Drawable, Input, Interaction, Mode},
|
||||
objects::{Labels, Object},
|
||||
Point,
|
||||
};
|
||||
use std::{collections::HashSet, error::Error, fs, io, thread, time::Duration};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Editor {
|
||||
file_name: String,
|
||||
cursor: Point,
|
||||
pen_down: bool,
|
||||
current_object: usize,
|
||||
damaged: HashSet<Point>,
|
||||
matrix: Vec<Vec<Object>>,
|
||||
}
|
||||
|
||||
impl Drawable for Editor {
|
||||
fn get_cursor(&self) -> Option<&Point> {
|
||||
Some(&self.cursor)
|
||||
}
|
||||
|
||||
fn get_damaged(&mut self) -> Vec<Point> {
|
||||
std::mem::take(&mut self.damaged).into_iter().collect()
|
||||
}
|
||||
fn get_objects(&self) -> &Vec<Vec<Object>> {
|
||||
&self.matrix
|
||||
}
|
||||
fn get_object(&self, (x, y): Point) -> Option<&Object> {
|
||||
self.matrix.get(y)?.get(x)
|
||||
}
|
||||
|
||||
fn get_status(&self) -> String {
|
||||
let (x, y) = self.cursor;
|
||||
let mut objects: Vec<String> = Object::get_all_displayable()
|
||||
.iter()
|
||||
.map(Labels::name)
|
||||
.collect();
|
||||
objects[self.current_object].insert(0, '[');
|
||||
objects[self.current_object].push(']');
|
||||
let pen = if self.pen_down { "down" } else { "up" };
|
||||
|
||||
format!("Pen {pen}\nCursor pos: ({x}, {y})\n{}", objects.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn new(args: &Arguments) -> io::Result<Self> {
|
||||
let mut editor = Self {
|
||||
file_name: args.level_paths[0].clone(),
|
||||
..Default::default()
|
||||
};
|
||||
editor.reload()?;
|
||||
Ok(editor)
|
||||
}
|
||||
|
||||
fn reload(&mut self) -> io::Result<()> {
|
||||
self.matrix = vec![];
|
||||
|
||||
let contents = fs::read_to_string(&self.file_name)?;
|
||||
for (y, line) in contents.trim().lines().map(str::trim).enumerate() {
|
||||
self.matrix.push(line.chars().map(Object::new).collect());
|
||||
self.damaged.extend((0..line.len()).map(|x| (x, y)));
|
||||
}
|
||||
|
||||
if self.matrix.is_empty() {
|
||||
self.matrix.push(vec![Object::default()]);
|
||||
self.damaged.insert((0, 0));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save(&mut self) -> io::Result<()> {
|
||||
let mut contents = String::new();
|
||||
|
||||
for row in &self.matrix {
|
||||
contents += row.iter().map(Labels::char).collect::<String>().trim();
|
||||
contents.push('\n');
|
||||
}
|
||||
|
||||
fs::write(&self.file_name, contents.trim())
|
||||
}
|
||||
|
||||
pub fn run(&mut self, interaction: &mut Mode) -> Result<(), Box<dyn Error>> {
|
||||
interaction.draw(self)?;
|
||||
|
||||
let objects = Object::get_all_displayable();
|
||||
|
||||
loop {
|
||||
thread::sleep(Duration::from_millis(25));
|
||||
|
||||
let mut direction = None;
|
||||
|
||||
let input = interaction.get_input();
|
||||
match input {
|
||||
Input::Quit | Input::Q => {
|
||||
self.save()?;
|
||||
return Ok(());
|
||||
}
|
||||
Input::R => {
|
||||
self.reload()?;
|
||||
self.pen_down = false;
|
||||
}
|
||||
Input::Esc => self.save()?,
|
||||
Input::Space => {
|
||||
self.pen_down = !self.pen_down;
|
||||
}
|
||||
Input::Comma => {
|
||||
if self.current_object == 0 {
|
||||
self.current_object = objects.len();
|
||||
}
|
||||
self.current_object -= 1;
|
||||
}
|
||||
Input::Period => {
|
||||
self.current_object += 1;
|
||||
if self.current_object >= objects.len() {
|
||||
self.current_object = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Input::Up
|
||||
| Input::Down
|
||||
| Input::Left
|
||||
| Input::Right
|
||||
| Input::W
|
||||
| Input::A
|
||||
| Input::S
|
||||
| Input::D => direction = Direction::try_from(input).ok(),
|
||||
|
||||
Input::Unknown => continue,
|
||||
}
|
||||
|
||||
if let Some(dir) = direction {
|
||||
match dir {
|
||||
Direction::Up => {
|
||||
if self.matrix.len() > 1
|
||||
&& self
|
||||
.matrix
|
||||
.last()
|
||||
.map_or(false, |l| l.iter().all(|o| *o == Object::default()))
|
||||
{
|
||||
self.matrix.pop();
|
||||
}
|
||||
}
|
||||
Direction::Left => {
|
||||
for row in &mut self.matrix {
|
||||
while self.cursor.0 < row.len()
|
||||
&& *row.last().unwrap() == Object::default()
|
||||
{
|
||||
row.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.damaged.insert(self.cursor);
|
||||
self.cursor = dir.apply_to(&self.cursor);
|
||||
}
|
||||
|
||||
let (x, y) = self.cursor;
|
||||
|
||||
while y + 1 > self.matrix.len() {
|
||||
self.matrix.push(vec![]);
|
||||
}
|
||||
// Moving up or down on a shorter row
|
||||
while x + 1 > self.matrix[y].len() {
|
||||
self.matrix[y].push(Object::default());
|
||||
self.damaged.insert((self.matrix[y].len() - 1, y));
|
||||
}
|
||||
|
||||
if self.pen_down {
|
||||
self.matrix[self.cursor.1][self.cursor.0] = objects[self.current_object].clone();
|
||||
self.damaged.insert(self.cursor);
|
||||
}
|
||||
|
||||
interaction.draw(self)?;
|
||||
}
|
||||
}
|
||||
}
|
151
2/coursework/src/src/game.rs
Normal file
@ -0,0 +1,151 @@
|
||||
use crate::{
|
||||
args::Arguments,
|
||||
direction::Direction,
|
||||
interaction::{Drawable, Input, Interaction, Mode},
|
||||
objects::Object,
|
||||
Point,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
fs, io, thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub mod level;
|
||||
use level::{Level, State};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Game {
|
||||
pause: bool,
|
||||
delay: Duration,
|
||||
level_idx: usize,
|
||||
levels: Vec<Level>,
|
||||
level_paths: Vec<String>,
|
||||
}
|
||||
|
||||
impl Drawable for Game {
|
||||
fn get_damaged(&mut self) -> Vec<Point> {
|
||||
self.get_level_mut().get_damaged().into_iter().collect()
|
||||
}
|
||||
fn get_objects(&self) -> &Vec<Vec<Object>> {
|
||||
self.get_level().get_objects()
|
||||
}
|
||||
fn get_object(&self, (x, y): Point) -> Option<&Object> {
|
||||
self.get_level().get_objects().get(y)?.get(x)
|
||||
}
|
||||
|
||||
fn get_status(&self) -> String {
|
||||
match self.get_level().get_state() {
|
||||
Some(State::Win) => "You have won!".to_string(),
|
||||
Some(State::Lose) => "You have lost!\nR - reload".to_string(),
|
||||
None => format!(
|
||||
"Score: {}/{}\nDelay: {}ms\nPaused: {}",
|
||||
self.get_level().get_score(),
|
||||
self.get_level().get_max_score(),
|
||||
self.delay.as_millis(),
|
||||
if self.pause { "yes" } else { "no" }
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Game {
|
||||
fn get_level(&self) -> &Level {
|
||||
&self.levels[self.level_idx]
|
||||
}
|
||||
fn get_level_mut(&mut self) -> &mut Level {
|
||||
&mut self.levels[self.level_idx]
|
||||
}
|
||||
fn get_level_path(&self) -> &str {
|
||||
&self.level_paths[self.level_idx]
|
||||
}
|
||||
|
||||
pub fn new(args: &Arguments) -> io::Result<Self> {
|
||||
let mut game = Self {
|
||||
pause: args.pause,
|
||||
delay: args.delay,
|
||||
level_paths: args.level_paths.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for path in &args.level_paths {
|
||||
game.levels.push(Level::new(&fs::read_to_string(path)?));
|
||||
}
|
||||
|
||||
Ok(game)
|
||||
}
|
||||
|
||||
pub fn run(&mut self, interaction: &mut Mode) -> Result<(), Box<dyn Error>> {
|
||||
let mut direction = None;
|
||||
let mut paused_on_start = true;
|
||||
let mut timer = Instant::now();
|
||||
|
||||
interaction.draw(self)?;
|
||||
|
||||
loop {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
|
||||
let input = interaction.get_input();
|
||||
match input {
|
||||
Input::Quit | Input::Q => return Ok(()),
|
||||
Input::Comma => {
|
||||
if self.delay.as_millis() >= 100 {
|
||||
self.delay -= Duration::from_millis(50);
|
||||
}
|
||||
}
|
||||
Input::Period => {
|
||||
if self.delay.as_millis() <= 950 {
|
||||
self.delay += Duration::from_millis(50);
|
||||
}
|
||||
}
|
||||
Input::Esc | Input::Space => self.pause = !self.pause,
|
||||
Input::R => {
|
||||
self.levels[self.level_idx] =
|
||||
Level::new(&fs::read_to_string(self.get_level_path())?);
|
||||
direction = None;
|
||||
paused_on_start = true;
|
||||
interaction.draw(self)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
Input::Up
|
||||
| Input::Down
|
||||
| Input::Left
|
||||
| Input::Right
|
||||
| Input::W
|
||||
| Input::A
|
||||
| Input::S
|
||||
| Input::D => direction = Direction::try_from(input.clone()).ok(),
|
||||
|
||||
Input::Unknown => (),
|
||||
}
|
||||
|
||||
if let Some(state) = self.get_level().get_state() {
|
||||
if *state == State::Win && self.level_idx + 1 < self.levels.len() {
|
||||
self.level_idx += 1;
|
||||
interaction.draw(self)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if timer.elapsed() < self.delay {
|
||||
if input != Input::Unknown {
|
||||
interaction.draw(self)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
timer = Instant::now();
|
||||
|
||||
if paused_on_start && direction.is_some() {
|
||||
paused_on_start = false;
|
||||
}
|
||||
if (self.pause && direction.is_none()) || paused_on_start {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.get_level_mut().tick(direction.take());
|
||||
interaction.draw(self)?;
|
||||
}
|
||||
}
|
||||
}
|
116
2/coursework/src/src/game/level.rs
Normal file
@ -0,0 +1,116 @@
|
||||
use crate::{
|
||||
direction::Direction,
|
||||
objects::{Behaviour, Object, Properties},
|
||||
Point,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum State {
|
||||
Win,
|
||||
Lose,
|
||||
}
|
||||
|
||||
pub enum Request {
|
||||
AddScore,
|
||||
AddMaxScore,
|
||||
UpdateState(State),
|
||||
MoveObj { from: Point, to: Point }, // (from, to)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Level {
|
||||
score: usize,
|
||||
max_score: usize,
|
||||
player: Point,
|
||||
state: Option<State>,
|
||||
damaged: HashSet<Point>,
|
||||
matrix: Vec<Vec<Object>>,
|
||||
}
|
||||
|
||||
// Getters
|
||||
impl Level {
|
||||
pub const fn get_score(&self) -> &usize {
|
||||
&self.score
|
||||
}
|
||||
pub const fn get_max_score(&self) -> &usize {
|
||||
&self.max_score
|
||||
}
|
||||
pub const fn get_state(&self) -> &Option<State> {
|
||||
&self.state
|
||||
}
|
||||
pub const fn get_player(&self) -> &Point {
|
||||
&self.player
|
||||
}
|
||||
pub fn get_damaged(&mut self) -> HashSet<Point> {
|
||||
std::mem::take(&mut self.damaged)
|
||||
}
|
||||
pub fn get_object(&self, (x, y): Point) -> &Object {
|
||||
&self.matrix[y][x]
|
||||
}
|
||||
pub const fn get_objects(&self) -> &Vec<Vec<Object>> {
|
||||
&self.matrix
|
||||
}
|
||||
}
|
||||
|
||||
impl Level {
|
||||
pub fn new(string: &str) -> Self {
|
||||
let mut level = Self::default();
|
||||
for (y, line) in string.trim().lines().enumerate() {
|
||||
let mut row = vec![];
|
||||
|
||||
for (x, chr) in line.trim().chars().enumerate() {
|
||||
let obj = Object::new(chr);
|
||||
level.handle_requests(obj.init());
|
||||
if obj.player() {
|
||||
level.player = (x, y);
|
||||
}
|
||||
|
||||
level.damaged.insert((x, y));
|
||||
row.push(obj);
|
||||
}
|
||||
level.matrix.push(row);
|
||||
}
|
||||
|
||||
level
|
||||
}
|
||||
|
||||
fn handle_requests(&mut self, requests: Vec<Request>) {
|
||||
for request in requests {
|
||||
match request {
|
||||
Request::UpdateState(state) => {
|
||||
if self.state.is_none() {
|
||||
self.state = Some(state);
|
||||
}
|
||||
}
|
||||
Request::AddScore => self.score += 1,
|
||||
Request::AddMaxScore => self.max_score += 1,
|
||||
Request::MoveObj { from, to } => {
|
||||
if self.get_object(from).player() {
|
||||
self.player = to;
|
||||
}
|
||||
|
||||
self.matrix[to.1][to.0] = std::mem::take(&mut self.matrix[from.1][from.0]);
|
||||
self.damaged.extend([from, to]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, direction: Option<Direction>) {
|
||||
// Player
|
||||
let requests = self
|
||||
.get_object(self.player)
|
||||
.tick(self, self.player, direction);
|
||||
self.handle_requests(requests);
|
||||
|
||||
// Rocks
|
||||
for y in (0..self.matrix.len()).rev() {
|
||||
for x in 0..self.matrix[y].len() {
|
||||
if self.matrix[y][x].can_be_moved() {
|
||||
self.handle_requests(self.matrix[y][x].tick(self, (x, y), None));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
82
2/coursework/src/src/interaction.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use crate::{
|
||||
args::{Arguments, InteractionMode},
|
||||
objects::Object,
|
||||
Point,
|
||||
};
|
||||
use enum_dispatch::enum_dispatch;
|
||||
use std::{cmp, error::Error};
|
||||
|
||||
mod cli;
|
||||
mod gui;
|
||||
mod tui;
|
||||
|
||||
use cli::Cli;
|
||||
use gui::Gui;
|
||||
use tui::Tui;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum Input {
|
||||
Quit,
|
||||
Esc,
|
||||
Unknown,
|
||||
Q,
|
||||
R,
|
||||
W,
|
||||
A,
|
||||
S,
|
||||
D,
|
||||
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Space,
|
||||
Comma,
|
||||
Period,
|
||||
}
|
||||
|
||||
#[enum_dispatch]
|
||||
pub enum Mode {
|
||||
Gui,
|
||||
Tui,
|
||||
Cli,
|
||||
}
|
||||
|
||||
#[enum_dispatch(Mode)]
|
||||
pub trait Interaction {
|
||||
fn get_input(&mut self) -> Input;
|
||||
fn draw(&mut self, drawable: &mut impl Drawable) -> Result<(), Box<dyn Error>>;
|
||||
}
|
||||
|
||||
pub trait Drawable {
|
||||
fn get_cursor(&self) -> Option<&Point> {
|
||||
None
|
||||
}
|
||||
fn get_width(&self) -> usize {
|
||||
cmp::max(
|
||||
self.get_objects()
|
||||
.iter()
|
||||
.max_by_key(|r| r.len())
|
||||
.map_or(0, Vec::len),
|
||||
self.get_status()
|
||||
.lines()
|
||||
.max_by_key(|r| r.len())
|
||||
.map_or(0, |s| s.len() * 3 / 5),
|
||||
)
|
||||
}
|
||||
fn get_height(&self) -> usize {
|
||||
self.get_objects().len() + self.get_status().lines().count()
|
||||
}
|
||||
fn get_status(&self) -> String;
|
||||
fn get_damaged(&mut self) -> Vec<Point>;
|
||||
fn get_objects(&self) -> &Vec<Vec<Object>>;
|
||||
fn get_object(&self, point: Point) -> Option<&Object>;
|
||||
}
|
||||
|
||||
pub fn get_mode(args: &Arguments) -> Result<Mode, String> {
|
||||
Ok(match args.interaction_mode {
|
||||
InteractionMode::Gui => Gui::new(args.size).map_err(|e| e.to_string())?.into(),
|
||||
InteractionMode::Tui => Tui::new().into(),
|
||||
InteractionMode::Cli => Cli::new().into(),
|
||||
})
|
||||
}
|
53
2/coursework/src/src/interaction/cli.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use super::{Drawable, Input, Interaction, Tui};
|
||||
use crate::objects::Labels;
|
||||
use std::error::Error;
|
||||
|
||||
pub struct Cli {
|
||||
tui: Tui,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
pub fn new() -> Self {
|
||||
let tui = Tui::default();
|
||||
tui.get_term().clear_screen().unwrap();
|
||||
|
||||
Self { tui }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Cli {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Interaction for Cli {
|
||||
fn get_input(&mut self) -> Input {
|
||||
self.tui.get_input()
|
||||
}
|
||||
|
||||
fn draw(&mut self, drawable: &mut impl Drawable) -> Result<(), Box<dyn Error>> {
|
||||
let term = self.tui.get_term();
|
||||
|
||||
for (x, y) in drawable.get_damaged() {
|
||||
if let Some(obj) = drawable.get_object((x, y)) {
|
||||
term.move_cursor_to(x, y)?;
|
||||
term.write_line(&obj.char().to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
term.move_cursor_to(0, drawable.get_objects().len())?;
|
||||
term.clear_to_end_of_screen()?;
|
||||
term.move_cursor_down(1)?;
|
||||
term.write_line(&drawable.get_status())?;
|
||||
|
||||
if let Some(&(x, y)) = drawable.get_cursor() {
|
||||
term.show_cursor()?;
|
||||
term.move_cursor_to(x, y)?;
|
||||
} else {
|
||||
term.hide_cursor()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
199
2/coursework/src/src/interaction/gui.rs
Normal file
@ -0,0 +1,199 @@
|
||||
use super::{Drawable, Input, Interaction};
|
||||
use crate::objects::Labels;
|
||||
use sdl2::{
|
||||
event::Event,
|
||||
image::LoadTexture,
|
||||
keyboard::Keycode,
|
||||
pixels::Color,
|
||||
rect::Rect,
|
||||
render::{Canvas, TextureCreator, TextureQuery},
|
||||
ttf::Sdl2TtfContext,
|
||||
video::{Window, WindowContext},
|
||||
EventPump, IntegerOrSdlError,
|
||||
};
|
||||
use std::{collections::BTreeMap, error::Error, fs};
|
||||
|
||||
pub struct Gui {
|
||||
scale: u32,
|
||||
canvas: Canvas<Window>,
|
||||
event_pump: EventPump,
|
||||
ttf_context: Sdl2TtfContext,
|
||||
texture_creator: TextureCreator<WindowContext>,
|
||||
texture_cache: BTreeMap<String, Box<[u8]>>,
|
||||
}
|
||||
|
||||
impl Gui {
|
||||
pub fn new(scale: u32) -> Result<Self, Box<dyn Error>> {
|
||||
let sdl_context = sdl2::init()?;
|
||||
let ttf_context = sdl2::ttf::init()?;
|
||||
|
||||
let canvas = sdl_context
|
||||
.video()?
|
||||
.window("Boulder Dash", 0, 0)
|
||||
.position_centered()
|
||||
.build()?
|
||||
.into_canvas()
|
||||
.software()
|
||||
.build()?;
|
||||
let event_pump = sdl_context.event_pump()?;
|
||||
let texture_creator = canvas.texture_creator();
|
||||
|
||||
let mut texture_cache = BTreeMap::new();
|
||||
for path in fs::read_dir("assets/sprites/")?.filter_map(Result::ok) {
|
||||
let contents = fs::read(path.path())?.into_boxed_slice();
|
||||
texture_cache.insert(path.file_name().into_string().expect("str path"), contents);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
scale,
|
||||
canvas,
|
||||
event_pump,
|
||||
ttf_context,
|
||||
texture_creator,
|
||||
texture_cache,
|
||||
})
|
||||
}
|
||||
|
||||
fn resize_window(&mut self, (width, height): (u32, u32)) -> Result<(), IntegerOrSdlError> {
|
||||
self.canvas.window_mut().set_minimum_size(width, height)?;
|
||||
self.canvas.window_mut().set_maximum_size(width, height)
|
||||
// self.canvas.window_mut().set_size(width, height)?;
|
||||
// self.canvas.set_logical_size(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
impl Interaction for Gui {
|
||||
fn get_input(&mut self) -> Input {
|
||||
let mut input = Input::Unknown;
|
||||
|
||||
while let Some(event) = self.event_pump.poll_event() {
|
||||
input = match event {
|
||||
Event::Quit { .. } => return Input::Quit,
|
||||
|
||||
Event::KeyDown {
|
||||
keycode: Some(key), ..
|
||||
} => match key {
|
||||
Keycode::Escape => Input::Esc,
|
||||
Keycode::Space => Input::Space,
|
||||
Keycode::Comma => Input::Comma,
|
||||
Keycode::Period => Input::Period,
|
||||
Keycode::Q => Input::Q,
|
||||
Keycode::P => Input::R,
|
||||
|
||||
Keycode::W => Input::W,
|
||||
Keycode::A => Input::A,
|
||||
Keycode::R => Input::S,
|
||||
Keycode::S => Input::D,
|
||||
Keycode::Up => Input::Up,
|
||||
Keycode::Down => Input::Down,
|
||||
Keycode::Left => Input::Left,
|
||||
Keycode::Right => Input::Right,
|
||||
_ => input,
|
||||
},
|
||||
|
||||
_ => input,
|
||||
}
|
||||
}
|
||||
|
||||
input
|
||||
}
|
||||
|
||||
fn draw(&mut self, drawable: &mut impl Drawable) -> Result<(), Box<dyn Error>> {
|
||||
self.canvas.set_draw_color(Color::BLACK);
|
||||
// Redraw objects using the damaged buffer
|
||||
let mut objects_to_redraw = drawable.get_damaged();
|
||||
|
||||
// WINDOW
|
||||
|
||||
let drawable_size = (
|
||||
u32::try_from(drawable.get_width())? * self.scale,
|
||||
// scale + 1 is padding for the status
|
||||
u32::try_from(drawable.get_height())? * (self.scale + 1),
|
||||
);
|
||||
if drawable_size != self.canvas.window().size() {
|
||||
// TODO: why it takes 2 calls to resize normally
|
||||
self.resize_window(drawable_size)?;
|
||||
self.resize_window(drawable_size)?;
|
||||
self.canvas.clear(); // clear the artifacts after resize
|
||||
|
||||
// Redraw all objects
|
||||
objects_to_redraw = drawable
|
||||
.get_objects()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(y, row)| (0..row.len()).map(move |x| (x, y)))
|
||||
.collect();
|
||||
}
|
||||
|
||||
// OBJECTS
|
||||
|
||||
for (x, y) in objects_to_redraw {
|
||||
let rect = Rect::new(
|
||||
i32::try_from(x)? * i32::try_from(self.scale)?,
|
||||
i32::try_from(y)? * i32::try_from(self.scale)?,
|
||||
self.scale,
|
||||
self.scale,
|
||||
);
|
||||
self.canvas.fill_rect(rect)?; // clear the old artifacts
|
||||
|
||||
if let Some(obj) = drawable.get_object((x, y)) {
|
||||
let bytes = &self.texture_cache[&obj.name()];
|
||||
let texture = self.texture_creator.load_texture_bytes(bytes)?;
|
||||
self.canvas.copy(&texture, None, rect)?;
|
||||
}
|
||||
}
|
||||
|
||||
// STATUS
|
||||
|
||||
let font = self
|
||||
.ttf_context
|
||||
.load_font("assets/font.ttf", u16::try_from(self.scale)?)?;
|
||||
let mut level_bottom = u32::try_from(drawable.get_objects().len())? * self.scale;
|
||||
|
||||
// Clear the bottom of the screen
|
||||
self.canvas.fill_rect(Rect::new(
|
||||
0,
|
||||
i32::try_from(level_bottom)?,
|
||||
drawable_size.0,
|
||||
drawable_size.1.saturating_sub(level_bottom),
|
||||
))?;
|
||||
|
||||
// Draw the status line by line
|
||||
for line in drawable.get_status().lines() {
|
||||
let font_surface = font.render(line).blended(Color::RGB(200, 255, 0))?;
|
||||
let font_texture = self
|
||||
.texture_creator
|
||||
.create_texture_from_surface(&font_surface)?;
|
||||
|
||||
let TextureQuery { width, height, .. } = font_texture.query();
|
||||
let rect = Rect::new(0, i32::try_from(level_bottom)?, width, height);
|
||||
self.canvas.copy(&font_texture, None, rect)?;
|
||||
|
||||
level_bottom += self.scale;
|
||||
}
|
||||
|
||||
// CURSOR
|
||||
|
||||
if let Some(&(x, y)) = drawable.get_cursor() {
|
||||
self.canvas.set_draw_color(Color::RGB(0, 255, 0));
|
||||
|
||||
for i in 0..self.scale / 6 {
|
||||
self.canvas.draw_rect(Rect::new(
|
||||
i32::try_from(x)? * i32::try_from(self.scale)? + i32::try_from(i)?,
|
||||
i32::try_from(y)? * i32::try_from(self.scale)? + i32::try_from(i)?,
|
||||
self.scale.saturating_sub(2 * i),
|
||||
self.scale.saturating_sub(2 * i),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
||||
// Displaying the backbuffer
|
||||
|
||||
// TODO: why 3 calls?
|
||||
self.canvas.present();
|
||||
self.canvas.present();
|
||||
self.canvas.present();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
84
2/coursework/src/src/interaction/tui.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use super::{Drawable, Input, Interaction};
|
||||
use crate::objects::Labels;
|
||||
use console::{Key, Term};
|
||||
use std::{error::Error, sync::mpsc, thread};
|
||||
|
||||
pub struct Tui {
|
||||
term: Term,
|
||||
input_rx: mpsc::Receiver<Key>,
|
||||
}
|
||||
|
||||
impl Default for Tui {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
pub fn new() -> Self {
|
||||
let (input_tx, input_rx) = mpsc::channel();
|
||||
|
||||
let term = Term::stdout();
|
||||
let term_moved = term.clone();
|
||||
thread::spawn(move || loop {
|
||||
let key = term_moved.read_key().expect("Should always get a key");
|
||||
input_tx.send(key).expect("Receiver should be present");
|
||||
});
|
||||
|
||||
Self { term, input_rx }
|
||||
}
|
||||
|
||||
pub const fn get_term(&self) -> &Term {
|
||||
&self.term
|
||||
}
|
||||
}
|
||||
|
||||
impl Interaction for Tui {
|
||||
fn get_input(&mut self) -> Input {
|
||||
let input = self.input_rx.try_recv();
|
||||
input.map_or(Input::Unknown, |key| match key {
|
||||
Key::Escape => Input::Esc,
|
||||
Key::Char(' ') => Input::Space,
|
||||
Key::Char(',') => Input::Comma,
|
||||
Key::Char('.') => Input::Period,
|
||||
Key::Char('q') => Input::Q,
|
||||
Key::Char('p') => Input::R,
|
||||
|
||||
Key::Char('w') => Input::W,
|
||||
Key::Char('a') => Input::A,
|
||||
Key::Char('r') => Input::S,
|
||||
Key::Char('s') => Input::D,
|
||||
Key::ArrowUp => Input::Up,
|
||||
Key::ArrowDown => Input::Down,
|
||||
Key::ArrowLeft => Input::Left,
|
||||
Key::ArrowRight => Input::Right,
|
||||
|
||||
_ => Input::Unknown,
|
||||
})
|
||||
}
|
||||
|
||||
fn draw(&mut self, drawable: &mut impl Drawable) -> Result<(), Box<dyn Error>> {
|
||||
self.term.clear_screen()?;
|
||||
|
||||
drawable.get_damaged(); // Empty damaged buffer
|
||||
for row in drawable.get_objects() {
|
||||
let mut line = String::new();
|
||||
for obj in row {
|
||||
line.push(obj.emoji());
|
||||
}
|
||||
self.term.write_line(&line)?;
|
||||
}
|
||||
|
||||
self.term.move_cursor_down(1)?;
|
||||
self.term.write_line(&drawable.get_status())?;
|
||||
|
||||
if let Some(&(x, y)) = drawable.get_cursor() {
|
||||
self.term.show_cursor()?;
|
||||
self.term.move_cursor_to(x, y)?;
|
||||
} else {
|
||||
self.term.hide_cursor()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
23
2/coursework/src/src/lib.rs
Normal file
@ -0,0 +1,23 @@
|
||||
mod args;
|
||||
mod direction;
|
||||
mod editor;
|
||||
mod game;
|
||||
mod interaction;
|
||||
mod objects;
|
||||
|
||||
pub use args::Arguments;
|
||||
use args::ProgramMode;
|
||||
use editor::Editor;
|
||||
use game::Game;
|
||||
use std::error::Error;
|
||||
|
||||
type Point = (usize, usize); // (x, y)
|
||||
|
||||
pub fn run(args: &Arguments) -> Result<(), Box<dyn Error>> {
|
||||
let mut mode = interaction::get_mode(args)?;
|
||||
|
||||
match args.program_mode {
|
||||
ProgramMode::Game => Game::new(args)?.run(&mut mode),
|
||||
ProgramMode::Editor => Editor::new(args)?.run(&mut mode),
|
||||
}
|
||||
}
|
16
2/coursework/src/src/main.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use std::env;
|
||||
use std::process;
|
||||
|
||||
use boulder_dash::Arguments;
|
||||
|
||||
fn main() {
|
||||
let config = Arguments::parse(env::args().skip(1)).unwrap_or_else(|err| {
|
||||
eprintln!("Problem parsing arguments: {err}");
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
if let Err(err) = boulder_dash::run(&config) {
|
||||
eprintln!("Application error: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
103
2/coursework/src/src/objects.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use crate::{
|
||||
direction::Direction,
|
||||
game::level::{Level, Request, State},
|
||||
Point,
|
||||
};
|
||||
use enum_dispatch::enum_dispatch;
|
||||
|
||||
mod dirt;
|
||||
mod gem;
|
||||
mod player;
|
||||
mod rock;
|
||||
mod unknown;
|
||||
mod void;
|
||||
mod wall;
|
||||
|
||||
use dirt::Dirt;
|
||||
use gem::Gem;
|
||||
use player::Player;
|
||||
use rock::Rock;
|
||||
use unknown::Unknown;
|
||||
use void::Void;
|
||||
use wall::Wall;
|
||||
|
||||
#[enum_dispatch]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Object {
|
||||
Gem,
|
||||
Wall,
|
||||
Dirt,
|
||||
Rock,
|
||||
Void,
|
||||
Player,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Default for Object {
|
||||
fn default() -> Self {
|
||||
Void.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Object {
|
||||
pub fn get_all_displayable() -> Vec<Self> {
|
||||
vec![
|
||||
Void.into(),
|
||||
Wall.into(),
|
||||
Rock.into(),
|
||||
Dirt.into(),
|
||||
Gem.into(),
|
||||
Player.into(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn new(chr: char) -> Self {
|
||||
match chr {
|
||||
'+' => Gem.into(),
|
||||
'#' => Wall.into(),
|
||||
'*' => Dirt.into(),
|
||||
'O' => Rock.into(),
|
||||
' ' => Void.into(),
|
||||
'p' => Player.into(),
|
||||
_ => Unknown.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[enum_dispatch(Object)]
|
||||
pub trait Labels: std::fmt::Debug {
|
||||
fn char(&self) -> char;
|
||||
fn emoji(&self) -> char;
|
||||
fn name(&self) -> String {
|
||||
format!("{self:?}").to_lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
#[enum_dispatch(Object)]
|
||||
pub trait Properties {
|
||||
fn placeholder(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn can_be_moved(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn player(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn can_be_broken(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[enum_dispatch(Object)]
|
||||
pub trait Behaviour {
|
||||
fn init(&self) -> Vec<Request> {
|
||||
vec![]
|
||||
}
|
||||
fn on_broken(&self, _: &Level) -> Vec<Request> {
|
||||
vec![]
|
||||
}
|
||||
fn tick(&self, _: &Level, _: Point, _: Option<Direction>) -> Vec<Request> {
|
||||
vec![]
|
||||
}
|
||||
}
|
21
2/coursework/src/src/objects/dirt.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use super::{Behaviour, Labels, Properties};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Dirt;
|
||||
|
||||
impl Labels for Dirt {
|
||||
fn char(&self) -> char {
|
||||
'*'
|
||||
}
|
||||
fn emoji(&self) -> char {
|
||||
'🟨'
|
||||
}
|
||||
}
|
||||
|
||||
impl Properties for Dirt {
|
||||
fn can_be_broken(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Behaviour for Dirt {}
|
33
2/coursework/src/src/objects/gem.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use super::{Behaviour, Labels, Level, Properties, Request, State};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Gem;
|
||||
|
||||
impl Labels for Gem {
|
||||
fn char(&self) -> char {
|
||||
'+'
|
||||
}
|
||||
fn emoji(&self) -> char {
|
||||
'💎'
|
||||
}
|
||||
}
|
||||
|
||||
impl Properties for Gem {
|
||||
fn can_be_broken(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Behaviour for Gem {
|
||||
fn init(&self) -> Vec<Request> {
|
||||
vec![Request::AddMaxScore]
|
||||
}
|
||||
fn on_broken(&self, level: &Level) -> Vec<Request> {
|
||||
let mut requests = vec![Request::AddScore];
|
||||
if level.get_score() + 1 == *level.get_max_score() {
|
||||
requests.push(Request::UpdateState(State::Win));
|
||||
}
|
||||
|
||||
requests
|
||||
}
|
||||
}
|
67
2/coursework/src/src/objects/player.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use super::{Behaviour, Direction, Labels, Level, Point, Properties, Request, State};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Player;
|
||||
|
||||
impl Labels for Player {
|
||||
fn char(&self) -> char {
|
||||
'p'
|
||||
}
|
||||
fn emoji(&self) -> char {
|
||||
'🦀'
|
||||
}
|
||||
}
|
||||
|
||||
impl Properties for Player {
|
||||
fn player(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Behaviour for Player {
|
||||
fn tick(&self, level: &Level, _: Point, direction: Option<Direction>) -> Vec<Request> {
|
||||
let mut requests = vec![];
|
||||
|
||||
// to prevent the rock from falling on player when object underneath is broken
|
||||
let mut player_broke = false;
|
||||
let mut above_point = Direction::Up.apply_to(level.get_player());
|
||||
|
||||
// Player
|
||||
if let Some(dir) = direction {
|
||||
let next_point = dir.apply_to(level.get_player());
|
||||
|
||||
player_broke = level.get_object(next_point).can_be_broken();
|
||||
let can_move_next = matches!(dir, Direction::Left | Direction::Right)
|
||||
&& level.get_object(next_point).can_be_moved()
|
||||
&& level.get_object(dir.apply_to(&next_point)).placeholder();
|
||||
|
||||
if player_broke {
|
||||
requests.extend(level.get_object(next_point).on_broken(level));
|
||||
} else if can_move_next {
|
||||
requests.push(Request::MoveObj {
|
||||
from: next_point,
|
||||
to: dir.apply_to(&next_point),
|
||||
});
|
||||
}
|
||||
|
||||
if level.get_object(next_point).placeholder() || player_broke || can_move_next {
|
||||
requests.push(Request::MoveObj {
|
||||
from: *level.get_player(),
|
||||
to: next_point,
|
||||
});
|
||||
above_point = Direction::Up.apply_to(&next_point);
|
||||
}
|
||||
}
|
||||
|
||||
// Check the rock above
|
||||
if !player_broke && level.get_object(above_point).can_be_moved() {
|
||||
requests.push(Request::UpdateState(State::Lose));
|
||||
requests.push(Request::MoveObj {
|
||||
from: above_point,
|
||||
to: Direction::Down.apply_to(&above_point),
|
||||
});
|
||||
}
|
||||
|
||||
requests
|
||||
}
|
||||
}
|
47
2/coursework/src/src/objects/rock.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use super::{Behaviour, Direction, Labels, Level, Point, Properties, Request};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Rock;
|
||||
|
||||
impl Labels for Rock {
|
||||
fn char(&self) -> char {
|
||||
'O'
|
||||
}
|
||||
fn emoji(&self) -> char {
|
||||
'🪨'
|
||||
}
|
||||
}
|
||||
|
||||
impl Properties for Rock {
|
||||
fn can_be_moved(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Behaviour for Rock {
|
||||
fn tick(&self, level: &Level, (x, y): Point, _: Option<Direction>) -> Vec<Request> {
|
||||
if (x, y) == *level.get_player() || (x, y) == Direction::Up.apply_to(level.get_player()) {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
if level.get_object((x, y + 1)).placeholder() {
|
||||
return vec![Request::MoveObj {
|
||||
from: (x, y),
|
||||
to: (x, y + 1),
|
||||
}];
|
||||
}
|
||||
|
||||
for side in [x - 1, x + 1] {
|
||||
if level.get_object((side, y)).placeholder()
|
||||
&& level.get_object((side, y + 1)).placeholder()
|
||||
{
|
||||
return vec![Request::MoveObj {
|
||||
from: (x, y),
|
||||
to: (side, y + 1),
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
vec![]
|
||||
}
|
||||
}
|
21
2/coursework/src/src/objects/unknown.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use super::{Behaviour, Labels, Properties};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Unknown;
|
||||
|
||||
impl Labels for Unknown {
|
||||
fn char(&self) -> char {
|
||||
'?'
|
||||
}
|
||||
fn emoji(&self) -> char {
|
||||
'🯄'
|
||||
}
|
||||
}
|
||||
|
||||
impl Properties for Unknown {
|
||||
fn placeholder(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Behaviour for Unknown {}
|
21
2/coursework/src/src/objects/void.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use super::{Behaviour, Labels, Properties};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Void;
|
||||
|
||||
impl Labels for Void {
|
||||
fn char(&self) -> char {
|
||||
' '
|
||||
}
|
||||
fn emoji(&self) -> char {
|
||||
' '
|
||||
}
|
||||
}
|
||||
|
||||
impl Properties for Void {
|
||||
fn placeholder(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Behaviour for Void {}
|
16
2/coursework/src/src/objects/wall.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use super::{Behaviour, Labels, Properties};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Wall;
|
||||
|
||||
impl Labels for Wall {
|
||||
fn char(&self) -> char {
|
||||
'#'
|
||||
}
|
||||
fn emoji(&self) -> char {
|
||||
'🧱'
|
||||
}
|
||||
}
|
||||
|
||||
impl Properties for Wall {}
|
||||
impl Behaviour for Wall {}
|
@ -3,5 +3,6 @@
|
||||
This is a monorepo where I will merge (or just add) some of my (somewhat good) works that I did (am doing / will do?) while studying at _Kharkiv National University of Radio Electronics_.
|
||||
|
||||
The root directories are semester numbers. Some links:
|
||||
- OOP coursework: [source](2/coursework/), [compiled paper](2/coursework/main.pdf)
|
||||
- Databases coursework: [source](3/coursework/), [compiled paper](3/coursework/main.pdf)
|
||||
|
||||
|