Skip to content

Instantly share code, notes, and snippets.

@james-d
Created March 17, 2017 18:34
Show Gist options
  • Save james-d/c9447999e6b3b41f4eae77013621e27d to your computer and use it in GitHub Desktop.
Save james-d/c9447999e6b3b41f4eae77013621e27d to your computer and use it in GitHub Desktop.
package tankgame;
import java.util.ArrayList;
import java.util.List;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class Game extends Application {
private static final int LINES_IN_TRAJECTORY = 4;
private final int widthInCells = 5 ;
private final int heightInCells = 5 ;
private final double cellSize = 120 ;
private final List<Wall> walls = new ArrayList<>();
private Circle tank;
@Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
pane.setMinSize(widthInCells*cellSize, heightInCells*cellSize);
StackPane view = new StackPane(pane);
view.setPadding(new Insets(10));
tank = new Circle(widthInCells*cellSize / 2, heightInCells * cellSize /2 , 10, Color.BLUE);
pane.getChildren().add(tank);
Button button = new Button("Generate walls");
WallGenerator wallGenerator = new WallGenerator(widthInCells, heightInCells);
button.setOnAction(e -> {
for (Wall w : walls) pane.getChildren().remove(w.asLine());
walls.clear();
walls.addAll(wallGenerator.generateWalls(10, cellSize, cellSize)) ;
for (Wall w : walls) pane.getChildren().add(w.asLine());
});
BorderPane root = new BorderPane(view);
BorderPane.setAlignment(button, Pos.CENTER);
BorderPane.setMargin(button, new Insets(5));
root.setBottom(button);
Scene scene = new Scene(root);
BooleanProperty up = new SimpleBooleanProperty();
BooleanProperty down = new SimpleBooleanProperty();
BooleanProperty left = new SimpleBooleanProperty();
BooleanProperty right = new SimpleBooleanProperty();
List<Line> fireTrajectory = new ArrayList<>();
scene.setOnKeyPressed(e -> {
switch(e.getCode()) {
case UP: up.set(true); break;
case DOWN: down.set(true); break ;
case LEFT: left.set(true); break ;
case RIGHT: right.set(true); break ;
case ENTER:
// fire...
pane.getChildren().removeAll(fireTrajectory);
fireTrajectory.clear();
fireTrajectory.addAll(getTrajectory(new Point2D(tank.getCenterX(), tank.getCenterY())));
pane.getChildren().addAll(fireTrajectory);
break ;
default: break ;
}
});
scene.setOnKeyReleased(e -> {
switch(e.getCode()) {
case UP: up.set(false); break;
case DOWN: down.set(false); break ;
case LEFT: left.set(false); break ;
case RIGHT: right.set(false); break ;
default: break ;
}
});
AnimationTimer timer = new AnimationTimer() {
private long lastUpdate ;
public void handle(long timeStamp) {
double elapsedSeconds = (timeStamp - lastUpdate) / 1_000_000_000.0 ;
lastUpdate = timeStamp ;
if (elapsedSeconds > 1) return ;
double deltaX = 0, deltaY = 0 ;
double delta = 100 * elapsedSeconds ;
if (up.get()) deltaY -= delta ;
if (down.get()) deltaY += delta ;
if (left.get()) deltaX -= delta ;
if (right.get()) deltaX += delta ;
tank.setCenterX(clamp(tank.getCenterX()+deltaX, 0, cellSize * widthInCells));
tank.setCenterY(clamp(tank.getCenterY()+deltaY, 0, cellSize * heightInCells));
}
};
timer.start();
primaryStage.setScene(scene);
primaryStage.show();
}
private List<Line> getTrajectory(Point2D start) {
List<Line> trajectory = new ArrayList<>();
double angle = Math.random() * 360 ;
Intersection lastIntersection = new Intersection(null, start) ;
for (int i = 0 ; i < LINES_IN_TRAJECTORY ; i++) {
Intersection intersection = findNearestIntersection(angle, lastIntersection);
Line l = new Line(lastIntersection.intersectingPoint.getX(), lastIntersection.intersectingPoint.getY(),
intersection.intersectingPoint.getX(), intersection.intersectingPoint.getY());
l.setStroke(Color.RED);
trajectory.add(l);
if (intersection.wall.isVertical()) {
angle = 180 - angle ;
}
if (intersection.wall.isHorizontal()) {
angle = 360 - angle ;
}
if (angle < 0) angle+=360 ;
lastIntersection = intersection ;
}
return trajectory ;
}
private Intersection findNearestIntersection(double angle, Intersection lastIntersection) {
Intersection intersection = null ;
double minDist = Double.MAX_VALUE ;
for (Wall w : walls) {
if (w == lastIntersection.wall) continue ;
Point2D wallIntersection = w.getIntersectionFrom(lastIntersection.intersectingPoint, angle);
if (wallIntersection != null) {
double dist = lastIntersection.intersectingPoint.distance(wallIntersection);
if (dist < minDist) {
intersection = new Intersection(w, wallIntersection);
minDist = dist ;
}
}
}
return intersection;
}
private double clamp(double value, double min, double max) {
return Math.min(max, Math.max(min, value));
}
private static class Intersection {
private final Wall wall ;
private final Point2D intersectingPoint ;
public Intersection(Wall wall, Point2D intersectingPoint) {
super();
this.wall = wall;
this.intersectingPoint = intersectingPoint;
}
}
public static void main(String[] args) {
launch(args);
}
}
package tankgame;
import javafx.geometry.Point2D;
public class HorizontalWall extends Wall {
private final double startX ;
private final double endX ;
private final double y ;
public HorizontalWall(double startX, double endX, double y) {
this.startX = Math.min(startX, endX);
this.endX = Math.max(startX, endX);
this.y = y ;
}
@Override
public double getStartX() {
return startX ;
}
@Override
public double getEndX() {
return endX ;
}
@Override
public double getStartY() {
return y ;
}
@Override
public double getEndY() {
return y ;
}
@Override
public Point2D getIntersectionFrom(Point2D origin, double angle) {
// if line is horizontal, there is no intersection:
if (angle % 180 == 0) return null ;
// if line is pointing away from wall, there is no intersection:
if ( ((int) angle / 180) % 2 == 0 /* downward (+ve y) */ && y < origin.getY()) return null ;
if ( ((int) angle / 180) % 2 == 1 /* upward (-ve y) */ && y > origin.getY()) return null ;
// find x-coordinate of intersection:
double slope = Math.tan(Math.toRadians(angle)) ;
double x = origin.getX() + (y - origin.getY()) / slope ;
// if x-coordinate is beyond ends of wall, there is no intersection:
if (x < startX || x > endX) return null ;
// return intersecting point:
return new Point2D(x, y);
}
@Override
public boolean isHorizontal() {
return true;
}
@Override
public boolean isVertical() {
return false;
}
}
package tankgame;
import javafx.geometry.Point2D;
public class VerticalWall extends Wall {
private final double x ;
private final double startY ;
private final double endY ;
public VerticalWall(double x, double startY, double endY) {
this.x = x;
this.startY = Math.min(startY, endY);
this.endY = Math.max(startY, endY);
}
@Override
public double getStartX() {
return x ;
}
@Override
public double getEndX() {
return x ;
}
@Override
public double getStartY() {
return startY;
}
@Override
public double getEndY() {
return endY;
}
@Override
public Point2D getIntersectionFrom(Point2D origin, double angle) {
// if line is vertical, there is no intersection:
if ((angle + 90) % 180 == 0) /* vertical */ return null;
// if line is pointing away from wall, there is no intersection:
if ((int)(angle+90) / 180 % 2 == 0 /* rightwards */ && x < origin.getX()) return null ;
if ((int)(angle+90) / 180 % 2 == 1 /* leftwards */ && x > origin.getX()) return null ;
// find y-coordinate of intersection:
double slope = Math.tan(Math.toRadians(angle)) ;
double y = origin.getY() + slope * (x - origin.getX());
// if y-coordinate is beyond ends of wall, there is no intersection:
if (y < startY || y > endY) return null ;
// return intersecting point:
return new Point2D(x, y);
}
@Override
public boolean isHorizontal() {
return false;
}
@Override
public boolean isVertical() {
return true;
}
}
package tankgame;
import javafx.geometry.Point2D;
import javafx.scene.shape.Line;
public abstract class Wall {
public abstract double getStartX() ;
public abstract double getEndX() ;
public abstract double getStartY() ;
public abstract double getEndY() ;
public abstract boolean isHorizontal() ;
public abstract boolean isVertical() ;
public abstract Point2D getIntersectionFrom(Point2D origin, double angle) ;
private Line line ;
public Line asLine() {
if (line == null) {
line = new Line(getStartX(), getStartY(), getEndX(), getEndY());
}
return line ;
}
@Override
public String toString() {
return String.format("[%.2f, %.2f] -> [%.2f, %.2f]", getStartX(), getStartY(), getEndX(), getEndY());
}
}
package tankgame;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class WallGenerator {
private final int widthInCells ;
private final int heightInCells ;
/*
* Class for generating a random set of walls. The walls must be non-intersecting (except endpoints),
* must bound the entire area with a perimeter, must be connected to each other,
* and must leave the interior of the area connected.
*
* The area is regarded as a grid of cells, with the walls following the boundaries of cells.
*
* The current strategy is to first draw the perimeter walls. Vertices of the cells are marked as "filled" if
* they are contained in an existing wall. Filled vertices are considered as "extensible" in each of four
* directions if they have two or more non-filled vertices in that direction. The amount by which they can
* be extended is defined as the number of contiguous adjacent extensible vertices in that direction.
*
* A vertex with non-zero extensibility in some direction is picked at random. A direction in which it has
* non-zero extensibility is then picked at random, and an amount by which to extend is picked at random in that direction.
* A wall is then added. This is repeated until the specified number of walls exists, or there is no option for
* further extensibility.
*
*/
public WallGenerator(int widthInCells, int heightInCells) {
this.widthInCells = widthInCells;
this.heightInCells = heightInCells;
}
public List<Wall> generateWalls(int numWalls, double cellWidth, double cellHeight) {
// TODO: rewrite this so the relationship between the code and the algorithm is clearer.
// TODO: only extend perpendicular to existing walls.
boolean[][] filled = new boolean[widthInCells+1][heightInCells+1];
List<IntWall> walls = new ArrayList<>();
Random rng = new Random();
List<IntWall> boundary = createBoundaryWalls();
boundary.forEach(w -> markFilled(w, filled));
walls.addAll(boundary);
Map<CellLocation, Integer> minHorizExtension = new HashMap<>();
Map<CellLocation, Integer> maxHorizExtension = new HashMap<>();
Map<CellLocation, Integer> minVertExtension = new HashMap<>();
Map<CellLocation, Integer> maxVertExtension = new HashMap<>();
computeExtensionsAvailable(minHorizExtension, maxHorizExtension, minVertExtension, maxVertExtension, filled) ;
Set<CellLocation> extensibleCells = new HashSet<>();
extensibleCells.addAll(minHorizExtension.keySet());
extensibleCells.addAll(maxHorizExtension.keySet());
extensibleCells.addAll(minVertExtension.keySet());
extensibleCells.addAll(maxVertExtension.keySet());
while (extensibleCells.size() > 0 && walls.size() < numWalls) {
CellLocation extendFrom = new ArrayList<>(extensibleCells).get(rng.nextInt(extensibleCells.size()));
List<Map<CellLocation,Integer>> availableDirections =
Stream.of(minHorizExtension, maxHorizExtension, minVertExtension, maxVertExtension)
.filter(m -> m.containsKey(extendFrom))
.collect(Collectors.toList());
Map<CellLocation, Integer> direction = availableDirections.get(rng.nextInt(availableDirections.size()));
int maxDist = direction.get(extendFrom);
int dist = rng.nextInt(Math.abs(maxDist))+1;
if (direction == minHorizExtension) {
IntWall newWall = makeWall(new CellLocation(extendFrom.x - dist, extendFrom.y), extendFrom, filled);
walls.add(newWall);
}
if (direction == maxHorizExtension) {
IntWall newWall = makeWall(extendFrom, new CellLocation(extendFrom.x + dist, extendFrom.y), filled);
walls.add(newWall);
}
if (direction == minVertExtension) {
IntWall newWall = makeWall(new CellLocation(extendFrom.x, extendFrom.y - dist), extendFrom, filled);
walls.add(newWall);
}
if (direction == maxVertExtension) {
IntWall newWall = makeWall(extendFrom, new CellLocation(extendFrom.x, extendFrom.y + dist), filled);
walls.add(newWall);
}
computeExtensionsAvailable(minHorizExtension, maxHorizExtension, minVertExtension, maxVertExtension, filled) ;
extensibleCells.clear();
extensibleCells.addAll(minHorizExtension.keySet());
extensibleCells.addAll(maxHorizExtension.keySet());
extensibleCells.addAll(minVertExtension.keySet());
extensibleCells.addAll(maxVertExtension.keySet());
}
return walls.stream().map(w -> w.toWall(cellWidth, cellHeight)).collect(Collectors.toList());
}
private IntWall makeWall(CellLocation start, CellLocation end, boolean[][] filled) {
IntWall wall = new IntWall(start, end);
markFilled(wall, filled);
return wall ;
}
private void computeExtensionsAvailable(
Map<CellLocation, Integer> minHorizExtension,
Map<CellLocation, Integer> maxHorizExtension,
Map<CellLocation, Integer> minVertExtension,
Map<CellLocation, Integer> maxVertExtension,
boolean[][] filled) {
Stream.of(minHorizExtension, maxHorizExtension, minVertExtension, maxVertExtension)
.forEach(Map::clear);
for (int x = 0 ; x < widthInCells ; x++) {
for (int y = 0 ; y < widthInCells ; y++) {
int count ;
if (filled[x][y]) {
count = 0 ;
for (int testX = x-1 ; testX > 0 && !filled[testX][y] && ! filled[testX-1][y] ; testX--) {
count-- ;
}
if (count < 0) {
minHorizExtension.put(new CellLocation(x,y), count);
}
count = 0 ;
for (int testX = x+1 ; testX < widthInCells && !filled[testX][y] && ! filled[testX+1][y] ; testX++) {
count++ ;
}
if (count > 0) {
maxHorizExtension.put(new CellLocation(x,y), count);
}
count = 0 ;
for (int testY = y-1 ; testY > 0 && !filled[x][testY] && ! filled[x][testY-1]; testY--) {
count-- ;
}
if (count < 0) {
minVertExtension.put(new CellLocation(x,y), count);
}
count = 0 ;
for (int testY = y+1 ; testY < heightInCells && !filled[x][testY] && ! filled[x][testY+1] ; testY++) {
count++ ;
}
if (count > 0) {
maxVertExtension.put(new CellLocation(x,y), count);
}
}
}
}
}
private List<IntWall> createBoundaryWalls() {
return Arrays.asList(
new IntWall(new CellLocation(0,0), new CellLocation(widthInCells, 0)),
new IntWall(new CellLocation(widthInCells, 0), new CellLocation(widthInCells, heightInCells)),
new IntWall(new CellLocation(0,0), new CellLocation(0, heightInCells)),
new IntWall(new CellLocation(0, heightInCells), new CellLocation(widthInCells, heightInCells))
);
}
private void markFilled(IntWall wall, boolean[][] vertices) {
if (wall.isHorizontal()) {
for (int x = wall.start.x ; x <= wall.end.x ; x++) {
vertices[x][wall.start.y] = true ;
}
}
if (wall.isVertical()) {
for (int y = wall.start.y ; y <= wall.end.y ; y++) {
vertices[wall.start.x][y] = true ;
}
}
}
private static class IntWall {
private final CellLocation start ;
private final CellLocation end ;
public IntWall(CellLocation start, CellLocation end) {
this.start = start;
this.end = end;
}
Wall toWall(double cellWidth, double cellHeight) {
if (isVertical()) {
return new VerticalWall(start.x * cellWidth, start.y * cellHeight, end.y * cellHeight);
}
if (isHorizontal()) {
return new HorizontalWall(start.x * cellWidth, end.x * cellWidth, start.y * cellHeight);
}
throw new IllegalStateException("Wall is neither horizontal nor vertical: "+this);
}
private boolean isHorizontal() {
return start.y == end.y;
}
private boolean isVertical() {
return start.x == end.x;
}
@Override
public String toString() {
return String.format("%s -> %s", start, end);
}
}
private static class CellLocation {
private final int x ;
private final int y ;
public CellLocation(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return String.format("[%d, %d]", x, y);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment