diff --git a/README.md b/README.md
index 758f0d7..a756775 100644
--- a/README.md
+++ b/README.md
@@ -38,22 +38,28 @@ Run unit tests:
mvn test
```
+Run tests with Checkstyle and SpotBugs checks:
+
+```bash
+mvn verify
+```
+
### Without Maven
If you only have the JDK, compile every `.java` file under `src/main/java`, then run `Game`:
-**macOS / Linux (shell expands the glob):**
+**macOS / Linux:**
```bash
-javac -d out -encoding UTF-8 src/main/java/*.java
-java -cp out Game
+javac -d out -encoding UTF-8 src/main/java/com/mapna/snake/*.java
+java -cp out com.mapna.snake.Game
```
**Windows (PowerShell):**
```powershell
-javac -d out -encoding UTF-8 (Get-ChildItem -Path src\main\java\*.java).FullName
-java -cp out Game
+javac -d out -encoding UTF-8 (Get-ChildItem -Recurse -Path src\main\java\*.java).FullName
+java -cp out com.mapna.snake.Game
```
The window icon loads from the classpath when run via Maven; with plain `javac`/`java`, the app falls back to `src/main/resources/images/icon.png` on disk.
@@ -64,7 +70,7 @@ The window icon loads from the classpath when run via Maven; with plain `javac`/
|--------|------|
| Move | **W A S D** or **arrow keys** |
| Pause / resume | **P** |
-| Restart (after game over) | **R** |
+| Restart (after game over or win) | **R** |
| Quit | **Esc** |
## Screenshots
@@ -87,6 +93,10 @@ The window icon loads from the classpath when run via Maven; with plain `javac`/

+## Gameplay
+
+The snake speeds up as it grows — after eating 10 pieces of food the tick rate begins to decrease, making the game progressively harder. Fill the entire board to win.
+
## High score
The best score is stored in **`highscore.txt`** in the process **working directory** (usually the folder you run the game from). That file is ignored by Git (see `.gitignore`).
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
new file mode 100644
index 0000000..8490b79
--- /dev/null
+++ b/config/checkstyle/checkstyle.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml
new file mode 100644
index 0000000..206e383
--- /dev/null
+++ b/config/spotbugs/exclude.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
index ba0edeb..0b10c4d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
com.mapna
javasnake
- 1.0.0
+ 1.1.0
JavaSnake
@@ -13,6 +13,8 @@
21
UTF-8
5.11.4
+ 3.4.0
+ 4.8.3.1
@@ -56,6 +58,41 @@
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ ${checkstyle.plugin.version}
+
+ config/checkstyle/checkstyle.xml
+ true
+ true
+ true
+
+
+
+ checkstyle
+ verify
+ check
+
+
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ ${spotbugs.plugin.version}
+
+ Max
+ Medium
+ config/spotbugs/exclude.xml
+
+
+
+ spotbugs
+ verify
+ check
+
+
+
org.codehaus.mojo
exec-maven-plugin
diff --git a/src/main/java/com/mapna/snake/Board.java b/src/main/java/com/mapna/snake/Board.java
index d059581..434605b 100644
--- a/src/main/java/com/mapna/snake/Board.java
+++ b/src/main/java/com/mapna/snake/Board.java
@@ -1,11 +1,15 @@
package com.mapna.snake;
-import javax.swing.*;
-import java.awt.*;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
public class Board extends JPanel implements ActionListener {
private final Timer timer = new Timer(BoardConfig.TICK_RATE_MS, this);
@@ -28,12 +32,14 @@ private void initBoard() {
engine.reset(state);
state.setHighScore(highScoreStore.load());
highScoreSaved = false;
+ timer.setDelay(BoardConfig.TICK_RATE_MS);
timer.start();
}
@Override
public void actionPerformed(ActionEvent e) {
engine.tick(state);
+ timer.setDelay(BoardConfig.tickRateMs(state.getSnake().growth()));
if (state.getMode() == GameMode.PAUSED || state.getMode() == GameMode.GAME_OVER || state.getMode() == GameMode.WON) {
timer.stop();
}
@@ -70,6 +76,7 @@ public void keyPressed(KeyEvent e) {
}
}
case KeyEvent.VK_ESCAPE -> SwingUtilities.getWindowAncestor(Board.this).dispose();
+ default -> { }
}
}
}
diff --git a/src/main/java/com/mapna/snake/BoardConfig.java b/src/main/java/com/mapna/snake/BoardConfig.java
index de8905b..01bea57 100644
--- a/src/main/java/com/mapna/snake/BoardConfig.java
+++ b/src/main/java/com/mapna/snake/BoardConfig.java
@@ -2,6 +2,9 @@
public final class BoardConfig {
public static final int TICK_RATE_MS = 80;
+ public static final int MIN_TICK_RATE_MS = 40;
+ public static final int SPEEDUP_THRESHOLD = 10;
+ public static final int SPEED_STEP_MS = 2;
public static final int BOARD_WIDTH = 480;
public static final int BOARD_HEIGHT = 480;
public static final int PIXEL_SIZE = 24;
@@ -14,4 +17,9 @@ public final class BoardConfig {
private BoardConfig() {
}
+
+ public static int tickRateMs(int growth) {
+ int speedups = Math.max(0, growth - SPEEDUP_THRESHOLD);
+ return Math.max(MIN_TICK_RATE_MS, TICK_RATE_MS - speedups * SPEED_STEP_MS);
+ }
}
diff --git a/src/main/java/com/mapna/snake/BoardRenderer.java b/src/main/java/com/mapna/snake/BoardRenderer.java
index dc617f3..1fc9e0d 100644
--- a/src/main/java/com/mapna/snake/BoardRenderer.java
+++ b/src/main/java/com/mapna/snake/BoardRenderer.java
@@ -1,6 +1,9 @@
package com.mapna.snake;
-import java.awt.*;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
public class BoardRenderer {
private static final Font TITLE_FONT = new Font("Arial", Font.BOLD, 64);
@@ -40,18 +43,22 @@ private void paintScoreOverlay(Graphics g, GameState state) {
g.setFont(CAPTION_FONT);
g.setColor(Color.white);
- g.drawString(highScoreText, (BoardConfig.BOARD_WIDTH - g.getFontMetrics().stringWidth(highScoreText)) / 2, BoardConfig.COMPONENT_HEIGHT / 4);
+ int hsX = (BoardConfig.BOARD_WIDTH - g.getFontMetrics().stringWidth(highScoreText)) / 2;
+ g.drawString(highScoreText, hsX, BoardConfig.COMPONENT_HEIGHT / 4);
g.setColor(Color.yellow);
- g.drawString(scoreText, (BoardConfig.BOARD_WIDTH - g.getFontMetrics().stringWidth(scoreText)) / 2, BoardConfig.COMPONENT_HEIGHT / 8);
+ int scoreX = (BoardConfig.BOARD_WIDTH - g.getFontMetrics().stringWidth(scoreText)) / 2;
+ g.drawString(scoreText, scoreX, BoardConfig.COMPONENT_HEIGHT / 8);
}
private void paintTitles(Graphics g, String title, String caption) {
g.setFont(TITLE_FONT);
- g.drawString(title, (BoardConfig.BOARD_WIDTH - g.getFontMetrics(TITLE_FONT).stringWidth(title)) / 2, BoardConfig.COMPONENT_HEIGHT / 2);
+ int titleX = (BoardConfig.BOARD_WIDTH - g.getFontMetrics(TITLE_FONT).stringWidth(title)) / 2;
+ g.drawString(title, titleX, BoardConfig.COMPONENT_HEIGHT / 2);
g.setColor(Color.white);
g.setFont(CAPTION_FONT);
- g.drawString(caption, (BoardConfig.BOARD_WIDTH - g.getFontMetrics(CAPTION_FONT).stringWidth(caption)) / 2, BoardConfig.COMPONENT_HEIGHT * 5 / 8);
+ int captionX = (BoardConfig.BOARD_WIDTH - g.getFontMetrics(CAPTION_FONT).stringWidth(caption)) / 2;
+ g.drawString(caption, captionX, BoardConfig.COMPONENT_HEIGHT * 5 / 8);
}
private void paintGameContent(Graphics g, GameState state, Color hudColor, Color foodColor, Color snakeColor) {
@@ -63,13 +70,15 @@ private void paintGameContent(Graphics g, GameState state, Color hudColor, Color
g2D.setPaint(Color.black);
paintScore(g2D, state.getSnake().growth());
- Point food = state.getFood();
+ Position food = state.getFood();
g2D.setPaint(foodColor);
- g2D.fillRect(food.x * BoardConfig.PIXEL_SIZE, food.y * BoardConfig.PIXEL_SIZE, BoardConfig.BORDERED_PIXEL_SIZE, BoardConfig.BORDERED_PIXEL_SIZE);
+ g2D.fillRect(food.x() * BoardConfig.PIXEL_SIZE, food.y() * BoardConfig.PIXEL_SIZE,
+ BoardConfig.BORDERED_PIXEL_SIZE, BoardConfig.BORDERED_PIXEL_SIZE);
g2D.setPaint(snakeColor);
- for (Point point : state.getSnake().getBody()) {
- g2D.fillRect(point.x * BoardConfig.PIXEL_SIZE, point.y * BoardConfig.PIXEL_SIZE, BoardConfig.BORDERED_PIXEL_SIZE, BoardConfig.BORDERED_PIXEL_SIZE);
+ for (Position point : state.getSnake().getBody()) {
+ g2D.fillRect(point.x() * BoardConfig.PIXEL_SIZE, point.y() * BoardConfig.PIXEL_SIZE,
+ BoardConfig.BORDERED_PIXEL_SIZE, BoardConfig.BORDERED_PIXEL_SIZE);
}
}
diff --git a/src/main/java/com/mapna/snake/Game.java b/src/main/java/com/mapna/snake/Game.java
index 30f841a..123c7db 100644
--- a/src/main/java/com/mapna/snake/Game.java
+++ b/src/main/java/com/mapna/snake/Game.java
@@ -1,6 +1,7 @@
package com.mapna.snake;
-import javax.swing.*;
+import javax.swing.JFrame;
+import javax.swing.SwingUtilities;
public class Game {
diff --git a/src/main/java/com/mapna/snake/GameEngine.java b/src/main/java/com/mapna/snake/GameEngine.java
index e43ed79..f37fc51 100644
--- a/src/main/java/com/mapna/snake/GameEngine.java
+++ b/src/main/java/com/mapna/snake/GameEngine.java
@@ -1,6 +1,5 @@
package com.mapna.snake;
-import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -50,7 +49,7 @@ public void tick(GameState state) {
}
Snake snake = state.getSnake();
- Point head = snake.nextHead(nextDirection, BoardConfig.PIXEL_WIDTH, BoardConfig.PIXEL_HEIGHT);
+ Position head = snake.nextHead(nextDirection, BoardConfig.PIXEL_WIDTH, BoardConfig.PIXEL_HEIGHT);
boolean growing = head.equals(state.getFood());
state.setDirection(nextDirection);
@@ -72,11 +71,11 @@ public void tick(GameState state) {
private void spawnFood(GameState state) {
Snake snake = state.getSnake();
- List free = new ArrayList<>();
+ List free = new ArrayList<>();
for (int x = 0; x < BoardConfig.PIXEL_WIDTH; x++) {
for (int y = 0; y < BoardConfig.PIXEL_HEIGHT; y++) {
- Point p = new Point(x, y);
- if (!snake.containsPoint(p)) {
+ Position p = new Position(x, y);
+ if (!snake.contains(p)) {
free.add(p);
}
}
diff --git a/src/main/java/com/mapna/snake/GameState.java b/src/main/java/com/mapna/snake/GameState.java
index fb720e0..a026097 100644
--- a/src/main/java/com/mapna/snake/GameState.java
+++ b/src/main/java/com/mapna/snake/GameState.java
@@ -1,10 +1,8 @@
package com.mapna.snake;
-import java.awt.*;
-
public class GameState {
private Snake snake;
- private Point food = new Point();
+ private Position food = new Position(0, 0);
private Direction direction = Direction.UP;
private GameMode mode = GameMode.RUNNING;
private int highScore = -1;
@@ -17,11 +15,11 @@ public void setSnake(Snake snake) {
this.snake = snake;
}
- public Point getFood() {
+ public Position getFood() {
return food;
}
- public void setFood(Point food) {
+ public void setFood(Position food) {
this.food = food;
}
diff --git a/src/main/java/com/mapna/snake/Position.java b/src/main/java/com/mapna/snake/Position.java
new file mode 100644
index 0000000..dbbfc99
--- /dev/null
+++ b/src/main/java/com/mapna/snake/Position.java
@@ -0,0 +1,4 @@
+package com.mapna.snake;
+
+public record Position(int x, int y) {
+}
diff --git a/src/main/java/com/mapna/snake/Snake.java b/src/main/java/com/mapna/snake/Snake.java
index 252ae16..bde202d 100644
--- a/src/main/java/com/mapna/snake/Snake.java
+++ b/src/main/java/com/mapna/snake/Snake.java
@@ -1,6 +1,5 @@
package com.mapna.snake;
-import java.awt.*;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
@@ -10,44 +9,44 @@
public class Snake {
private static final int INITIAL_LENGTH = 3;
- private final LinkedList body = new LinkedList<>();
- private final List unmodifiableBody = Collections.unmodifiableList(body);
- private final Set occupied = new HashSet<>();
+ private final LinkedList body = new LinkedList<>();
+ private final List unmodifiableBody = Collections.unmodifiableList(body);
+ private final Set occupied = new HashSet<>();
public Snake(Random random, int width, int height) {
int x = random.nextInt(width);
int y = random.nextInt(height - INITIAL_LENGTH);
- addSegment(new Point(x, y));
- addSegment(new Point(x, y + 1));
- addSegment(new Point(x, y + 2));
+ addSegment(new Position(x, y));
+ addSegment(new Position(x, y + 1));
+ addSegment(new Position(x, y + 2));
}
/** Creates a length-3 vertical snake at the given head position. */
public static Snake createFixed(int headX, int headY) {
- return new Snake(new Point(headX, headY), new Point(headX, headY + 1), new Point(headX, headY + 2));
+ return new Snake(new Position(headX, headY), new Position(headX, headY + 1), new Position(headX, headY + 2));
}
- private Snake(Point head, Point seg2, Point seg3) {
+ private Snake(Position head, Position seg2, Position seg3) {
addSegment(head);
addSegment(seg2);
addSegment(seg3);
}
- private void addSegment(Point p) {
+ private void addSegment(Position p) {
body.add(p);
occupied.add(p);
}
- public Point getHead() {
+ public Position getHead() {
return body.getFirst();
}
- public List getBody() {
+ public List getBody() {
return unmodifiableBody;
}
- public boolean containsPoint(Point point) {
+ public boolean contains(Position point) {
return occupied.contains(point);
}
@@ -58,17 +57,17 @@ public boolean eatingSelf() {
}
/** Returns the next head position without mutating state. */
- public Point nextHead(Direction direction, int boardWidth, int boardHeight) {
- Point head = body.getFirst();
+ public Position nextHead(Direction direction, int boardWidth, int boardHeight) {
+ Position head = body.getFirst();
return switch (direction) {
- case DOWN -> new Point(head.x, (head.y + 1) % boardHeight);
- case UP -> new Point(head.x, (head.y - 1 + boardHeight) % boardHeight);
- case LEFT -> new Point((head.x - 1 + boardWidth) % boardWidth, head.y);
- case RIGHT -> new Point((head.x + 1) % boardWidth, head.y);
+ case DOWN -> new Position(head.x(), (head.y() + 1) % boardHeight);
+ case UP -> new Position(head.x(), (head.y() - 1 + boardHeight) % boardHeight);
+ case LEFT -> new Position((head.x() - 1 + boardWidth) % boardWidth, head.y());
+ case RIGHT -> new Position((head.x() + 1) % boardWidth, head.y());
};
}
- public void move(Point newHead, boolean growing) {
+ public void move(Position newHead, boolean growing) {
if (!growing) {
occupied.remove(body.removeLast());
}
diff --git a/src/main/java/com/mapna/snake/Window.java b/src/main/java/com/mapna/snake/Window.java
index df4caa9..96471d2 100644
--- a/src/main/java/com/mapna/snake/Window.java
+++ b/src/main/java/com/mapna/snake/Window.java
@@ -1,9 +1,9 @@
package com.mapna.snake;
-import javax.swing.*;
-import java.awt.*;
+import java.awt.Toolkit;
import java.net.URL;
import java.nio.file.Path;
+import javax.swing.JFrame;
public class Window extends JFrame {
public Window() {
diff --git a/src/test/java/com/mapna/snake/BoardConfigTest.java b/src/test/java/com/mapna/snake/BoardConfigTest.java
new file mode 100644
index 0000000..51619c0
--- /dev/null
+++ b/src/test/java/com/mapna/snake/BoardConfigTest.java
@@ -0,0 +1,26 @@
+package com.mapna.snake;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class BoardConfigTest {
+
+ @Test
+ void tickRateUnchangedBelowThreshold() {
+ for (int g = 0; g <= BoardConfig.SPEEDUP_THRESHOLD; g++) {
+ assertEquals(BoardConfig.TICK_RATE_MS, BoardConfig.tickRateMs(g));
+ }
+ }
+
+ @Test
+ void tickRateDecreasesAboveThreshold() {
+ int g = BoardConfig.SPEEDUP_THRESHOLD + 1;
+ assertEquals(BoardConfig.TICK_RATE_MS - BoardConfig.SPEED_STEP_MS, BoardConfig.tickRateMs(g));
+ }
+
+ @Test
+ void tickRateFloorsAtMinimum() {
+ assertEquals(BoardConfig.MIN_TICK_RATE_MS, BoardConfig.tickRateMs(1000));
+ }
+}
diff --git a/src/test/java/com/mapna/snake/GameEngineTest.java b/src/test/java/com/mapna/snake/GameEngineTest.java
index 507c53b..88e4db6 100644
--- a/src/test/java/com/mapna/snake/GameEngineTest.java
+++ b/src/test/java/com/mapna/snake/GameEngineTest.java
@@ -2,7 +2,6 @@
import org.junit.jupiter.api.Test;
-import java.awt.Point;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -56,7 +55,7 @@ void togglePauseCyclesRunningAndPaused() {
void tickDoesNothingWhenPaused() {
GameEngine engine = new GameEngine(new Random(1L));
Snake snake = Snake.createFixed(10, 10);
- Point headBefore = new Point(snake.getHead());
+ Position headBefore = snake.getHead();
GameState state = runningState(snake);
state.setMode(GameMode.PAUSED);
@@ -70,11 +69,11 @@ void tickDoesNothingWhenPaused() {
void tickMovesHeadAccordingToNextDirection() {
GameEngine engine = new GameEngine(new Random(1L));
GameState state = runningState(Snake.createFixed(10, 10));
- state.setFood(new Point(0, 0));
+ state.setFood(new Position(0, 0));
engine.tick(state);
- assertEquals(new Point(10, 9), state.getSnake().getHead());
+ assertEquals(new Position(10, 9), state.getSnake().getHead());
assertEquals(Direction.UP, state.getDirection());
}
@@ -82,25 +81,25 @@ void tickMovesHeadAccordingToNextDirection() {
void tickWrapsVerticallyAtTopEdge() {
GameEngine engine = new GameEngine(new Random(1L));
GameState state = runningState(Snake.createFixed(10, 0));
- state.setFood(new Point(0, 0));
+ state.setFood(new Position(0, 0));
engine.tick(state);
- assertEquals(new Point(10, BoardConfig.PIXEL_HEIGHT - 1), state.getSnake().getHead());
+ assertEquals(new Position(10, BoardConfig.PIXEL_HEIGHT - 1), state.getSnake().getHead());
}
@Test
void tickEatingFoodGrowsSnakeAndRespawnsFood() {
GameEngine engine = new GameEngine(new Random(42L));
GameState state = runningState(Snake.createFixed(10, 10));
- state.setFood(new Point(10, 9));
+ state.setFood(new Position(10, 9));
engine.tick(state);
- assertEquals(new Point(10, 9), state.getSnake().getHead());
+ assertEquals(new Position(10, 9), state.getSnake().getHead());
assertEquals(4, state.getSnake().getBody().size());
assertEquals(1, state.getSnake().growth());
- assertFalse(state.getSnake().containsPoint(state.getFood()));
+ assertFalse(state.getSnake().contains(state.getFood()));
}
@Test
@@ -109,7 +108,7 @@ void tickDetectsSelfCollision() {
GameState state = runningState(Snake.createFixed(5, 5));
state.setDirection(Direction.DOWN);
engine.requestDirection(state, Direction.DOWN);
- state.setFood(new Point(0, 0));
+ state.setFood(new Position(0, 0));
engine.tick(state);
@@ -122,12 +121,12 @@ void tickAllowsMovingToVacatedTailPosition() {
GameState state = runningState(Snake.createFixed(5, 5));
// Grow snake to length 4 by eating food
- state.setFood(new Point(5, 4));
+ state.setFood(new Position(5, 4));
engine.tick(state);
assertEquals(4, state.getSnake().getBody().size());
// Navigate a tight U-turn where head lands on the vacated tail position
- state.setFood(new Point(0, 0));
+ state.setFood(new Position(0, 0));
engine.requestDirection(state, Direction.RIGHT);
engine.tick(state); // (6,4), (5,4), (5,5), (5,6)
@@ -138,7 +137,7 @@ void tickAllowsMovingToVacatedTailPosition() {
engine.tick(state); // head → (5,5), old tail was at (5,5)
assertEquals(GameMode.RUNNING, state.getMode());
- assertEquals(new Point(5, 5), state.getSnake().getHead());
+ assertEquals(new Position(5, 5), state.getSnake().getHead());
}
@Test
@@ -152,6 +151,83 @@ void resetStartsRunningAndPlacesFoodOffSnake() {
assertEquals(Direction.UP, state.getDirection());
assertEquals(Direction.UP, engine.getNextDirection());
assertEquals(3, state.getSnake().getBody().size());
- assertFalse(state.getSnake().containsPoint(state.getFood()));
+ assertFalse(state.getSnake().contains(state.getFood()));
+ }
+
+ @Test
+ void tickWinsWhenSnakeFillsBoard() {
+ int w = BoardConfig.PIXEL_WIDTH;
+ int h = BoardConfig.PIXEL_HEIGHT;
+ int total = w * h;
+
+ GameEngine engine = new GameEngine(new Random(1L));
+ Snake snake = Snake.createFixed(w - 1, h - 3);
+ GameState state = runningState(snake);
+
+ Position food = new Position(0, 0);
+ Position finalHead = new Position(0, 1);
+
+ for (int y = 0; y < h; y++) {
+ for (int x = 0; x < w; x++) {
+ Position p = new Position(x, y);
+ if (p.equals(food) || p.equals(finalHead) || snake.contains(p)) {
+ continue;
+ }
+ snake.move(p, true);
+ }
+ }
+ snake.move(finalHead, true);
+
+ assertEquals(total - 1, snake.getBody().size());
+
+ state.setFood(food);
+ engine.tick(state);
+
+ assertEquals(GameMode.WON, state.getMode());
+ assertEquals(total, snake.getBody().size());
+ }
+
+ @Test
+ void fullGameLifecycle() {
+ GameEngine engine = new GameEngine(new Random(42L));
+ GameState state = runningState(Snake.createFixed(10, 10));
+
+ // Eat three foods heading up — snake grows from 3 to 6
+ state.setFood(new Position(10, 9));
+ engine.tick(state);
+ assertEquals(4, state.getSnake().getBody().size());
+ assertFalse(state.getSnake().contains(state.getFood()));
+
+ state.setFood(new Position(10, 8));
+ engine.tick(state);
+ assertEquals(5, state.getSnake().getBody().size());
+
+ state.setFood(new Position(10, 7));
+ engine.tick(state);
+ assertEquals(6, state.getSnake().getBody().size());
+ assertEquals(3, state.getSnake().growth());
+ assertEquals(GameMode.RUNNING, state.getMode());
+
+ // U-turn: right, down, left — head lands on an occupied segment
+ state.setFood(new Position(0, 0));
+ engine.requestDirection(state, Direction.RIGHT);
+ engine.tick(state);
+ assertEquals(new Position(11, 7), state.getSnake().getHead());
+ assertEquals(GameMode.RUNNING, state.getMode());
+
+ engine.requestDirection(state, Direction.DOWN);
+ engine.tick(state);
+ assertEquals(new Position(11, 8), state.getSnake().getHead());
+ assertEquals(GameMode.RUNNING, state.getMode());
+
+ engine.requestDirection(state, Direction.LEFT);
+ engine.tick(state);
+ assertEquals(GameMode.GAME_OVER, state.getMode());
+
+ // Reset restores a playable state
+ engine.reset(state);
+ assertEquals(GameMode.RUNNING, state.getMode());
+ assertEquals(3, state.getSnake().getBody().size());
+ assertFalse(state.getSnake().contains(state.getFood()));
}
}
diff --git a/src/test/java/com/mapna/snake/SnakeTest.java b/src/test/java/com/mapna/snake/SnakeTest.java
index e0a82f2..9ec67a0 100644
--- a/src/test/java/com/mapna/snake/SnakeTest.java
+++ b/src/test/java/com/mapna/snake/SnakeTest.java
@@ -2,10 +2,12 @@
import org.junit.jupiter.api.Test;
-import java.awt.Point;
import java.util.Random;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
class SnakeTest {
@@ -14,9 +16,9 @@ void createFixedPlacesThreeSegmentsVertically() {
Snake snake = Snake.createFixed(5, 3);
assertEquals(3, snake.getBody().size());
- assertEquals(new Point(5, 3), snake.getHead());
- assertEquals(new Point(5, 4), snake.getBody().get(1));
- assertEquals(new Point(5, 5), snake.getBody().get(2));
+ assertEquals(new Position(5, 3), snake.getHead());
+ assertEquals(new Position(5, 4), snake.getBody().get(1));
+ assertEquals(new Position(5, 5), snake.getBody().get(2));
}
@Test
@@ -24,26 +26,26 @@ void randomConstructorPlacesWithinBounds() {
Snake snake = new Snake(new Random(42), 20, 20);
assertEquals(3, snake.getBody().size());
- for (Point p : snake.getBody()) {
- assertTrue(p.x >= 0 && p.x < 20);
- assertTrue(p.y >= 0 && p.y < 20);
+ for (Position p : snake.getBody()) {
+ assertTrue(p.x() >= 0 && p.x() < 20);
+ assertTrue(p.y() >= 0 && p.y() < 20);
}
}
@Test
- void containsPointMatchesBodySegments() {
+ void containsMatchesBodyPositions() {
Snake snake = Snake.createFixed(5, 5);
- assertTrue(snake.containsPoint(new Point(5, 5)));
- assertTrue(snake.containsPoint(new Point(5, 6)));
- assertTrue(snake.containsPoint(new Point(5, 7)));
- assertFalse(snake.containsPoint(new Point(0, 0)));
+ assertTrue(snake.contains(new Position(5, 5)));
+ assertTrue(snake.contains(new Position(5, 6)));
+ assertTrue(snake.contains(new Position(5, 7)));
+ assertFalse(snake.contains(new Position(0, 0)));
}
@Test
void nextHeadDoesNotMutateSnake() {
Snake snake = Snake.createFixed(5, 5);
- Point originalHead = new Point(snake.getHead());
+ Position originalHead = snake.getHead();
snake.nextHead(Direction.UP, 20, 20);
@@ -54,48 +56,48 @@ void nextHeadDoesNotMutateSnake() {
@Test
void nextHeadWrapsAtTopEdge() {
Snake snake = Snake.createFixed(5, 0);
- assertEquals(new Point(5, 19), snake.nextHead(Direction.UP, 20, 20));
+ assertEquals(new Position(5, 19), snake.nextHead(Direction.UP, 20, 20));
}
@Test
void nextHeadWrapsAtBottomEdge() {
Snake snake = Snake.createFixed(5, 19);
- assertEquals(new Point(5, 0), snake.nextHead(Direction.DOWN, 20, 20));
+ assertEquals(new Position(5, 0), snake.nextHead(Direction.DOWN, 20, 20));
}
@Test
void nextHeadWrapsAtLeftEdge() {
Snake snake = Snake.createFixed(0, 5);
- assertEquals(new Point(19, 5), snake.nextHead(Direction.LEFT, 20, 20));
+ assertEquals(new Position(19, 5), snake.nextHead(Direction.LEFT, 20, 20));
}
@Test
void nextHeadWrapsAtRightEdge() {
Snake snake = Snake.createFixed(19, 5);
- assertEquals(new Point(0, 5), snake.nextHead(Direction.RIGHT, 20, 20));
+ assertEquals(new Position(0, 5), snake.nextHead(Direction.RIGHT, 20, 20));
}
@Test
void moveWithoutGrowingRemovesTail() {
Snake snake = Snake.createFixed(5, 5);
- Point oldTail = snake.getBody().getLast();
+ Position oldTail = snake.getBody().getLast();
- snake.move(new Point(5, 4), false);
+ snake.move(new Position(5, 4), false);
assertEquals(3, snake.getBody().size());
- assertEquals(new Point(5, 4), snake.getHead());
- assertFalse(snake.containsPoint(oldTail));
+ assertEquals(new Position(5, 4), snake.getHead());
+ assertFalse(snake.contains(oldTail));
}
@Test
void moveWithGrowingKeepsTail() {
Snake snake = Snake.createFixed(5, 5);
- snake.move(new Point(5, 4), true);
+ snake.move(new Position(5, 4), true);
assertEquals(4, snake.getBody().size());
- assertEquals(new Point(5, 4), snake.getHead());
- assertTrue(snake.containsPoint(new Point(5, 7)));
+ assertEquals(new Position(5, 4), snake.getHead());
+ assertTrue(snake.contains(new Position(5, 7)));
}
@Test
@@ -107,9 +109,9 @@ void eatingSelfFalseInitially() {
void eatingSelfDetectsOverlap() {
Snake snake = Snake.createFixed(5, 5);
// Grow to length 4
- snake.move(new Point(5, 4), true);
+ snake.move(new Position(5, 4), true);
// Move head onto an existing body segment
- snake.move(new Point(5, 5), false);
+ snake.move(new Position(5, 5), false);
assertTrue(snake.eatingSelf());
}
@@ -117,13 +119,13 @@ void eatingSelfDetectsOverlap() {
@Test
void moveToVacatedTailIsNotSelfCollision() {
Snake snake = Snake.createFixed(5, 5);
- snake.move(new Point(5, 4), true); // len 4: (5,4),(5,5),(5,6),(5,7)
- snake.move(new Point(6, 4), false); // len 4: (6,4),(5,4),(5,5),(5,6)
- snake.move(new Point(6, 5), false); // len 4: (6,5),(6,4),(5,4),(5,5)
- snake.move(new Point(6, 6), false); // len 4: (6,6),(6,5),(6,4),(5,4)
+ snake.move(new Position(5, 4), true); // len 4: (5,4),(5,5),(5,6),(5,7)
+ snake.move(new Position(6, 4), false); // len 4: (6,4),(5,4),(5,5),(5,6)
+ snake.move(new Position(6, 5), false); // len 4: (6,5),(6,4),(5,4),(5,5)
+ snake.move(new Position(6, 6), false); // len 4: (6,6),(6,5),(6,4),(5,4)
// (5,4) is the current tail — this move vacates it, then places the head there
- snake.move(new Point(5, 4), false);
+ snake.move(new Position(5, 4), false);
assertFalse(snake.eatingSelf());
}
@@ -133,16 +135,16 @@ void growthReturnsSegmentsBeyondInitialLength() {
Snake snake = Snake.createFixed(5, 5);
assertEquals(0, snake.growth());
- snake.move(new Point(5, 4), true);
+ snake.move(new Position(5, 4), true);
assertEquals(1, snake.growth());
- snake.move(new Point(5, 3), true);
+ snake.move(new Position(5, 3), true);
assertEquals(2, snake.growth());
}
@Test
void bodyListIsUnmodifiable() {
Snake snake = Snake.createFixed(5, 5);
- assertThrows(UnsupportedOperationException.class, () -> snake.getBody().add(new Point(0, 0)));
+ assertThrows(UnsupportedOperationException.class, () -> snake.getBody().add(new Position(0, 0)));
}
}