Commit c2fd7505 authored by Markus Klinik's avatar Markus Klinik
Browse files

snake: add test case with inner class

parent 74c50651
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER">
<attributes>
<attribute name="module" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/JavaFX 11.0.2">
<attributes>
<attribute name="module" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="bin"/>
</classpath>
**/bin/
**/build/
**/nbproject/private/
**/dist/
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>assignment-snake-graphics-in-inner-class</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
package snake;
public enum Direction {
UP(0, -1),
RIGHT(1, 0),
DOWN(0, 1),
LEFT(-1, 0);
private final int dX, dY;
private Direction(int dX, int dY) {
this.dX = dX;
this.dY = dY;
}
public int getDX() {
return dX;
}
public int getDY() {
return dY;
}
public Direction rotateLeft() {
switch(this) {
case UP: return LEFT;
case LEFT: return DOWN;
case DOWN: return RIGHT;
default: return UP;
}
}
public Direction rotateRight() {
switch(this) {
case UP: return RIGHT;
case RIGHT: return DOWN;
case DOWN: return LEFT;
default: return UP;
}
}
}
package snake;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class Food {
private final IntegerProperty x = new SimpleIntegerProperty(), y = new SimpleIntegerProperty();
public void moveTo(int x, int y) {
this.x.set(x);
this.y.set(y);
}
public int getX() {
return x.get();
}
public int getY() {
return y.get();
}
public IntegerProperty getXProperty() {
return x;
}
public IntegerProperty getYProperty() {
return y;
}
}
package snake;
import javafx.event.EventHandler;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
/**
* Handles controls of a snake game, where the 'a' and 'd' keys can be used to move and 's' (un)pauses the game
*/
public class InputHandler {
private final EventHandler<KeyEvent> keyHandler;
private final EventHandler<MouseEvent> mouseHandler;
public InputHandler(World world) {
Snake snake = world.getSnake();
keyHandler = keyEvent -> {
switch (keyEvent.getCode()) {
case A:
snake.setDirection(snake.getDirection().rotateLeft());
break;
case D:
snake.setDirection(snake.getDirection().rotateRight());
break;
case S:
world.setRunning(!world.isRunning());
break;
}
keyEvent.consume();
};
mouseHandler = mouseEvent -> {
int x = (int) (mouseEvent.getX() / SnakeGame.SCALE);
int y = (int) (mouseEvent.getY() / SnakeGame.SCALE);
if(x >= 0 && y >= 0 && x < world.getSize() && y < world.getSize()) {
world.getFood().moveTo(x, y);
}
mouseEvent.consume();
};
}
public EventHandler<KeyEvent> getKeyHandler() {
return keyHandler;
}
public EventHandler<MouseEvent> getMouseHandler() {
return mouseHandler;
}
}
package snake;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
World world = new World(25);
BorderPane root = new BorderPane();
SnakeGame game = new SnakeGame(world);
Pane ui = SnakeGame.createUserInterface(world);
game.setStyle("-fx-background-color: #30B080;");
ui.setPadding(new Insets(10));
root.setLeft(game);
root.setRight(ui);
Scene scene = new Scene(root);
InputHandler inputHandler = new InputHandler(world);
scene.setOnKeyPressed(inputHandler.getKeyHandler());
scene.setOnMouseClicked(inputHandler.getMouseHandler());
primaryStage.setTitle("Snake");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
package snake;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
/**
* Represents one body part of a snake
*/
public class Segment {
private final IntegerProperty x, y;
public Segment(int x, int y) {
this.x = new SimpleIntegerProperty(x);
this.y = new SimpleIntegerProperty(y);
}
public void setPosition(int x, int y) {
this.x.setValue(x);
this.y.setValue(y);
}
public int getX() {
return x.get();
}
public int getY() {
return y.get();
}
public IntegerProperty getXProperty() {
return x;
}
public IntegerProperty getYProperty() {
return y;
}
}
package snake;
import java.util.LinkedList;
import java.util.List;
/**
* Snake consists of segments, where this head segment keeps track of the other body segments
*/
public class Snake extends Segment {
public interface SnakeSegmentListener {
public void onNewSegment(Segment segment);
}
private Direction direction = Direction.RIGHT;
private final World world;
private final List<Segment> body = new LinkedList<>();
private final List<SnakeSegmentListener> listeners = new LinkedList<>();
public Snake(int x, int y, World world) {
super(x, y);
this.world = world;
}
public void move() {
int newX = getX() + direction.getDX();
int newY = getY() + direction.getDY();
if (isAt(newX, newY) || newX < 0 || newY < 0 || newX >= world.getSize() || newY >= world.getSize()) {
// Bitten itself or the border, game over
world.endGame();
} else {
Food food = world.getFood();
if (food.getX() == newX && food.getY() == newY) {
// Eating fruit, increment score and add new segment before the head
world.setScore(world.getScore() + 1);
world.moveFoodRandomly();
Segment segment = new Segment(getX(), getY());
for (SnakeSegmentListener listener : listeners) {
listener.onNewSegment(segment);
}
body.add(segment);
} else {
// Moving normally, recycle tail and move it before head
if (!body.isEmpty()) {
Segment tail = body.remove(0);
body.add(tail);
tail.setPosition(getX(), getY());
}
}
// Move head to new location
setPosition(newX, newY);
}
}
public void addListener(SnakeSegmentListener listener) {
listeners.add(listener);
}
public void setDirection(Direction newDirection) {
direction = newDirection;
}
public boolean isAt(int x, int y) {
for (Segment segment : body) {
if (segment.getX() == x && segment.getY() == y) {
return true;
}
}
return false;
}
public Direction getDirection() {
return direction;
}
}
package snake;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import snake.Snake.SnakeSegmentListener;
/**
* A JavaFX Pane that displays the snake game represented by the given world
*/
public class SnakeGame extends Pane {
public static final int SCALE = 16;
private class SegmentCallback implements SnakeSegmentListener
{
public void onNewSegment(Segment segment) {
Rectangle body = new Rectangle(SCALE, SCALE, Color.GREEN);
body.translateXProperty().bind(segment.getXProperty().multiply(SCALE));
body.translateYProperty().bind(segment.getYProperty().multiply(SCALE));
getChildren().add(body);
}
}
public SnakeGame(World world) {
setPrefSize(world.getSize() * SCALE, world.getSize() * SCALE);
// Snake
Snake snake = world.getSnake();
Rectangle head = new Rectangle(SCALE, SCALE, Color.RED);
head.translateXProperty().bind(snake.getXProperty().multiply(SCALE));
head.translateYProperty().bind(snake.getYProperty().multiply(SCALE));
getChildren().add(head);
snake.addListener(new SegmentCallback());
// Food
Food food = world.getFood();
Circle circle = new Circle(SCALE / 2F, Color.BLUE);
circle.translateXProperty().bind(food.getXProperty().multiply(SCALE).add(SCALE / 2F));
circle.translateYProperty().bind(food.getYProperty().multiply(SCALE).add(SCALE / 2F));
getChildren().add(circle);
}
public static Pane createUserInterface(World world) {
VBox ui = new VBox();
Label scoreText = new Label();
Label runningText = new Label("Press 's' to start");
scoreText.textProperty().bind(world.getScoreProperty().asString("%d points"));
world.getRunningProperty().addListener((observableValue, aBoolean, t1) -> {
if (t1) {
runningText.textProperty().set("");
} else {
runningText.textProperty().set("Paused");
}
});
ui.getChildren().addAll(scoreText, runningText);
return ui;
}
}
package snake;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.util.Duration;
import java.util.Random;
/**
* World keeps track of the state of a snake game
*/
public class World {
public final static int DELAY = 200;
private final int size;
private final Snake snake;
private final Food food;
private final Random random = new Random();
private final BooleanProperty running = new SimpleBooleanProperty(false);
private final IntegerProperty score = new SimpleIntegerProperty(0);
public World(int size) {
this.size = size;
snake = new Snake(size / 2, size / 2, this);
food = new Food();
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(DELAY), e -> snake.move()));
running.addListener((observableValue, aBoolean, t1) -> {
if (t1) {
timeline.play();
} else {
timeline.pause();
}
});
timeline.setCycleCount(Animation.INDEFINITE);
moveFoodRandomly();
}
public void moveFoodRandomly() {
do {
food.moveTo(random.nextInt(size), random.nextInt(size));
} while (snake.isAt(food.getX(), food.getY()));
}
public void endGame() {
running.set(false);
}
public void setRunning(boolean running) {
this.running.set(running);
}
public void setScore(int score) {
this.score.set(score);
}
public boolean isRunning() {
return running.get();
}
public int getSize() {
return size;
}
public int getScore() {
return score.get();
}
public Snake getSnake() {
return snake;
}
public Food getFood() {
return food;
}
public BooleanProperty getRunningProperty() {
return running;
}
public IntegerProperty getScoreProperty() {
return score;
}
}
...@@ -7,6 +7,7 @@ import \test::SpecUtil; ...@@ -7,6 +7,7 @@ import \test::SpecUtil;
import AssignmentSnakeRules; import AssignmentSnakeRules;
import CodingRules; import CodingRules;
import Util; import Util;
import IO;
test bool aSnakeexampleSolution() test bool aSnakeexampleSolution()
{ {
...@@ -15,6 +16,14 @@ test bool aSnakeexampleSolution() ...@@ -15,6 +16,14 @@ test bool aSnakeexampleSolution()
return isEmpty(errors); return isEmpty(errors);
} }
test bool aSnakeGraphicsInInnerClass()
{
M3 model = loadTestProject("assignment-snake-graphics-in-inner-class");
errors = allAssignmentSnakeRules(model) + allCodingRules(model);
for( error <- errors ) println(error);
return isEmpty(errors);
}
test bool aSnakegraphicsNotInView() test bool aSnakegraphicsNotInView()
{ {
M3 model = loadTestProject("assignment-snake-graphics-not-in-view"); M3 model = loadTestProject("assignment-snake-graphics-not-in-view");
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment