diff --git a/src/main/java/org/hamiltoniansnakeai/HamiltonianSnakeAI.java b/src/main/java/org/hamiltoniansnakeai/HamiltonianSnakeAI.java index 9447cad..95656c2 100644 --- a/src/main/java/org/hamiltoniansnakeai/HamiltonianSnakeAI.java +++ b/src/main/java/org/hamiltoniansnakeai/HamiltonianSnakeAI.java @@ -18,6 +18,8 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { private int[][] hamiltonianPath; private boolean gameRunning = false; private boolean showPath = false; + private boolean showNumbers = false; + private int score = 0; // UI Components private JSlider speedSlider; @@ -25,11 +27,14 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { private JButton startButton; private JButton pauseButton; private JCheckBox showPathBox; + private JCheckBox showNumbersBox; private JLabel scoreLabel; private JLabel statusLabel; private JPanel gamePanel; + private JFrame parentFrame; - public HamiltonianSnakeAI() { + public HamiltonianSnakeAI(JFrame parentFrame) { + this.parentFrame = parentFrame; initGame(); setupUI(); } @@ -55,7 +60,7 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { }; 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)); + updateGamePanelSize(); // Control panel with modern look JPanel controlPanel = new JPanel(); @@ -80,15 +85,13 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { controlPanel.add(speedPanel); // Grid size control - JPanel gridPanel = createControlPanel("Grid Size:", gridSlider = new JSlider(10, 30, 20)); + JPanel gridPanel = createControlPanel("Grid Size:", gridSlider = new JSlider(10, 40, 20)); gridSlider.addChangeListener(e -> { if (!gameRunning) { gridSize = gridSlider.getValue(); - cellSize = Math.max(15, Math.min(30, 600 / gridSize)); // Better cell size range - gamePanel.setPreferredSize(new Dimension(gridSize * cellSize, gridSize * cellSize)); + updateGamePanelSize(); resetGame(); - revalidate(); - repaint(); + parentFrame.pack(); } }); controlPanel.add(gridPanel); @@ -114,8 +117,9 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { controlPanel.add(buttonPanel); // Checkbox panel - JPanel checkBoxPanel = new JPanel(); + JPanel checkBoxPanel = new JPanel(new GridLayout(2, 1, 5, 5)); checkBoxPanel.setBackground(new Color(40, 40, 40)); + showPathBox = new JCheckBox("Show Hamiltonian Path"); showPathBox.setForeground(Color.WHITE); showPathBox.setBackground(new Color(40, 40, 40)); @@ -125,6 +129,17 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { repaint(); }); checkBoxPanel.add(showPathBox); + + showNumbersBox = new JCheckBox("Show Path Numbers"); + showNumbersBox.setForeground(Color.WHITE); + showNumbersBox.setBackground(new Color(40, 40, 40)); + showNumbersBox.setFocusPainted(false); + showNumbersBox.addActionListener(e -> { + showNumbers = showNumbersBox.isSelected(); + repaint(); + }); + checkBoxPanel.add(showNumbersBox); + controlPanel.add(checkBoxPanel); // Status panel @@ -144,6 +159,11 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { add(controlPanel, BorderLayout.EAST); } + private void updateGamePanelSize() { + cellSize = Math.max(10, Math.min(30, 600 / gridSize)); + gamePanel.setPreferredSize(new Dimension(gridSize * cellSize, gridSize * cellSize)); + } + private JPanel createControlPanel(String labelText, JSlider slider) { JPanel panel = new JPanel(new BorderLayout(10, 0)); panel.setBackground(new Color(40, 40, 40)); @@ -188,15 +208,15 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { private void generateHamiltonianCycle() { hamiltonianPath = new int[gridSize][gridSize]; - // Generate a more efficient Hamiltonian cycle that's better for longer snakes + // Generate an optimized Hamiltonian cycle that minimizes the chance of self-collision if (gridSize % 2 == 0) { - generateEvenGridCycle(); + generateOptimizedEvenGridCycle(); } else { - generateOddGridCycle(); + generateOptimizedOddGridCycle(); } } - private void generateEvenGridCycle() { + private void generateOptimizedEvenGridCycle() { int pathIndex = 0; for (int row = 0; row < gridSize; row++) { if (row % 2 == 0) { @@ -205,87 +225,163 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { hamiltonianPath[row][col] = pathIndex++; } } else { - // Right to left - for (int col = gridSize - 1; col >= 0; col--) { - hamiltonianPath[row][col] = pathIndex++; + // Right to left with a small modification to reduce tail collisions + if (row == gridSize - 1) { + // Last row - special pattern to connect back to start + hamiltonianPath[row][gridSize - 1] = pathIndex++; + for (int col = gridSize - 2; col >= 1; col--) { + hamiltonianPath[row][col] = pathIndex++; + } + hamiltonianPath[row][0] = pathIndex++; + } else { + // Standard 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 that reduces tail collisions + private void generateOptimizedOddGridCycle() { + // More sophisticated cycle for odd grids that reduces dead ends int pathIndex = 0; int row = 0, col = 0; boolean movingRight = true; + boolean zigzag = true; while (row < gridSize) { hamiltonianPath[row][col] = pathIndex++; - if (movingRight) { - if (col < gridSize - 1) { - col++; - } else { - // At right edge, move down and reverse direction - row++; - if (row < gridSize) { - hamiltonianPath[row][col] = pathIndex++; + if (zigzag) { + if (movingRight) { + if (col < gridSize - 1) { + col++; + } else { + row++; + if (row < gridSize) { + hamiltonianPath[row][col] = pathIndex++; + } + movingRight = false; + } + } else { + if (col > 0) { + col--; + } else { + row++; + if (row < gridSize) { + hamiltonianPath[row][col] = pathIndex++; + } + movingRight = true; } - movingRight = false; } } else { - if (col > 0) { - col--; - } else { - // At left edge, move down and reverse direction - row++; - if (row < gridSize) { - hamiltonianPath[row][col] = pathIndex++; + // Alternate pattern for some rows to improve path efficiency + if (row % 2 == 0) { + if (col < gridSize - 1) { + col++; + } else { + row++; + if (row < gridSize) { + hamiltonianPath[row][col] = pathIndex++; + } + } + } else { + if (col > 0) { + col--; + } else { + row++; + if (row < gridSize) { + hamiltonianPath[row][col] = pathIndex++; + } } - movingRight = true; } } + + // Switch patterns periodically + if (row > 0 && row % 3 == 0) { + zigzag = !zigzag; + } } } private void spawnFood() { Random rand = new Random(); int attempts = 0; - int maxAttempts = 100; + int maxAttempts = gridSize * gridSize * 2; // More attempts for larger grids do { food = new Point(rand.nextInt(gridSize), rand.nextInt(gridSize)); attempts++; - // Ensure food isn't placed where it would be impossible to reach - if (attempts >= maxAttempts || !snake.contains(food)) { + if (attempts >= maxAttempts) { + // Emergency fallback - find any empty spot + for (int y = 0; y < gridSize; y++) { + for (int x = 0; x < gridSize; x++) { + Point p = new Point(x, y); + if (!snake.contains(p)) { + food = p; + return; + } + } + } + // If we get here, the board is full (game should be won) 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 + // Advanced reachability check using BFS with early termination 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()); + // Early exit if food is adjacent to head (always reachable) + if (estimateDistance(snake.getHead(), foodPos) == 1) { + return false; + } - while (!queue.isEmpty()) { - Point current = queue.poll(); - if (current.equals(foodPos)) { + // Use A* algorithm for more efficient pathfinding + PriorityQueue openSet = new PriorityQueue<>(); + Set closedSet = new HashSet<>(); + + openSet.add(new PathNode(snake.getHead(), 0, estimateDistance(snake.getHead(), foodPos))); + + while (!openSet.isEmpty()) { + PathNode current = openSet.poll(); + + if (current.position.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); + closedSet.add(current.position); + + for (Point neighbor : getNeighbors(current.position)) { + if (closedSet.contains(neighbor) || obstacles.contains(neighbor)) { + continue; } + + int tentativeG = current.g + 1; + boolean inOpenSet = false; + + for (PathNode node : openSet) { + if (node.position.equals(neighbor)) { + inOpenSet = true; + if (tentativeG < node.g) { + node.g = tentativeG; + node.f = node.g + node.h; + } + break; + } + } + + if (!inOpenSet) { + openSet.add(new PathNode(neighbor, tentativeG, estimateDistance(neighbor, foodPos))); + } + } + + // Early termination if we've searched enough + if (closedSet.size() > gridSize * gridSize / 2) { + break; } } @@ -315,6 +411,7 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { timer.stop(); gameRunning = false; snake = new Snake(gridSize); + score = 0; generateHamiltonianCycle(); spawnFood(); startButton.setEnabled(true); @@ -364,78 +461,278 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { // Check if food eaten if (snake.getHead().equals(food)) { snake.grow(); + score += (int)(10 * Math.sqrt(gridSize)); // Score based on grid size spawnFood(); - scoreLabel.setText("Score: " + (snake.getLength() - 1)); + scoreLabel.setText("Score: " + score); } } - private Point getNextMove(Point head) { List possibleMoves = getSafeMoves(head); if (possibleMoves.isEmpty()) { - return head; // Stay in place (will trigger collision detection) + return head; // No safe moves available (will trigger game over) } - // 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; + // 1. First priority: Don't die next turn + possibleMoves = filterImmediatelyDeadlyMoves(possibleMoves); + if (possibleMoves.isEmpty()) return head; + + // 2. Second priority: Don't create future traps + possibleMoves = filterFutureTraps(possibleMoves, 3); // Look 3 moves ahead + if (possibleMoves.isEmpty()) return head; + + // 3. If food is reachable and safe, go for it + Point foodPathMove = findSafePathToFood(head, possibleMoves); + if (foodPathMove != null) { + return foodPathMove; + } + + // 4. Follow Hamiltonian cycle with safety checks + return followSuperSafeHamiltonianCycle(head, possibleMoves); + } + + private List filterImmediatelyDeadlyMoves(List moves) { + List safeMoves = new ArrayList<>(); + for (Point move : moves) { + if (!willCollideImmediately(move)) { + safeMoves.add(move); + } + } + return safeMoves; + } + + private boolean willCollideImmediately(Point move) { + // Check if this move would collide with body (except tail) + return snake.contains(move) && !move.equals(snake.getTail()); + } + + private List filterFutureTraps(List moves, int lookAhead) { + if (snake.getLength() < gridSize * 0.6) return moves; // Not long enough to worry + + List safeMoves = new ArrayList<>(); + for (Point move : moves) { + if (!wouldCreateTrap(move, lookAhead)) { + safeMoves.add(move); + } + } + return safeMoves; + } + + private boolean wouldCreateTrap(Point move, int depth) { + // Simulate future moves to detect potential traps + List simulatedBody = new ArrayList<>(snake.getBody()); + simulatedBody.add(0, move); + simulatedBody.remove(simulatedBody.size() - 1); + + if (depth == 0) return false; + + // Check if this move reduces future options too much + int safeExits = countSafeExits(move, new HashSet<>(simulatedBody)); + if (safeExits == 0) return true; + + // Recursively check future moves + if (depth > 1) { + List nextMoves = getNeighbors(move); + nextMoves.removeAll(simulatedBody); + + for (Point nextMove : nextMoves) { + if (wouldCreateTrap(nextMove, depth - 1)) { + return true; + } } } - // Follow the Hamiltonian cycle with tail collision awareness - return followHamiltonianCycle(head, possibleMoves); + return false; } - private Point findBestShortcut(Point head, List possibleMoves) { + private int countSafeExits(Point position, Set obstacles) { + int count = 0; + for (Point neighbor : getNeighbors(position)) { + if (!obstacles.contains(neighbor)) { + count++; + } + } + return count; + } + + private Point findSafePathToFood(Point head, List possibleMoves) { + if (estimateDistance(head, food) > gridSize * 0.7) return null; + + // Find the safest path to food using flood fill + Map distanceMap = new HashMap<>(); + Queue queue = new LinkedList<>(); + Set obstacles = new HashSet<>(snake.getBody()); + obstacles.remove(snake.getTail()); + + queue.add(food); + distanceMap.put(food, 0); + + while (!queue.isEmpty()) { + Point current = queue.poll(); + + for (Point neighbor : getNeighbors(current)) { + if (!distanceMap.containsKey(neighbor) && !obstacles.contains(neighbor)) { + distanceMap.put(neighbor, distanceMap.get(current) + 1); + queue.add(neighbor); + } + } + } + + // Find the move with shortest path to food Point bestMove = null; int minDistance = Integer.MAX_VALUE; for (Point move : possibleMoves) { - int distance = estimateDistance(move, food); - if (distance < minDistance && isPathSafe(move, food)) { - // Additional check for tail collision risk - if (!willCollideWithTail(move)) { - minDistance = distance; - bestMove = move; - } + Integer dist = distanceMap.get(move); + if (dist != null && dist < minDistance) { + minDistance = dist; + 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 + private Point followSuperSafeHamiltonianCycle(Point head, List possibleMoves) { + int currentIndex = hamiltonianPath[head.y][head.x]; + int nextIndex = (currentIndex + 1) % (gridSize * gridSize); + + // Find the safest move along the cycle + Point bestMove = null; + int bestSafetyScore = -1; + + for (Point move : possibleMoves) { + int moveIndex = hamiltonianPath[move.y][move.x]; + int cycleProgress = (moveIndex - currentIndex + gridSize * gridSize) % (gridSize * gridSize); + + // Calculate safety score (higher is better) + int safetyScore = (gridSize * gridSize - cycleProgress) + countSafeExits(move, new HashSet<>(snake.getBody())); + + if (safetyScore > bestSafetyScore || + (safetyScore == bestSafetyScore && cycleProgress < (gridSize * gridSize) / 2)) { + bestSafetyScore = safetyScore; + bestMove = move; + } } - // 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 + return bestMove != null ? bestMove : possibleMoves.get(0); } - private boolean isPathSafe(Point from, Point to) { - // More efficient path safety check with early termination + private Point findOptimalPathToFood(Point head, List possibleMoves) { + // Use A* algorithm to find the best path to food + Map cameFrom = new HashMap<>(); + Map gScore = new HashMap<>(); + Map fScore = new HashMap<>(); + PriorityQueue openSet = new PriorityQueue<>( + Comparator.comparingInt(p -> fScore.getOrDefault(p, Integer.MAX_VALUE)) + ); + + Set obstacles = new HashSet<>(snake.getBody()); + obstacles.remove(snake.getTail()); + + for (Point move : possibleMoves) { + gScore.put(move, 1); + fScore.put(move, estimateDistance(move, food)); + cameFrom.put(move, head); + openSet.add(move); + } + + while (!openSet.isEmpty()) { + Point current = openSet.poll(); + + if (current.equals(food)) { + // Reconstruct path + Point bestMove = null; + while (cameFrom.get(current) != head) { + current = cameFrom.get(current); + } + bestMove = current; + + // Additional safety check + if (!willCauseImmediateDanger(bestMove)) { + return bestMove; + } + } + + for (Point neighbor : getNeighbors(current)) { + if (obstacles.contains(neighbor) && !neighbor.equals(snake.getTail())) { + continue; + } + + int tentativeG = gScore.getOrDefault(current, Integer.MAX_VALUE) + 1; + + if (tentativeG < gScore.getOrDefault(neighbor, Integer.MAX_VALUE)) { + cameFrom.put(neighbor, current); + gScore.put(neighbor, tentativeG); + fScore.put(neighbor, tentativeG + estimateDistance(neighbor, food)); + if (!openSet.contains(neighbor)) { + openSet.add(neighbor); + } + } + } + } + + return null; + } + + private Point avoidFutureCollisions(Point head, List possibleMoves) { + // Predict future snake positions and avoid moves that could lead to traps + if (snake.getLength() < gridSize * 0.6) { + return null; // Not long enough to worry about + } + + Point bestMove = null; + double bestScore = Double.NEGATIVE_INFINITY; + + for (Point move : possibleMoves) { + // Simulate the move + List simulatedBody = new ArrayList<>(snake.getBody()); + simulatedBody.add(0, move); + simulatedBody.remove(simulatedBody.size() - 1); + + // Calculate safety score + double score = calculateMoveSafety(move, simulatedBody); + + if (score > bestScore) { + bestScore = score; + bestMove = move; + } + } + + return bestMove; + } + + private double calculateMoveSafety(Point move, List simulatedBody) { + // Calculate how "safe" this move is based on several factors + double score = 0.0; + + // 1. Distance to food (closer is better) + score -= 0.5 * estimateDistance(move, food); + + // 2. Number of reachable cells after this move + score += 0.3 * countReachableCells(move, new HashSet<>(simulatedBody)); + + // 3. Distance to tail (farther is better) + score += 0.2 * estimateDistance(move, snake.getTail()); + + // 4. Progress along Hamiltonian cycle + int currentIndex = hamiltonianPath[snake.getHead().y][snake.getHead().x]; + int nextIndex = hamiltonianPath[move.y][move.x]; + int desiredNextIndex = (currentIndex + 1) % (gridSize * gridSize); + score += 0.1 * (1.0 - (double)Math.abs(nextIndex - desiredNextIndex) / (gridSize * gridSize)); + + return score; + } + + private int countReachableCells(Point from, Set obstacles) { + // BFS to count reachable cells 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)) { @@ -445,31 +742,47 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { } } + return visited.size(); + } + + private boolean willCauseImmediateDanger(Point move) { + // Check if this move would put us in a position where we have no good next moves + List nextPossibleMoves = getNeighbors(move); + nextPossibleMoves.removeAll(snake.getBody()); + + if (nextPossibleMoves.isEmpty()) { + return true; + } + return false; } - private Point followHamiltonianCycle(Point head, List possibleMoves) { + private Point followOptimizedHamiltonianCycle(Point head, List possibleMoves) { int currentIndex = hamiltonianPath[head.y][head.x]; int nextIndex = (currentIndex + 1) % (gridSize * gridSize); + int prevIndex = (currentIndex - 1 + gridSize * gridSize) % (gridSize * gridSize); // Find the next point in the cycle that's a valid move + Point bestMove = null; + int bestDistance = Integer.MAX_VALUE; + for (Point move : possibleMoves) { - if (hamiltonianPath[move.y][move.x] == nextIndex) { - return move; + int moveIndex = hamiltonianPath[move.y][move.x]; + int distance = (moveIndex - currentIndex + gridSize * gridSize) % (gridSize * gridSize); + + // Prefer moves that are closest to the next in cycle, but also consider safety + if (distance < bestDistance || + (distance == bestDistance && !willCauseImmediateDanger(move))) { + bestDistance = distance; + bestMove = move; } } - // 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; - return diff > 0 ? diff : diff + gridSize * gridSize; - })) - .orElse(possibleMoves.get(0)); + return bestMove != null ? bestMove : possibleMoves.get(0); } private int estimateDistance(Point a, Point b) { - // Manhattan distance + // Manhattan distance with small tie-breaker return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); } @@ -529,7 +842,7 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { for (int x = 0; x < gridSize; x++) { g.fillRect(x * cellSize + 1, y * cellSize + 1, cellSize - 2, cellSize - 2); - if (cellSize > 15) { + if (showNumbers && 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(hamiltonianPath[y][x]); @@ -598,7 +911,7 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { public static void main(String[] args) { SwingUtilities.invokeLater(() -> { JFrame frame = new JFrame("Hamiltonian Snake AI"); - HamiltonianSnakeAI game = new HamiltonianSnakeAI(); + HamiltonianSnakeAI game = new HamiltonianSnakeAI(frame); frame.add(game); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); @@ -608,6 +921,25 @@ public class HamiltonianSnakeAI extends JPanel implements ActionListener { frame.setVisible(true); }); } + + private static class PathNode implements Comparable { + Point position; + int g; // Cost from start + int h; // Heuristic to target + int f; // Total cost (g + h) + + public PathNode(Point position, int g, int h) { + this.position = position; + this.g = g; + this.h = h; + this.f = g + h; + } + + @Override + public int compareTo(PathNode other) { + return Integer.compare(this.f, other.f); + } + } } class Snake {