This commit is contained in:
2026-08-19 10:50:21 +02:00
parent 695449442b
commit 5e21331cde
8 changed files with 110 additions and 102 deletions
+1 -1
View File
@@ -1,8 +1,8 @@
package fr.gasser.autosnake; package fr.gasser.autosnake;
import fr.gasser.autosnake.core.AStarSearch;
import fr.gasser.autosnake.model.Direction; import fr.gasser.autosnake.model.Direction;
import fr.gasser.autosnake.model.Snake; import fr.gasser.autosnake.model.Snake;
import fr.gasser.autosnake.path.AStarSearch;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
-5
View File
@@ -1,12 +1,10 @@
package fr.gasser.autosnake; package fr.gasser.autosnake;
import fr.gasser.autosnake.core.AStarSearch;
import fr.gasser.autosnake.model.Snake; import fr.gasser.autosnake.model.Snake;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Queue;
public class GamePanel extends JPanel { public class GamePanel extends JPanel {
@@ -61,9 +59,6 @@ public class GamePanel extends JPanel {
final Rectangle head = model.getHead(); final Rectangle head = model.getHead();
if (head != null) g2d.fill(head); if (head != null) g2d.fill(head);
// Avoid choppy movement when not moving mouse cursor // Avoid choppy movement when not moving mouse cursor
// https://stackoverflow.com/a/26388175 // https://stackoverflow.com/a/26388175
Toolkit.getDefaultToolkit().sync(); Toolkit.getDefaultToolkit().sync();
+65 -89
View File
@@ -1,12 +1,11 @@
package fr.gasser.autosnake.model; package fr.gasser.autosnake.model;
import fr.gasser.autosnake.core.AStarSearch; import fr.gasser.autosnake.path.AStarSearch;
import fr.gasser.autosnake.core.Cell; import fr.gasser.autosnake.path.Cell;
import fr.gasser.autosnake.core.Grid; import fr.gasser.autosnake.path.Grid;
import java.awt.*; import java.awt.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -23,7 +22,7 @@ public class Snake {
this.cellSize = cellSize; this.cellSize = cellSize;
this.head = new V2d(5 * cellSize, 5 * cellSize); this.head = new V2d(5 * cellSize, 5 * cellSize);
this.length = cellSize; this.length = cellSize;
this.trail = new ArrayList<>(); // TODO trim trail this.trail = new ArrayList<>();
this.bounds = new V2d(screen.width, screen.height); this.bounds = new V2d(screen.width, screen.height);
this.apple = this.randomApple(); this.apple = this.randomApple();
} }
@@ -44,8 +43,7 @@ public class Snake {
this.speed += 10; this.speed += 10;
} }
trimTrail(); // trim excess trail trimTrail(); // trim excess trail.
// check for collisions with snake body // check for collisions with snake body
var collisionPoint = checkBodyCollisions(); var collisionPoint = checkBodyCollisions();
@@ -57,53 +55,26 @@ public class Snake {
return false; return false;
} }
private Point checkBodyCollisions() { private V2d checkBodyCollisions() {
Point headCell = cellOf(head); boolean leftHead = false;
boolean leftHeadCell = false; for (V2d segment : sampleTrail()) {
for (int i = trail.size() - 1; i >= 0; --i) { double dist = segment.wrap(bounds).wrappedDist(head, bounds);
var segment = trail.get(i);
Point bodyCell = cellOf(segment); // Do not collide the head with itself
// Ignore the contiguous trail portion created while the head if (!leftHead) {
// was moving through its current cell. if (dist < cellSize) {
if (!leftHeadCell) {
if (bodyCell.equals(headCell)) {
continue; continue;
} }
leftHeadCell = true; leftHead = true;
} }
if (headCell.equals(bodyCell)) {
return headCell; if (dist < cellSize) {
return segment;
} }
} }
return null; return null;
} }
private HashSet<Point> getBodyCells() {
Point headCell = cellOf(head);
HashSet<Point> cells = new HashSet<>();
boolean leftHeadCell = false;
for (int i = trail.size() - 1; i >= 0; --i) {
Point bodyCell = cellOf(trail.get(i));
// Ignore all historical samples belonging to the head's
// current cell. These are the neck/current-cell samples.
if (!leftHeadCell) {
if (bodyCell.equals(headCell)) {
continue;
}
leftHeadCell = true;
}
cells.add(bodyCell);
}
return cells;
}
private Point cellOf(V2d pos) {
var wrapped = pos.wrap(bounds);
return new Point((int) (wrapped.x / cellSize), (int) (wrapped.y / cellSize));
}
private void trimTrail() { private void trimTrail() {
int trimPoint; int trimPoint;
V2d current = head; V2d current = head;
@@ -132,19 +103,48 @@ public class Snake {
var wrappedHead = this.head.wrap(this.bounds); var wrappedHead = this.head.wrap(this.bounds);
var start = new Point((int) (wrappedHead.x / cellSize), (int) (wrappedHead.y / cellSize)); var start = new Point((int) (wrappedHead.x / cellSize), (int) (wrappedHead.y / cellSize));
var goal = new Point((int) (this.apple.x / cellSize), (int) (this.apple.y / cellSize)); var goal = new Point((int) (this.apple.x / cellSize), (int) (this.apple.y / cellSize));
var gridSize = new Dimension((int) (bounds.x / cellSize), (int) (bounds.y / cellSize));
var grid = new Grid() { var grid = new Grid() {
@Override @Override
public Dimension getSize() { public Dimension getSize() {
return new Dimension((int) (bounds.x / cellSize), (int) (bounds.y / cellSize)); return gridSize;
} }
@Override @Override
public Cell get(Point loc) { public Cell get(Point loc) {
V2d pos = new V2d(loc.x * cellSize, loc.y * cellSize); V2d pos = new V2d(loc.x * cellSize, loc.y * cellSize);
if (pos.wrappedDist(wrappedHead, bounds) < cellSize) return () -> true; /*if (pos.wrappedDist(wrappedHead, bounds) < cellSize) return () -> true;
var bodyCells = getBodyCells(); // TODO inline trail sampling in here to remove cellOf var bodyCells = getBodyCells(); // TODO inline trail sampling in here to remove cellOf
if (bodyCells.contains(loc)) return () -> true; // body is an obstacle if (bodyCells.contains(loc)) return () -> true; // body is an obstacle
return () -> false; return () -> false;*/
return () -> checkCollisions(pos);
}
private boolean checkCollisions(V2d pos) {
var trail = sampleTrail();
return false;
}
private HashSet<Point> getBodyCells() {
Point headCell = cellOf(head);
HashSet<Point> cells = new HashSet<>();
boolean leftHeadCell = false;
for (int i = trail.size() - 1; i >= 0; --i) {
Point bodyCell = cellOf(trail.get(i));
// Ignore all historical samples belonging to the head's
// current cell. These are the neck/current-cell samples.
if (!leftHeadCell) {
if (bodyCell.equals(headCell)) {
continue;
}
leftHeadCell = true;
}
cells.add(bodyCell);
}
return cells;
} }
}; };
return new AStarSearch(grid, start, goal, maxIterations); return new AStarSearch(grid, start, goal, maxIterations);
@@ -155,11 +155,15 @@ public class Snake {
} }
public Stream<Rectangle> getTrail() { public Stream<Rectangle> getTrail() {
var visibleTrail = new ArrayList<Rectangle>(); return this.trail.stream().map(v -> getRect(v.x, v.y));
}
private ArrayList<V2d> sampleTrail() {
// Sample points from the trail until the accumulated distance is >= this.length.
var visibleTrail = new ArrayList<V2d>();
var remaining = this.length; var remaining = this.length;
var current = this.head; var current = this.head;
// Sample points from the trail every cellSize until the accumulated distance is >= this.length.
for (var i = this.trail.size() - 1; i >= 0 && remaining > 0; --i) { for (var i = this.trail.size() - 1; i >= 0 && remaining > 0; --i) {
var previous = this.trail.get(i); var previous = this.trail.get(i);
var dx = previous.x - current.x; var dx = previous.x - current.x;
@@ -168,20 +172,19 @@ public class Snake {
if (d > remaining) { if (d > remaining) {
// The tail ends somewhere between current and previous. // The tail ends somewhere between current and previous.
var ratio = remaining / d; var ratio = remaining / d;
var tailX = current.x + dx * ratio; var tailX = current.x + dx * ratio;
var tailY = current.y + dy * ratio; var tailY = current.y + dy * ratio;
visibleTrail.add(new V2d(tailX, tailY));
visibleTrail.add(getRect(tailX, tailY));
break; break;
} }
visibleTrail.add(getRect(previous.x, previous.y));
visibleTrail.add(previous);
remaining -= d; remaining -= d;
current = previous; current = previous;
} }
return visibleTrail;
return visibleTrail.stream();
} }
public Rectangle getHead() { public Rectangle getHead() {
@@ -196,6 +199,11 @@ public class Snake {
return new V2d(column * cellSize, row * cellSize); return new V2d(column * cellSize, row * cellSize);
} }
private Point cellOf(V2d pos) {
var wrapped = pos.wrap(bounds);
return new Point((int) (wrapped.x / cellSize), (int) (wrapped.y / cellSize));
}
private Rectangle getRect(double x, double y) { private Rectangle getRect(double x, double y) {
var wrapped = new V2d(x, y).wrap(bounds); var wrapped = new V2d(x, y).wrap(bounds);
return new Rectangle((int) wrapped.x, (int) wrapped.y, cellSize, cellSize); return new Rectangle((int) wrapped.x, (int) wrapped.y, cellSize, cellSize);
@@ -205,36 +213,4 @@ private Rectangle getRect(double x, double y) {
// mod that works correctly for negative numbers // mod that works correctly for negative numbers
return ((a % b) + b) % b; return ((a % b) + b) % b;
} }
private static class V2d {
private final double x;
private final double y;
public V2d(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return String.format("x=%s y=%s", x, y);
}
private double wrappedDist(V2d other, V2d bounds) {
var dx = Math.abs(this.x - other.x);
var dy = Math.abs(this.y - other.y);
dx = Math.min(dx, bounds.x - dx);
dy = Math.min(dy, bounds.y - dy);
return Math.hypot(dx, dy);
}
private V2d wrap(V2d bounds) {
// convert world space coordinates to wrapped coordinates
var newX = trueMod(x, bounds.x);
var newY = trueMod(y, bounds.y);
return new V2d(newX, newY);
}
}
} }
+38
View File
@@ -0,0 +1,38 @@
package fr.gasser.autosnake.model;
class V2d {
final double x;
final double y;
V2d(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return String.format("x=%s y=%s", x, y);
}
double wrappedDist(V2d other, V2d bounds) {
var dx = Math.abs(this.x - other.x);
var dy = Math.abs(this.y - other.y);
dx = Math.min(dx, bounds.x - dx);
dy = Math.min(dy, bounds.y - dy);
return Math.hypot(dx, dy);
}
V2d wrap(V2d bounds) {
// convert world space coordinates to wrapped coordinates
var newX = trueMod(x, bounds.x);
var newY = trueMod(y, bounds.y);
return new V2d(newX, newY);
}
private static double trueMod(double a, double b) {
// mod that works correctly for negative numbers
return ((a % b) + b) % b;
}
}
@@ -1,9 +1,9 @@
package fr.gasser.autosnake.core; package fr.gasser.autosnake.path;
import java.awt.*; import java.awt.*;
import java.util.*;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.*;
public class AStarSearch { public class AStarSearch {
private final Grid grid; private final Grid grid;
@@ -1,4 +1,4 @@
package fr.gasser.autosnake.core; package fr.gasser.autosnake.path;
public interface Cell { public interface Cell {
boolean isObstacle(); boolean isObstacle();
@@ -1,6 +1,5 @@
package fr.gasser.autosnake.core; package fr.gasser.autosnake.path;
import java.awt.*; import java.awt.*;
import java.util.Random;
public enum Direction { public enum Direction {
UP(0, -1), UP(0, -1),
@@ -1,4 +1,4 @@
package fr.gasser.autosnake.core; package fr.gasser.autosnake.path;
import java.awt.*; import java.awt.*;