From 31d90004a0f983cfd78806d9537b7c85e71814ed Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Wed, 4 Jun 2025 20:03:40 +0200 Subject: [PATCH] works --- .gitignore | 38 ++ .idea/encodings.xml | 7 + pom.xml | 17 + .../HamiltonianSnakeAI.java | 496 ++++++++++++++++++ 4 files changed, 558 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/encodings.xml create mode 100644 pom.xml create mode 100644 src/main/java/org/hamiltoniansnakeai/HamiltonianSnakeAI.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5401ee8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + org.hamiltoniansnakeai + HamiltonianSnakeAI + 1.0-SNAPSHOT + + + 24 + 24 + UTF-8 + + + \ No newline at end of file diff --git a/src/main/java/org/hamiltoniansnakeai/HamiltonianSnakeAI.java b/src/main/java/org/hamiltoniansnakeai/HamiltonianSnakeAI.java new file mode 100644 index 0000000..6d4be7e --- /dev/null +++ b/src/main/java/org/hamiltoniansnakeai/HamiltonianSnakeAI.java @@ -0,0 +1,496 @@ +package org.hamiltoniansnakeai; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.*; +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 delay = 100; + + private Snake snake; + private Point food; + private int[][] hamiltonianPath; + private boolean gameRunning = false; + private boolean showPath = false; + + // UI Components + private JSlider speedSlider; + private JSlider gridSlider; + private JButton startButton; + private JButton pauseButton; + private JCheckBox showPathBox; + private JLabel scoreLabel; + private JLabel statusLabel; + + public HamiltonianSnakeAI() { + initGame(); + setupUI(); + } + + private void initGame() { + snake = new Snake(gridSize); + generateHamiltonianCycle(); + spawnFood(); + timer = new javax.swing.Timer(delay, this); + } + + private void setupUI() { + setLayout(new BorderLayout()); + setBackground(Color.BLACK); + + // Game panel + JPanel gamePanel = new JPanel() { + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + draw(g); + } + }; + gamePanel.setBackground(Color.BLACK); + 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)); + + // Speed control + controlPanel.add(new JLabel("Speed:", SwingConstants.CENTER)); + speedSlider = new JSlider(1, 20, 10); + speedSlider.addChangeListener(e -> { + delay = 210 - speedSlider.getValue() * 10; + if (timer != null) timer.setDelay(delay); + }); + controlPanel.add(speedSlider); + + // Grid size control + controlPanel.add(new JLabel("Grid Size:", SwingConstants.CENTER)); + gridSlider = new JSlider(10, 50, 20); // Reduced max grid size for better performance + gridSlider.addChangeListener(e -> { + if (!gameRunning) { + gridSize = gridSlider.getValue(); + cellSize = Math.max(5, Math.min(20, 800 / gridSize)); // Minimum cell size of 5 + gamePanel.setPreferredSize(new Dimension(gridSize * cellSize, gridSize * cellSize)); + resetGame(); + revalidate(); + repaint(); + } + }); + controlPanel.add(gridSlider); + + // Control buttons + startButton = new JButton("Start"); + startButton.addActionListener(e -> startGame()); + controlPanel.add(startButton); + + pauseButton = new JButton("Pause"); + pauseButton.addActionListener(e -> pauseGame()); + pauseButton.setEnabled(false); + controlPanel.add(pauseButton); + + // Show path checkbox + showPathBox = new JCheckBox("Show Path"); + showPathBox.addActionListener(e -> { + showPath = showPathBox.isSelected(); + repaint(); + }); + controlPanel.add(showPathBox); + + JButton resetButton = new JButton("Reset"); + resetButton.addActionListener(e -> resetGame()); + controlPanel.add(resetButton); + + // Status labels + scoreLabel = new JLabel("Score: 0", SwingConstants.CENTER); + scoreLabel.setForeground(Color.WHITE); + controlPanel.add(scoreLabel); + + statusLabel = new JLabel("Ready", SwingConstants.CENTER); + statusLabel.setForeground(Color.GREEN); + controlPanel.add(statusLabel); + + add(gamePanel, BorderLayout.CENTER); + add(controlPanel, BorderLayout.EAST); + } + + private void generateHamiltonianCycle() { + hamiltonianPath = new int[gridSize][gridSize]; + + // Generate a more efficient Hamiltonian cycle + if (gridSize % 2 == 0) { + generateEvenGridCycle(); + } else { + generateOddGridCycle(); + } + } + + private void generateEvenGridCycle() { + int pathIndex = 0; + for (int row = 0; row < gridSize; row++) { + if (row % 2 == 0) { + // Left to right + for (int col = 0; col < gridSize; col++) { + hamiltonianPath[row][col] = pathIndex++; + } + } else { + // Right to left + for (int col = gridSize - 1; col >= 0; col--) { + hamiltonianPath[row][col] = pathIndex++; + } + } + } + } + + private void generateOddGridCycle() { + // More complex cycle for odd-sized grids + int pathIndex = 0; + int row = 0, col = 0; + boolean movingRight = true; + + while (row < gridSize) { + hamiltonianPath[row][col] = pathIndex++; + + if (movingRight) { + if (col < gridSize - 1) { + col++; + } else { + row++; + movingRight = false; + } + } else { + if (col > 0) { + col--; + } else { + row++; + movingRight = true; + } + } + } + + // Connect the last cell back to the first + hamiltonianPath[gridSize-1][0] = pathIndex; + } + + private void spawnFood() { + Random rand = new Random(); + do { + food = new Point(rand.nextInt(gridSize), rand.nextInt(gridSize)); + } while (snake.contains(food)); + } + + private void startGame() { + gameRunning = true; + timer.start(); + startButton.setEnabled(false); + pauseButton.setEnabled(true); + gridSlider.setEnabled(false); + statusLabel.setText("Running"); + statusLabel.setForeground(Color.GREEN); + } + + private void pauseGame() { + gameRunning = false; + timer.stop(); + startButton.setEnabled(true); + pauseButton.setEnabled(false); + statusLabel.setText("Paused"); + statusLabel.setForeground(Color.YELLOW); + } + + private void resetGame() { + timer.stop(); + gameRunning = false; + snake = new Snake(gridSize); + generateHamiltonianCycle(); + spawnFood(); + startButton.setEnabled(true); + pauseButton.setEnabled(false); + gridSlider.setEnabled(true); + scoreLabel.setText("Score: 0"); + statusLabel.setText("Ready"); + statusLabel.setForeground(Color.GREEN); + repaint(); + } + + @Override + public void actionPerformed(ActionEvent e) { + if (gameRunning) { + move(); + repaint(); + + if (snake.getLength() >= gridSize * gridSize) { + gameRunning = false; + timer.stop(); + statusLabel.setText("YOU WIN!"); + statusLabel.setForeground(Color.yellow); + startButton.setEnabled(true); + pauseButton.setEnabled(false); + } + } + } + + private void move() { + Point head = snake.getHead(); + Point nextMove = getNextMove(head); + + // Check for collision with body before moving + if (snake.contains(nextMove) && !nextMove.equals(snake.getTail())) { + gameRunning = false; + timer.stop(); + statusLabel.setText("GAME OVER - Collision!"); + statusLabel.setForeground(Color.RED); + startButton.setEnabled(true); + pauseButton.setEnabled(false); + gridSlider.setEnabled(true); + return; + } + + snake.move(nextMove); + + // Check if food eaten + if (snake.getHead().equals(food)) { + snake.grow(); + spawnFood(); + scoreLabel.setText("Score: " + (snake.getLength() - 1)); + } + } + + private Point getNextMove(Point head) { + List possibleMoves = getSafeMoves(head); + + if (possibleMoves.isEmpty()) { + 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; + } + + // Otherwise follow the Hamiltonian cycle + 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; + } + } + + return bestMove; + } + + private boolean isPathSafe(Point from, Point to) { + // Simple flood fill to check if path exists + Set visited = new HashSet<>(); + Queue queue = new LinkedList<>(); + Set obstacles = new HashSet<>(snake.getBody()); + obstacles.remove(snake.getTail()); // Tail will move + + queue.add(from); + visited.add(from); + + while (!queue.isEmpty()) { + Point current = queue.poll(); + if (current.equals(to)) { + return true; + } + + for (Point neighbor : getNeighbors(current)) { + if (!visited.contains(neighbor) && !obstacles.contains(neighbor)) { + visited.add(neighbor); + queue.add(neighbor); + } + } + } + + return false; + } + + private Point followHamiltonianCycle(Point head, List possibleMoves) { + int currentIndex = hamiltonianPath[head.y][head.x]; + int nextIndex = (currentIndex + 1) % (gridSize * gridSize); + + // Find the next point in the cycle that's a valid move + for (Point move : possibleMoves) { + if (hamiltonianPath[move.y][move.x] == nextIndex) { + return move; + } + } + + // If exact next in cycle isn't possible, find the closest + return possibleMoves.stream() + .min(Comparator.comparingInt(p -> { + int diff = hamiltonianPath[p.y][p.x] - currentIndex; + return diff > 0 ? diff : diff + gridSize * gridSize; + })) + .orElse(possibleMoves.get(0)); + } + + private int estimateDistance(Point a, Point b) { + // Manhattan distance + return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); + } + + private List getSafeMoves(Point head) { + List safeMoves = new ArrayList<>(); + int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // down, right, up, left + + for (int[] dir : directions) { + int newX = head.x + dir[0]; + int newY = head.y + dir[1]; + + // Check bounds + if (newX >= 0 && newX < gridSize && newY >= 0 && newY < gridSize) { + Point newPos = new Point(newX, newY); + + // Check if this position is safe + if (!snake.contains(newPos) || newPos.equals(snake.getTail())) { + safeMoves.add(newPos); + } + } + } + + return safeMoves; + } + + private List getNeighbors(Point p) { + List neighbors = new ArrayList<>(); + int[][] dirs = {{0,1}, {1,0}, {0,-1}, {-1,0}}; + + for (int[] dir : dirs) { + int nx = p.x + dir[0]; + int ny = p.y + dir[1]; + if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) { + neighbors.add(new Point(nx, ny)); + } + } + + return neighbors; + } + + private void draw(Graphics g) { + // Clear background + g.setColor(Color.BLACK); + g.fillRect(0, 0, getWidth(), getHeight()); + + // Draw grid + g.setColor(Color.DARK_GRAY); + 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); + } + + // Draw Hamiltonian path if enabled + if (showPath) { + g.setColor(new Color(50, 50, 100, 100)); + 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); + g.setFont(new Font("Arial", Font.PLAIN, Math.max(8, cellSize / 3))); + String text = String.valueOf(pathNum); + 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)); + } + } + } + } + + // Draw food + g.setColor(Color.RED); + g.fillOval(food.x * cellSize + 2, food.y * cellSize + 2, cellSize - 4, cellSize - 4); + + // Draw snake + List body = snake.getBody(); + for (int i = 0; i < body.size(); i++) { + Point segment = body.get(i); + if (i == 0) { + g.setColor(Color.GREEN); // Head + } else { + g.setColor(new Color(0, 255 - i * 2, 0)); // Body gradient + } + g.fillRect(segment.x * cellSize + 1, segment.y * cellSize + 1, cellSize - 2, cellSize - 2); + } + } + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + JFrame frame = new JFrame("Hamiltonian Snake AI"); + HamiltonianSnakeAI game = new HamiltonianSnakeAI(); + + frame.add(game); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setResizable(true); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + }); + } +} + +class Snake { + private LinkedList body; + private int gridSize; + + public Snake(int gridSize) { + this.gridSize = gridSize; + this.body = new LinkedList<>(); + // Start at top-left corner + this.body.add(new Point(0, 0)); + } + + public void move(Point newHead) { + body.addFirst(new Point(newHead.x, newHead.y)); + body.removeLast(); + } + + public void grow() { + Point tail = body.getLast(); + body.addLast(new Point(tail.x, tail.y)); + } + + public Point getHead() { + return body.getFirst(); + } + + public Point getTail() { + return body.getLast(); + } + + public List getBody() { + return new ArrayList<>(body); + } + + public int getLength() { + return body.size(); + } + + public boolean contains(Point point) { + return body.contains(point); + } +} \ No newline at end of file