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); + } + } +}