Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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`).
Expand Down
50 changes: 50 additions & 0 deletions config/checkstyle/checkstyle.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<property name="severity" value="error"/>
<property name="charset" value="UTF-8"/>

<module name="NewlineAtEndOfFile"/>
<module name="FileTabCharacter"/>
<module name="LineLength">
<property name="max" value="140"/>
<property name="ignorePattern" value="^package.*|^import.*"/>
</module>

<module name="TreeWalker">
<!-- Imports -->
<module name="AvoidStarImport"/>
<module name="UnusedImports"/>
<module name="RedundantImport"/>

<!-- Naming -->
<module name="TypeName"/>
<module name="MethodName"/>
<module name="ConstantName"/>
<module name="LocalVariableName"/>
<module name="ParameterName"/>
<module name="MemberName"/>

<!-- Coding -->
<module name="FallThrough"/>
<module name="OneStatementPerLine"/>
<module name="MultipleVariableDeclarations"/>
<module name="ModifierOrder"/>
<module name="UpperEll"/>
<module name="ArrayTypeStyle"/>
<module name="DefaultComesLast"/>
<module name="SimplifyBooleanExpression"/>
<module name="SimplifyBooleanReturn"/>
<module name="StringLiteralEquality"/>

<!-- Blocks -->
<module name="NeedBraces"/>
<module name="LeftCurly"/>
<module name="RightCurly"/>
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
</module>
</module>
</module>
15 changes: 15 additions & 0 deletions config/spotbugs/exclude.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
<!-- Swing components are Serializable but never serialized in this app -->
<Match>
<Class name="com.mapna.snake.Board"/>
<Bug pattern="SE_BAD_FIELD,SE_BAD_FIELD_STORE"/>
</Match>

<!-- GameState is a mutable state holder — sharing the Snake reference is intentional -->
<Match>
<Class name="com.mapna.snake.GameState"/>
<Bug pattern="EI_EXPOSE_REP,EI_EXPOSE_REP2"/>
</Match>

</FindBugsFilter>
39 changes: 38 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

<groupId>com.mapna</groupId>
<artifactId>javasnake</artifactId>
<version>1.0.0</version>
<version>1.1.0</version>
<name>JavaSnake</name>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.11.4</junit.version>
<checkstyle.plugin.version>3.4.0</checkstyle.plugin.version>
<spotbugs.plugin.version>4.8.3.1</spotbugs.plugin.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -56,6 +58,41 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${checkstyle.plugin.version}</version>
<configuration>
<configLocation>config/checkstyle/checkstyle.xml</configLocation>
<consoleOutput>true</consoleOutput>
<failsOnError>true</failsOnError>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
</configuration>
<executions>
<execution>
<id>checkstyle</id>
<phase>verify</phase>
<goals><goal>check</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>${spotbugs.plugin.version}</version>
<configuration>
<effort>Max</effort>
<threshold>Medium</threshold>
<excludeFilterFile>config/spotbugs/exclude.xml</excludeFilterFile>
</configuration>
<executions>
<execution>
<id>spotbugs</id>
<phase>verify</phase>
<goals><goal>check</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
Expand Down
11 changes: 9 additions & 2 deletions src/main/java/com/mapna/snake/Board.java
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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();
}
Expand Down Expand Up @@ -70,6 +76,7 @@ public void keyPressed(KeyEvent e) {
}
}
case KeyEvent.VK_ESCAPE -> SwingUtilities.getWindowAncestor(Board.this).dispose();
default -> { }
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/mapna/snake/BoardConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
27 changes: 18 additions & 9 deletions src/main/java/com/mapna/snake/BoardRenderer.java
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/mapna/snake/Game.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.mapna.snake;

import javax.swing.*;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class Game {

Expand Down
9 changes: 4 additions & 5 deletions src/main/java/com/mapna/snake/GameEngine.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.mapna.snake;

import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -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);
Expand All @@ -72,11 +71,11 @@ public void tick(GameState state) {

private void spawnFood(GameState state) {
Snake snake = state.getSnake();
List<Point> free = new ArrayList<>();
List<Position> 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);
}
}
Expand Down
8 changes: 3 additions & 5 deletions src/main/java/com/mapna/snake/GameState.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/mapna/snake/Position.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.mapna.snake;

public record Position(int x, int y) {
}
Loading
Loading