From 107dec6aed1166460ef5b5389edb9290c25be910 Mon Sep 17 00:00:00 2001 From: Klim Sadov Date: Tue, 24 Mar 2026 22:31:18 +0700 Subject: [PATCH 01/10] init --- .../ru/nsu/ksadov/find/task_2_3_1/Bot.java | 6 + .../ksadov/find/task_2_3_1/Controller.java | 133 ++++++++++++++++ .../nsu/ksadov/find/task_2_3_1/Direction.java | 6 + .../nsu/ksadov/find/task_2_3_1/GameState.java | 150 ++++++++++++++++++ .../find/task_2_3_1/HelloApplication.java | 19 +++ .../ru/nsu/ksadov/find/task_2_3_1/Point.java | 4 + .../find/task_2_3_1/RandomSnakeBot.java | 27 ++++ .../ksadov/find/task_2_3_1/SmartSnakeBot.java | 29 ++++ .../ru/nsu/ksadov/find/task_2_3_1/Snake.java | 64 ++++++++ .../ksadov/find/task_2_3_1/hello-view.fxml | 11 ++ .../ksadov/find/task_2_3_1/DirectionTest.java | 16 ++ .../ksadov/find/task_2_3_1/GameStateTest.java | 63 ++++++++ .../nsu/ksadov/find/task_2_3_1/PointTest.java | 24 +++ .../find/task_2_3_1/RandomSnakeBotTest.java | 19 +++ .../find/task_2_3_1/SmartSnakeBotTest.java | 20 +++ .../nsu/ksadov/find/task_2_3_1/SnakeTest.java | 60 +++++++ 16 files changed, 651 insertions(+) create mode 100644 Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Bot.java create mode 100644 Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Controller.java create mode 100644 Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Direction.java create mode 100644 Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/GameState.java create mode 100644 Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/HelloApplication.java create mode 100644 Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Point.java create mode 100644 Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/RandomSnakeBot.java create mode 100644 Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/SmartSnakeBot.java create mode 100644 Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Snake.java create mode 100644 Task_2_3_1/src/main/resources/ru/nsu/ksadov/find/task_2_3_1/hello-view.fxml create mode 100644 Task_2_3_1/src/test/java/ru/nsu/ksadov/find/task_2_3_1/DirectionTest.java create mode 100644 Task_2_3_1/src/test/java/ru/nsu/ksadov/find/task_2_3_1/GameStateTest.java create mode 100644 Task_2_3_1/src/test/java/ru/nsu/ksadov/find/task_2_3_1/PointTest.java create mode 100644 Task_2_3_1/src/test/java/ru/nsu/ksadov/find/task_2_3_1/RandomSnakeBotTest.java create mode 100644 Task_2_3_1/src/test/java/ru/nsu/ksadov/find/task_2_3_1/SmartSnakeBotTest.java create mode 100644 Task_2_3_1/src/test/java/ru/nsu/ksadov/find/task_2_3_1/SnakeTest.java diff --git a/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Bot.java b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Bot.java new file mode 100644 index 0000000..8c8c811 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Bot.java @@ -0,0 +1,6 @@ +package ru.nsu.ksadov.find.task_2_3_1; + +/** Interface for defining snake bot behavior strategies. */ +public interface Bot { + Direction chooseDirection(Snake bot, GameState state); +} \ No newline at end of file diff --git a/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Controller.java b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Controller.java new file mode 100644 index 0000000..cf8b266 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Controller.java @@ -0,0 +1,133 @@ +package ru.nsu.ksadov.find.task_2_3_1; + +import javafx.animation.AnimationTimer; +import javafx.fxml.FXML; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.control.Label; +import javafx.scene.input.KeyEvent; +import javafx.scene.paint.Color; + +/** Handles JavaFX user interactions, game rendering, and the main animation loop. */ +public class Controller { + private static final int CELL_SIZE = 20; + private static final int GAME_GRID_SIZE = 20; + private static final long BASE_DELAY_NS = 150_000_000L; + private static final long SPEED_MULTIPLIER_NS = 2_000_000L; + private static final long MIN_DELAY_NS = 50_000_000L; + private static final int TEXT_X_POS = 150; + private static final int TEXT_Y_POS = 200; + + @FXML private Canvas gameCanvas; + @FXML private Label label; + + private GameState gameState; + private long lastUpdate = 0; + private boolean movedThisFrame = false; + + private final AnimationTimer timer = new AnimationTimer() { + @Override + public void handle(long now) { + long delay = BASE_DELAY_NS - (gameState.getScore() * SPEED_MULTIPLIER_NS); + if (delay < MIN_DELAY_NS) { + delay = MIN_DELAY_NS; + } + + if (now - lastUpdate >= delay) { + gameState.step(); + movedThisFrame = false; + lastUpdate = now; + } + draw(); + } + }; + + @FXML + public void initialize() { + gameState = new GameState(GAME_GRID_SIZE, GAME_GRID_SIZE); + draw(); + timer.start(); + } + + private void draw() { + GraphicsContext gc = gameCanvas.getGraphicsContext2D(); + + gc.setFill(Color.BLACK); + gc.fillRect(0, 0, gameCanvas.getWidth(), gameCanvas.getHeight()); + + gc.setFill(Color.GRAY); + for (Point p : gameState.getObstacles()) { + gc.fillRect(p.x() * CELL_SIZE, p.y() * CELL_SIZE, CELL_SIZE, CELL_SIZE); + } + + gc.setFill(Color.RED); + for (Point food : gameState.getFoods()) { + gc.fillOval(food.x() * CELL_SIZE, food.y() * CELL_SIZE, CELL_SIZE, CELL_SIZE); + } + + drawSnake(gameState.getSnake(), Color.LIME); + for (Snake bot : gameState.getBots()) { + drawSnake(bot, Color.DARKORCHID); + } + + label.setText("Score: " + gameState.getScore()); + + if (gameState.isGameOver()) { + gc.setFill(Color.WHITE); + gc.fillText("GAME OVER", TEXT_X_POS, TEXT_Y_POS); + timer.stop(); + } else if (gameState.isGameWon()) { + gc.setFill(Color.WHITE); + gc.fillText("YOU WON", TEXT_X_POS, TEXT_Y_POS); + timer.stop(); + } + } + + private void drawSnake(Snake s, Color bodyColor) { + GraphicsContext gc = gameCanvas.getGraphicsContext2D(); + for (int i = 0; i < s.getBody().size(); i++) { + Point p = s.getBody().get(i); + if (i == 0) { + gc.setFill(bodyColor.darker()); + gc.fillRoundRect(p.x() * CELL_SIZE, p.y() * CELL_SIZE, CELL_SIZE - 1, CELL_SIZE - 1, 8, 8); + gc.setFill(Color.WHITE); + gc.fillOval(p.x() * CELL_SIZE + 4, p.y() * CELL_SIZE + 4, 4, 4); + gc.fillOval(p.x() * CELL_SIZE + 12, p.y() * CELL_SIZE + 4, 4, 4); + } else { + gc.setFill(bodyColor); + gc.fillRoundRect(p.x() * CELL_SIZE, p.y() * CELL_SIZE, CELL_SIZE - 1, CELL_SIZE - 1, 8, 8); + } + } + } + + @FXML + public void handleRestart() { + gameState = new GameState(GAME_GRID_SIZE, GAME_GRID_SIZE); + movedThisFrame = false; + timer.start(); + draw(); + gameCanvas.requestFocus(); + } + + public void handleButtonsPressed(KeyEvent event) { + if (movedThisFrame) { + return; + } + + Direction currDir = gameState.getSnake().getDirection(); + Direction newDir = currDir; + + switch (event.getCode()) { + case UP, W -> newDir = Direction.UP; + case DOWN, S -> newDir = Direction.DOWN; + case LEFT, A -> newDir = Direction.LEFT; + case RIGHT, D -> newDir = Direction.RIGHT; + default -> {} + } + + if (newDir != currDir) { + gameState.getSnake().setDirection(newDir); + movedThisFrame = true; + } + } +} \ No newline at end of file diff --git a/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Direction.java b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Direction.java new file mode 100644 index 0000000..434f703 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Direction.java @@ -0,0 +1,6 @@ +package ru.nsu.ksadov.find.task_2_3_1; + +/** Defines the possible movement directions for the snake. */ +public enum Direction { + UP, DOWN, LEFT, RIGHT +} \ No newline at end of file diff --git a/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/GameState.java b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/GameState.java new file mode 100644 index 0000000..3f79499 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/GameState.java @@ -0,0 +1,150 @@ +package ru.nsu.ksadov.find.task_2_3_1; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +/** Manages the core game logic, state, entities, and collision detection. */ +public class GameState { + private static final int MAX_FOOD = 3; + private static final int TARGET_LENGTH = 10; + private static final int COUNT_OBS = 10; + + private final int width; + private final int height; + private final Snake snake; + private final List bots = new ArrayList<>(); + private final List botStrategies = new ArrayList<>(); + private final List foods = new ArrayList<>(); + private final List obstacles = new ArrayList<>(); + private final Random random = new Random(); + + private int score = 1; + private boolean gameOver = false; + private boolean gameWon = false; + + public GameState(int width, int height) { + this.width = width; + this.height = height; + this.snake = new Snake(new Point(width / 2, height / 2)); + + bots.add(new Snake(new Point(5, 5))); + botStrategies.add(new SmartSnakeBot()); + + bots.add(new Snake(new Point(15, 15))); + botStrategies.add(new RandomSnakeBot()); + + spawnObs(); + spawnFood(); + } + + private void spawnFood() { + while (foods.size() < MAX_FOOD) { + int x = random.nextInt(width); + int y = random.nextInt(height); + Point newFood = new Point(x, y); + if (!snake.getBody().contains(newFood) && !foods.contains(newFood) && !obstacles.contains(newFood)) { + foods.add(newFood); + } + } + } + + private void spawnObs() { + while (obstacles.size() < COUNT_OBS) { + int x = random.nextInt(width); + int y = random.nextInt(height); + Point newObs = new Point(x, y); + if (!snake.getBody().contains(newObs) && !obstacles.contains(newObs)) { + obstacles.add(newObs); + } + } + } + + public void step() { + if (gameOver || gameWon) { + return; + } + + moveSnake(snake, snake.getDirection()); + + Iterator botIterator = bots.iterator(); + Iterator strategyIterator = botStrategies.iterator(); + + while (botIterator.hasNext() && strategyIterator.hasNext()) { + Snake bot = botIterator.next(); + Bot strategy = strategyIterator.next(); + + Direction nextDir = strategy.chooseDirection(bot, this); + bot.setDirection(nextDir); + + if (!moveSnake(bot, nextDir)) { + botIterator.remove(); + strategyIterator.remove(); + } + } + } + + private boolean moveSnake(Snake currentSnake, Direction direction) { + Point head = currentSnake.getBody().getFirst(); + Point nextPoint = switch (direction) { + case UP -> new Point(head.x(), head.y() - 1); + case DOWN -> new Point(head.x(), head.y() + 1); + case LEFT -> new Point(head.x() - 1, head.y()); + case RIGHT -> new Point(head.x() + 1, head.y()); + }; + + if (nextPoint.x() < 0 || nextPoint.x() >= width || nextPoint.y() < 0 || nextPoint.y() >= height) { + if (currentSnake == snake) gameOver = true; + return false; + } + + if (currentSnake.checkSelfCollision(nextPoint) || obstacles.contains(nextPoint)) { + if (currentSnake == snake) gameOver = true; + return false; + } + + if (foods.contains(nextPoint)) { + foods.remove(nextPoint); + currentSnake.eat(); + spawnFood(); + if (currentSnake == snake) { + score++; + if (snake.getBody().size() >= TARGET_LENGTH) { + gameWon = true; + } + } + } else { + currentSnake.move(false); + } + return true; + } + + public Snake getSnake() { + return snake; + } + + public List getFoods() { + return foods; + } + + public int getScore() { + return score; + } + + public boolean isGameOver() { + return gameOver; + } + + public boolean isGameWon() { + return gameWon; + } + + public List getObstacles() { + return obstacles; + } + + public List getBots() { + return bots; + } +} \ No newline at end of file diff --git a/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/HelloApplication.java b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/HelloApplication.java new file mode 100644 index 0000000..13d3703 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/HelloApplication.java @@ -0,0 +1,19 @@ +package ru.nsu.ksadov.find.task_2_3_1; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.stage.Stage; + +import java.io.IOException; + +public class HelloApplication extends Application { + @Override + public void start(Stage stage) throws IOException { + FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml")); + Scene scene = new Scene(fxmlLoader.load(), 320, 240); + stage.setTitle("Hello!"); + stage.setScene(scene); + stage.show(); + } +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Point.java b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Point.java new file mode 100644 index 0000000..f4fcd3d --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Point.java @@ -0,0 +1,4 @@ +package ru.nsu.ksadov.find.task_2_3_1; + +/** Represents a 2D coordinate point in the game grid. */ +public record Point(int x, int y) {} \ No newline at end of file diff --git a/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/RandomSnakeBot.java b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/RandomSnakeBot.java new file mode 100644 index 0000000..dffed8e --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/RandomSnakeBot.java @@ -0,0 +1,27 @@ +package ru.nsu.ksadov.find.task_2_3_1; + +import java.util.Random; + +/** Defines a bot strategy that moves randomly across the grid. */ +public class RandomSnakeBot implements Bot { + private static final int KEEP_DIRECTION_CHANCE = 80; + private static final int MAX_PERCENTAGE = 100; + private final Random random = new Random(); + + @Override + public Direction chooseDirection(Snake bot, GameState state) { + if (random.nextInt(MAX_PERCENTAGE) < KEEP_DIRECTION_CHANCE) { + return bot.getDirection(); + } + + Direction current = bot.getDirection(); + boolean turnLeft = random.nextBoolean(); + + return switch (current) { + case UP -> turnLeft ? Direction.LEFT : Direction.RIGHT; + case DOWN -> turnLeft ? Direction.RIGHT : Direction.LEFT; + case LEFT -> turnLeft ? Direction.DOWN : Direction.UP; + case RIGHT -> turnLeft ? Direction.UP : Direction.DOWN; + }; + } +} \ No newline at end of file diff --git a/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/SmartSnakeBot.java b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/SmartSnakeBot.java new file mode 100644 index 0000000..e945bce --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/SmartSnakeBot.java @@ -0,0 +1,29 @@ +package ru.nsu.ksadov.find.task_2_3_1; + +/** Defines a bot strategy that actively moves towards the first available food. */ +public class SmartSnakeBot implements Bot { + @Override + public Direction chooseDirection(Snake bot, GameState state) { + if (state.getFoods().isEmpty()) { + return bot.getDirection(); + } + + Point head = bot.getBody().getFirst(); + Point target = state.getFoods().get(0); + + if (target.x() > head.x() && bot.getDirection() != Direction.LEFT) { + return Direction.RIGHT; + } + if (target.x() < head.x() && bot.getDirection() != Direction.RIGHT) { + return Direction.LEFT; + } + if (target.y() > head.y() && bot.getDirection() != Direction.UP) { + return Direction.DOWN; + } + if (target.y() < head.y() && bot.getDirection() != Direction.DOWN) { + return Direction.UP; + } + + return bot.getDirection(); + } +} \ No newline at end of file diff --git a/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Snake.java b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Snake.java new file mode 100644 index 0000000..a989985 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/ksadov/find/task_2_3_1/Snake.java @@ -0,0 +1,64 @@ +package ru.nsu.ksadov.find.task_2_3_1; + +import java.util.LinkedList; + +/** Represents a snake entity with a body and movement direction. */ +public class Snake { + private final LinkedList body = new LinkedList<>(); + private Direction direction = Direction.RIGHT; + + public Snake(Point startPos) { + body.add(startPos); + } + + private boolean isOpposite(Direction curr, Direction next) { + return (curr == Direction.UP && next == Direction.DOWN) || + (curr == Direction.DOWN && next == Direction.UP) || + (curr == Direction.LEFT && next == Direction.RIGHT) || + (curr == Direction.RIGHT && next == Direction.LEFT); + } + + public void move(boolean mustGrow) { + Point head = body.getFirst(); + Point newHead = switch (direction) { + case UP -> new Point(head.x(), head.y() - 1); + case DOWN -> new Point(head.x(), head.y() + 1); + case RIGHT -> new Point(head.x() + 1, head.y()); + case LEFT -> new Point(head.x() - 1, head.y()); + }; + + body.addFirst(newHead); + if (!mustGrow) { + body.removeLast(); + } + } + + public LinkedList getBody() { + return body; + } + + public void setDirection(Direction newDir) { + if (!isOpposite(this.direction, newDir)) { + this.direction = newDir; + } + } + + public Direction getDirection() { + return this.direction; + } + + public void eat() { + Point head = body.getFirst(); + Point newHead = switch (direction) { + case UP -> new Point(head.x(), head.y() - 1); + case DOWN -> new Point(head.x(), head.y() + 1); + case RIGHT -> new Point(head.x() + 1, head.y()); + case LEFT -> new Point(head.x() - 1, head.y()); + }; + body.addFirst(newHead); + } + + public boolean checkSelfCollision(Point newHead) { + return body.contains(newHead); + } +} \ No newline at end of file diff --git a/Task_2_3_1/src/main/resources/ru/nsu/ksadov/find/task_2_3_1/hello-view.fxml b/Task_2_3_1/src/main/resources/ru/nsu/ksadov/find/task_2_3_1/hello-view.fxml new file mode 100644 index 0000000..8ff86dd --- /dev/null +++ b/Task_2_3_1/src/main/resources/ru/nsu/ksadov/find/task_2_3_1/hello-view.fxml @@ -0,0 +1,11 @@ + + + + +