Created
March 17, 2017 18:34
-
-
Save james-d/c9447999e6b3b41f4eae77013621e27d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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