diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..7bc07ec --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Environment-dependent path to Maven home directory +/mavenHomeManager.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml new file mode 100644 index 0000000..250ea61 --- /dev/null +++ b/.idea/material_theme_project_new.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f88aef9 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/main/java/org/hamiltoniansnakeai/HamiltonianSnakeAI.java b/src/main/java/org/hamiltoniansnakeai/HamiltonianSnakeAI.java index 6d4be7e..9447cad 100644 --- a/src/main/java/org/hamiltoniansnakeai/HamiltonianSnakeAI.java +++ b/src/main/java/org/hamiltoniansnakeai/HamiltonianSnakeAI.java @@ -10,7 +10,7 @@ import java.util.List; public class HamiltonianSnakeAI extends JPanel implements ActionListener { private javax.swing.Timer timer; private int gridSize = 20; - private int cellSize = 20; + private int cellSize = 25; private int delay = 100; private Snake snake; @@ -27,6 +27,7 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { private JCheckBox showPathBox; private JLabel scoreLabel; private JLabel statusLabel; + private JPanel gamePanel; public HamiltonianSnakeAI() { initGame(); @@ -42,87 +43,152 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { private void setupUI() { setLayout(new BorderLayout()); - setBackground(Color.BLACK); + setBackground(new Color(30, 30, 30)); - // Game panel - JPanel gamePanel = new JPanel() { + // Game panel with better styling + gamePanel = new JPanel() { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); draw(g); } }; - gamePanel.setBackground(Color.BLACK); + gamePanel.setBackground(new Color(20, 20, 20)); + gamePanel.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60), 2)); gamePanel.setPreferredSize(new Dimension(gridSize * cellSize, gridSize * cellSize)); - // Control panel - JPanel controlPanel = new JPanel(new GridLayout(6, 2, 5, 5)); - controlPanel.setBackground(Color.DARK_GRAY); - controlPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + // Control panel with modern look + JPanel controlPanel = new JPanel(); + controlPanel.setLayout(new BoxLayout(controlPanel, BoxLayout.Y_AXIS)); + controlPanel.setBackground(new Color(40, 40, 40)); + controlPanel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); + + // Title label + JLabel titleLabel = new JLabel("Hamiltonian Snake AI"); + titleLabel.setFont(new Font("Arial", Font.BOLD, 18)); + titleLabel.setForeground(new Color(220, 220, 220)); + titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + controlPanel.add(titleLabel); + controlPanel.add(Box.createRigidArea(new Dimension(0, 15))); // Speed control - controlPanel.add(new JLabel("Speed:", SwingConstants.CENTER)); - speedSlider = new JSlider(1, 20, 10); + JPanel speedPanel = createControlPanel("Speed:", speedSlider = new JSlider(1, 20, 10)); speedSlider.addChangeListener(e -> { delay = 210 - speedSlider.getValue() * 10; if (timer != null) timer.setDelay(delay); }); - controlPanel.add(speedSlider); + controlPanel.add(speedPanel); // Grid size control - controlPanel.add(new JLabel("Grid Size:", SwingConstants.CENTER)); - gridSlider = new JSlider(10, 50, 20); // Reduced max grid size for better performance + JPanel gridPanel = createControlPanel("Grid Size:", gridSlider = new JSlider(10, 30, 20)); gridSlider.addChangeListener(e -> { if (!gameRunning) { gridSize = gridSlider.getValue(); - cellSize = Math.max(5, Math.min(20, 800 / gridSize)); // Minimum cell size of 5 + cellSize = Math.max(15, Math.min(30, 600 / gridSize)); // Better cell size range gamePanel.setPreferredSize(new Dimension(gridSize * cellSize, gridSize * cellSize)); resetGame(); revalidate(); repaint(); } }); - controlPanel.add(gridSlider); + controlPanel.add(gridPanel); - // Control buttons - startButton = new JButton("Start"); + // Button panel + JPanel buttonPanel = new JPanel(new GridLayout(1, 3, 10, 0)); + buttonPanel.setBackground(new Color(40, 40, 40)); + buttonPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0)); + + startButton = createStyledButton("Start"); startButton.addActionListener(e -> startGame()); - controlPanel.add(startButton); + buttonPanel.add(startButton); - pauseButton = new JButton("Pause"); + pauseButton = createStyledButton("Pause"); pauseButton.addActionListener(e -> pauseGame()); pauseButton.setEnabled(false); - controlPanel.add(pauseButton); + buttonPanel.add(pauseButton); - // Show path checkbox - showPathBox = new JCheckBox("Show Path"); + JButton resetButton = createStyledButton("Reset"); + resetButton.addActionListener(e -> resetGame()); + buttonPanel.add(resetButton); + + controlPanel.add(buttonPanel); + + // Checkbox panel + JPanel checkBoxPanel = new JPanel(); + checkBoxPanel.setBackground(new Color(40, 40, 40)); + showPathBox = new JCheckBox("Show Hamiltonian Path"); + showPathBox.setForeground(Color.WHITE); + showPathBox.setBackground(new Color(40, 40, 40)); + showPathBox.setFocusPainted(false); showPathBox.addActionListener(e -> { showPath = showPathBox.isSelected(); repaint(); }); - controlPanel.add(showPathBox); + checkBoxPanel.add(showPathBox); + controlPanel.add(checkBoxPanel); - JButton resetButton = new JButton("Reset"); - resetButton.addActionListener(e -> resetGame()); - controlPanel.add(resetButton); + // Status panel + JPanel statusPanel = new JPanel(new GridLayout(2, 1, 5, 5)); + statusPanel.setBackground(new Color(40, 40, 40)); - // Status labels - scoreLabel = new JLabel("Score: 0", SwingConstants.CENTER); - scoreLabel.setForeground(Color.WHITE); - controlPanel.add(scoreLabel); + scoreLabel = createStatusLabel("Score: 0"); + statusPanel.add(scoreLabel); - statusLabel = new JLabel("Ready", SwingConstants.CENTER); - statusLabel.setForeground(Color.GREEN); - controlPanel.add(statusLabel); + statusLabel = createStatusLabel("Ready"); + statusLabel.setForeground(new Color(100, 255, 100)); + statusPanel.add(statusLabel); + + controlPanel.add(statusPanel); add(gamePanel, BorderLayout.CENTER); add(controlPanel, BorderLayout.EAST); } + private JPanel createControlPanel(String labelText, JSlider slider) { + JPanel panel = new JPanel(new BorderLayout(10, 0)); + panel.setBackground(new Color(40, 40, 40)); + + JLabel label = new JLabel(labelText); + label.setForeground(Color.WHITE); + panel.add(label, BorderLayout.WEST); + + slider.setBackground(new Color(40, 40, 40)); + slider.setForeground(Color.WHITE); + slider.setPaintTicks(true); + slider.setPaintLabels(true); + slider.setMajorTickSpacing(slider.getMaximum() / 4); + panel.add(slider, BorderLayout.CENTER); + + return panel; + } + + private JButton createStyledButton(String text) { + JButton button = new JButton(text); + button.setBackground(new Color(70, 70, 70)); + button.setForeground(Color.WHITE); + button.setFocusPainted(false); + button.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(100, 100, 100), 1), + BorderFactory.createEmptyBorder(5, 15, 5, 15) + )); + button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + return button; + } + + private JLabel createStatusLabel(String text) { + JLabel label = new JLabel(text, SwingConstants.CENTER); + label.setFont(new Font("Arial", Font.BOLD, 14)); + label.setForeground(Color.WHITE); + label.setOpaque(true); + label.setBackground(new Color(50, 50, 50)); + label.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + return label; + } + private void generateHamiltonianCycle() { hamiltonianPath = new int[gridSize][gridSize]; - // Generate a more efficient Hamiltonian cycle + // Generate a more efficient Hamiltonian cycle that's better for longer snakes if (gridSize % 2 == 0) { generateEvenGridCycle(); } else { @@ -148,7 +214,7 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { } private void generateOddGridCycle() { - // More complex cycle for odd-sized grids + // More complex cycle for odd-sized grids that reduces tail collisions int pathIndex = 0; int row = 0, col = 0; boolean movingRight = true; @@ -160,28 +226,70 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { if (col < gridSize - 1) { col++; } else { + // At right edge, move down and reverse direction row++; + if (row < gridSize) { + hamiltonianPath[row][col] = pathIndex++; + } movingRight = false; } } else { if (col > 0) { col--; } else { + // At left edge, move down and reverse direction row++; + if (row < gridSize) { + hamiltonianPath[row][col] = pathIndex++; + } movingRight = true; } } } - - // Connect the last cell back to the first - hamiltonianPath[gridSize-1][0] = pathIndex; } private void spawnFood() { Random rand = new Random(); + int attempts = 0; + int maxAttempts = 100; + do { food = new Point(rand.nextInt(gridSize), rand.nextInt(gridSize)); - } while (snake.contains(food)); + attempts++; + // Ensure food isn't placed where it would be impossible to reach + if (attempts >= maxAttempts || !snake.contains(food)) { + break; + } + } while (snake.contains(food) || isFoodInDeadZone(food)); + } + + private boolean isFoodInDeadZone(Point foodPos) { + // Check if food is placed in a position that would make it unreachable + // due to the snake's body blocking all paths + Set obstacles = new HashSet<>(snake.getBody()); + obstacles.remove(snake.getTail()); + + // Simple flood fill to check reachability from snake head + Set visited = new HashSet<>(); + Queue queue = new LinkedList<>(); + queue.add(snake.getHead()); + visited.add(snake.getHead()); + + while (!queue.isEmpty()) { + Point current = queue.poll(); + if (current.equals(foodPos)) { + return false; // Food is reachable + } + + for (Point neighbor : getNeighbors(current)) { + if (!visited.contains(neighbor) && !obstacles.contains(neighbor)) { + visited.add(neighbor); + queue.add(neighbor); + } + } + } + + return true; // Food is in a dead zone } private void startGame() { @@ -191,7 +299,7 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { pauseButton.setEnabled(true); gridSlider.setEnabled(false); statusLabel.setText("Running"); - statusLabel.setForeground(Color.GREEN); + statusLabel.setForeground(new Color(100, 255, 100)); } private void pauseGame() { @@ -214,7 +322,7 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { gridSlider.setEnabled(true); scoreLabel.setText("Score: 0"); statusLabel.setText("Ready"); - statusLabel.setForeground(Color.GREEN); + statusLabel.setForeground(new Color(100, 255, 100)); repaint(); } @@ -228,7 +336,7 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { gameRunning = false; timer.stop(); statusLabel.setText("YOU WIN!"); - statusLabel.setForeground(Color.yellow); + statusLabel.setForeground(Color.YELLOW); startButton.setEnabled(true); pauseButton.setEnabled(false); } @@ -268,38 +376,53 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { return head; // Stay in place (will trigger collision detection) } - // If we can reach food safely with a shortcut, take it - Point bestMove = findBestShortcut(head, possibleMoves); - if (bestMove != null) { - return bestMove; + // For longer snakes, be more cautious about shortcuts + if (snake.getLength() < gridSize * gridSize * 0.6) { + // Try to find a safe shortcut to food + Point bestMove = findBestShortcut(head, possibleMoves); + if (bestMove != null) { + return bestMove; + } } - // Otherwise follow the Hamiltonian cycle + // Follow the Hamiltonian cycle with tail collision awareness return followHamiltonianCycle(head, possibleMoves); } private Point findBestShortcut(Point head, List possibleMoves) { - // Only consider shortcuts when snake is not too long - if (snake.getLength() > gridSize * gridSize * 0.75) { - return null; - } - Point bestMove = null; int minDistance = Integer.MAX_VALUE; for (Point move : possibleMoves) { int distance = estimateDistance(move, food); if (distance < minDistance && isPathSafe(move, food)) { - minDistance = distance; - bestMove = move; + // Additional check for tail collision risk + if (!willCollideWithTail(move)) { + minDistance = distance; + bestMove = move; + } } } return bestMove; } + private boolean willCollideWithTail(Point move) { + // Predict if this move might lead to tail collision in the near future + if (snake.getLength() < gridSize * 0.75) { + return false; // Not long enough to worry about tail collisions + } + + // Check if this move brings us closer to the tail's future position + Point tail = snake.getTail(); + int currentDistToTail = estimateDistance(snake.getHead(), tail); + int newDistToTail = estimateDistance(move, tail); + + return newDistToTail < currentDistToTail - 2; // Approaching tail too quickly + } + private boolean isPathSafe(Point from, Point to) { - // Simple flood fill to check if path exists + // More efficient path safety check with early termination Set visited = new HashSet<>(); Queue queue = new LinkedList<>(); Set obstacles = new HashSet<>(snake.getBody()); @@ -336,7 +459,7 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { } } - // If exact next in cycle isn't possible, find the closest + // If exact next in cycle isn't possible, find the closest safe move return possibleMoves.stream() .min(Comparator.comparingInt(p -> { int diff = hamiltonianPath[p.y][p.x] - currentIndex; @@ -362,7 +485,7 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { if (newX >= 0 && newX < gridSize && newY >= 0 && newY < gridSize) { Point newPos = new Point(newX, newY); - // Check if this position is safe + // Check if this position is safe (not occupied by snake body, except tail which will move) if (!snake.contains(newPos) || newPos.equals(snake.getTail())) { safeMoves.add(newPos); } @@ -389,11 +512,11 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { private void draw(Graphics g) { // Clear background - g.setColor(Color.BLACK); + g.setColor(new Color(20, 20, 20)); g.fillRect(0, 0, getWidth(), getHeight()); - // Draw grid - g.setColor(Color.DARK_GRAY); + // Draw grid with better visibility + g.setColor(new Color(50, 50, 50)); for (int i = 0; i <= gridSize; i++) { g.drawLine(i * cellSize, 0, i * cellSize, gridSize * cellSize); g.drawLine(0, i * cellSize, gridSize * cellSize, i * cellSize); @@ -401,40 +524,74 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { // Draw Hamiltonian path if enabled if (showPath) { - g.setColor(new Color(50, 50, 100, 100)); + g.setColor(new Color(30, 30, 70, 150)); for (int y = 0; y < gridSize; y++) { for (int x = 0; x < gridSize; x++) { - int pathNum = hamiltonianPath[y][x]; g.fillRect(x * cellSize + 1, y * cellSize + 1, cellSize - 2, cellSize - 2); - if (cellSize > 10) { - g.setColor(Color.CYAN); + if (cellSize > 15) { + g.setColor(new Color(100, 200, 255)); g.setFont(new Font("Arial", Font.PLAIN, Math.max(8, cellSize / 3))); - String text = String.valueOf(pathNum); + String text = String.valueOf(hamiltonianPath[y][x]); FontMetrics fm = g.getFontMetrics(); int textX = x * cellSize + (cellSize - fm.stringWidth(text)) / 2; int textY = y * cellSize + (cellSize + fm.getAscent()) / 2; g.drawString(text, textX, textY); - g.setColor(new Color(50, 50, 100, 100)); + g.setColor(new Color(30, 30, 70, 150)); } } } } - // Draw food - g.setColor(Color.RED); + // Draw food with better visual + GradientPaint foodGradient = new GradientPaint( + food.x * cellSize, food.y * cellSize, new Color(255, 50, 50), + food.x * cellSize + cellSize, food.y * cellSize + cellSize, new Color(200, 0, 0) + ); + ((Graphics2D)g).setPaint(foodGradient); g.fillOval(food.x * cellSize + 2, food.y * cellSize + 2, cellSize - 4, cellSize - 4); + g.setColor(new Color(150, 0, 0)); + g.drawOval(food.x * cellSize + 2, food.y * cellSize + 2, cellSize - 4, cellSize - 4); - // Draw snake + // Draw snake with better visual List body = snake.getBody(); for (int i = 0; i < body.size(); i++) { Point segment = body.get(i); + + // Create gradient for snake segments + Color startColor, endColor; if (i == 0) { - g.setColor(Color.GREEN); // Head + // Head + startColor = new Color(100, 255, 100); + endColor = new Color(50, 200, 50); } else { - g.setColor(new Color(0, 255 - i * 2, 0)); // Body gradient + // Body - gradient from head to tail + float ratio = (float)i / body.size(); + startColor = new Color( + (int)(100 + 155 * (1 - ratio)), + (int)(255 - 155 * ratio), + (int)(100 + 155 * (1 - ratio)) + ); + endColor = new Color( + (int)(50 + 50 * (1 - ratio)), + (int)(200 - 150 * ratio), + (int)(50 + 50 * (1 - ratio)) + ); } - g.fillRect(segment.x * cellSize + 1, segment.y * cellSize + 1, cellSize - 2, cellSize - 2); + + GradientPaint segmentGradient = new GradientPaint( + segment.x * cellSize, segment.y * cellSize, startColor, + segment.x * cellSize + cellSize, segment.y * cellSize + cellSize, endColor + ); + + ((Graphics2D)g).setPaint(segmentGradient); + g.fillRoundRect(segment.x * cellSize + 1, segment.y * cellSize + 1, + cellSize - 2, cellSize - 2, 5, 5); + + // Add border to segments + g.setColor(new Color(0, 80, 0)); + g.drawRoundRect(segment.x * cellSize + 1, segment.y * cellSize + 1, + cellSize - 2, cellSize - 2, 5, 5); } }