algo. changed to be better

This commit is contained in:
rattatwinko
2025-06-04 20:22:42 +02:00
parent a7365e0f27
commit 431a229157

View File

@@ -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<Point> obstacles = new HashSet<>(snake.getBody());
obstacles.remove(snake.getTail());
// Simple flood fill to check reachability from snake head
Set<Point> visited = new HashSet<>();
Queue<Point> 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<PathNode> openSet = new PriorityQueue<>();
Set<Point> 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<Point> 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<Point> filterImmediatelyDeadlyMoves(List<Point> moves) {
List<Point> 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<Point> filterFutureTraps(List<Point> moves, int lookAhead) {
if (snake.getLength() < gridSize * 0.6) return moves; // Not long enough to worry
List<Point> 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<Point> 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<Point> 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<Point> possibleMoves) {
private int countSafeExits(Point position, Set<Point> obstacles) {
int count = 0;
for (Point neighbor : getNeighbors(position)) {
if (!obstacles.contains(neighbor)) {
count++;
}
}
return count;
}
private Point findSafePathToFood(Point head, List<Point> possibleMoves) {
if (estimateDistance(head, food) > gridSize * 0.7) return null;
// Find the safest path to food using flood fill
Map<Point, Integer> distanceMap = new HashMap<>();
Queue<Point> queue = new LinkedList<>();
Set<Point> 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<Point> 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<Point> possibleMoves) {
// Use A* algorithm to find the best path to food
Map<Point, Point> cameFrom = new HashMap<>();
Map<Point, Integer> gScore = new HashMap<>();
Map<Point, Integer> fScore = new HashMap<>();
PriorityQueue<Point> openSet = new PriorityQueue<>(
Comparator.comparingInt(p -> fScore.getOrDefault(p, Integer.MAX_VALUE))
);
Set<Point> 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<Point> 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<Point> 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<Point> 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<Point> obstacles) {
// BFS to count reachable cells
Set<Point> visited = new HashSet<>();
Queue<Point> queue = new LinkedList<>();
Set<Point> 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<Point> nextPossibleMoves = getNeighbors(move);
nextPossibleMoves.removeAll(snake.getBody());
if (nextPossibleMoves.isEmpty()) {
return true;
}
return false;
}
private Point followHamiltonianCycle(Point head, List<Point> possibleMoves) {
private Point followOptimizedHamiltonianCycle(Point head, List<Point> 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<PathNode> {
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 {