Files
pacman.js/Ghost.js
2019-06-11 18:46:14 +02:00

278 lines
8.3 KiB
JavaScript

function dirNameToValue(dirName) {
switch (dirName) {
case 'left':
return {x: -1, y: 0};
case 'right':
return {x: 1, y: 0};
case 'top':
return {x: 0, y: -1};
case 'bottom':
return {x: 0, y: 1};
default:
return null;
}
}
class Ghost extends MovingObject {
constructor(game, x, y) {
super(game, x, y);
this.speed = 2;
this.dir = {x: 0, y: 0};
this.lastDirChange = {x: Infinity, y: Infinity};
}
draw(ctx, conf) {
const size = conf.tileSize;
const cx = this.x * size;
const cy = this.y * size;
const left = -0.5 * size;
const top = -0.5 * size;
const right = 0.5 * size;
const base = 0.5 * size - 3;
const inc = size / 10;
const high = 3;
const low = -3;
ctx.save();
ctx.translate(cx, cy);
ctx.fillStyle = "#0f0";
ctx.beginPath();
// Draw body
ctx.moveTo(left, base);
ctx.quadraticCurveTo(left, top, 0, top);
ctx.quadraticCurveTo(right, top, right, base);
// Wavy things at the bottom
ctx.quadraticCurveTo(right - (inc * 1), base + high, right - (inc * 2), base);
ctx.quadraticCurveTo(right - (inc * 3), base + low, right - (inc * 4), base);
ctx.quadraticCurveTo(right - (inc * 5), base + high, right - (inc * 6), base);
ctx.quadraticCurveTo(right - (inc * 7), base + low, right - (inc * 8), base);
ctx.quadraticCurveTo(right - (inc * 9), base + high, right - (inc * 10), base);
ctx.closePath();
ctx.fill();
// Draw eye background
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(left + size * .3, top + size * .3, size / 6, 0, 2 * Math.PI);
ctx.arc(right - size * .3, top + size * .3, size / 6, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
// Draw eyes
const f = size / 12;
const offset = {
x: this.dir.x * f,
y: this.dir.y * f,
};
ctx.fillStyle = "#000";
ctx.beginPath();
ctx.arc(left + size * .3 + offset.x, top + size * .3 + offset.y, size / 15, 0, 2 * Math.PI);
ctx.arc(right - size * .3 + offset.x, top + size * .3 + offset.y, size / 15, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
ctx.restore();
}
update(deltaTime) {
const nextTile = this.getNextTile(0, 0);
if (nextTile.distanceX <= 0.5 && nextTile.distanceY <= 0.5 && !(nextTile.tile.x === this.lastDirChange.x && nextTile.tile.y === this.lastDirChange.y)) {
const surroundings = this.game.map.getSurroundingTiles(nextTile.tile.x, nextTile.tile.y);
const nonSolid = Object.entries(surroundings).filter(surrounding => !surrounding[1].solid);
if (nonSolid.length === 1) {
this.changeDir({x: -this.dir.x, y: -this.dir.y});
this.lastDirChange = nextTile.tile;
} else {
const possibleDirections = [];
nonSolid.forEach(entry => {
const dir = dirNameToValue(entry[0]);
if (dir.x === -this.dir.x && dir.y === -this.dir.y)
return;
possibleDirections.push(dir);
});
if (possibleDirections.length === 1) {
this.changeDir(possibleDirections[0]);
this.lastDirChange = nextTile.tile;
} else if (possibleDirections.length === 2) {
const newDirection = possibleDirections.find(dir => dir.x !== -this.dir.x || dir.y !== -this.dir.y);
this.changeDir(newDirection);
this.lastDirChange = nextTile.tile;
} else {
let bestScore = -Infinity;
let bestDir;
const player = this.game.getPlayer().getNextTile(0, 0).tile;
possibleDirections.forEach(dir => {
const score = this.getDirectionValue(nextTile.tile, dir, player);
if (score > bestScore) {
bestDir = dir;
bestScore = score;
}
});
if (bestDir) {
this.changeDir(bestDir);
this.lastDirChange = nextTile.tile;
}
}
}
}
super.update(deltaTime);
}
getDirectionValue(tile, dir, goal) {
const newTile = this.game.map.getTile(tile.x + dir.x, tile.y + dir.y);
if (!newTile || newTile.solid) {
return -Infinity;
}
const pathFinder = new Pathfinder(this.game.map.tiles);
const path = pathFinder.findShortestPath(newTile, goal);
// const curDistance = (tile.x - goal.x) ** 2 + (tile.y - goal.y) ** 2;
// const newDistance = (newTile.x - goal.x) ** 2 + (newTile.y - goal.y) ** 2;
//
// let score = curDistance - newDistance;
let score = 1 / path.length * 10;
if (this.dir.x === -dir.x || this.dir.y === -dir.y) {
score -= Math.abs(score) / 2;
}
return score;
}
}
class Pathfinder {
constructor(grid) {
this.grid = [];
this.queue = [];
grid.forEach((col, x) => {
this.grid.push([]);
col.forEach(tile => {
this.grid[x].push(tile.solid ? 2 : 0);
});
});
}
findShortestPath(start, end) {
this.grid[end.x][end.y] = 10;
const location = {
x: start.x,
y: start.y,
path: [],
status: 1,
};
this.queue = [location];
return this.loop();
}
loop() {
while (this.queue.length > 0) {
const current = this.queue.shift();
let newLocation;
// Explore north
newLocation = this.exploreInDirection(current, {x: 0, y: -1});
if (newLocation.status === 10) {
return newLocation.path;
} else if (newLocation.status === 5) {
this.queue.push(newLocation);
}
// Explore east
newLocation = this.exploreInDirection(current, {x: 1, y: 0});
if (newLocation.status === 10) {
return newLocation.path;
} else if (newLocation.status === 5) {
this.queue.push(newLocation);
}
// Explore south
newLocation = this.exploreInDirection(current, {x: 0, y: 1});
if (newLocation.status === 10) {
return newLocation.path;
} else if (newLocation.status === 5) {
this.queue.push(newLocation);
}
// Explore west
newLocation = this.exploreInDirection(current, {x: -1, y: 0});
if (newLocation.status === 10) {
return newLocation.path;
} else if (newLocation.status === 5) {
this.queue.push(newLocation);
}
}
return false;
}
locationStatus(location) {
const width = this.grid.length,
height = this.grid[0].length;
const x = location.x,
y = location.y;
if (x < 0 || x >= width || y < 0 || y >= height) {
return 7; // Invalid
}
if (this.grid[x][y] === 10) {
return 10; // Goal
}
if (this.grid[x][y] !== 0) {
return 6; // Blocked: location is either an obstacle or has already been visited
} else {
return 5; // Valid
}
}
exploreInDirection(location, direction) {
const newPath = location.path.slice();
const x = location.x + direction.x,
y = location.y + direction.y;
newPath.push({
x: x,
y: y,
direction: direction,
});
const newLocation = {
x: x,
y: y,
path: newPath,
status: -1, // unknown status
};
newLocation.status = this.locationStatus(newLocation);
if (newLocation.status === 5) {
this.grid[x][y] = 4; // Set as visited
}
return newLocation;
}
}