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`/ ![Game over screenshot](docs/images/screenshot3.png) +## 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))); } }