diff --git a/.gitignore b/.gitignore
index 2c6eb3893..028bd3cdc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,29 @@
-/.metadata
-/robots/.settings
-/robots/bin
-eclipse.bat
\ No newline at end of file
+# IntelliJ IDEA
+.idea/
+*.iml
+*.iws
+
+# Eclipse
+.classpath
+.project
+.settings/
+
+# VS Code
+.vscode/
+
+# Build output
+/out/
+target/
+bin/
+
+# Logs and temporary files
+*.log
+*.tmp
+
+# OS junk
+.DS_Store
+Thumbs.db
+
+# Personal/test files
+my.txt
+/net/
diff --git a/robots/.classpath b/robots/.classpath
deleted file mode 100644
index fceb4801b..000000000
--- a/robots/.classpath
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/robots/.gitignore b/robots/.gitignore
deleted file mode 100644
index 2757ffa76..000000000
--- a/robots/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/bin/
-/.settings/
diff --git a/robots/.project b/robots/.project
deleted file mode 100644
index 78e165663..000000000
--- a/robots/.project
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
- Robots
-
-
-
-
-
- org.eclipse.jdt.core.javabuilder
-
-
-
-
-
- org.eclipse.jdt.core.javanature
-
-
diff --git a/robots/src/gui/MainApplicationFrame.java b/robots/src/gui/MainApplicationFrame.java
deleted file mode 100644
index 62e943ee1..000000000
--- a/robots/src/gui/MainApplicationFrame.java
+++ /dev/null
@@ -1,156 +0,0 @@
-package gui;
-
-import java.awt.Dimension;
-import java.awt.Toolkit;
-import java.awt.event.KeyEvent;
-
-import javax.swing.JDesktopPane;
-import javax.swing.JFrame;
-import javax.swing.JInternalFrame;
-import javax.swing.JMenu;
-import javax.swing.JMenuBar;
-import javax.swing.JMenuItem;
-import javax.swing.SwingUtilities;
-import javax.swing.UIManager;
-import javax.swing.UnsupportedLookAndFeelException;
-
-import log.Logger;
-
-/**
- * Что требуется сделать:
- * 1. Метод создания меню перегружен функционалом и трудно читается.
- * Следует разделить его на серию более простых методов (или вообще выделить отдельный класс).
- *
- */
-public class MainApplicationFrame extends JFrame
-{
- private final JDesktopPane desktopPane = new JDesktopPane();
-
- public MainApplicationFrame() {
- //Make the big window be indented 50 pixels from each edge
- //of the screen.
- int inset = 50;
- Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
- setBounds(inset, inset,
- screenSize.width - inset*2,
- screenSize.height - inset*2);
-
- setContentPane(desktopPane);
-
-
- LogWindow logWindow = createLogWindow();
- addWindow(logWindow);
-
- GameWindow gameWindow = new GameWindow();
- gameWindow.setSize(400, 400);
- addWindow(gameWindow);
-
- setJMenuBar(generateMenuBar());
- setDefaultCloseOperation(EXIT_ON_CLOSE);
- }
-
- protected LogWindow createLogWindow()
- {
- LogWindow logWindow = new LogWindow(Logger.getDefaultLogSource());
- logWindow.setLocation(10,10);
- logWindow.setSize(300, 800);
- setMinimumSize(logWindow.getSize());
- logWindow.pack();
- Logger.debug("Протокол работает");
- return logWindow;
- }
-
- protected void addWindow(JInternalFrame frame)
- {
- desktopPane.add(frame);
- frame.setVisible(true);
- }
-
-// protected JMenuBar createMenuBar() {
-// JMenuBar menuBar = new JMenuBar();
-//
-// //Set up the lone menu.
-// JMenu menu = new JMenu("Document");
-// menu.setMnemonic(KeyEvent.VK_D);
-// menuBar.add(menu);
-//
-// //Set up the first menu item.
-// JMenuItem menuItem = new JMenuItem("New");
-// menuItem.setMnemonic(KeyEvent.VK_N);
-// menuItem.setAccelerator(KeyStroke.getKeyStroke(
-// KeyEvent.VK_N, ActionEvent.ALT_MASK));
-// menuItem.setActionCommand("new");
-//// menuItem.addActionListener(this);
-// menu.add(menuItem);
-//
-// //Set up the second menu item.
-// menuItem = new JMenuItem("Quit");
-// menuItem.setMnemonic(KeyEvent.VK_Q);
-// menuItem.setAccelerator(KeyStroke.getKeyStroke(
-// KeyEvent.VK_Q, ActionEvent.ALT_MASK));
-// menuItem.setActionCommand("quit");
-//// menuItem.addActionListener(this);
-// menu.add(menuItem);
-//
-// return menuBar;
-// }
-
- private JMenuBar generateMenuBar()
- {
- JMenuBar menuBar = new JMenuBar();
-
- JMenu lookAndFeelMenu = new JMenu("Режим отображения");
- lookAndFeelMenu.setMnemonic(KeyEvent.VK_V);
- lookAndFeelMenu.getAccessibleContext().setAccessibleDescription(
- "Управление режимом отображения приложения");
-
- {
- JMenuItem systemLookAndFeel = new JMenuItem("Системная схема", KeyEvent.VK_S);
- systemLookAndFeel.addActionListener((event) -> {
- setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
- this.invalidate();
- });
- lookAndFeelMenu.add(systemLookAndFeel);
- }
-
- {
- JMenuItem crossplatformLookAndFeel = new JMenuItem("Универсальная схема", KeyEvent.VK_S);
- crossplatformLookAndFeel.addActionListener((event) -> {
- setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
- this.invalidate();
- });
- lookAndFeelMenu.add(crossplatformLookAndFeel);
- }
-
- JMenu testMenu = new JMenu("Тесты");
- testMenu.setMnemonic(KeyEvent.VK_T);
- testMenu.getAccessibleContext().setAccessibleDescription(
- "Тестовые команды");
-
- {
- JMenuItem addLogMessageItem = new JMenuItem("Сообщение в лог", KeyEvent.VK_S);
- addLogMessageItem.addActionListener((event) -> {
- Logger.debug("Новая строка");
- });
- testMenu.add(addLogMessageItem);
- }
-
- menuBar.add(lookAndFeelMenu);
- menuBar.add(testMenu);
- return menuBar;
- }
-
- private void setLookAndFeel(String className)
- {
- try
- {
- UIManager.setLookAndFeel(className);
- SwingUtilities.updateComponentTreeUI(this);
- }
- catch (ClassNotFoundException | InstantiationException
- | IllegalAccessException | UnsupportedLookAndFeelException e)
- {
- // just ignore
- }
- }
-}
diff --git a/robots/src/gui/RobotsProgram.java b/robots/src/gui/RobotsProgram.java
deleted file mode 100644
index ae0930a8b..000000000
--- a/robots/src/gui/RobotsProgram.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package gui;
-
-import java.awt.Frame;
-
-import javax.swing.SwingUtilities;
-import javax.swing.UIManager;
-
-public class RobotsProgram
-{
- public static void main(String[] args) {
- try {
- UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel");
-// UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
-// UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
-// UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
- } catch (Exception e) {
- e.printStackTrace();
- }
- SwingUtilities.invokeLater(() -> {
- MainApplicationFrame frame = new MainApplicationFrame();
- frame.pack();
- frame.setVisible(true);
- frame.setExtendedState(Frame.MAXIMIZED_BOTH);
- });
- }}
diff --git a/robots/src/log/LogChangeListener.java b/robots/src/log/LogChangeListener.java
deleted file mode 100644
index 0b0fb85dd..000000000
--- a/robots/src/log/LogChangeListener.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package log;
-
-public interface LogChangeListener
-{
- public void onLogChanged();
-}
diff --git a/robots/src/log/LogWindowSource.java b/robots/src/log/LogWindowSource.java
deleted file mode 100644
index ca0ce4426..000000000
--- a/robots/src/log/LogWindowSource.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package log;
-
-import java.util.ArrayList;
-import java.util.Collections;
-
-/**
- * Что починить:
- * 1. Этот класс порождает утечку ресурсов (связанные слушатели оказываются
- * удерживаемыми в памяти)
- * 2. Этот класс хранит активные сообщения лога, но в такой реализации он
- * их лишь накапливает. Надо же, чтобы количество сообщений в логе было ограничено
- * величиной m_iQueueLength (т.е. реально нужна очередь сообщений
- * ограниченного размера)
- */
-public class LogWindowSource
-{
- private int m_iQueueLength;
-
- private ArrayList m_messages;
- private final ArrayList m_listeners;
- private volatile LogChangeListener[] m_activeListeners;
-
- public LogWindowSource(int iQueueLength)
- {
- m_iQueueLength = iQueueLength;
- m_messages = new ArrayList(iQueueLength);
- m_listeners = new ArrayList();
- }
-
- public void registerListener(LogChangeListener listener)
- {
- synchronized(m_listeners)
- {
- m_listeners.add(listener);
- m_activeListeners = null;
- }
- }
-
- public void unregisterListener(LogChangeListener listener)
- {
- synchronized(m_listeners)
- {
- m_listeners.remove(listener);
- m_activeListeners = null;
- }
- }
-
- public void append(LogLevel logLevel, String strMessage)
- {
- LogEntry entry = new LogEntry(logLevel, strMessage);
- m_messages.add(entry);
- LogChangeListener [] activeListeners = m_activeListeners;
- if (activeListeners == null)
- {
- synchronized (m_listeners)
- {
- if (m_activeListeners == null)
- {
- activeListeners = m_listeners.toArray(new LogChangeListener [0]);
- m_activeListeners = activeListeners;
- }
- }
- }
- for (LogChangeListener listener : activeListeners)
- {
- listener.onLogChanged();
- }
- }
-
- public int size()
- {
- return m_messages.size();
- }
-
- public Iterable range(int startFrom, int count)
- {
- if (startFrom < 0 || startFrom >= m_messages.size())
- {
- return Collections.emptyList();
- }
- int indexTo = Math.min(startFrom + count, m_messages.size());
- return m_messages.subList(startFrom, indexTo);
- }
-
- public Iterable all()
- {
- return m_messages;
- }
-}
diff --git a/robots/src/log/Logger.java b/robots/src/log/Logger.java
deleted file mode 100644
index b008a5d01..000000000
--- a/robots/src/log/Logger.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package log;
-
-public final class Logger
-{
- private static final LogWindowSource defaultLogSource;
- static {
- defaultLogSource = new LogWindowSource(100);
- }
-
- private Logger()
- {
- }
-
- public static void debug(String strMessage)
- {
- defaultLogSource.append(LogLevel.Debug, strMessage);
- }
-
- public static void error(String strMessage)
- {
- defaultLogSource.append(LogLevel.Error, strMessage);
- }
-
- public static LogWindowSource getDefaultLogSource()
- {
- return defaultLogSource;
- }
-}
diff --git a/src/main/java/robots/gui/AppContext.java b/src/main/java/robots/gui/AppContext.java
new file mode 100644
index 000000000..32a3c223e
--- /dev/null
+++ b/src/main/java/robots/gui/AppContext.java
@@ -0,0 +1,13 @@
+package robots.gui;
+
+public class AppContext {
+ public final LogWindow logWindow;
+ public final GameWindow gameWindow;
+ public final RobotPositionWindow positionWindow;
+
+ public AppContext(LogWindow logWindow, GameWindow gameWindow, RobotPositionWindow positionWindow) {
+ this.logWindow = logWindow;
+ this.gameWindow = gameWindow;
+ this.positionWindow = positionWindow;
+ }
+}
diff --git a/robots/src/gui/GameVisualizer.java b/src/main/java/robots/gui/GameVisualizer.java
similarity index 80%
rename from robots/src/gui/GameVisualizer.java
rename to src/main/java/robots/gui/GameVisualizer.java
index f82cfd8f8..2a0024117 100644
--- a/robots/src/gui/GameVisualizer.java
+++ b/src/main/java/robots/gui/GameVisualizer.java
@@ -1,4 +1,6 @@
-package gui;
+package robots.gui;
+import robots.log.Logger;
+import robots.log.RobotModel;
import java.awt.Color;
import java.awt.EventQueue;
@@ -11,18 +13,11 @@
import java.util.Timer;
import java.util.TimerTask;
-import javax.swing.JPanel;
+import javax.swing.*;
public class GameVisualizer extends JPanel
{
- private final Timer m_timer = initTimer();
-
- private static Timer initTimer()
- {
- Timer timer = new Timer("events generator", true);
- return timer;
- }
-
+
private volatile double m_robotPositionX = 100;
private volatile double m_robotPositionY = 100;
private volatile double m_robotDirection = 0;
@@ -31,26 +26,19 @@ private static Timer initTimer()
private volatile int m_targetPositionY = 100;
private static final double maxVelocity = 0.1;
- private static final double maxAngularVelocity = 0.001;
+ private static final double maxAngularVelocity = 0.01;
public GameVisualizer()
{
- m_timer.schedule(new TimerTask()
- {
- @Override
- public void run()
- {
- onRedrawEvent();
- }
- }, 0, 50);
- m_timer.schedule(new TimerTask()
- {
+ Timer m_timer = new Timer("events generator", true);
+ m_timer.schedule(new TimerTask() {
@Override
- public void run()
- {
- onModelUpdateEvent();
+ public void run() {
+ onModelUpdateEvent(); // сначала обновляем модель
+ onRedrawEvent(); // потом вызываем repaint
}
- }, 0, 10);
+ }, 0, 5); // например, 50 кадров в секунду
+
addMouseListener(new MouseAdapter()
{
@Override
@@ -61,12 +49,14 @@ public void mouseClicked(MouseEvent e)
}
});
setDoubleBuffered(true);
+
}
protected void setTargetPosition(Point p)
{
m_targetPositionX = p.x;
m_targetPositionY = p.y;
+ EventQueue.invokeLater(this::repaint);
}
protected void onRedrawEvent()
@@ -100,18 +90,22 @@ protected void onModelUpdateEvent()
double velocity = maxVelocity;
double angleToTarget = angleTo(m_robotPositionX, m_robotPositionY, m_targetPositionX, m_targetPositionY);
double angularVelocity = 0;
- if (angleToTarget > m_robotDirection)
- {
- angularVelocity = maxAngularVelocity;
- }
- if (angleToTarget < m_robotDirection)
- {
- angularVelocity = -maxAngularVelocity;
- }
+ double angleDiff = normalizeAngle(angleToTarget - m_robotDirection);
+ angularVelocity = angleDiff > 0 ? maxAngularVelocity : -maxAngularVelocity;
+
moveRobot(velocity, angularVelocity, 10);
+
+ RobotModel.getRobotPositionModel().setPosition(m_robotPositionX, m_robotPositionY);
}
-
+
+ private static double normalizeAngle(double angle) {
+ while (angle < -Math.PI) angle += 2 * Math.PI;
+ while (angle > Math.PI) angle -= 2 * Math.PI;
+ return angle;
+ }
+
+
private static double applyLimits(double value, double min, double max)
{
if (value < min)
@@ -197,14 +191,15 @@ private void drawRobot(Graphics2D g, int x, int y, double direction)
g.setColor(Color.BLACK);
drawOval(g, robotCenterX + 10, robotCenterY, 5, 5);
}
-
- private void drawTarget(Graphics2D g, int x, int y)
- {
- AffineTransform t = AffineTransform.getRotateInstance(0, 0, 0);
- g.setTransform(t);
+
+ private void drawTarget(Graphics2D g, int x, int y) {
+ AffineTransform oldTransform = g.getTransform(); // сохранение
+ g.setTransform(new AffineTransform()); // сброс
g.setColor(Color.GREEN);
fillOval(g, x, y, 5, 5);
g.setColor(Color.BLACK);
drawOval(g, x, y, 5, 5);
+ g.setTransform(oldTransform); // восстановление
}
+
}
diff --git a/robots/src/gui/GameWindow.java b/src/main/java/robots/gui/GameWindow.java
similarity index 96%
rename from robots/src/gui/GameWindow.java
rename to src/main/java/robots/gui/GameWindow.java
index ecb63c00f..a04d78a7b 100644
--- a/robots/src/gui/GameWindow.java
+++ b/src/main/java/robots/gui/GameWindow.java
@@ -1,4 +1,4 @@
-package gui;
+package robots.gui;
import java.awt.BorderLayout;
diff --git a/robots/src/gui/LogWindow.java b/src/main/java/robots/gui/LogWindow.java
similarity index 84%
rename from robots/src/gui/LogWindow.java
rename to src/main/java/robots/gui/LogWindow.java
index 723d3e2fc..66b620680 100644
--- a/robots/src/gui/LogWindow.java
+++ b/src/main/java/robots/gui/LogWindow.java
@@ -1,4 +1,4 @@
-package gui;
+package robots.gui;
import java.awt.BorderLayout;
import java.awt.EventQueue;
@@ -7,14 +7,14 @@
import javax.swing.JInternalFrame;
import javax.swing.JPanel;
-import log.LogChangeListener;
-import log.LogEntry;
-import log.LogWindowSource;
+import robots.log.LogChangeListener;
+import robots.log.LogEntry;
+import robots.log.LogWindowSource;
public class LogWindow extends JInternalFrame implements LogChangeListener
{
- private LogWindowSource m_logSource;
- private TextArea m_logContent;
+ private final LogWindowSource m_logSource;
+ private final TextArea m_logContent;
public LogWindow(LogWindowSource logSource)
{
diff --git a/src/main/java/robots/gui/MainApplicationFrame.java b/src/main/java/robots/gui/MainApplicationFrame.java
new file mode 100644
index 000000000..e75f20f00
--- /dev/null
+++ b/src/main/java/robots/gui/MainApplicationFrame.java
@@ -0,0 +1,248 @@
+package robots.gui;
+
+import java.io.File;
+
+import java.awt.*;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+
+import java.io.FileOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+import java.util.Properties;
+
+import javax.swing.*;
+
+import robots.log.Logger;
+
+
+public class MainApplicationFrame extends JFrame
+{
+ private final LogWindow logWindow;
+ private final GameWindow gameWindow;
+ private final MainController controller;
+ private final RobotPositionWindow robotPositionWindow;
+
+
+ private final JDesktopPane desktopPane = new JDesktopPane();
+
+ public MainApplicationFrame(LogWindow logWindow, GameWindow gameWindow, MainController mainController, RobotPositionWindow robotPositionWindow) {
+ this.logWindow = logWindow;
+ this.gameWindow = gameWindow;
+ this.controller = mainController;
+ this.robotPositionWindow = robotPositionWindow;
+
+ initialize();
+ }
+
+ private void initialize() {
+ gameWindow.setName("gameWindow");
+ logWindow.setName("logWindow");
+
+
+ setLogWindow(logWindow);
+ int inset = 50;
+ Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+ setBounds(inset, inset,
+ screenSize.width - inset*2,
+ screenSize.height - inset*2);
+
+ setContentPane(desktopPane);
+ gameWindow.setSize(400, 400);
+
+ addWindow(logWindow);
+ // addWindow(gameWindow);
+ addWindow(robotPositionWindow);
+
+ addWindow(new NewSnakeWindow());
+
+ add(new SnakePanel());
+
+ setJMenuBar(generateMenuBar());
+ setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+
+ restoreWindowStates();
+
+ addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ handleExit();
+ }
+ });
+ }
+
+
+ protected void setLogWindow(LogWindow logWindow )
+ {
+ logWindow.setLocation(10,10);
+ logWindow.setSize(300, 800);
+ setMinimumSize(logWindow.getSize());
+ logWindow.pack();
+ Logger.debug("Протокол работает");
+ }
+
+ protected void addWindow(JInternalFrame frame)
+ {
+ desktopPane.add(frame);
+ frame.setVisible(true);
+ }
+
+ private JMenuBar generateMenuBar() {
+ JMenuBar menuBar = new JMenuBar();
+ menuBar.add(createFileMenu());
+ menuBar.add(createLookAndFeelMenu());
+ menuBar.add(createTestMenu());
+ return menuBar;
+ }
+
+ private JMenu createFileMenu() {
+ JMenu fileMenu = new JMenu("Файл");
+ JMenuItem exitItem = new JMenuItem("Выход");
+
+ exitItem.addActionListener(_ -> handleExit());
+ fileMenu.add(exitItem);
+
+ return fileMenu;
+ }
+
+ private JMenu createLookAndFeelMenu() {
+ JMenu lookAndFeelMenu = new JMenu("Режим отображения");
+ lookAndFeelMenu.setMnemonic(KeyEvent.VK_V);
+ lookAndFeelMenu.getAccessibleContext().setAccessibleDescription("Управление режимом отображения приложения");
+
+ JMenuItem systemItem = new JMenuItem("Системная схема", KeyEvent.VK_S);
+ systemItem.addActionListener(_ -> {
+ setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ invalidate();
+ });
+
+ JMenuItem crossItem = new JMenuItem("Универсальная схема", KeyEvent.VK_S);
+ crossItem.addActionListener(_ -> {
+ setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
+ invalidate();
+ });
+
+ lookAndFeelMenu.add(systemItem);
+ lookAndFeelMenu.add(crossItem);
+
+ return lookAndFeelMenu;
+ }
+
+
+ private JMenu createTestMenu() {
+ JMenu testMenu = new JMenu("Тесты");
+ testMenu.setMnemonic(KeyEvent.VK_T);
+ testMenu.getAccessibleContext().setAccessibleDescription("Тестовые команды");
+
+ {
+ JMenuItem addLogMessageItem = new JMenuItem("Сообщение в лог", KeyEvent.VK_S);
+ addLogMessageItem.addActionListener(_ -> controller.onAddLogMessage());
+ testMenu.add(addLogMessageItem);
+ }
+
+ return testMenu;
+ }
+
+
+ private void handleExit() {
+ UIManager.put("OptionPane.yesButtonText", "Да");
+ UIManager.put("OptionPane.noButtonText", "Нет");
+
+ int choice = JOptionPane.showConfirmDialog(
+ this,
+ "Вы действительно хотите выйти?",
+ "Подтверждение выхода",
+ JOptionPane.YES_NO_OPTION
+ );
+
+ if (choice == JOptionPane.YES_OPTION) {
+ controller.handleExit();
+ }
+ }
+
+
+ private void setLookAndFeel(String className) {
+ try
+ {
+ UIManager.setLookAndFeel(className);
+ SwingUtilities.updateComponentTreeUI(this);
+ }
+ catch (ClassNotFoundException | InstantiationException
+ | IllegalAccessException | UnsupportedLookAndFeelException e)
+ {
+ // just ignore
+ }
+ }
+
+
+ public void saveWindowStates() {
+ Properties props = new Properties();
+
+ for (JInternalFrame frame : desktopPane.getAllFrames()) {
+ String name = frame.getName();
+ Rectangle bounds = frame.getBounds();
+
+ props.setProperty(name + ".x", String.valueOf(bounds.x));
+ props.setProperty(name + ".y", String.valueOf(bounds.y));
+ props.setProperty(name + ".width", String.valueOf(bounds.width));
+ props.setProperty(name + ".height", String.valueOf(bounds.height));
+ try {
+ props.setProperty(name + ".isIcon", String.valueOf(frame.isIcon()));
+ } catch (Exception e) {
+ Logger.error("Ошибка при получении isIcon: " + e.getMessage());
+ }
+ props.setProperty(name + ".isMaximized", String.valueOf(frame.isMaximum()));
+ }
+
+ try (FileOutputStream out = new FileOutputStream(WindowConfig.getConfigFile())) {
+ props.store(out, "Window state");
+ } catch (IOException e) {
+ Logger.error("Ошибка при восстановлении положения окна " + e.getMessage());
+ }
+ }
+
+
+ public void restoreWindowStates() {
+ File file = WindowConfig.getConfigFile();
+ if (!file.exists()) return;
+
+ Properties props = new Properties();
+ try (FileInputStream in = new FileInputStream(file)) {
+ props.load(in);
+ } catch (IOException e) {
+ Logger.error("Ошибка при установке LookAndFeel: " + e.getMessage());
+ }
+
+ for (JInternalFrame frame : desktopPane.getAllFrames()) {
+ String name = frame.getName();
+ try {
+ int x = parseIntSafe(props, name + ".x", 100);
+ int y = parseIntSafe(props, name + ".y", 100);
+ int width = parseIntSafe(props, name + ".width", 300);
+ int height = parseIntSafe(props, name + ".height", 300);
+
+ frame.setBounds(x, y, width, height);
+
+ if (Boolean.parseBoolean(props.getProperty(name + ".isIcon", "false")))
+ frame.setIcon(true);
+ if (Boolean.parseBoolean(props.getProperty(name + ".isMaximized", "false")))
+ frame.setMaximum(true);
+
+ } catch (Exception e) {
+ Logger.error("Ошибка при восстановлении положения окна " + name + ": " + e.getMessage());
+ }
+ }
+ }
+
+
+ private int parseIntSafe(Properties props, String key, int defaultValue) {
+ try {
+ return Integer.parseInt(props.getProperty(key));
+ } catch (NumberFormatException | NullPointerException e) {
+ return defaultValue;
+ }
+ }
+
+}
diff --git a/src/main/java/robots/gui/MainController.java b/src/main/java/robots/gui/MainController.java
new file mode 100644
index 000000000..49296afb4
--- /dev/null
+++ b/src/main/java/robots/gui/MainController.java
@@ -0,0 +1,39 @@
+package robots.gui;
+
+import java.awt.*;
+
+import robots.log.Logger;
+import robots.log.RobotPositionSource;
+
+public class MainController {
+ private final LogWindow logWindow;
+ private final GameWindow gameWindow;
+ private MainApplicationFrame frame;
+ private final RobotPositionWindow robotPositionWindow;
+ private final SnakePanel snakePanel;
+
+ public MainController() {
+ this.snakePanel = new SnakePanel();
+ this.logWindow = new LogWindow(Logger.getDefaultLogSource());
+ this.gameWindow = new GameWindow();
+ this.robotPositionWindow = new RobotPositionWindow(Logger.getrobotPositionModel());
+ }
+
+ public void createFrame() {
+ frame = new MainApplicationFrame(logWindow, gameWindow, this, robotPositionWindow);
+ frame.pack();
+ frame.setVisible(true);
+ frame.setExtendedState(Frame.MAXIMIZED_BOTH);
+ }
+
+ public void onAddLogMessage() {
+ Logger.debug("Новая строка");
+ }
+
+ public void handleExit() {
+ if (frame != null)
+ frame.saveWindowStates();
+ System.exit(0);
+ }
+
+}
diff --git a/src/main/java/robots/gui/NewSnakeWindow.java b/src/main/java/robots/gui/NewSnakeWindow.java
new file mode 100644
index 000000000..3fd42ddfa
--- /dev/null
+++ b/src/main/java/robots/gui/NewSnakeWindow.java
@@ -0,0 +1,15 @@
+package robots.gui;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class NewSnakeWindow extends JInternalFrame {
+ public NewSnakeWindow() {
+ super("Новая змейка", true, true, true, true);
+ setSize(500, 500);
+ setLocation(100, 100);
+ setLayout(new BorderLayout());
+ add(new SnakePanel(), BorderLayout.CENTER);
+ pack();
+ }
+}
diff --git a/src/main/java/robots/gui/RobotPositionWindow.java b/src/main/java/robots/gui/RobotPositionWindow.java
new file mode 100644
index 000000000..d81944499
--- /dev/null
+++ b/src/main/java/robots/gui/RobotPositionWindow.java
@@ -0,0 +1,23 @@
+package robots.gui;
+
+import robots.log.RobotPositionListener;
+import robots.log.RobotPositionSource;
+
+import javax.swing.*;
+
+public class RobotPositionWindow extends JInternalFrame implements RobotPositionListener {
+ private final JLabel label;
+
+ public RobotPositionWindow(RobotPositionSource source) {
+ super("Координаты робота", true, true, true, true);
+ label = new JLabel("x: 0, y: 0");
+ getContentPane().add(label);
+ pack();
+ source.registerListener(this);
+ }
+
+ @Override
+ public void onPositionChanged(double x, double y) {
+ SwingUtilities.invokeLater(() -> label.setText("x: " + (int)x + ", y: " + (int)y));
+ }
+}
diff --git a/src/main/java/robots/gui/RobotsProgram.java b/src/main/java/robots/gui/RobotsProgram.java
new file mode 100644
index 000000000..956bd789d
--- /dev/null
+++ b/src/main/java/robots/gui/RobotsProgram.java
@@ -0,0 +1,25 @@
+package robots.gui;
+
+import robots.log.Logger;
+
+import javax.swing.SwingUtilities;
+import javax.swing.UIManager;
+import java.util.Locale;
+
+public class RobotsProgram
+{
+ public static void main(String[] args) {
+ Locale.setDefault(new Locale("ru", "RU"));
+
+ try {
+ UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel");
+ } catch (Exception e) {
+ Logger.error("Ошибка при установке LookAndFeel: " + e.getMessage());
+ }
+
+ SwingUtilities.invokeLater(() -> {
+ new MainController().createFrame();
+ });
+
+ }
+}
diff --git a/src/main/java/robots/gui/SnakePanel.java b/src/main/java/robots/gui/SnakePanel.java
new file mode 100644
index 000000000..8a7510555
--- /dev/null
+++ b/src/main/java/robots/gui/SnakePanel.java
@@ -0,0 +1,162 @@
+package robots.gui;
+
+import robots.log.Logger;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class SnakePanel extends JPanel {
+ private static final double LEADER_SPEED = 1.5;
+ private static final double FOLLOWER_SPEED = 0.8; // медленнее
+
+ private int lives = 3;
+ private static final int MAX_LIVES = 5;
+ private static final double CATCH_DISTANCE = 10.0; // если ближе — атака
+
+ private double snakeX = 100;
+ private double snakeY = 100;
+ private double direction = 0;
+
+ private double followerX = 50;
+ private double followerY = 50;
+ private double followerDirection = 0;
+
+ private int targetX = 150;
+ private int targetY = 100;
+
+ private static final double SPEED = 1.5;
+ private static final double ROTATION_SPEED = 0.05;
+
+ public SnakePanel() {
+ setBackground(Color.WHITE);
+
+ addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ targetX = e.getX();
+ targetY = e.getY();
+ SwingUtilities.invokeLater(() -> {
+ repaint();
+ });
+
+ }
+ });
+
+ Timer timer = new Timer(true);
+ timer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ updateModel();
+ SwingUtilities.invokeLater(() -> {
+ repaint();
+ }); }
+ }, 0, 10);
+ }
+
+ private void updateModel() {
+ // === ОСНОВНАЯ ЗМЕЯ ===
+ double angleToTarget = Math.atan2(targetY - snakeY, targetX - snakeX);
+ double angleDiff = normalizeAngle(angleToTarget - direction);
+
+ if (Math.abs(angleDiff) > ROTATION_SPEED) {
+ direction += Math.signum(angleDiff) * ROTATION_SPEED;
+ } else {
+ direction = angleToTarget;
+ }
+
+ double dx = LEADER_SPEED * Math.cos(direction);
+ double dy = LEADER_SPEED * Math.sin(direction);
+
+ if (distance(snakeX, snakeY, targetX, targetY) > SPEED) {
+ snakeX += dx;
+ snakeY += dy;
+ }
+
+ Logger.getrobotPositionModel().setPosition(snakeX, snakeY);
+
+ // === ВТОРАЯ ЗМЕЯ: ПРЕСЛЕДОВАТЕЛЬ ===
+ double angleToLeader = Math.atan2(snakeY - followerY, snakeX - followerX);
+ double followerAngleDiff = normalizeAngle(angleToLeader - followerDirection);
+
+ if (Math.abs(followerAngleDiff) > ROTATION_SPEED) {
+ followerDirection += Math.signum(followerAngleDiff) * ROTATION_SPEED;
+ } else {
+ followerDirection = angleToLeader;
+ }
+
+ double followerDx = FOLLOWER_SPEED * Math.cos(followerDirection);
+ double followerDy = FOLLOWER_SPEED * Math.sin(followerDirection);
+
+ if (distance(followerX, followerY, snakeX, snakeY) > SPEED) {
+ followerX += followerDx;
+ followerY += followerDy;
+ }
+
+ // если змея добралась до цели
+ if (distance(snakeX, snakeY, targetX, targetY) <= LEADER_SPEED) {
+ if (lives < MAX_LIVES) lives++;
+ // можно обновить
+ }
+
+ if (distance(followerX, followerY, snakeX, snakeY) < CATCH_DISTANCE) {
+ if (lives > 0) lives--;
+ }
+
+ }
+
+ private double normalizeAngle(double angle) {
+ while (angle < -Math.PI) angle += 2 * Math.PI;
+ while (angle > Math.PI) angle -= 2 * Math.PI;
+ return angle;
+ }
+
+ private double distance(double x1, double y1, double x2, double y2) {
+ return Math.hypot(x1 - x2, y1 - y2);
+ }
+
+ @Override
+ protected void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ drawSnake((Graphics2D) g, snakeX, snakeY, direction, Color.MAGENTA);
+ drawSnake((Graphics2D) g, followerX, followerY, followerDirection, Color.BLUE);
+ drawTarget((Graphics2D) g);
+
+ drawLives((Graphics2D) g);
+ }
+
+ private void drawLives(Graphics2D g) {
+ g.setColor(Color.RED);
+ g.setFont(new Font("Arial", Font.BOLD, 16));
+ g.drawString("Lives: " + "❤️".repeat(lives), 10, 20);
+ }
+
+
+ private void drawSnake(Graphics2D g, double x, double y, double dir, Color color) {
+ int bodyLength = 30;
+ int bodyWidth = 10;
+
+ AffineTransform old = g.getTransform();
+ g.rotate(dir, x, y);
+
+ g.setColor(color);
+ g.fillOval((int)(x - bodyLength / 2), (int)(y - bodyWidth / 2), bodyLength, bodyWidth);
+ g.setColor(Color.BLACK);
+ g.drawOval((int)(x - bodyLength / 2), (int)(y - bodyWidth / 2), bodyLength, bodyWidth);
+
+ g.setTransform(old);
+ }
+
+
+ private void drawTarget(Graphics2D g) {
+ g.setColor(Color.GREEN);
+ g.fillOval(targetX - 4, targetY - 4, 8, 8);
+ g.setColor(Color.BLACK);
+ g.drawOval(targetX - 4, targetY - 4, 8, 8);
+ }
+
+}
diff --git a/src/main/java/robots/gui/WindowConfig.java b/src/main/java/robots/gui/WindowConfig.java
new file mode 100644
index 000000000..2f95f25ef
--- /dev/null
+++ b/src/main/java/robots/gui/WindowConfig.java
@@ -0,0 +1,9 @@
+package robots.gui;
+
+import java.io.File;
+
+public class WindowConfig {
+ public static File getConfigFile() {
+ return new File(System.getProperty("user.home"), ".robot-windows.config");
+ }
+}
diff --git a/src/main/java/robots/log/LogChangeListener.java b/src/main/java/robots/log/LogChangeListener.java
new file mode 100644
index 000000000..26ddea5cb
--- /dev/null
+++ b/src/main/java/robots/log/LogChangeListener.java
@@ -0,0 +1,6 @@
+package robots.log;
+
+public interface LogChangeListener
+{
+ void onLogChanged();
+}
diff --git a/robots/src/log/LogEntry.java b/src/main/java/robots/log/LogEntry.java
similarity index 95%
rename from robots/src/log/LogEntry.java
rename to src/main/java/robots/log/LogEntry.java
index 3d9147107..ab539b24d 100644
--- a/robots/src/log/LogEntry.java
+++ b/src/main/java/robots/log/LogEntry.java
@@ -1,4 +1,4 @@
-package log;
+package robots.log;
public class LogEntry
{
diff --git a/robots/src/log/LogLevel.java b/src/main/java/robots/log/LogLevel.java
similarity index 93%
rename from robots/src/log/LogLevel.java
rename to src/main/java/robots/log/LogLevel.java
index 582d010cc..898e8dbfb 100644
--- a/robots/src/log/LogLevel.java
+++ b/src/main/java/robots/log/LogLevel.java
@@ -1,4 +1,4 @@
-package log;
+package robots.log;
public enum LogLevel
{
diff --git a/src/main/java/robots/log/LogWindowSource.java b/src/main/java/robots/log/LogWindowSource.java
new file mode 100644
index 000000000..40691e9f0
--- /dev/null
+++ b/src/main/java/robots/log/LogWindowSource.java
@@ -0,0 +1,83 @@
+package robots.log;
+
+import java.lang.ref.WeakReference;
+import java.util.*;
+
+public class LogWindowSource {
+ private final int m_iQueueLength;
+ private final Deque m_messages;
+ private final List> m_listeners = new ArrayList<>();
+ private volatile LogChangeListener[] m_activeListeners;
+
+ public LogWindowSource(int iQueueLength) {
+ this.m_iQueueLength = iQueueLength;
+ this.m_messages = new ArrayDeque<>(iQueueLength);
+ }
+
+ public void registerListener(LogChangeListener listener) {
+ synchronized (m_listeners) {
+ m_listeners.add(new WeakReference<>(listener));
+ m_activeListeners = null;
+ }
+ }
+
+ public void unregisterListener(LogChangeListener listener) {
+ synchronized (m_listeners) {
+ m_listeners.removeIf(ref -> {
+ LogChangeListener l = ref.get();
+ return l == null || l == listener;
+ });
+ m_activeListeners = null;
+ }
+ }
+
+ public void append(LogLevel logLevel, String strMessage) {
+ LogEntry entry = new LogEntry(logLevel, strMessage);
+ synchronized (m_messages) {
+ if (m_messages.size() >= m_iQueueLength) {
+ m_messages.pollFirst();
+ }
+ m_messages.addLast(entry);
+ }
+
+ LogChangeListener[] activeListeners = m_activeListeners;
+ if (activeListeners == null) {
+ synchronized (m_listeners) {
+ List snapshot = new ArrayList<>();
+ for (WeakReference ref : m_listeners) {
+ LogChangeListener l = ref.get();
+ if (l != null) {
+ snapshot.add(l);
+ }
+ }
+ activeListeners = snapshot.toArray(new LogChangeListener[0]);
+ m_activeListeners = activeListeners;
+ }
+ }
+
+ for (LogChangeListener listener : activeListeners) {
+ listener.onLogChanged();
+ }
+ }
+
+ public int size() {
+ return m_messages.size();
+ }
+
+ public Iterable range(int startFrom, int count) {
+ synchronized (m_messages) {
+ if (startFrom < 0 || startFrom >= m_messages.size()) {
+ return Collections.emptyList();
+ }
+ List list = new ArrayList<>(m_messages);
+ int indexTo = Math.min(startFrom + count, list.size());
+ return list.subList(startFrom, indexTo);
+ }
+ }
+
+ public Iterable all() {
+ synchronized (m_messages) {
+ return new ArrayList<>(m_messages);
+ }
+ }
+}
diff --git a/src/main/java/robots/log/Logger.java b/src/main/java/robots/log/Logger.java
new file mode 100644
index 000000000..5636d11a2
--- /dev/null
+++ b/src/main/java/robots/log/Logger.java
@@ -0,0 +1,20 @@
+package robots.log;
+
+public final class Logger {
+ private static final LogWindowSource defaultLogSource = new LogWindowSource(100);
+
+ private Logger() {
+ }
+
+ public static void debug(String strMessage) {
+ defaultLogSource.append(LogLevel.Debug, strMessage);
+ }
+
+ public static void error(String strMessage) {
+ defaultLogSource.append(LogLevel.Error, strMessage);
+ }
+
+ public static LogWindowSource getDefaultLogSource() {
+ return defaultLogSource;
+ }
+}
diff --git a/src/main/java/robots/log/RobotModel.java b/src/main/java/robots/log/RobotModel.java
new file mode 100644
index 000000000..9950bf463
--- /dev/null
+++ b/src/main/java/robots/log/RobotModel.java
@@ -0,0 +1,11 @@
+package robots.log;
+
+public final class RobotModel {
+ private static final RobotPositionSource robotPositionModel = new RobotPositionSource();
+
+ private RobotModel() {}
+
+ public static RobotPositionSource getRobotPositionModel() {
+ return robotPositionModel;
+ }
+}
diff --git a/src/main/java/robots/log/RobotPositionListener.java b/src/main/java/robots/log/RobotPositionListener.java
new file mode 100644
index 000000000..00b7caf84
--- /dev/null
+++ b/src/main/java/robots/log/RobotPositionListener.java
@@ -0,0 +1,6 @@
+package robots.log;
+
+public interface RobotPositionListener {
+ void onPositionChanged(double x, double y);
+
+}
diff --git a/src/main/java/robots/log/RobotPositionSource.java b/src/main/java/robots/log/RobotPositionSource.java
new file mode 100644
index 000000000..0e5a56f65
--- /dev/null
+++ b/src/main/java/robots/log/RobotPositionSource.java
@@ -0,0 +1,56 @@
+package robots.log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+public class RobotPositionSource {
+ private final ArrayList listeners = new ArrayList<>();
+ private volatile RobotPositionListener[] activeListeners;
+
+ private double x = 0;
+ private double y = 0;
+
+ public synchronized void setPosition(double x, double y) {
+ this.x = x;
+ this.y = y;
+ notifyListeners();
+ }
+
+ public synchronized double getX() {
+ return x;
+ }
+
+ public synchronized double getY() {
+ return y;
+ }
+
+ public void registerListener(RobotPositionListener listener) {
+ synchronized (listeners) {
+ listeners.add(listener);
+ activeListeners = null;
+ }
+ }
+
+ public void unregisterListener(RobotPositionListener listener) {
+ synchronized (listeners) {
+ listeners.remove(listener);
+ activeListeners = null;
+ }
+ }
+
+ private void notifyListeners() {
+ RobotPositionListener[] currentListeners = activeListeners;
+ if (currentListeners == null) {
+ synchronized (listeners) {
+ if (activeListeners == null) {
+ activeListeners = listeners.toArray(new RobotPositionListener[0]);
+ currentListeners = activeListeners;
+ }
+ }
+ }
+
+ for (RobotPositionListener listener : currentListeners) {
+ listener.onPositionChanged(x, y);
+ }
+ }
+}