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;
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.util.Direction;
import se.urmo.game.util.MyPoint;
import java.util.List;
@ -15,19 +16,16 @@ public class GhostCollisionChecker {
this.map = map;
}
public List<Direction> calculateDirectionAlternatives(MyPoint position) {
List<Direction> intersection = map.directionAlternatives((int) position.x, (int) position.y);
public List<Direction> calculateDirectionAlternatives(Ghost ghost, MyPoint position) {
List<Direction> intersection = map.directionAlternatives(ghost, (int) position.x, (int) position.y);
log.info("Possible travel directions: {}", intersection);
return intersection;
}
public MyPoint canMoveTo(Direction dir, double x, double y) {
public MyPoint canMoveTo(Ghost ghost, Direction dir, double x, double y) {
// -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(
return !map.isSolidXY(ghost,
(int) (x) + dir.dx * (GameMap.MAP_TILESIZE/2 - 1),
(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(
GameMap.colToScreen(13) + ((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) {
super(ANIMATION_UPDATE_FREQUENCY, GhostManager.MAX_SPRITE_FRAMES);
@ -81,6 +85,9 @@ public class Ghost extends BaseAnimated {
(int) position.y - GHOST_SIZE / 2,
GHOST_SIZE,
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) {
@ -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() {
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.image.BufferedImage;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -61,7 +62,7 @@ public abstract class AbstractGhostModeImpl implements GhostState {
ghost.setDirection(chooseDirection(
ghost,
prioritizeDirections(
ghost.getCollisionChecker().calculateDirectionAlternatives(getPosition())),
ghost.getCollisionChecker().calculateDirectionAlternatives(ghost, getPosition())),
getStrategy().chooseTarget(ghost, pacman, map)));
log.debug("Ghost moving to {}", getPosition());
@ -83,7 +84,7 @@ public abstract class AbstractGhostModeImpl implements GhostState {
}
private void moveTo(Ghost ghost, MyPoint newPosition) {
MyPoint destination = ghost.getCollisionChecker().canMoveTo(
MyPoint destination = ghost.getCollisionChecker().canMoveTo(ghost,
getDirection(), newPosition.x, newPosition.y);
if (destination != null) {
ghost.setPosition(destination);
@ -113,23 +114,31 @@ public abstract class AbstractGhostModeImpl implements GhostState {
.map(Map.Entry::getKey)
.toList();
// Calculate the direction that has the lowest distance to the target
Direction best = directions.getFirst();
double bestDist = Double.MAX_VALUE;
MyPoint position = getPosition();
for (Direction d : directions) {
// Create a record to hold direction and distance
record DirectionDistance(Direction direction, double distance) {
}
// Stream through directions and find the one with the minimum distance to the target
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);
if (dist < bestDist) {
bestDist = dist;
best = d;
}
}
return new DirectionDistance(d, dist);
})
.toList();
log.debug("Ghost coming from {}, choosing {}, from {} (dist={})",
ghost.getPrevDirection(), best, directions, bestDist);
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;
}

View File

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

View File

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

View File

@ -1,10 +1,12 @@
package se.urmo.game.entities.ghost.mode;
public enum GhostMode {
FRIGHTENED,
EATEN,
HOUSE,
FROZEN,
CHASE,
SCATTER
// Highest priority first
EATEN, // 0: Ghost was eaten - highest priority
FRIGHTENED,// 1: Ghost is frightened by power pellet
FROZEN, // 2: Game is paused/frozen
HOUSE, // 3: Ghost is in the house
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.map.GameMap;
import java.awt.Point;
@Slf4j
public class HouseGhostMode extends AbstractGhostModeImpl {
public HouseGhostMode(Ghost ghost) {
@ -16,10 +14,9 @@ public class HouseGhostMode extends AbstractGhostModeImpl {
@Override
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");
ghost.setMode(GhostMode.CHASE);
return;
ghost.requestModeChange(GhostMode.CHASE);
}
updatePosition(ghost, pacman, map);
}

View File

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

View File

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

View File

@ -17,7 +17,7 @@ public class FearStrategy implements GhostStrategy {
// Frightened ghosts do not target Pacman.
// Instead, they pick a random adjacent valid tile.
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()) {
return ghost.getPosition().asPoint(); // stuck

View File

@ -9,6 +9,6 @@ import java.awt.Point;
public class HouseStrategy implements GhostStrategy {
@Override
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;
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 java.awt.Graphics;
@ -114,18 +116,35 @@ public class GameMap {
return b;
}
public boolean isSolidXY(int screenX, int screenY) {
return isSolid(screenToRow(screenY), screenToCol(screenX));
public boolean isSolidXY(Ghost ghost, int screenX, int screenY) {
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 (row >= rows() || row < 0) return true;
// Get the tile information
MapTile mapTile = mapData[row][col];
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());
return solid;
}
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 col = (screenX - GameMap.OFFSET_X) / GameMap.MAP_TILESIZE;
record DirectionCheck(int rowOffset, int colOffset, Direction direction) {
}
log.debug("At [{}][{}]", row, col);
log.debug("At ({},{}), [{}][{}]", screenX, screenY, row, col);
return Stream.of(
new DirectionCheck(0, 1, Direction.RIGHT),
new DirectionCheck(0, -1, Direction.LEFT),
@ -236,7 +255,7 @@ public class GameMap {
.filter(dc -> {
int r = row + dc.rowOffset;
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");
return !solid;
})

View File

@ -43,7 +43,7 @@ public enum TileType {
TILE_35(35, true, SpriteLocation.MAP, 3, 1, false ,0),
TILE_36(36, true, SpriteLocation.MAP, 3, 2, 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_40(40, true, SpriteLocation.MAP, 3, 6, false ,0),
TILE_41(41, true, SpriteLocation.MAP, 3, 7, false ,0),