Compare commits

..

5 Commits

Author SHA1 Message Date
1f9e6f4e4a Minor improvments 2025-08-22 12:11:41 +02:00
b9a43be3c6 Basic score count working
update takes movement into account
2025-08-22 11:00:19 +02:00
9bb76ce682 Lives, Score 2025-08-21 20:14:18 +02:00
712d58e8e3 Rudimentary colisioncheck 2025-08-21 15:14:20 +02:00
e8112f1cbb Tidying up Ghost 2025-08-21 14:31:08 +02:00
12 changed files with 260 additions and 130 deletions

View File

@ -65,8 +65,4 @@ public class CollisionChecker {
return new Point(x, y);
}
public void removeTile(Point destination) {
map.removeTileImage(destination);
}
}

View File

@ -7,6 +7,6 @@ import java.awt.Point;
public class BlinkyStrategy implements GhostStrategy {
@Override
public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) {
return pacman.getTilePosition();
return pacman.getPosition();
}
}

View File

@ -7,7 +7,7 @@ import java.awt.Point;
public class ClydeStrategy implements GhostStrategy {
@Override
public Point chooseTarget(Ghost clyde, PacMan pacman, GameMap map) {
Point pacTile = pacman.getTilePosition();
Point pacTile = pacman.getPosition();
Point clydeTile = clyde.getPosition(); // ghosts current tile
double distance = pacTile.distance(clydeTile);

View File

@ -27,6 +27,7 @@ public class Ghost {
private final GhostCollisionChecker collisionChecker;
private final GhostStrategy chaseStrategy;
private final Point startPos;
@Getter
private Point position;
@ -50,7 +51,7 @@ public class Ghost {
position = new Point(
13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X + (GameMap.MAP_TILESIZE / 2),
4 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + (GameMap.MAP_TILESIZE / 2) );
startPos = position;
this.currentStrategy = chaseStrategy;
}
@ -74,30 +75,58 @@ public class Ghost {
}
private void updatePosition(PacMan pacman, GameMap map) {
// only move ghost on update interval - this is basically ghost speed;
if (movementTick >= GHOST_MOVEMENT_UPDATE_FREQUENCY) {
if (isAlligned(position)) {
log.info("Evaluating possible directions");
prevDirection = direction;
direction = chooseDirection(
prioritize(collisionChecker.calculateDirectionAlternatives(position)),
currentStrategy.chooseTarget(this, pacman, map));
log.info("selecting direction {}", direction);
}
chooseDirection(pacman, map);
Point newPosition = new Point(
position.x + direction.dx,
position.y + direction.dy);
log.debug("Next position {}", newPosition);
Point newPosition = getNewPosition();
Point destination = collisionChecker.canMoveTo(direction, newPosition);
move(newPosition);
if (destination != null) {
position = destination;
}
movementTick = 0;
} else movementTick++;
}
/**
* Given a position and a direction - calculate the new position
*
* @return new position
*/
private Point getNewPosition() {
Point point = new Point(
position.x + direction.dx,
position.y + direction.dy);
//log.debug("Next position {}", point);
return point;
}
/**
* Choose a new direction when 'aligned' ie when in the exact middle of a tile
* else continue with the existing direction.
*
* @param pacman
* @param map
*/
private void chooseDirection(PacMan pacman, GameMap map) {
if (isAlligned(position)) {
log.info("Evaluating possible directions");
prevDirection = direction;
direction = chooseDirection(
prioritize(collisionChecker.calculateDirectionAlternatives(position)),
currentStrategy.chooseTarget(this, pacman, map));
log.info("selecting direction {}", direction);
}
}
private void move(Point newPosition) {
Point destination = collisionChecker.canMoveTo(direction, newPosition);
if (destination != null) {
position = destination;
}
}
private Map<Direction, Integer> prioritize(List<Direction> directions) {
return directions.stream()
.filter(d -> d != Direction.NONE)
@ -167,4 +196,11 @@ public class Ghost {
}
}
public boolean isFrightened() {
return false;
}
public void resetPosition() {
position = startPos;
}
}

View File

@ -15,7 +15,7 @@ public class InkyStrategy implements GhostStrategy {
public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) {
// 1. Two tiles ahead of pacman
Direction pacmanDir = pacman.getDirection();
Point pacmanPos = pacman.getTilePosition();
Point pacmanPos = pacman.getPosition();
Point ahead = switch (pacmanDir){
case RIGHT -> new Point(pacmanPos.x + 8 * GameMap.MAP_TILESIZE, pacmanPos.y);
case LEFT -> new Point(pacmanPos.x - 8 * GameMap.MAP_TILESIZE, pacmanPos.y);

View File

@ -22,6 +22,7 @@ public class PacMan {
private static final int COLLISION_BOX_SIZE = 16;
private static final int COLLISION_BOX_OFFSET = (PACMAN_SIZE - COLLISION_BOX_SIZE) / 2;
private final Game game;
private final Point startPosition;
private int aniTick = 0;
private int aniIndex = 0;
private static final int ANIMATION_UPDATE_FREQUENCY = 10;
@ -29,6 +30,7 @@ public class PacMan {
@Setter
private boolean moving;
private final BufferedImage[][] movmentImages = new BufferedImage[4][4];
@Getter
private Point position;
private static final BufferedImage COLLISION_BOX = MiscUtil.createOutlinedBox(COLLISION_BOX_SIZE, COLLISION_BOX_SIZE, Color.yellow, 2);
private final CollisionChecker collisionChecker;
@ -39,9 +41,10 @@ public class PacMan {
public PacMan(Game game, CollisionChecker collisionChecker) {
this.game = game;
this.collisionChecker = collisionChecker;
position = new Point(
this.position = new Point(
26 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X,
13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + (GameMap.MAP_TILESIZE / 2));
this.startPosition = this.position;
loadAnimation();
}
@ -80,21 +83,21 @@ public class PacMan {
public void update() {
updateAnimationTick();
if(direction == Direction.NONE) return;
//if(direction == Direction.NONE) return;
if(moving) {
Point newPosition = switch (direction) {
case RIGHT -> new Point(position.x + speed, position.y);
case LEFT -> new Point(position.x - speed, position.y);
case UP -> new Point(position.x, position.y - speed);
case DOWN -> new Point(position.x, position.y + speed);
default -> throw new IllegalStateException("Unexpected value: " + direction);
};
log.debug("At: {},trying to move {} to {}", position, direction.name(), newPosition);
Point destination = collisionChecker.getValidDestination(direction, newPosition, COLLISION_BOX_SIZE, COLLISION_BOX_SIZE);
Point newPosition = switch (direction){
case RIGHT -> new Point(position.x + speed, position.y);
case LEFT -> new Point(position.x - speed, position.y);
case UP -> new Point(position.x , position.y - speed);
case DOWN -> new Point(position.x, position.y + speed);
default -> throw new IllegalStateException("Unexpected value: " + direction);
};
log.debug("At: {},trying to move {} to {}", position, direction.name(), newPosition);
Point destination = collisionChecker.getValidDestination(direction, newPosition, COLLISION_BOX_SIZE, COLLISION_BOX_SIZE);
if(destination != null) {
collisionChecker.removeTile(destination);
position = destination;
if (destination != null) {
position = destination;
}
}
}
@ -112,7 +115,19 @@ public class PacMan {
}
}
public Point getTilePosition() {
return position;
public double distanceTo(Point point) {
return position.distance(point);
}
public void loseLife() {
}
public void resetPosition() {
position = startPosition;
}
public Image getLifeIcon() {
return movmentImages[0][1];
}
}

View File

@ -9,7 +9,7 @@ public class PinkyStrategy implements GhostStrategy{
@Override
public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) {
Direction pacmanDir = pacman.getDirection();
Point pacmanPos = pacman.getTilePosition();
Point pacmanPos = pacman.getPosition();
return switch (pacmanDir){
case RIGHT -> new Point(pacmanPos.x + 8 * GameMap.MAP_TILESIZE, pacmanPos.y);
case LEFT -> new Point(pacmanPos.x - 8 * GameMap.MAP_TILESIZE, pacmanPos.y);

View File

@ -16,41 +16,43 @@ public class GameMap {
public static final int MAP_ROW_SIZE = 30;
public static final int OFFSET_Y = 7 * MAP_TILESIZE; // 160px from top
public static final int OFFSET_X = MAP_TILESIZE; // 16px from left
private final BufferedImage[][] images = new BufferedImage[13][19];
private final BufferedImage[][] mapSpriteBuffer;
private final MapTile[][] mapData;
private final BufferedImage[][] mapItemSpriteBuffer;
public GameMap() {
loadSprites();
mapData = loadMap("maps/map1.csv");
this.mapSpriteBuffer = LoadSave.loadSprites("sprites/PacMan-custom-spritemap-0-3.png", 5, 11, MAP_TILESIZE);
this.mapItemSpriteBuffer = LoadSave.loadSprites("sprites/PacManAssets-Items.png", 2, 8, MAP_TILESIZE);
this.mapData = loadMap("maps/map1.csv", MAP_ROW_SIZE, MAP_COL_SIZE);
}
private MapTile[][] loadMap(String path) {
MapTile[][] data = new MapTile[MAP_ROW_SIZE][MAP_COL_SIZE];
private MapTile[][] loadMap(String path, int mapRowSize, int mapColSize) {
MapTile[][] data = new MapTile[mapRowSize][mapColSize];
try (InputStream is = getClass().getClassLoader().getResourceAsStream(path);
BufferedReader br = new BufferedReader(new InputStreamReader(Objects.requireNonNull(is)))) {
String line;
int rowIndex = 0;
while ((line = br.readLine()) != null && rowIndex < MAP_ROW_SIZE) {
while ((line = br.readLine()) != null && rowIndex < mapRowSize) {
String[] tokens = line.split(",");
if (tokens.length != MAP_COL_SIZE) {
if (tokens.length != mapColSize) {
throw new IllegalStateException(
"Invalid map format: row " + rowIndex + " has " + tokens.length +
" columns, expected " + MAP_COL_SIZE
" columns, expected " + mapColSize
);
}
for (int col = 0; col < MAP_COL_SIZE; col++) {
for (int col = 0; col < mapColSize; col++) {
int value = Integer.parseInt(tokens[col].trim());
data[rowIndex][col] = new MapTile(getSprite(value), value);
}
rowIndex++;
}
if (rowIndex != MAP_ROW_SIZE) {
if (rowIndex != mapRowSize) {
throw new IllegalStateException(
"Invalid map format: found " + rowIndex + " rows, expected " + MAP_ROW_SIZE
"Invalid map format: found " + rowIndex + " rows, expected " + mapRowSize
);
}
@ -64,76 +66,67 @@ public class GameMap {
private BufferedImage getSprite(int value) {
return switch (value){
case 0 -> images[1][1];
case 1 -> images[0][0];
case 2 -> images[0][1];
case 3 -> images[0][2];
case 4 -> images[0][3];
case 5 -> images[0][4];
case 6 -> images[0][5];
case 7 -> images[0][6];
case 8 -> images[0][7];
case 9 -> images[0][8];
case 10 -> images[0][9];
case 11 -> images[0][10];
case 12 -> images[1][0];
case 13 -> images[1][1];
case 14 -> images[1][2];
case 15 -> images[1][3];
case 16 -> images[1][4];
case 17 -> images[1][5];
case 18 -> images[1][6];
case 19 -> images[1][7];
case 20 -> images[1][8];
case 21 -> images[1][9];
case 22 -> images[1][10];
case 23 -> images[2][0];
case 24 -> images[2][1];
case 25 -> images[2][2];
case 26 -> images[2][3];
case 27 -> images[2][4];
case 28 -> images[2][5];
case 29 -> images[2][6];
case 30 -> images[2][7];
case 31 -> images[2][8];
case 32 -> images[2][9];
case 33 -> images[2][10];
case 34 -> images[3][0];
case 35 -> images[3][1];
case 36 -> images[3][2];
case 37 -> images[3][3];
case 38 -> images[3][4];
case 39 -> images[3][5];
case 40 -> images[3][6];
case 41 -> images[3][7];
case 42 -> images[3][8];
case 43 -> images[3][9];
case 44 -> images[3][10];
case 45 -> images[4][0];
case 46 -> images[4][1];
case 47 -> images[4][2];
case 48 -> images[4][3];
case 49 -> images[4][4];
case 50 -> images[4][5];
case 51 -> images[4][6];
case 52 -> images[4][7];
case 53 -> images[4][8];
case 54 -> images[4][9];
case 55 -> images[4][10];
case 0 -> mapSpriteBuffer[1][1];
case 1 -> mapSpriteBuffer[0][0];
case 2 -> mapSpriteBuffer[0][1];
case 3 -> mapSpriteBuffer[0][2];
case 4 -> mapSpriteBuffer[0][3];
case 5 -> mapSpriteBuffer[0][4];
case 6 -> mapSpriteBuffer[0][5];
case 7 -> mapSpriteBuffer[0][6];
case 8 -> mapSpriteBuffer[0][7];
case 9 -> mapSpriteBuffer[0][8];
case 10 -> mapSpriteBuffer[0][9];
case 11 -> mapSpriteBuffer[0][10];
case 12 -> mapSpriteBuffer[1][0];
case 13 -> mapSpriteBuffer[1][1];
case 14 -> mapSpriteBuffer[1][2];
case 15 -> mapSpriteBuffer[1][3];
case 16 -> mapSpriteBuffer[1][4];
case 17 -> mapSpriteBuffer[1][5];
case 18 -> mapSpriteBuffer[1][6];
case 19 -> mapSpriteBuffer[1][7];
case 20 -> mapSpriteBuffer[1][8];
case 21 -> mapSpriteBuffer[1][9];
case 22 -> mapSpriteBuffer[1][10];
case 23 -> mapSpriteBuffer[2][0];
case 24 -> mapSpriteBuffer[2][1];
case 25 -> mapSpriteBuffer[2][2];
case 26 -> mapSpriteBuffer[2][3];
case 27 -> mapSpriteBuffer[2][4];
case 28 -> mapSpriteBuffer[2][5];
case 29 -> mapSpriteBuffer[2][6];
case 30 -> mapSpriteBuffer[2][7];
case 31 -> mapSpriteBuffer[2][8];
case 32 -> mapSpriteBuffer[2][9];
case 33 -> mapSpriteBuffer[2][10];
case 34 -> mapSpriteBuffer[3][0];
case 35 -> mapSpriteBuffer[3][1];
case 36 -> mapSpriteBuffer[3][2];
case 37 -> mapSpriteBuffer[3][3];
case 38 -> mapSpriteBuffer[3][4];
case 39 -> mapSpriteBuffer[3][5];
case 40 -> mapSpriteBuffer[3][6];
case 41 -> mapSpriteBuffer[3][7];
case 42 -> mapSpriteBuffer[3][8];
case 43 -> mapSpriteBuffer[3][9];
case 44 -> mapSpriteBuffer[3][10];
case 45 -> mapSpriteBuffer[4][0];
case 46 -> mapSpriteBuffer[4][1];
case 47 -> mapSpriteBuffer[4][2];
case 48 -> mapSpriteBuffer[4][3];
case 49 -> mapSpriteBuffer[4][4];
case 50 -> mapSpriteBuffer[4][5];
case 51 -> mapSpriteBuffer[4][6];
case 52 -> mapSpriteBuffer[4][7];
case 53 -> mapSpriteBuffer[4][8];
case 54 -> mapSpriteBuffer[4][9];
case 55 -> mapSpriteBuffer[4][10];
default -> null;
};
}
private void loadSprites() {
BufferedImage img = LoadSave.GetSpriteAtlas("sprites/PacMan-custom-spritemap-0-3.png");//473B78
for (int row = 0; row < 5; row++) {
for (int col = 0; col < 11; col++) {
images[row][col] = img.getSubimage(MAP_TILESIZE * col, MAP_TILESIZE * row, MAP_TILESIZE, MAP_TILESIZE);
}
}
}
public void draw(Graphics g) {
for (int row = 0; row < mapData.length; row++) {
for (int col = 0; col < mapData[row].length; col++) {
@ -176,15 +169,30 @@ public class GameMap {
if (col >= columns() || col < 0 ) return true;
MapTile mapTile = mapData[row][col];
boolean solid = mapTile.isSolid();
log.debug("[{}][{}] is {} ({})", row, col, solid ? "solid" : " not solid", mapTile.getValue());
// log.debug("[{}][{}] is {} ({})", row, col, solid ? "solid" : " not solid", mapTile.getValue());
return solid;
}
public void removeTileImage(Point destination) {
int row = screenToRow(destination);
int col = screenToCol(destination);
public boolean removeTileImage(Point screen) {
if (screen == null || mapData == null) {
return false;
}
int row = screenToRow(screen);
int col = screenToCol(screen);
if (row < 0 || row >= mapData.length || col < 0 || col >= mapData[0].length) {
return false;
}
MapTile tile = mapData[row][col];
if(tile.getValue() == 0) tile.setImage(null);
if (tile != null && tile.getValue() == 0 && tile.getImage() != null) {
tile.setImage(null);
return true;
}
return false;
}
private static int screenToCol(Point point) {
@ -230,11 +238,12 @@ public class GameMap {
return new Point(screen.x - OFFSET_X, screen.y - OFFSET_Y);
}
private static int screenToCol(int screenX) {
public static int screenToCol(int screenX) {
return (screenX - OFFSET_X) / MAP_TILESIZE;
}
private static int screenToRow(int screenY) {
public static int screenToRow(int screenY) {
return (screenY - OFFSET_Y) / MAP_TILESIZE;
}
}

View File

@ -50,8 +50,7 @@ public class GhostManager {
ghosts.add(blinky);
ghosts.add(new Ghost(ghostCollisionChecker, new PinkyStrategy(),new ScatterToTopLeft(), image[2]));
ghosts.add(new Ghost(ghostCollisionChecker,new InkyStrategy(blinky), new ScatterToBottomRight(), image[1]));
Ghost clyde = new Ghost(ghostCollisionChecker, new ClydeStrategy(), new ScatterToBottomLeft(), image[3]);
ghosts.add(clyde);
ghosts.add(new Ghost(ghostCollisionChecker, new ClydeStrategy(), new ScatterToBottomLeft(), image[3]));
setMode(GhostMode.CHASE);
}
@ -68,7 +67,7 @@ public class GhostManager {
public void setMode(GhostMode mode) {
this.globalMode = mode;
log.debug("Mode changed to {}", globalMode);
log.info("Mode changed to {}", globalMode);
for (Ghost g : ghosts) {
g.setMode(mode);
}

View File

@ -1,9 +1,9 @@
package se.urmo.game.state;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.collision.CollisionChecker;
import se.urmo.game.collision.GhostCollisionChecker;
import se.urmo.game.entities.BlinkyStrategy;
import se.urmo.game.entities.Ghost;
import se.urmo.game.entities.PacMan;
import se.urmo.game.main.Game;
@ -12,14 +12,19 @@ import se.urmo.game.util.Direction;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.io.InputStream;
@Slf4j
public class PlayingState implements GameState {
private final Game game;
private final GameStateManager gameStateManager;
private final GhostManager ghostManager;
private final Font arcadeFont;
private PacMan pacman;
@Getter
private GameMap map;
private int score;
private int lives = 3;
public PlayingState(Game game, GameStateManager gameStateManager) {
this.game = game;
@ -27,12 +32,24 @@ public class PlayingState implements GameState {
this.map = new GameMap();
this.pacman = new PacMan(game, new CollisionChecker(map));
this.ghostManager = new GhostManager(new GhostCollisionChecker(map));
this.arcadeFont = loadArcadeFont();
}
@Override
public void update() {
pacman.update();
ghostManager.update(pacman, map);
checkCollisions();
handleDots();
}
private void handleDots() {
Point pacmanScreenPos = pacman.getPosition();
boolean wasRemoved = map.removeTileImage(pacmanScreenPos);
if(wasRemoved){
score+=10;
}
}
@Override
@ -40,6 +57,24 @@ public class PlayingState implements GameState {
map.draw(g);
pacman.draw(g);
ghostManager.draw(g);
drawUI(g);
}
private void drawUI(Graphics2D g) {
g.setColor(Color.WHITE);
g.setFont(arcadeFont);
// Score (above map, left)
g.drawString("Your Score", 48, 48);
g.drawString("" + score, 48, 72);
// Lives (below map, left)
for (int i = 1; i < lives; i++) {
g.drawImage(pacman.getLifeIcon(),
6 * GameMap.OFFSET_X - i * 24,
map.rows() * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y,
null);
}
}
@Override
@ -60,6 +95,31 @@ public class PlayingState implements GameState {
@Override
public void keyReleased(KeyEvent e) {
pacman.setMoving(false);
pacman.setDirection(Direction.NONE);
}
private void checkCollisions() {
for (Ghost ghost : ghostManager.getGhosts()) {
double dist = pacman.distanceTo(ghost.getPosition());
if (dist < GameMap.MAP_TILESIZE / 2.0) {
if (ghost.isFrightened()) {
// Pac-Man eats ghost
score += 200;
ghost.resetPosition();
} else {
// Pac-Man loses a life
lives--;
if(lives == 0)System.exit(1);
else pacman.resetPosition();
}
}
}
}
private Font loadArcadeFont() {
try (InputStream is = getClass().getResourceAsStream("/fonts/PressStart2P-Regular.ttf")) {
return Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(16f);
} catch (Exception e) {
return new Font("Monospaced", Font.BOLD, 16);
}
}
}

View File

@ -9,6 +9,21 @@ import java.io.InputStream;
public class LoadSave {
public static BufferedImage[][] loadSprites(final String fileName, final int rows, final int columns, int mapTilesize) {
BufferedImage spriteSheet = LoadSave.GetSpriteAtlas(fileName);
if(spriteSheet.getHeight() != rows * mapTilesize) throw new IllegalStateException("Wrong sprite row size");
if(spriteSheet.getWidth() != columns * mapTilesize) throw new IllegalStateException("Wrong sprite colum size");
BufferedImage[][] spriteBuffer = new BufferedImage[rows][columns];
for (int row = 0; row < rows; row++) {
for (int col = 0; col < columns; col++) {
spriteBuffer[row][col] = spriteSheet.getSubimage(mapTilesize * col, mapTilesize * row, mapTilesize, mapTilesize);
}
}
return spriteBuffer;
}
public static BufferedImage GetSpriteAtlas(String fileName) {
BufferedImage img = null;
InputStream is = LoadSave.class.getResourceAsStream("/" + fileName);

Binary file not shown.