Refactor ghost behavior and improve mode transition logic

Replaced hardcoded coordinates with `Ghost.getHouseEntrance` for clarity and flexibility. Introduced `requestModeChange` to streamline state transitions. Updated collision and movement logic to handle ghost-specific tile interactions, enhancing gameplay accuracy.
This commit is contained in:
Urban Modig
2025-09-06 18:55:25 +02:00
parent 291f929064
commit c1c998c1cd
13 changed files with 90 additions and 57 deletions

View File

@ -1,9 +1,10 @@
package se.urmo.game.collision; package se.urmo.game.collision;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import se.urmo.game.util.MyPoint; import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.map.GameMap; import se.urmo.game.map.GameMap;
import se.urmo.game.util.Direction; import se.urmo.game.util.Direction;
import se.urmo.game.util.MyPoint;
import java.util.List; import java.util.List;
@ -15,19 +16,16 @@ public class GhostCollisionChecker {
this.map = map; this.map = map;
} }
public List<Direction> calculateDirectionAlternatives(MyPoint position) { public List<Direction> calculateDirectionAlternatives(Ghost ghost, MyPoint position) {
List<Direction> intersection = map.directionAlternatives((int) position.x, (int) position.y); List<Direction> intersection = map.directionAlternatives(ghost, (int) position.x, (int) position.y);
log.info("Possible travel directions: {}", intersection); log.info("Possible travel directions: {}", intersection);
return intersection; return intersection;
} }
public MyPoint canMoveTo(Ghost ghost, Direction dir, double x, double y) {
public MyPoint canMoveTo(Direction dir, double x, double y) {
// -1 is because else we endup in next tile // -1 is because else we endup in next tile
//Point pp = new Point((int) (x + dir.dx * (GameMap.MAP_TILESIZE/2 - 1)), (int) (y + dir.dy * (GameMap.MAP_TILESIZE/2 -1))); return !map.isSolidXY(ghost,
return ! map.isSolidXY(
(int) (x) + dir.dx * (GameMap.MAP_TILESIZE/2 - 1), (int) (x) + dir.dx * (GameMap.MAP_TILESIZE/2 - 1),
(int) (y) + dir.dy * (GameMap.MAP_TILESIZE/2 - 1)) ? new MyPoint(x,y) : null; (int) (y) + dir.dy * (GameMap.MAP_TILESIZE/2 - 1)) ? new MyPoint(x,y) : null;
} }

View File

@ -58,6 +58,10 @@ public class Ghost extends BaseAnimated {
private static final MyPoint startPosition = new MyPoint( private static final MyPoint startPosition = new MyPoint(
GameMap.colToScreen(13) + ((double) GameMap.MAP_TILESIZE / 2), GameMap.colToScreen(13) + ((double) GameMap.MAP_TILESIZE / 2),
GameMap.rowToScreen(12) + ((double) GameMap.MAP_TILESIZE / 2)); GameMap.rowToScreen(12) + ((double) GameMap.MAP_TILESIZE / 2));
@Getter
private static final MyPoint houseEntrance = new MyPoint(
GameMap.colToScreen(13) + ((double) GameMap.MAP_TILESIZE / 2),
GameMap.rowToScreen(10) + ((double) GameMap.MAP_TILESIZE / 2));
public Ghost(GhostCollisionChecker collisionChecker, GhostStrategy chaseStrategy, GhostStrategy scaterStrategy, int animation, LevelManager levelManager) { public Ghost(GhostCollisionChecker collisionChecker, GhostStrategy chaseStrategy, GhostStrategy scaterStrategy, int animation, LevelManager levelManager) {
super(ANIMATION_UPDATE_FREQUENCY, GhostManager.MAX_SPRITE_FRAMES); super(ANIMATION_UPDATE_FREQUENCY, GhostManager.MAX_SPRITE_FRAMES);
@ -81,6 +85,9 @@ public class Ghost extends BaseAnimated {
(int) position.y - GHOST_SIZE / 2, (int) position.y - GHOST_SIZE / 2,
GHOST_SIZE, GHOST_SIZE,
GHOST_SIZE, null); GHOST_SIZE, null);
// g.setColor(Color.YELLOW);
// g.fillRect((int) Ghost.startPosition.x, (int) Ghost.startPosition.y, 2, 2);
// g.fillRect((int) Ghost.houseEntrance.x, (int) Ghost.houseEntrance.y, 2, 2);
} }
public void update(PacMan pacman, GameMap map) { public void update(PacMan pacman, GameMap map) {
@ -94,6 +101,15 @@ public class Ghost extends BaseAnimated {
} }
} }
/**
* Used by a state itself to request a transition to any other state
* This bypasses the priority system since the state itself is requesting the change
*/
public void requestModeChange(GhostMode mode) {
currentState = states.get(mode);
}
public boolean isFrightened() { public boolean isFrightened() {
return states.get(GhostMode.FRIGHTENED) == currentState; return states.get(GhostMode.FRIGHTENED) == currentState;
} }

View File

@ -13,6 +13,7 @@ import se.urmo.game.util.MyPoint;
import java.awt.Point; import java.awt.Point;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -61,7 +62,7 @@ public abstract class AbstractGhostModeImpl implements GhostState {
ghost.setDirection(chooseDirection( ghost.setDirection(chooseDirection(
ghost, ghost,
prioritizeDirections( prioritizeDirections(
ghost.getCollisionChecker().calculateDirectionAlternatives(getPosition())), ghost.getCollisionChecker().calculateDirectionAlternatives(ghost, getPosition())),
getStrategy().chooseTarget(ghost, pacman, map))); getStrategy().chooseTarget(ghost, pacman, map)));
log.debug("Ghost moving to {}", getPosition()); log.debug("Ghost moving to {}", getPosition());
@ -83,7 +84,7 @@ public abstract class AbstractGhostModeImpl implements GhostState {
} }
private void moveTo(Ghost ghost, MyPoint newPosition) { private void moveTo(Ghost ghost, MyPoint newPosition) {
MyPoint destination = ghost.getCollisionChecker().canMoveTo( MyPoint destination = ghost.getCollisionChecker().canMoveTo(ghost,
getDirection(), newPosition.x, newPosition.y); getDirection(), newPosition.x, newPosition.y);
if (destination != null) { if (destination != null) {
ghost.setPosition(destination); ghost.setPosition(destination);
@ -113,23 +114,31 @@ public abstract class AbstractGhostModeImpl implements GhostState {
.map(Map.Entry::getKey) .map(Map.Entry::getKey)
.toList(); .toList();
// Calculate the direction that has the lowest distance to the target
Direction best = directions.getFirst();
double bestDist = Double.MAX_VALUE;
MyPoint position = getPosition(); MyPoint position = getPosition();
for (Direction d : directions) { // Create a record to hold direction and distance
double nx = position.x + d.dx * GameMap.MAP_TILESIZE; record DirectionDistance(Direction direction, double distance) {
double ny = position.y + d.dy * GameMap.MAP_TILESIZE;
double dist = target.distance(nx, ny);
if (dist < bestDist) {
bestDist = dist;
best = d;
}
} }
log.debug("Ghost coming from {}, choosing {}, from {} (dist={})", // Stream through directions and find the one with the minimum distance to the target
ghost.getPrevDirection(), best, directions, bestDist); List<DirectionDistance> dd = directions.stream()
.map(d -> {
double nx = position.x + d.dx * GameMap.MAP_TILESIZE;
double ny = position.y + d.dy * GameMap.MAP_TILESIZE;
double dist = target.distance(nx, ny);
return new DirectionDistance(d, dist);
})
.toList();
log.debug("Target: {}, Position: {}", target, getPosition());
log.debug("Directions: {}", dd);
Direction best = dd.stream()
.min(Comparator.comparingDouble(DirectionDistance::distance))
.map(DirectionDistance::direction)
.orElse(directions.getFirst()); // Fallback to first direction if stream is empty
log.debug("Ghost coming from {}, choosing {}, from {}",
ghost.getPrevDirection(), best, directions);
return best; return best;
} }

View File

@ -18,25 +18,19 @@ public class EatenGhostMode extends AbstractGhostModeImpl {
public EatenGhostMode(Ghost ghost) { public EatenGhostMode(Ghost ghost) {
super(ghost, new EatenStrategy(), ghost.getLevelManager(), 9); super(ghost, new EatenStrategy(), ghost.getLevelManager(), 9);
// Eaten mode uses a specific strategy to return home
} }
@Override @Override
public void update(Ghost ghost, PacMan pacman, GameMap map) { public void update(Ghost ghost, PacMan pacman, GameMap map) {
// Check if ghost has reached its starting position if (getPosition().asPoint().distance(Ghost.getHouseEntrance().asPoint()) < 10) {
if (getPosition().asPoint().distance(Ghost.getStartPosition().asPoint()) < 10) {
log.debug("Ghost reached home, returning to chase mode"); log.debug("Ghost reached home, returning to chase mode");
ghost.setMode(GhostMode.CHASE); ghost.requestModeChange(GhostMode.CHASE);
return; return;
} }
// Update position using eaten strategy
updatePosition(ghost, pacman, map); updatePosition(ghost, pacman, map);
} }
// @Override
// public void enter(Ghost ghost) {}
@Override @Override
public BufferedImage[] getAnimation() { public BufferedImage[] getAnimation() {
return EATEN_ANIMATION; return EATEN_ANIMATION;

View File

@ -57,7 +57,7 @@ public class FrightenedGhostMode extends AbstractGhostModeImpl {
// Check if frightened mode should end // Check if frightened mode should end
if (frightenedTimer <= 0) { if (frightenedTimer <= 0) {
log.debug("Frightened mode ended"); log.debug("Frightened mode ended");
ghost.setMode(GhostMode.CHASE); ghost.requestModeChange(GhostMode.CHASE);
frightenedTimer = FRIGHTENED_DURATION_TICKS; frightenedTimer = FRIGHTENED_DURATION_TICKS;
isBlinking = false; isBlinking = false;
} }

View File

@ -1,10 +1,12 @@
package se.urmo.game.entities.ghost.mode; package se.urmo.game.entities.ghost.mode;
public enum GhostMode { public enum GhostMode {
FRIGHTENED, // Highest priority first
EATEN, EATEN, // 0: Ghost was eaten - highest priority
HOUSE, FRIGHTENED,// 1: Ghost is frightened by power pellet
FROZEN, FROZEN, // 2: Game is paused/frozen
CHASE, HOUSE, // 3: Ghost is in the house
SCATTER SCATTER, // 4: Ghost is scattering to corners
CHASE // 5: Ghost is chasing Pacman - lowest priority
} }

View File

@ -6,8 +6,6 @@ import se.urmo.game.entities.ghost.strategy.HouseStrategy;
import se.urmo.game.entities.pacman.PacMan; import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap; import se.urmo.game.map.GameMap;
import java.awt.Point;
@Slf4j @Slf4j
public class HouseGhostMode extends AbstractGhostModeImpl { public class HouseGhostMode extends AbstractGhostModeImpl {
public HouseGhostMode(Ghost ghost) { public HouseGhostMode(Ghost ghost) {
@ -16,10 +14,9 @@ public class HouseGhostMode extends AbstractGhostModeImpl {
@Override @Override
public void update(Ghost ghost, PacMan pacman, GameMap map) { public void update(Ghost ghost, PacMan pacman, GameMap map) {
if (getPosition().asPoint().distance(new Point(232, 154)) < 1) { if (getPosition().asPoint().distance(Ghost.getHouseEntrance().asPoint()) < 15) {
log.debug("Ghost left home, switching to chase mode"); log.debug("Ghost left home, switching to chase mode");
ghost.setMode(GhostMode.CHASE); ghost.requestModeChange(GhostMode.CHASE);
return;
} }
updatePosition(ghost, pacman, map); updatePosition(ghost, pacman, map);
} }

View File

@ -34,7 +34,7 @@ public class ScatterGhostMode extends AbstractGhostModeImpl {
scatterTimer--; scatterTimer--;
if (scatterTimer <= 0) { if (scatterTimer <= 0) {
log.debug("Scatter mode timed out, returning to chase"); log.debug("Scatter mode timed out, returning to chase");
ghost.setMode(GhostMode.CHASE); ghost.requestModeChange(GhostMode.CHASE);
scatterTimer = SCATTER_DURATION; scatterTimer = SCATTER_DURATION;
} }
} }

View File

@ -9,8 +9,6 @@ import java.awt.Point;
public class EatenStrategy implements GhostStrategy { public class EatenStrategy implements GhostStrategy {
@Override @Override
public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) { public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) {
return new Point( return Ghost.getHouseEntrance().asPoint();
13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X + (GameMap.MAP_TILESIZE / 2),
4 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + (GameMap.MAP_TILESIZE / 2));
} }
} }

View File

@ -17,7 +17,7 @@ public class FearStrategy implements GhostStrategy {
// Frightened ghosts do not target Pacman. // Frightened ghosts do not target Pacman.
// Instead, they pick a random adjacent valid tile. // Instead, they pick a random adjacent valid tile.
Point ghostPos = ghost.getPosition().asPoint(); Point ghostPos = ghost.getPosition().asPoint();
List<Direction> neighbors = map.directionAlternatives(ghostPos.x, ghostPos.y); List<Direction> neighbors = map.directionAlternatives(ghost, ghostPos.x, ghostPos.y);
if (neighbors.isEmpty()) { if (neighbors.isEmpty()) {
return ghost.getPosition().asPoint(); // stuck return ghost.getPosition().asPoint(); // stuck

View File

@ -9,6 +9,6 @@ import java.awt.Point;
public class HouseStrategy implements GhostStrategy { public class HouseStrategy implements GhostStrategy {
@Override @Override
public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) { public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) {
return new Point(232, 154); return new Point(232, 280);
} }
} }

View File

@ -1,6 +1,8 @@
package se.urmo.game.map; package se.urmo.game.map;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import se.urmo.game.entities.BaseAnimated;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.util.Direction; import se.urmo.game.util.Direction;
import java.awt.Graphics; import java.awt.Graphics;
@ -114,18 +116,35 @@ public class GameMap {
return b; return b;
} }
public boolean isSolidXY(int screenX, int screenY) { public boolean isSolidXY(Ghost ghost, int screenX, int screenY) {
return isSolid(screenToRow(screenY), screenToCol(screenX)); return isSolid(ghost, screenToRow(screenY), screenToCol(screenX));
} }
public boolean isSolid(int row, int col) { public boolean isSolid(BaseAnimated entity, int row, int col) {
// Check for out of bounds
if (col >= columns() || col < 0) return true; if (col >= columns() || col < 0) return true;
if (row >= rows() || row < 0) return true; if (row >= rows() || row < 0) return true;
// Get the tile information
MapTile mapTile = mapData[row][col]; MapTile mapTile = mapData[row][col];
boolean solid = mapTile.isSolid(); boolean solid = mapTile.isSolid();
// Special case: If the entity is a Ghost, it can pass through certain solid tiles
// This allows ghosts to move through areas that would be solid for other entities
if (entity instanceof Ghost) {
// For Ghost entities, check if this is a ghost-passable tile
// You might want to refine this logic based on specific tile types or ghost states
// Example: Allow ghosts to pass through ghost house door
if (mapTile.getTileType() == TileType.DOOR) {
return false; // Not solid for ghosts
}
}
//log.debug("[{}][{}] {}", row, col, mapTile.getTileType()); //log.debug("[{}][{}] {}", row, col, mapTile.getTileType());
return solid; return solid;
} }
public boolean removeTileImage(Point screen) { public boolean removeTileImage(Point screen) {
@ -220,13 +239,13 @@ public class GameMap {
} }
public List<Direction> directionAlternatives(int screenX, int screenY) { public List<Direction> directionAlternatives(BaseAnimated entity, int screenX, int screenY) {
int row = (screenY - GameMap.OFFSET_Y) / GameMap.MAP_TILESIZE; int row = (screenY - GameMap.OFFSET_Y) / GameMap.MAP_TILESIZE;
int col = (screenX - GameMap.OFFSET_X) / GameMap.MAP_TILESIZE; int col = (screenX - GameMap.OFFSET_X) / GameMap.MAP_TILESIZE;
record DirectionCheck(int rowOffset, int colOffset, Direction direction) { record DirectionCheck(int rowOffset, int colOffset, Direction direction) {
} }
log.debug("At [{}][{}]", row, col); log.debug("At ({},{}), [{}][{}]", screenX, screenY, row, col);
return Stream.of( return Stream.of(
new DirectionCheck(0, 1, Direction.RIGHT), new DirectionCheck(0, 1, Direction.RIGHT),
new DirectionCheck(0, -1, Direction.LEFT), new DirectionCheck(0, -1, Direction.LEFT),
@ -236,7 +255,7 @@ public class GameMap {
.filter(dc -> { .filter(dc -> {
int r = row + dc.rowOffset; int r = row + dc.rowOffset;
int c = col + dc.colOffset; int c = col + dc.colOffset;
boolean solid = isSolid(r, c); boolean solid = isSolid(entity, r, c);
log.debug("[{}][{}] {} is {}", r, c, dc.direction, solid ? "solid" : " not solid"); log.debug("[{}][{}] {} is {}", r, c, dc.direction, solid ? "solid" : " not solid");
return !solid; return !solid;
}) })

View File

@ -43,7 +43,7 @@ public enum TileType {
TILE_35(35, true, SpriteLocation.MAP, 3, 1, false ,0), TILE_35(35, true, SpriteLocation.MAP, 3, 1, false ,0),
TILE_36(36, true, SpriteLocation.MAP, 3, 2, false ,0), TILE_36(36, true, SpriteLocation.MAP, 3, 2, false ,0),
TILE_37(37, true, SpriteLocation.MAP, 3, 3, false ,0), TILE_37(37, true, SpriteLocation.MAP, 3, 3, false ,0),
TILE_38(38, true, SpriteLocation.MAP, 3, 4, false ,0), DOOR(38, true, SpriteLocation.MAP, 3, 4, false, 0),
TILE_39(39, true, SpriteLocation.MAP, 3, 5, false ,0), TILE_39(39, true, SpriteLocation.MAP, 3, 5, false ,0),
TILE_40(40, true, SpriteLocation.MAP, 3, 6, false ,0), TILE_40(40, true, SpriteLocation.MAP, 3, 6, false ,0),
TILE_41(41, true, SpriteLocation.MAP, 3, 7, false ,0), TILE_41(41, true, SpriteLocation.MAP, 3, 7, false ,0),