Autosnake
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Compiled class file
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Log file
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# BlueJ files
|
||||||
|
*.ctxt
|
||||||
|
|
||||||
|
# Mobile Tools for Java (J2ME)
|
||||||
|
.mtj.tmp/
|
||||||
|
|
||||||
|
# Package Files #
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.nar
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
|
||||||
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
|
hs_err_pid*
|
||||||
|
replay_pid*
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package fr.gasser.autosnake;
|
||||||
|
|
||||||
|
import fr.gasser.autosnake.core.AStarSearch;
|
||||||
|
import fr.gasser.autosnake.model.Direction;
|
||||||
|
import fr.gasser.autosnake.model.Snake;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.KeyAdapter;
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
public class Game extends JFrame {
|
||||||
|
private enum GameMode {
|
||||||
|
MOVE, FOLLOW
|
||||||
|
}
|
||||||
|
|
||||||
|
static final Dimension SIZE = new Dimension(600, 600);
|
||||||
|
private static final int CELL_SIZE = 20;
|
||||||
|
private Direction direction = Direction.DOWN;
|
||||||
|
private GameMode mode = GameMode.MOVE;
|
||||||
|
private final int maxAstarIterations = 1000;
|
||||||
|
private static final int FRAME_DELAY_MS = 1000 / 60;
|
||||||
|
private static final double FIXED_STEP_SECONDS = 1.0 / 60.0;
|
||||||
|
private static final double MAX_FRAME_SECONDS = 0.25;
|
||||||
|
|
||||||
|
private final ExecutorService pathExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
|
private Game() throws HeadlessException {
|
||||||
|
super("Autosnake");
|
||||||
|
setDefaultCloseOperation(EXIT_ON_CLOSE);
|
||||||
|
final Snake snake = new Snake(CELL_SIZE, SIZE);
|
||||||
|
GamePanel gamePanel = new GamePanel(snake);
|
||||||
|
this.addKeyListener(new KeyAdapter() {
|
||||||
|
@Override
|
||||||
|
public void keyPressed(KeyEvent e) {
|
||||||
|
if (e.getKeyCode() == KeyEvent.VK_M){
|
||||||
|
mode = mode == GameMode.FOLLOW ? GameMode.MOVE : GameMode.FOLLOW;
|
||||||
|
setTitle(mode == GameMode.FOLLOW ? "Autosnake -- FOLLOW" : "Autosnake -- MOVE");
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapped = KeyMap.getDirection(e.getKeyCode());
|
||||||
|
if (mapped != null) direction = mapped;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
getContentPane().add(gamePanel);
|
||||||
|
pack();
|
||||||
|
setVisible(true);
|
||||||
|
|
||||||
|
var state = new Object() {
|
||||||
|
long last = System.nanoTime();
|
||||||
|
double accumulator = 0.0;
|
||||||
|
java.util.Queue<AStarSearch.Node> path = new ArrayDeque<>();
|
||||||
|
boolean isPathComputing = false;
|
||||||
|
boolean isDead = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Avoid a large burst after a pause or debugger break.
|
||||||
|
Timer gameLoop = new Timer(FRAME_DELAY_MS, e -> {
|
||||||
|
long now = System.nanoTime();
|
||||||
|
double frameSeconds = (now - state.last) / 1_000_000_000.0;
|
||||||
|
state.last = now;
|
||||||
|
|
||||||
|
// Avoid a large burst after a pause or debugger break.
|
||||||
|
frameSeconds = Math.min(frameSeconds, MAX_FRAME_SECONDS);
|
||||||
|
state.accumulator += frameSeconds;
|
||||||
|
|
||||||
|
if (state.isDead) return;
|
||||||
|
|
||||||
|
while (state.accumulator >= FIXED_STEP_SECONDS) {
|
||||||
|
state.accumulator -= FIXED_STEP_SECONDS;
|
||||||
|
|
||||||
|
if (mode == GameMode.MOVE) {
|
||||||
|
state.path.clear();
|
||||||
|
gamePanel.clearPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == GameMode.FOLLOW && state.path.isEmpty() && !state.isPathComputing) {
|
||||||
|
state.isPathComputing = true;
|
||||||
|
gamePanel.clearPath();
|
||||||
|
var astar = snake.newAStarSearch(maxAstarIterations);
|
||||||
|
pathExecutor.submit(() -> {
|
||||||
|
var computedPath = astar.computePath();
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
state.isPathComputing = false;
|
||||||
|
if (mode != GameMode.FOLLOW) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.path = computedPath;
|
||||||
|
if (state.path.isEmpty()) {
|
||||||
|
System.err.println("Unable to compute path.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == GameMode.FOLLOW && !state.path.isEmpty() && !state.isPathComputing) {
|
||||||
|
// Draw path
|
||||||
|
gamePanel.clearPath();
|
||||||
|
for (var node : state.path) {
|
||||||
|
var loc = node.getLoc();
|
||||||
|
gamePanel.addToPath(loc.x * CELL_SIZE, loc.y * CELL_SIZE);
|
||||||
|
}
|
||||||
|
followPath(state.path, snake);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isDead = snake.update(direction, FIXED_STEP_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
gamePanel.repaint();
|
||||||
|
});
|
||||||
|
gameLoop.setCoalesce(true);
|
||||||
|
gameLoop.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void followPath(java.util.Queue<AStarSearch.Node> path, Snake snake) {
|
||||||
|
// TODO modify astar search to account for wrapping
|
||||||
|
Point next;
|
||||||
|
next = Objects.requireNonNull(path.peek()).getLoc();
|
||||||
|
Rectangle head = snake.getHead();
|
||||||
|
int headCellX = head.x / CELL_SIZE;
|
||||||
|
int headCellY = head.y / CELL_SIZE;
|
||||||
|
if (headCellX == next.x && headCellY == next.y) {
|
||||||
|
path.poll();
|
||||||
|
} else {
|
||||||
|
int dx = next.x - headCellX;
|
||||||
|
int dy = next.y - headCellY;
|
||||||
|
|
||||||
|
if (dx > 0) {
|
||||||
|
direction = Direction.RIGHT;
|
||||||
|
} else if (dx < 0) {
|
||||||
|
direction = Direction.LEFT;
|
||||||
|
} else if (dy > 0) {
|
||||||
|
direction = Direction.DOWN;
|
||||||
|
} else if (dy < 0) {
|
||||||
|
direction = Direction.UP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
EventQueue.invokeLater(Game::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
pathExecutor.shutdownNow();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package fr.gasser.autosnake;
|
||||||
|
|
||||||
|
import fr.gasser.autosnake.core.AStarSearch;
|
||||||
|
import fr.gasser.autosnake.model.Snake;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
|
public class GamePanel extends JPanel {
|
||||||
|
|
||||||
|
private static final Color APPLE_COLOR = Color.RED;
|
||||||
|
private static final Color TRAIL_COLOR = Color.GREEN;
|
||||||
|
private static final Color HEAD_COLOR = Color.GREEN.darker();
|
||||||
|
|
||||||
|
private final Snake model;
|
||||||
|
private final java.util.List<Point> path = new ArrayList<>();
|
||||||
|
|
||||||
|
GamePanel(Snake model) {
|
||||||
|
this.model = model;
|
||||||
|
setOpaque(true);
|
||||||
|
setBackground(Color.BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addToPath(int x, int y) {
|
||||||
|
path.add(new Point(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearPath(){
|
||||||
|
path.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize() {
|
||||||
|
return Game.SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics g) {
|
||||||
|
super.paintComponent(g);
|
||||||
|
Graphics2D g2d = (Graphics2D) g;
|
||||||
|
|
||||||
|
// JPanel clears an opaque component using its background before
|
||||||
|
// paintComponent returns. Do not clear the whole surface twice.
|
||||||
|
|
||||||
|
// draw path
|
||||||
|
for (var current : path) {
|
||||||
|
g2d.setColor(Color.blue);
|
||||||
|
g2d.fill(new Rectangle(current.x, current.y, 20, 20));
|
||||||
|
}
|
||||||
|
|
||||||
|
g2d.setColor(APPLE_COLOR);
|
||||||
|
g2d.fill(model.getApple());
|
||||||
|
|
||||||
|
g2d.setColor(TRAIL_COLOR);
|
||||||
|
|
||||||
|
model.getTrail().forEach(g2d::fill);
|
||||||
|
|
||||||
|
g2d.setColor(HEAD_COLOR);
|
||||||
|
final Rectangle head = model.getHead();
|
||||||
|
if (head != null) g2d.fill(head);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Avoid choppy movement when not moving mouse cursor
|
||||||
|
// https://stackoverflow.com/a/26388175
|
||||||
|
Toolkit.getDefaultToolkit().sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package fr.gasser.autosnake;
|
||||||
|
|
||||||
|
import fr.gasser.autosnake.model.Direction;
|
||||||
|
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
|
|
||||||
|
public final class KeyMap {
|
||||||
|
private static final int[] KEY_CODES = {
|
||||||
|
KeyEvent.VK_UP, KeyEvent.VK_Z,
|
||||||
|
KeyEvent.VK_DOWN, KeyEvent.VK_S,
|
||||||
|
KeyEvent.VK_LEFT, KeyEvent.VK_Q,
|
||||||
|
KeyEvent.VK_RIGHT, KeyEvent.VK_D
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final Direction[] DIRECTIONS = {
|
||||||
|
Direction.UP, Direction.UP,
|
||||||
|
Direction.DOWN, Direction.DOWN,
|
||||||
|
Direction.LEFT, Direction.LEFT,
|
||||||
|
Direction.RIGHT, Direction.RIGHT
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Direction getDirection(int keyCode) {
|
||||||
|
Direction direction = null;
|
||||||
|
for (int i = 0; i < KEY_CODES.length; i++) {
|
||||||
|
if (KEY_CODES[i] == keyCode) {
|
||||||
|
direction = DIRECTIONS[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package fr.gasser.autosnake.core;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class AStarSearch {
|
||||||
|
private final Grid grid;
|
||||||
|
private final Node start;
|
||||||
|
private final Node goal;
|
||||||
|
private final int maxIterations;
|
||||||
|
|
||||||
|
public AStarSearch(Grid grid, Point start, Point goal, int maxIterations) {
|
||||||
|
this(grid, new Node(start), new Node(goal), maxIterations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AStarSearch(Grid grid, Node start, Node goal, int maxIterations) {
|
||||||
|
this.grid = grid;
|
||||||
|
this.start = start;
|
||||||
|
this.goal = goal;
|
||||||
|
this.maxIterations = maxIterations;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node search(Node start, Node goal) {
|
||||||
|
long startTime = System.nanoTime();
|
||||||
|
PriorityQueue<Node> frontier = new PriorityQueue<>();
|
||||||
|
Set<Node> explored = new HashSet<>();
|
||||||
|
frontier.add(start);
|
||||||
|
int n = 0;
|
||||||
|
while (!frontier.isEmpty() && n < maxIterations) {
|
||||||
|
Node currentNode = frontier.poll();
|
||||||
|
explored.add(currentNode);
|
||||||
|
|
||||||
|
if (currentNode.loc.equals(goal.loc)) {
|
||||||
|
long endTime = System.nanoTime();
|
||||||
|
System.out.printf("Found path in %s iterations and %s ms%n", n, (endTime - startTime) / 1000_000.0);
|
||||||
|
return currentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Node neighbor : getNeighbors(currentNode)) {
|
||||||
|
// Ignore the neighbor which is already evaluated.
|
||||||
|
if (explored.contains(neighbor)) continue;
|
||||||
|
if (!frontier.contains(neighbor)) {
|
||||||
|
neighbor.setParent(currentNode);
|
||||||
|
neighbor.cost++;
|
||||||
|
var dx = neighbor.loc.x - goal.loc.x;
|
||||||
|
var dy = neighbor.loc.y - goal.loc.y;
|
||||||
|
neighbor.hValue = neighbor.cost + (dx*dx + dy*dy);
|
||||||
|
frontier.add(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Node> getNeighbors(Node current) {
|
||||||
|
final Dimension gridSize = grid.getSize();
|
||||||
|
final LinkedList<Node> neighbors = new LinkedList<>();
|
||||||
|
for (Direction direction : Direction.values()) {
|
||||||
|
final Node node = new Node(current);
|
||||||
|
final Point loc = new Point(current.loc.x + direction.loc.x, current.loc.y + direction.loc.y);
|
||||||
|
// Don't overflow off the grid
|
||||||
|
if (loc.x < 0 || loc.x >= gridSize.width || loc.y < 0 || loc.y >= gridSize.height) continue;
|
||||||
|
// If the node is a wall, don't add it to the list
|
||||||
|
if (grid.get(loc).isObstacle()) continue;
|
||||||
|
node.setLoc(loc);
|
||||||
|
neighbors.add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return neighbors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Queue<Node> computePath() {
|
||||||
|
Node currentNode = search(start, goal);
|
||||||
|
LinkedList<Node> path = new LinkedList<>();
|
||||||
|
|
||||||
|
if (currentNode != null) {
|
||||||
|
while (currentNode.getParent() != null) {
|
||||||
|
path.addFirst(currentNode);
|
||||||
|
currentNode = currentNode.getParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Node implements Comparable<Node> {
|
||||||
|
private Node parent;
|
||||||
|
private Point loc;
|
||||||
|
private int cost;
|
||||||
|
private double hValue; // heuristic value
|
||||||
|
|
||||||
|
Node(Point loc) {
|
||||||
|
this.loc = loc;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node(Node other) {
|
||||||
|
this.parent = other.parent;
|
||||||
|
this.loc = other.loc;
|
||||||
|
this.cost = other.cost;
|
||||||
|
this.hValue = other.hValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Point getLoc() {
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setLoc(Point point) {
|
||||||
|
this.loc = point;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node getParent() {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setParent(Node parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(Node other) {
|
||||||
|
return Double.compare(this.hValue, other.hValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("Node(%s)", getLoc());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package fr.gasser.autosnake.core;
|
||||||
|
|
||||||
|
public interface Cell {
|
||||||
|
boolean isObstacle();
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package fr.gasser.autosnake.core;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public enum Direction {
|
||||||
|
UP(0, -1),
|
||||||
|
DOWN(0, 1),
|
||||||
|
LEFT(-1, 0),
|
||||||
|
RIGHT(1, 0),
|
||||||
|
;
|
||||||
|
|
||||||
|
public final Point loc;
|
||||||
|
|
||||||
|
Direction(int x, int y) {
|
||||||
|
loc = new Point(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package fr.gasser.autosnake.core;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
|
||||||
|
public interface Grid {
|
||||||
|
Dimension getSize();
|
||||||
|
|
||||||
|
Cell get(Point loc);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package fr.gasser.autosnake.model;
|
||||||
|
|
||||||
|
public enum Direction {
|
||||||
|
UP, DOWN, LEFT, RIGHT;
|
||||||
|
|
||||||
|
public int dx() {
|
||||||
|
return switch (this) {
|
||||||
|
case LEFT -> -1;
|
||||||
|
case RIGHT -> 1;
|
||||||
|
default -> 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public int dy() {
|
||||||
|
return switch (this) {
|
||||||
|
case UP -> -1;
|
||||||
|
case DOWN -> 1;
|
||||||
|
default -> 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package fr.gasser.autosnake.model;
|
||||||
|
|
||||||
|
import fr.gasser.autosnake.core.AStarSearch;
|
||||||
|
import fr.gasser.autosnake.core.Cell;
|
||||||
|
import fr.gasser.autosnake.core.Grid;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class Snake {
|
||||||
|
private final int cellSize;
|
||||||
|
private final V2d bounds;
|
||||||
|
private V2d head;
|
||||||
|
private final ArrayList<V2d> trail;
|
||||||
|
private double length;
|
||||||
|
private V2d apple;
|
||||||
|
private int speed = 100;
|
||||||
|
|
||||||
|
public Snake(int cellSize, Dimension screen) {
|
||||||
|
this.cellSize = cellSize;
|
||||||
|
this.head = new V2d(5 * cellSize, 5 * cellSize);
|
||||||
|
this.length = cellSize;
|
||||||
|
this.trail = new ArrayList<>(); // TODO trim trail
|
||||||
|
this.bounds = new V2d(screen.width, screen.height);
|
||||||
|
this.apple = this.randomApple();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean update(Direction d, double dt) {
|
||||||
|
double x = head.x + dt * d.dx() * this.speed;
|
||||||
|
double y = head.y + dt * d.dy() * this.speed;
|
||||||
|
|
||||||
|
// record previous head pos into trail and move head
|
||||||
|
trail.add(head);
|
||||||
|
head = new V2d(x, y);
|
||||||
|
|
||||||
|
// check collision with apple
|
||||||
|
var wrappedHead = head.wrap(bounds);
|
||||||
|
if (wrappedHead.wrappedDist(apple, bounds) < cellSize) {
|
||||||
|
this.apple = randomApple();
|
||||||
|
this.length += cellSize;
|
||||||
|
this.speed += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
trimTrail(); // trim excess trail
|
||||||
|
|
||||||
|
|
||||||
|
// check for collisions with snake body
|
||||||
|
var collisionPoint = checkBodyCollisions();
|
||||||
|
if (collisionPoint != null) {
|
||||||
|
System.out.println("Collision at " + collisionPoint);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Point checkBodyCollisions() {
|
||||||
|
Point headCell = cellOf(head);
|
||||||
|
boolean leftHeadCell = false;
|
||||||
|
for (int i = trail.size() - 1; i >= 0; --i) {
|
||||||
|
var segment = trail.get(i);
|
||||||
|
Point bodyCell = cellOf(segment);
|
||||||
|
// Ignore the contiguous trail portion created while the head
|
||||||
|
// was moving through its current cell.
|
||||||
|
if (!leftHeadCell) {
|
||||||
|
if (bodyCell.equals(headCell)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
leftHeadCell = true;
|
||||||
|
}
|
||||||
|
if (headCell.equals(bodyCell)) {
|
||||||
|
return headCell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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() {
|
||||||
|
int trimPoint;
|
||||||
|
V2d current = head;
|
||||||
|
double cum = 0;
|
||||||
|
for (trimPoint = trail.size() - 1; trimPoint >= 0; --trimPoint) {
|
||||||
|
var segment = trail.get(trimPoint);
|
||||||
|
var dx = segment.x - current.x;
|
||||||
|
var dy = segment.y - current.y;
|
||||||
|
var distance = Math.hypot(dx, dy);
|
||||||
|
cum += distance;
|
||||||
|
if (cum > length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimPoint > 0) {
|
||||||
|
var before = trail.size();
|
||||||
|
trail.subList(0, trimPoint).clear();
|
||||||
|
var after = trail.size();
|
||||||
|
//System.out.printf("Trail trimmed from %s to %s%n", before, after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AStarSearch newAStarSearch(int maxIterations) {
|
||||||
|
var wrappedHead = this.head.wrap(this.bounds);
|
||||||
|
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 grid = new Grid() {
|
||||||
|
@Override
|
||||||
|
public Dimension getSize() {
|
||||||
|
return new Dimension((int) (bounds.x / cellSize), (int) (bounds.y / cellSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cell get(Point loc) {
|
||||||
|
V2d pos = new V2d(loc.x * cellSize, loc.y * cellSize);
|
||||||
|
if (pos.wrappedDist(wrappedHead, bounds) < cellSize) return () -> true;
|
||||||
|
var bodyCells = getBodyCells(); // TODO inline trail sampling in here to remove cellOf
|
||||||
|
if (bodyCells.contains(loc)) return () -> true; // body is an obstacle
|
||||||
|
return () -> false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return new AStarSearch(grid, start, goal, maxIterations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Shape getApple() {
|
||||||
|
return new Rectangle((int) this.apple.x, (int) this.apple.y, cellSize, cellSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream<Rectangle> getTrail() {
|
||||||
|
var visibleTrail = new ArrayList<Rectangle>();
|
||||||
|
var remaining = this.length;
|
||||||
|
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) {
|
||||||
|
var previous = this.trail.get(i);
|
||||||
|
var dx = previous.x - current.x;
|
||||||
|
var dy = previous.y - current.y;
|
||||||
|
var d = Math.hypot(dx, dy);
|
||||||
|
if (d > remaining) {
|
||||||
|
// The tail ends somewhere between current and previous.
|
||||||
|
var ratio = remaining / d;
|
||||||
|
|
||||||
|
var tailX = current.x + dx * ratio;
|
||||||
|
var tailY = current.y + dy * ratio;
|
||||||
|
|
||||||
|
visibleTrail.add(getRect(tailX, tailY));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
visibleTrail.add(getRect(previous.x, previous.y));
|
||||||
|
remaining -= d;
|
||||||
|
current = previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return visibleTrail.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Rectangle getHead() {
|
||||||
|
return getRect(this.head.x, this.head.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private V2d randomApple(){
|
||||||
|
int columns = (int) bounds.x / cellSize;
|
||||||
|
int rows = (int) bounds.y / cellSize;
|
||||||
|
int column = (int) (Math.random() * columns);
|
||||||
|
int row = (int) (Math.random() * rows);
|
||||||
|
return new V2d(column * cellSize, row * cellSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Rectangle getRect(double x, double y) {
|
||||||
|
var wrapped = new V2d(x, y).wrap(bounds);
|
||||||
|
return new Rectangle((int) wrapped.x, (int) wrapped.y, cellSize, cellSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double trueMod(double a, double b) {
|
||||||
|
// mod that works correctly for negative numbers
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user