gb-java-devel/jtd1-06-abstract.tex

880 lines
71 KiB
TeX
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\documentclass[j-spec.tex]{subfiles}
\begin{document} \sloppy
%\setcounter{section}{5}
\setlength{\columnsep}{22pt}
\pagestyle{plain}
%\tableofcontents
\section{Инструментарий: GUI -- графический интерфейс пользователя}
\subsection*{В предыдущей главе}
\begin{itemize}
\item ООП;
\item процедурное программирование;
\item исключения;
\item устройство языка Java;
\item устройство платформы для создания и запуска приложений на JVM-языках;
\item базовые средства ввода-вывода;
\item базовые терминальные приложения;
\item алгоритмические задачи, не требующие сложных программных решений.
\end{itemize}
\subsection*{В этом разделе}
Интерфейс пользователя -- это важно, поскольку это именно то, что видят пользователи в первую очередь.
\begin{frm} \excl
Это самое сложное занятие начального этапа, но не потому что оно содержит очень сложную информацию, а потому что заставит под необычным углом взглянуть на те принципы ООП, которые уже известны.
\end{frm}
Будут рассмотрены вопросы создания окон, менеджеров размещений, элементов графического интерфейса, и обработчиков событий.
\begin{itemize}
\item \nom{Swing}{библиотека для создания графического интерфейса для программ на языке Java. Swing разработан компанией Sun Microsystems и содержит ряд графических компонентов (Swing widgets), таких как кнопки, поля ввода, таблицы и тому подобные.};
\item \nom{Асинхронность}{вид программирования, позволяющий вынести выполняемые задачи отдельными блоками кода. Применяется в сервисах, где предыдущее действие тормозит следующее. Синхронный процесс выполняется поэтапно. Пользователь совершает действие, ждёт, пока программа обработает часть кода, переходит к другому блоку. При использовании асинхронности, код убирает операцию, блокирующую следующие действия.};
\item \nom{Параллельность}{способ организации компьютерных вычислений, при котором программы разрабатываются, как набор взаимодействующих вычислительных процессов, работающих асинхронно и при этом одновременно. Параллельное программирование -- это техника программирования, которая использует преимущества многоядерных или многопроцессорных компьютеров и является подмножеством более широкого понятия многопоточности (multithreading).};
\item \nom{Окно}{это прямоугольная область экрана, в котором приложение отображает информацию и получает реакцию от пользователя. Одновременно на экране может отображаться несколько окон, в том числе, окон других приложений, однако лишь одно из них может получать реакцию от пользователя -- активное окно. Пользователь использует клавиатуру, мышь и прочие устройства ввода для взаимодействия с приложением, которому принадлежит активное окно.};
\item \nom{Компонент}{независимый модуль программы, предназначенный для повторного использования. Часто компоненты объединяют по общим признакам и организовывают в соответствии с определёнными правилами и ограничениями. Например, компоненты графического интерфейса.};
\item \nom{Компоновщик}{Компоновщик (layout manager) в библиотеке Java Swing отвечает за расположение и организацию компонентов (например, кнопок, текстовых полей, панелей). Он определяет, как компоненты будут выравниваться, размещаться и изменяться при изменении размеров окна.};
\item \nom{Панель}{Панель в библиотеке Java Swing представляет собой контейнер, который используется для группировки и организации других компонентов в пользовательском интерфейсе. Она представляет собой область с фиксированным размером на которую можно добавить другие компоненты, такие как кнопки, текстовые поля или изображения.};
\item \nom{События}{это действия или случаи, возникающие в программируемой системе, о которых система сообщает для того, чтобы было возможно с ними взаимодействовать. Например, если пользователь нажимает кнопку на графическом интерфейсе, возможно ответить на это действие, отобразив информационное окно.};
\item \nom{Обработчик}{это механизм, с помощью которого приложение может перехватить события, такие как сообщения, действия мыши и нажатия клавиш. Функция, которая перехватывает события определенного типа, называется процедурой-обработчиком. Процедура-обработчик может действовать для каждого получаемого события, а затем изменить или отменить событие.}.
\end{itemize}
\subsection{Почему именно Swing?}
Вместо вступления следует кратко ответить на самый популярный вопрос.
\begin{itemize}
\item[\faRemove] это популярный и современный фреймворк;
\item[\faRemove] пригодится любому программисту на Java;
\item[\faCheck] поможет лучше понять ООП;
\item[\faCheck] работа композиции из объектов;
\item[\faCheck] обмен информацией между объектами;
\item[\faCheck] явно использует ссылочную природу данных;
\item[\faCheck] улучшает запоминание базовых взаимосвязей объектов;
\item[\faCheck] без искусственных примеров (в результате будет разработана простая игра, крестики-нолики с графическим интерфейсом).
\end{itemize}
\begin{frm}
\faQuestion~ Почему не JavaFX? Фреймворк был выведен из стандартной библиотеки языка, начиная с Java 9, и достаточно сложен для базового знакомства с графическими библиотеками.
\faQuestion~ Почему не LibGDX? Фреймворк является надстройкой над Swing, объясняя его всё равно необходимо будет объяснять Swing/AWT.
\faExclamation~ Intellij IDEA — написана на Swing.
\end{frm}
\subsection{JFrame: Главный класс окна}
\subsubsection{Создание окна}
В этом разделе происходит изучение программирования и ООП, на примере и с использованием фреймворка Swing, поэтому необходимо сосредоточиться на объектах, их свойствах и взаимосвязях.
\begin{frm} \info
Изучение фреймворка Swing -- не основная задача раздела, поэтому важно помнить, что не нужно зазубривать все названия всех компонентов.
\end{frm}
Окно графического интерфейса, как и любая другая программа -- это класс и объекты. В листинге \hrf{lst:init-game-window} создаётся новый класс с названием \code{GameWindow}. Для начала, необходимо получить доступ к методам, содержащимся в библиотеке, для этого применяется наследование от класса \code{JFrame}, и создаётся конструктор.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Класс окна},label={lst:init-game-window}]
package ru.gb.jdk.one.online;
import javax.swing.*;
public class GameWindow extends JFrame {
GameWindow() {
//...
}
}
\end{lstlisting}
В основном классе программы просто создаётся новый объект вновь описанного класса с окном. С точки зрения программирования и ООП не происходит ничего значительно отличающегося от котиков и пёсиков. Если запустить получившееся приложение, оно должно запуститься, и сразу завершиться, это признак верно написанного кода.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Создание нового окна}]
package ru.gb.jdk.one.online;
public class Main {
public static void main(String[] args) {
new GameWindow();
}
}
\end{lstlisting}
\subsubsection{Закрытие окна, завершение приложения}
Б\'{о}льшая часть свойств окна не меняется за всё время существования окна и задаётся в конструкторе, то есть окно при создании будет наделено какими-то свойствами, которые в некоторых случаях можно будет поменять во время работы приложения. Самое не очевидное на первый взгляд то, что в Swing при нажатии на крестик в углу окна программа не завершается. По умолчанию, все создаваемые окна -- невидимые. Это сделано потому что есть возможность создавать сколько угодно окон для приложения и такое поведение значительно снижает риск неожиданного поведения.
\begin{frm} \excl
Все окна по умолчанию невидимые. Нажатие на крестик по умолчанию делает окно невидимым, а не завершает программу.
\end{frm}
Для того, чтобы программа закрылась, необходимо принять решение, какое окно в ней будет главным. Чтобы программа завершалась при закрытии главного окна (обычно, это первое, что делается при создании одно оконных приложений) экземпляру JFrame устанавливается свойство \code{DefaultCloseOperation}. То есть устанавливается, что нужно сделать, когда это (главное) окно закроется. В это свойство записывается константа \code{EXIT_ON_CLOSE}. Если этого не сделать, то будет использовано поведение по умолчанию, окно сделается невидимым, и приложение не завершится.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Завершение приложения по закрытию окна}]
setDefaultCloseOperation(EXIT_ON_CLOSE);
\end{lstlisting}
\subsubsection{Свойства окна}
Добавив констант\footnote{Считается хорошим тоном не держать в коде никаких «магических цифр», чтобы не думать, что они значат и почему они именно такие, и что будет если их поменять.} с шириной, высотой и положением окна по осям относительно рабочего стола, становится возможным настроить размеры и положение окна в конструкторе.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Базовые свойства окна}]
private static final int WINDOW_HEIGHT = 555;
private static final int WINDOW_WIDTH = 507;
private static final int WINDOW_POSX = 800;
private static final int WINDOW_POSY = 300;
GameWindow() {
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocation(WINDOW_POSX, WINDOW_POSY);
setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
setVisible(true);
}
\end{lstlisting}
Установка всех начальных свойств окна осуществляется вызовом соответствующих сеттеров в конструкторе. По умолчанию окно -- невидимое, поэтому для его демонстрации в конструкторе вызывается метод \code{setVisible} с передаваемым аргументом \code{true}.
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-01-jframe-01.png}
\caption{Пустое окно указанного размера}
\end{figure}
Окно -- это всегда \textit{отдельный поток программы}, внутри которого работает бесконечный цикл. В объекте окна существует очередь сообщений, которую цикл опрашивает и выполняет.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Пример сообщения, показывающего многопоточность}]
public static void main(String[] args) {
new GameWindow();
System.out.println("Method main() is over");
}
\end{lstlisting}
Запустив программу и внимательно изучив результат, очевидно, что окно создалось, в консоли видно, что работа метода \code{main} закончилась, а окно всё равно выполняется, его, при желании, можно подвигать, изменить размер и так далее. Это и есть наглядная демонстрация \textit{многопоточности}. Таким образом, получается, что когда создаётся новое окно -- нет необходимости его ни в какой контейнер помещать, ни думать, как оно будет взаимодействовать с пользователем, оно создастся и будет жить своей жизнью. \textbf{Инкапсуляция}. Если появится необходимость что-то ещё выполнить в методе \code{main}, запрета на написание действий не существует и действия будут выполняться \textit{параллельно}, \textit{асинхронно}.
\subsubsection{Вопросы для самопроверки}
\begin{enumerate}
\item Почему в классе \code{GameWindow} доступны методы фреймворка?
\begin{enumerate}
\item Из-за устройства фреймворка Swing;
\item из-за наследования от JFrame;
\item из-за импорта классов Swing.
\end{enumerate}
\item Чтобы создать пустое окно в программе нужно
\begin{enumerate}
\item импортировать библиотеку Swing;
\item создать класс MainWindow;
\item создать класс-наследник JFrame.
\end{enumerate}
\item Свойства окна, такие как размер и заголовок возможно задать
\begin{enumerate}
\item написанием методов в классе-наследнике;
\item вызовом методов в конструкторе;
\item созданием констант в первых строках класса.
\end{enumerate}
\end{enumerate}
\subsection{Компоненты окна}
\subsubsection{Кнопка}
Для того, чтобы точно ничего не перепутать в процессе разработки, возможно придать окну больше индивидуальности, задав заголовок и запретив пользователю изменять его размеры, для игры в крестики-нолики это будет важно, чтобы красиво отображалось поле. Для этого вызовем методы \code{setTitle()} и \code{setResizeable()}, соответственно.
\begin{frm} \info
Элементы графического интерфейса -- это хорошо знакомые кнопки, текстовые поля, надписи (лейблы), и тому подобные.
\end{frm}
За кнопки отвечает класс \code{JButton}, при создании экземпляра есть возможность сразу в конструкторе задать надпись, которая будет отображаться на кнопке. Сразу создадим несколько кнопок, например, «выход». Кнопки недостаточно просто создать, поскольку неизвестно, где они должны находиться. Одну из созданных кнопок добавим на окно -- для этого внутри конструктора необходимо воспользоваться методом \code{add()}, который требует в качестве аргумента передать ему какой-то \code{Component}. Все кнопки, лейблы и прочие элементы интерфейса -- это наследники класса \code{Component}.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Компонент «Кнопка»}]
JButton btnStart = new JButton("New Game");
JButton btnExit = new JButton("Exit");
GameWindow() {
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocation(WINDOW_POSX, WINDOW_POSY);
setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
setTitle("TicTacToe");
setResizable(false);
add(btnStart);
setVisible(true);
}
\end{lstlisting}
После добавления в конструкторе кнопки, при запуске приложения видно, что она заняла всё окно приложения. Если убрать вызов метода \code{setResizeable()}, то также возможно удостовериться, что при изменении размеров окна, размер кнопки также будет меняться.
\begin{figure}[H]
\centering
\begin{subfigure}[b]{0.47\textwidth}
\centering
\includegraphics[width=\textwidth]{jd-01-btn-01.png}
\caption{ }
\end{subfigure}
\hfill
\begin{subfigure}[b]{0.47\textwidth}
\centering
\includegraphics[width=\textwidth]{jd-01-btn-02.png}
\caption{ }
\end{subfigure}
\caption{Изменение размеров кнопки в следствие изменения размеров окна}
\end{figure}
При попытке добавить вторую кнопку на это же окно очевидно (рис. \hrf{pic:btn-overhead}), что вторая кнопка полностью перекрыла первую.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Вторая кнопка}]
add(btnExit);
\end{lstlisting}
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-01-btn-03.png}
\caption{Перекрытие компонентов}
\label{pic:btn-overhead}
\end{figure}
\subsubsection{Компоновщики (менеджеры размещений)}
Перекрытие происходит из-за использования компоновщика, или, как их ещё называют, менеджера размещений.
\begin{frm} \info
Менеджеры размещений нужны для того, чтобы не думать каждый раз о том, как изменится размер и координаты конкретного элемента, допустим, при изменении окна и не писать сложное поведение вложенных компонентов чтобы просто отобразить то, что привычно пользователю.
\end{frm}
Компоновщики активно используются в любом программировании графических интерфейсов в любых языках программирования, от C++ до JavaScript, потому что это достаточно удобный механизм, берущий на себя значительный пласт работы. Использование компоновщиков позволяет эффективно управлять и размещать компоненты в окне или панели пользовательского интерфейса, обеспечивая гибкость и адаптивность приложения к изменениям размеров и расположения компонентов на экране.
Компоновщик -- это специальный объект, который помещается на некоторые (\code{RootPaneContainer}\footnote{\href{https://docs.oracle.com/javase/tutorial/uiswing/layout/index.html}{docs.oracle.com: Компоновщики}}) компоненты и осуществляет автоматическую расстановку добавляемых в него компонентов, согласно правилам. Компоновщиков существует несколько типов, каждый из которых предоставляет свои специфические возможности и алгоритмы расположения. Компоновщик выбирается в зависимости от требуемых задач и желаемого внешнего вида интерфейса.
\begin{itemize}
\item \code{BorderLayout} (по умолчанию);
\item \code{BoxLayout};
\item \code{CardLayout};
\item \code{FlowLayout};
\item \code{GridBagLayout};
\item \code{GridLayout};
\item \code{GroupLayout};
\item \code{SpringLayout}.
\end{itemize}
По умолчанию в Swing используется компоновщик \code{BorderLayout} (рис. \hrf{pic:layout-border}). Он располагает всё, что ему передаётся в центре, но также у него есть ещё четыре положения, маленькие области по краям.
\begin{figure}[H]
\centering
\fontsize{12}{1}\selectfont
\begin{subfigure}[b]{0.32\textwidth}
\centering
\includesvg[scale=1.01]{pics/jd-01-border-layout.svg}
\caption{\code{BorderLayout}}
\label{pic:layout-border}
\end{subfigure}
\hfill
\begin{subfigure}[b]{0.32\textwidth}
\centering
\includesvg[scale=1.01]{pics/jd-01-flow-layout.svg}
\caption{\code{FlowLayout}}
\label{pic:layout-flow}
\end{subfigure}
\hfill
\begin{subfigure}[b]{0.32\textwidth}
\centering
\includesvg[scale=1.01]{pics/jd-01-grid-layout.svg}
\caption{\code{GridLayout}}
\label{pic:layout-grid}
\end{subfigure}
\caption{Популярные менеджеры размещений}
\end{figure}
Если какая-то область не занята компонентом, она автоматически уменьшается до нулевых размеров, оставляя место другим компонентам. Поэтому, если необходимо какой-то компонент расположить не в центре, это нужно явно указать при добавлении. На первый взгляд, это немного не очевидно, поэтому лучше запомнить, что при добавлении надо указать ещё один параметр, константу, например, \code{BorderLayout.SOUTH}. \code{FlowLayout} будет располагать элементы друг за другом слева направо, сверху вниз. Компоновщик-сетка \code{GridLayout} при создании принимает на вход число строк и столбцов и располагает компоненты в получившейся сетке.
\begin{frm} \info
Основная идея, которую надо понять, это не названия компоновщиков, а то, что в Swing вся работа происходит через компоновщики -- Layout, которые каждый по-своему располагают элементы в окне.
\end{frm}
\subsubsection{Панель для размещения JPanel}
Разнообразие требований к разработке графических интерфейсов может привести к необходимости создания бесконечного числа компоновщиков. Поэтому разработчики библиотеки Swing придумали использовать не только компоненты сами по себе, но и группы элементов, которые располагаются на так называемых панелях (\code{JPanel}). Главная особенность панелей в том, что внутри каждой панели возможно использовать свой собственный компоновщик. \code{JPanel} -- это по умолчанию невидимый прямоугольник, на котором может находиться собственный компоновщик. Например, становится доступным создание для окна панели с кнопками, а остальное пространство оставить под другие важные вещи. В листинге \hrf{lst:jpanel-init} описан код создания панели, добавление её в нижнюю часть основного экрана, расположение внутри панели компоновщика и двух кнопок. Важно, что на экран добавляются не кнопки по отдельности, а компонент, на который предварительно добавили кнопки.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Создание и применение панели},label={lst:jpanel-init}]
GameWindow() {
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocation(WINDOW_POSX,WINDOW_POSY);
setSize(WINDOW_WIDTH,WINDOW_HEIGHT);
setTitle("TicTacToe");
setResizable(false);
JPanel panBottom = new JPanel(new GridLayout(1, 2));
panBottom.add(btnStart);
panBottom.add(btnExit);
add(panBottom, BorderLayout.SOUTH);
setVisible(true);
}
\end{lstlisting}
\code{JPanel} позволяет также осуществлять рисование и взаимодействие с пользователем. Основные графические интерактивности в демонстрационном приложении будут сделаны именно на панели. Такую панель с достаточно большой функциональностью логично выделить в отдельный класс. В случае игры в крестики-нолики это будет карта поля сражения (листинг \hrf{lst:jpanel-map}). В описании конструктора для простоты панель перекрашивается в чёрный цвет (строка \hrf{line:map-background}), чтобы увидеть, что панель создаётся без ошибок.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Создание панели с полем боя},label={lst:jpanel-map}]
package ru.gb.jdk.one.online;
import javax.swing.*;
import java.awt.*;
public class Map extends JPanel {
Map() {
setBackground(Color.BLACK); <@\label{line:map-background}@>
}
}
\end{lstlisting}
Естественно, панель также недостаточно просто создать (листинг \hrf{lst:jpanel-map-add}, строка \hrf{line:map-init}), но нужно её куда-то разместить. Например, на основной экран (строка \hrf{line:map-add}). Поскольку не была указана сторона экрана, панель заняла всё свободное место на окне, кроме юга, где расположилась панель с кнопками.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Добавление панели с полем боя},label={lst:jpanel-map-add}]
GameWindow() {
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocation(WINDOW_POSX, WINDOW_POSY);
setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
setTitle("TicTacToe");
setResizable(false);
Map map = new Map(); <@\label{line:map-init}@>
JPanel panBottom = new JPanel(new GridLayout(1, 2));
panBottom.add(btnStart);
panBottom.add(btnExit);
add(panBottom, BorderLayout.SOUTH);
add(map); <@\label{line:map-add}@>
setVisible(true);
}
\end{lstlisting}
\subsection{Многооконное приложение, взаимосвязи}
\subsubsection{Структура}
Полученных знаний достаточно, чтобы начать описывать так называемую бизнес-логику. Созданная панель \code{Map} будет выполнять функции поля боя, поэтому логично расположить в ней метод startNewGame(), начинающий новую игру. В качестве параметров метод должен принимать какие-то начальные настройки самой игры. Например, будут два режима игры, компьютер против игрока и игрок против игрока, размер поля, и сразу не будем привязываться к квадратному полю 3х3, для полей больше, чем 3х3 понадобится выигрышная длина, то есть число крестиков или ноликов, расположенных подряд на одной прямой для победы той или иной стороны. В теле метода сразу будет установлена так называемая заглушка, чтобы знать, что метод вызывается и все параметры передаются верно.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Метод начала новой игры},label={lst:map-start-new-game}]
void startNewGame(int mode, int fSzX, int fSzY, int wLen) {
System.out.printf("Mode: %d;\nSize: x=%d, y=%d;\nWin Length: %d",
mode, fSzX, fSzY, wLen);
}
\end{lstlisting}
Если сразу описать архитектуру проекта, его будет проще наполнять логикой и расширять, чем если писать последовательно, удерживая общую картину в голове. Итоговое приложение приложение будет работать в двух окнах: первое -- стартовое, где будут задаваться настройки поля и производиться выбор режима игры; второе -- основное, где будет происходить собственно игра. Основное окно уже написано, и при его закрытии происходит выход из программы. Для создания второго окна необходимо написать ещё один класс, названный, например, \code{SettingsWindow}, наследник \code{JFrame}. Конструктор второго окна будет принимать экземпляр игрового окна. В первую очередь это сделано для передачи параметров игры, а во-вторых, чтобы красиво отцентрировать его относительно основного.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Заготовка окна настроек новой игры игры},label={lst:frame-settings}]
package ru.gb.jdk.one.online;
import javax.swing.*;
public class SettingsWindow extends JFrame {
SettingsWindow(GameWindow gameWindow) {
}
}
\end{lstlisting}
В основном окне \code{GameWindow} понадобится два поля, одно класса \code{SettingsWindow} чтобы иметь возможность экземпляр этого окна показывать когда появится необходимость и второе -- это панель \code{Map}. В основном окне при создании экземпляра окна настроек в него передаётся \code{this}.
\begin{frm} \info
Обратите внимание, на этот способ применять this, когда неоюходимо передать в метод ссылку на объект, который вызывает этот метод, фактически, основное окно передаёт себя.
\end{frm}
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Создание окна настроек в основном окне},label={lst:frame-create-settings}]
Map map;
SettingsWindow settings;
GameWindow() {
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocation(WINDOW_POSX, WINDOW_POSY);
setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
setTitle("TicTacToe");
setResizable(false);
map = new Map();
settings = new SettingsWindow(this);
// ...
}
\end{lstlisting}
На рисунке \hrf{pic:ttt-class-diagram} первый черновик диаграммы классов разрабатываемого приложения\footnote{\href{https://www.plantuml.com/plantuml/uml/ZPBVQl904CNlzodckujV3GX5MbOhKhHggNyWGfHIojQiSMco2TbHIyK-UySqQJIbs5mCpEGt9pc7QHiK2Qx3WFt3bGmbn85Gch5588o1dWYbgxGNRQ7PlAl2TgLGjbgmOq2F3JlQHhNOmr9f4O3I2EvWr1cxp_tkeDUVmWtKw_Mpi3leJFi7jdPrbfsCdHcXrxNQNz0vePSP-W7tjsl4ICCBQkTW--Uu-wRowL3448guaRMEH5JQDraS9ciRB7jVH6LLs3uFCD_wFSIoikL_2ntf38NIj3qfRryKzZUHyY0apd8m8Rt79n29RogDOvMu959ujIgvqrGeFOkHt1viMM7aoIeidVTPMkSay23rbtBwPtQY_1NQn_V2GUbDz2eDj5WnvfmYXVyvJz_bdCe9aKSBaMsNmk7yj6TjgJqwtay0 }{Исходный код PlantUML}}. Создание идеальной диаграммы классов или модели данных не входит в цели курса, более важно понятное объяснение того, что в данный момент программируется. Буквами F обозначены экземпляры \code{JFrame}, буквой P \code{Jpanel}, а A это Application, то есть основной класс приложения. На диаграмме видно, что основное приложение создаёт основное окно, на которое добавлена панель \code{Map} и которое время от времени будет обращаться к \code{SettingsWindow} за настройками новой игры.
\begin{figure}[H]
\centering
\fontsize{7}{1}\selectfont
\includesvg[scale=.6]{pics/jd-01-ttt-01.svg}
\caption{Диаграмма классов приложения}
\label{pic:ttt-class-diagram}
\end{figure}
\subsubsection{Окно с настройками игры и обработчики кнопок}
Окно настроек игры на данный момент будет представлено одной кнопкой старта игры, вызывающей метод старта игры с одним зафиксированным набором настроек -- игра против компьютера, поле 3х3, чтобы выиграть необходимо собрать 3 крестика (или нолика) подряд.
В данный момент окно создаётся в координатах (0,0) и имеет размер (0,0), то есть в левом верхнем углу экрана видно только кнопки свернуть, развернуть, закрыть. В конструкторе окна задаются его размеры и то, что его местоположение должно быть относительным главному окну. Аналогично основному окну добавлена кнопка подтверждения правильности настроек и старта игры.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Окно настроек новой игры},label={lst:frame-settings-basic}]
public class SettingsWindow extends JFrame {
private static final int WINDOW_HEIGHT = 230;
private static final int WINDOW_WIDTH = 350;
JButton btnStart = new JButton("Start new game");
SettingsWindow(GameWindow gameWindow) {
setLocationRelativeTo(gameWindow);
setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
add(btnStart);
}
}
\end{lstlisting}
Далее необходимо «оживить» кнопки на окнах, это делается специальной конструкцией, синтаксис которой пока что придётся запомнить. Синтаксически всё написанное уже понятно и оговорено -- у объекта кнопки \code{btnExit} вызывается метод добавления к этому объекту некоторого слушателя действия. Какое может у кнопки быть самое очевидное действие? Нажатие. В аргумент метода добавления передаётся некий новый объект класса «слушатель действия», у которого переопределяется метод «действие произошло». Кнопка старта игры будет делать видимым окно с будущими настройками. В листинге \hrf{lst:main-btn-lsnr} показаны обработчики кнопок старта новой игры и завершения приложения, находящихся на основном окне программы. Эти обработчики необходимо поместить внутрь конструктора основного окна.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обработчики нажатий на кнопки основного окна},label={lst:main-btn-lsnr}]
btnExit.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
btnStart.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
settings.setVisible(true);
}
});
\end{lstlisting}
\subsubsection{Последовательность выполнения программы}
В основном окне понадобится метод, инициализирующий новое игровое поле, поскольку прямой вызов по кнопке старта новой игры на окне настроек метода в панели основного окна противоречит инкапсуляции.
Зачем мы так делаем, казалось бы усложняем? Но нет: панель находится на основном окне, а кнопка начала игры будет находиться на окне настроек, которое не может «знать», какие на основном окне есть панели. Или может оказаться, что дальше нет никакого интерфейса, а игра происходит по сети.
\begin{frm} \excl
В этом и есть суть ООП, когда один объект максимально отделён от другого, и каждому из них вообще не важно, как реализован другой.
\end{frm}
Соответственно, когда в окне настроек нажата кнопка «начать игру», обработчик вызывает метод главного окна, а главное окно в свою очередь уже знает, что оно разделено на панели, и вынуждает панель \code{Map} начинать (рисунок \hrf{pic:starting-flow})\footnote{\href{www.plantuml.com/plantuml/uml/ZL1TQy9047o_Nx5zKp1WAzAYLaIqQcbLY125GhcQqnmab-1jQgdOtzwzSjQFizWy16vsPdPdMXhv2lCaPbSOYKH05dEf69l7N6leyKG4KeNf6XgDXnAi8ucYsOGD0_eys90QvNmB2wbu358X18DXPnIylFQxWrv_0lTGhLOliuD1Pz8tvFBjPV9uv4-9UrSk_uix8sx5Sh_WiPqZfWhUKFackWjtF-GEVUOP93ohswSl4ALQQbk9jiywi_DzNOMYXVIHn8yEHsRzKAoDYi2jBVrqYrkySqbX-Rlud7aRrNWbj1RXuHhAHjvZrvi6XU8kydigm-DBapGK9LZudzEV_umCdeGY0Jdl2wZLtEJWULxpD5uDhjaHHDBpnvCypyZ0eEUOP7N3_XnwAxcCq3Ff75c5jOGAyoJ-1W00 }{Исходный код диаграммы в PlantUML}}. Для чего нужен промежуточный метод? Чтобы не делать лишних связей между классами. Это логично с точки зрения инкапсуляции. Одно окно не должно никак управлять панелью на другом окне.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обработчик кнопки старта новой игры}]
SettingsWindow(GameWindow gameWindow) {
setLocationRelativeTo(gameWindow);
setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
btnStart.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
gameWindow.startNewGame(0, 3, 3, 3);
setVisible(false);
}
});
add(btnStart);
}
\end{lstlisting}
В окне настроек описан обработчик нажатия на единственную кнопку, из этого обработчика вызывается единственный доступный метод -- «старт новой игры» на основном окне. По факту нажатия, также, целесообразно спрятать окно настроек. Из метода основного класса \code{startNewGame()} вызывается \code{map.startNewGame()} класса мэп.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Цепочка вызовов методов старта новой игры}]
void startNewGame(int mode, int fSzX, int fSzY, int wLen) {
map.startNewGame(mode, fSzX, fSzY, wLen);
}
\end{lstlisting}
Ещё раз цепочка вызовов (рис. \hrf{pic:starting-flow}):
\begin{enumerate}
\item основное окно делает окно настроек видимым;
\item окно настроек говорит основному, что пора начинать игру;
\item основное окно в свою очередь знает, как именно надо игру начинать и просит панель стартовать.
\end{enumerate}
Если всё сделано верно, в терминале появится вывод из заглушки на панели \code{Map}.
\begin{verbatim}
Mode: 0;
Size: x=3, y=3;
Win Length: 3
\end{verbatim}
\begin{figure}[H]
\centering
\fontsize{8}{1}\selectfont
\includesvg[scale=.7]{pics/jd-01-ttt-invoke.svg}
\caption{Цепочка вызовов при старте новой игры}
\label{pic:starting-flow}
\end{figure}
\subsubsection{Вопросы для самопроверки}
\begin{enumerate}
\item Менеджер размещений - это
\begin{enumerate}
\item сотрудник, занимающийся разработкой интерфейса;
\item объект, выполняющий расстановку компонентов на окне приложения;
\item механизм, проверяющий возможность отображения окна в ОС.
\end{enumerate}
\item Экземпляр JPanel позволяет
\begin{enumerate}
\item применять комбинации из компоновщиков
\item добавить к интерфейсу больше компонентов
\item создавать группы компонентов
\item всё вышеперечисленное
\end{enumerate}
\item Для выполнения кода по нажатию кнопки на интерфейсе нужно
\begin{enumerate}
\item создать обработчик кнопки и вписать код в него
\item переопределить метод нажатия у компонента кнопки
\item использовать специальный класс “слушателя” кнопок
\end{enumerate}
\end{enumerate}
\subsection{Основная панель с игрой}
\subsubsection{Рисование}
Далее всё будет происходить на панели с полем для игры. Для рисования самой панели фреймворком определён метод \code{paintComponent()}. Этот метод вызывается фреймворком когда что-то происходит, например, когда основное окно перекрывается другим, перемещается на другой экран, или если его развернуть из свёрнутого состояния, вызывается он гораздо реже, чем это необходимо для логики игры. Для описания игрового процесса необходимо перерисовывать компонент по каждому клику мышкой и по каждому действию оппонента.
\begin{frm} \excl
Важно помнить, что метод \code{paintComponent()} не следует напрямую вызывать из кода. Этот метод должен вызываться только фреймворком. Для того чтобы запросить у фреймворка вызов этого метода тоже есть специальный метод.
\end{frm}
Для дальнейшей разработки важно отделить стандартный метод рисования компонента от пользовательского рисования на этом компоненте, так называемую бизнес-логику. Для этого будет создан ещё один метод \code{void render(Graphics g)}, который будет вызываться из переопределённого \code{paintComponent()}. из самого \code{paintComponent()} вызов метода родительского класса удалять не следует, поскольку там, скорее всего, происходит что-то важное. Для вызова же метода фреймворка, необходимо в нужный момент сказать фреймворку что требуется перерисовать панель, фреймворк поставит метод \code{paintComponent()} в очередь сообщений окна, и когда очередь дойдёт до выполнения этого метода -- окно выполнит перерисовку.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Отделение бизнес-логики рисования}]
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
render(g);
}
private void render(Graphics g) { }
\end{lstlisting}
\begin{frm} \excl
Это действие полностью асинхронно и \textit{косвенно} зависит от наших вызовов
\end{frm}
Чтобы рисовать нужен объект класса \code{Graphics}, который умеет рисовать геометрические фигуры, линии, текст и тому подобное. Чтобы нарисовать поле для игры понадобится ширина и высота поля в пикселях. Их возможно узнать из свойств панели -- ширины и высоты. Всё, что связано с размерами лучше вынести в переменные объекта, поскольку они понадобятся в других методах. Помимо ширины и высоты понадобятся переменные, в которых будет храниться высота и ширина каждой ячейки. Размеры каждой ячейки пригодятся для создания отступа одной линии от другой. Далее циклически делаются отступы и рисуются горизонтальные и вертикальные линии.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Разлиновка поля для игры}]
private int panelWidth;
private int panelHeight;
private int cellHeight;
private int cellWidth;
private void render(Graphics g) {
panelWidth = getWidth();
panelHeight = getHeight();
cellHeight = panelHeight / 3;
cellWidth = panelWidth / 3;
g.setColor(Color.BLACK);
for (int h = 0; h < 3; h++) {
int y = h * cellHeight;
g.drawLine(0, y, panelWidth, y);
}
for (int w = 0; w < 3; w++) {
int x = w * cellHeight;
g.drawLine(x, 0, x, panelHeight);
}
repaint();
}
\end{lstlisting}
У многих разработчиков, в зависимости от используемой операционной системы после запуска этого кода не полностью или вовсе не рисуется разлиновка, это происходит из-за асинхронности рисования, скорее всего метод с линиями отработал позже того, как Swing нарисовал панель.
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-01-ttt-lined-result.png}
\caption{Результат разлиновки поля для игры}
\end{figure}
Чтобы всё увидеть, необходимо заставить компонент панели полностью перерисоваться. Это делается вызовом метода \code{repaint()} из метода старта новой игры.
\subsubsection{Обработчик мышки}
Обработчик действий мышки очень похож на те обработчики, которые уже написаны. В конструкторе панели описывается метод добавления слушателя, в котором переопределяется метод \code{mouseReleased()}, то есть для приложения важно когда пользователь отпустит кнопку и аналогично методу отрисовки следует сразу отделить обработчик от основной исполняемой логики.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обработчик отпускания кнопки мышки}]
Map() {
addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
update(e);
}
});
}
\end{lstlisting}
Внутри метода обновления также принудительно вызывается метод перерисовки компонента, чтобы получился игровой цикл: старт -- отрисовка -- клик мыши -- отрисовка -- клик -- отрисовка...
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обработчик отпускания кнопки мышки}]
private void update(MouseEvent e) {
int cellX = e.getX() / cellWidth;
int cellY = e.getY() / cellHeight;
System.out.printf("x=%d, y=%d\n", cellX, cellY);
repaint();
}
\end{lstlisting}
В методе обновления из объекта \code{MouseEvent} получаются координаты клика, делятся на размер ячейки и тем самым получается номер ячейки, в которую произошёл клик.
\subsubsection{Логика игры}
Поскольку лекция про графические интерфейсы и ООП, а все используемые конструкции примитивны, код логики игры будет приведён без подробных пояснений.
Для работы понадобится генератор псевдослучайных чисел, символы, которыми будет обозначаться на поле игрок, компьютер и пустая ячейка, собственно поле и его размеры. Размеры -- на будущее.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Субъекты игры}]
private static final Random RANDOM = new Random();
private final int HUMAN_DOT = 1;
private final int AI_DOT = 2;
private final int EMPTY_DOT = 0;
private int fieldSizeY = 3;
private int fieldSizeX = 3;
private char[][] field;
\end{lstlisting}
Метод инициализации поля -- создаётся новый массив и заполняется пустыми символами. Его вызов логично сразу разместить в метод старта новой игры.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Инициализация карты}]
private void initMap() {
fieldSizeY = 3;
fieldSizeX = 3;
field = new char[fieldSizeY][fieldSizeX];
for (int i = 0; i < fieldSizeY; i++) {
for (int j = 0; j < fieldSizeX; j++) {
field[i][j] = EMPTY_DOT;
}
}
}
\end{lstlisting}
Когда кто-то (игрок или компьютер) будет совершать ход, будет важно, попал ли игрок в какую-то ячейку поля и пустота ли она, потому что нельзя ставить крестик поверх нолика и наоборот.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Попал ли игрок в пустую ячейку и в поле}]
private boolean isValidCell(int x, int y) {
return x >= 0 && x < fieldSizeX && y >= 0 && y < fieldSizeY;
}
private boolean isEmptyCell(int x, int y) {
return field[y][x] == EMPTY_DOT;
}
\end{lstlisting}
Компьютер будет очень примитивный -- он будет делать ход в случайные места на карте.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Ход компьютера}]
private void aiTurn() {
int x, y;
do {
x = RANDOM.nextInt(fieldSizeX);
y = RANDOM.nextInt(fieldSizeY);
} while (!isEmptyCell(x, y));
field[y][x] = AI_DOT;
}
\end{lstlisting}
Очевидно, что учебные цели предполагают не только демонстрацию того, как надо делать, но также и демонстрацию того как делать не надо. Далее приведён код, который не следует допускать при работе над приложениями. Метод принимает на вход символ, который нужно проверить и проверяет - не победил ли он.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Проверка победы одного из игроков}]
private boolean checkWin(char c) {
if (field[0][0]==c && field[0][1]==c && field[0][2]==c) return true;
if (field[1][0]==c && field[1][1]==c && field[1][2]==c) return true;
if (field[2][0]==c && field[2][1]==c && field[2][2]==c) return true;
if (field[0][0]==c && field[1][0]==c && field[2][0]==c) return true;
if (field[0][1]==c && field[1][1]==c && field[2][1]==c) return true;
if (field[0][2]==c && field[1][2]==c && field[2][2]==c) return true;
if (field[0][0]==c && field[1][1]==c && field[2][2]==c) return true;
if (field[0][2]==c && field[1][1]==c && field[2][0]==c) return true;
return false;
}
\end{lstlisting}
\begin{frm} \excl
Всегда пишите с помощью циклов, потому что стоит захотеть изменить размер поля на 4х4 или 5х5 -- размер и сложность этого метода будет расти в геометрической прогрессии.
\end{frm}
И метод проверки поля на состояние ничьей. Ничья в крестиках-ноликах наступает, когда не победил ни игрок ни оппонент, и не осталось пустых клеток.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Проверка на ничью}]
private boolean isMapFull() {
for (int i = 0; i < fieldSizeY; i++) {
for (int j = 0; j < fieldSizeX; j++) {
if (field[i][j] == EMPTY_DOT) return false;
}
}
return true;
}
\end{lstlisting}
Вся дальнейшая работа будет сконцентрирована на методах обновления игрового состояния и отрисовки игрового поля. В результате клика в ячейку необходимо проверить, валидная-ли ячейка, и можно-ли туда ходить. Если какое-то из условий не прошло, клик игнорируется, а если всё хорошо -- делается ход.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Ход игрока}]
int cellX = e.getX()/cellWidth;
int cellY = e.getY()/cellHeight;
if (!isValidCell(cellX, cellY) || !isEmptyCell(cellX, cellY)) return;
field[cellY][cellX] = HUMAN_DOT;
repaint();
\end{lstlisting}
В метод отрисовки также необходимо добавить логику. Если в ячейке поля ничего нет -- ничего делать не нужно, далее условие, если в ячейке крестик -- что-то сделаем, если нолик -- сделаем что-то другое и в противном случае выбросим исключение.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Отрисовка поля}]
for (int y = 0; y < fieldSizeY; y++) {
for (int x = 0; x < fieldSizeX; x++) {
if (field[y][x] == EMPTY_DOT) continue;
if (field[y][x] == HUMAN_DOT) {
g.setColor(Color.BLUE);
g.fillOval(x * cellWidth + DOT_PADDING,
y * cellHeight + DOT_PADDING,
cellWidth - DOT_PADDING * 2,
cellHeight - DOT_PADDING * 2);
} else if (field[y][x] == AI_DOT) {
g.setColor(new Color(0xff0000));
g.fillOval(x * cellWidth + DOT_PADDING,
y * cellHeight + DOT_PADDING,
cellWidth - DOT_PADDING * 2,
cellHeight - DOT_PADDING * 2);
} else {
throw new RuntimeException("Unexpected value " + field[y][x] +
" in cell: x=" + x + " y=" + y);
}
}
}
\end{lstlisting}
Далее -- непосредственно отрисовка. Сюда можно картинку вставлять, закрашивать квадраты, рисовать крестики и нолики. Для простоты будут рисоваться кружки. Методу объекта графики \code{g.fillOval()} в сигнатуре передаётся левая верхняя координата прямоугольника, в который затем будет вписан овал, его ширина и высота соответственно. Чтобы задать цвет -- перед тем как рисовать необходимо изменить цвет объекта графики \code{g.setColor(Color.BLUE)}. Для человека далее будут рисоваться синие кружки, а для компьютера красные.
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-01-ttt-hum-turn.png}
\end{figure}
\subsubsection{Последние приготовления}
По сути, осталось сделать две вещи -- описать так называемую бизнес-логику, то есть в правильном порядке вызвать методы с логикой игры, избавиться от исключений и вывести сообщение об окончании игры. Для того, чтобы вывести результат, поверх игрового поля будет выводиться сообщение.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Состояния игрового поля}]
private int gameOverType;
private static final int STATE_DRAW = 0;
private static final int STATE_WIN_HUMAN = 1;
private static final int STATE_WIN_AI = 2;
private static final String MSG_WIN_HUMAN = "Победил игрок!";
private static final String MSG_WIN_AI = "Победил компьютер!";
private static final String MSG_DRAW = "Ничья!";
\end{lstlisting}
В методе обновления уточняется, что когда пользователь поставил точку, необходимо проверить состояние поля на наличие победы или ничьей, дать возможность компьютеру поставить точку и сделать тоже самое.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Логика обновления}]
// update
if (checkEndGame(HUMAN_DOT, STATE_WIN_HUMAN)) return;
aiTurn();
repaint();
if (checkEndGame(AI_DOT, STATE_WIN_AI)) return;
// end update
private boolean checkEndGame(int dot, int gameOverType) {
if (checkWin(dot)) {
this.gameOverType = gameOverType;
repaint();
return true;
}
if (isMapFull()) {
this.gameOverType = STATE_DRAW;
repaint();
return true;
}
return false;
}
\end{lstlisting}
В методе рендеринга, как только поле выведено и если игра закончилась необходимо вывести сообщение с одним из вариантов исхода игры. Для упрощения также следует завести классовую переменную с признаком окончания игры. Метод окончания игры рисует тёмно серый прямоугольник с жёлтой надписью о победе одного из игроков или ничьей в зависимости от состояния.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Отрисовка сообщения об окончании игры}]
//render
if (isGameOver) showMessageGameOver(g);
// end render
private void showMessageGameOver(Graphics g) {
g.setColor(Color.DARK_GRAY);
g.fillRect(0, 200, getWidth(), 70);
g.setColor(Color.YELLOW);
g.setFont(new Font("Times new roman", Font.BOLD, 48));
switch (gameOverType) {
case STATE_DRAW:
g.drawString(MSG_DRAW, 180, getHeight() / 2); break;
case STATE_WIN_AI:
g.drawString(MSG_WIN_AI, 20, getHeight() / 2); break;
case STATE_WIN_HUMAN:
g.drawString(MSG_WIN_HUMAN, 70, getHeight() / 2); break;
default:
throw new RuntimeException("Unexpected gameOver state: " + gameOverType);
}
}
\end{lstlisting}
Далее в листинге \hrf{lst:minor-additions} приведены несколько мелких правок в методах. Прочитав текст исключения при старте приложения становится ясно, что оно возникает, когда программа не может что-то поделить на ноль. Размеры поля до их инициализации равны нулю, поэтому понадобится ещё одна булева переменная -- инициализирована ли игра.
\begin{itemize}
\item В конструкторе панели поле не инициализировано;
\item в методе обновления нет смысла обрабатывать клики по неинициализированному полю или полю на котором закончилась игра;
\item при старте новой игры игра перестаёт быть законченой, а поле становится инициализированным;
\item рендеринг на неинициализированном поле не имеет смысла;
\item в методе проверки на победу нужно добавить присвоение истинности булевой переменной с фактом окончания игры.
\end{itemize}
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Незначительные добавления в методах панели},label={lst:minor-additions}]
private boolean isGameOver;
private boolean isInitialized;
Map() {
isInitialized = false;
}
private void update(MouseEvent e) {
if (isGameOver || !isInitialized) return;
}
void startNewGame(int mode, int fSzX, int fSzY, int wLen) {
isGameOver = false;
isInitialized = true;
}
private void render(Graphics g) {
if (!isInitialized) return;
}
private boolean checkEndGame(int dot, int gameOverType) {
if (checkWin(dot)) {
isGameOver = true;
}
if (isMapFull()) {
isGameOver = true;
}
return false;
}
\end{lstlisting}
Результат запуска получившегося приложения представлен на рисунке \hrf{pic:ttt-results}.
\begin{figure}[H]
\centering
\begin{subfigure}[b]{0.3\textwidth}
\centering
\includegraphics[width=\textwidth]{jd-01-ttt-rslt-cmp.png}
\caption{Победа компьютера}
\end{subfigure}
\hfill
\begin{subfigure}[b]{0.3\textwidth}
\centering
\includegraphics[width=\textwidth]{jd-01-ttt-rslt-hum.png}
\caption{Победа игрока}
\end{subfigure}
\hfill
\begin{subfigure}[b]{0.3\textwidth}
\centering
\includegraphics[width=\textwidth]{jd-01-ttt-rslt-drw.png}
\caption{Ничья}
\end{subfigure}
\caption{Результаты игры}
\label{pic:ttt-results}
\end{figure}
\subsection*{Практическое задание}
\begin{enumerate}
\item Полностью разобраться с кодом.
\item Переделать проверку победы, чтобы она не была реализована просто набором условий.
\item Попробовать переписать логику проверки победы, чтобы она работала для поля 5х5 и количества фигур 4.
\item ** Доработать искусственный интеллект, чтобы он мог примитивно блокировать ходы игрока, и примитивно пытаться выиграть сам.
\end{enumerate}
\newpage
\printnomenclature[40mm]
\end{document}