This commit is contained in:
rattatwinko
2025-06-04 20:03:40 +02:00
commit 31d90004a0
4 changed files with 558 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@@ -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

7
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

17
pom.xml Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.hamiltoniansnakeai</groupId>
<artifactId>HamiltonianSnakeAI</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>24</maven.compiler.source>
<maven.compiler.target>24</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@@ -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<Point> 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<Point> 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<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)) {
visited.add(neighbor);
queue.add(neighbor);
}
}
}
return false;
}
private Point followHamiltonianCycle(Point head, List<Point> 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<Point> getSafeMoves(Point head) {
List<Point> 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<Point> getNeighbors(Point p) {
List<Point> 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<Point> 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<Point> 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<Point> getBody() {
return new ArrayList<>(body);
}
public int getLength() {
return body.size();
}
public boolean contains(Point point) {
return body.contains(point);
}
}