summaryrefslogtreecommitdiff
path: root/client/src/logic/ai.ts
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--client/src/logic/ai.ts237
1 files changed, 212 insertions, 25 deletions
diff --git a/client/src/logic/ai.ts b/client/src/logic/ai.ts
index 875621a..25e5d25 100644
--- a/client/src/logic/ai.ts
+++ b/client/src/logic/ai.ts
@@ -1,7 +1,73 @@
import { getMap } from "../map.js";
import { Map, Vec2, GhostType, GameState, SpawnIndex, Player, Rotation, GhostState, Tile, Ghost } from "../types.js";
import { random } from "./logic.js";
-import { MOVE_SPEED, roundPos, isStablePos, getTile, getTileFrontWithRot, incrementPos } from "./movement.js";
+import { MOVE_SPEED, roundPos, isStablePos, getTile, getTileFrontWithRot, incrementPos, wrapPos } from "./movement.js";
+
+const vec2eq = (a: Vec2, b: Vec2): boolean => {
+ return a.x == b.x && a.y == b.y
+}
+
+const canPath = (tile: Tile): boolean => tile != Tile.WALL
+const canCollide = (tile: Tile, isPathing: boolean): boolean => tile != Tile.WALL && (isPathing ? true : tile != Tile.GHOST_EXIT)
+
+const path = (start: Vec2, end: Vec2, map: Map) => {
+ let mask: Vec2[] = new Array(map.width * map.height)
+ let queue: Vec2[] = []
+ queue.push(start)
+
+ while (true) {
+
+ let next: Vec2 = queue.shift()
+ if (!next) {
+ console.log('failed')
+ return undefined
+ }
+ if (vec2eq(next, end)) break
+
+ let north = canPath(getTile(map, next, 0, -1))
+ let south = canPath(getTile(map, next, 0, 1))
+ let east = canPath(getTile(map, next, 1, 0))
+ let west = canPath(getTile(map, next, -1, 0))
+
+ if (north && !mask[(next.y - 1) * map.width + (next.x)]) {
+ queue.push({x: next.x, y: next.y - 1})
+ mask[(next.y - 1) * map.width + (next.x)] = next
+ }
+
+ if (south && !mask[(next.y + 1) * map.width + (next.x)]) {
+ queue.push({x: next.x, y: next.y + 1})
+ mask[(next.y + 1) * map.width + (next.x)] = next
+ }
+
+ if (east && !mask[(next.y) * map.width + (next.x + 1)]) {
+ queue.push({x: next.x + 1, y: next.y})
+ mask[(next.y) * map.width + (next.x + 1)] = next
+ }
+
+ if (west && !mask[(next.y) * map.width + (next.x - 1)]) {
+ queue.push({x: next.x - 1, y: next.y})
+ mask[(next.y) * map.width + (next.x - 1)] = next
+ }
+ }
+
+ let solution = []
+
+ let next = end
+ while (true) {
+ solution.push(structuredClone(next))
+ if (vec2eq(next, start)) {
+ break
+ } else {
+ next = mask[next.y * map.width + next.x]
+ }
+ }
+
+ solution = solution.reverse()
+ solution.shift()
+
+ return solution
+
+}
const diff = (a: Vec2, b:Vec2): Vec2 => {
return {x: a.x - b.x, y: a.y - b.y}
@@ -80,6 +146,12 @@ const pickTargetChase = (state: GameState, type: GhostType): Vec2 => {
const pickTarget = (state: GameState, map: Map, type: GhostType): Vec2 => {
let ghost: Ghost = state.ghosts[type]
+
+ let target = ghost.targetQueue.shift()
+ if (target) {
+ return target
+ }
+
switch (ghost.state) {
case GhostState.SCATTER:
return pickTargetScatter(state, type)
@@ -95,17 +167,105 @@ const pickTarget = (state: GameState, map: Map, type: GhostType): Vec2 => {
}
}
-const flipRot = (rot: Rotation) => {
- switch (rot) {
- case Rotation.NORTH:
- return Rotation.SOUTH
- case Rotation.SOUTH:
- return Rotation.NORTH
- case Rotation.EAST:
- return Rotation.WEST
- case Rotation.WEST:
- return Rotation.EAST
+const checkIfEaten = (ghost: Ghost, state: GameState): boolean => {
+
+ if (ghost.state != GhostState.SCARED) {
+ return false
}
+
+ for (let id in state.players) {
+ let player = state.players[id]
+
+ if (player.thiccLeft > 0 && dist(player.pos, ghost.pos) <= 1) {
+ return true
+ }
+ }
+
+ return false
+}
+
+const updateKilled = (ghost: Ghost, state: GameState) => {
+
+ if (ghost.state == GhostState.EATEN) {
+ return
+ }
+
+ for (let id in state.players) {
+ let player = state.players[id]
+
+ if (dist(player.pos, ghost.pos) > 1) {
+ continue
+ }
+
+ if (ghost.state != GhostState.SCARED || player.thiccLeft < 1) {
+ player.dead = true
+ player.framesDead = 0
+ }
+ }
+
+}
+
+const getGhostState = (ghost: Ghost, state: GameState): GhostState => {
+
+ if (ghost.state == GhostState.EATEN || checkIfEaten(ghost, state)) {
+ ghost.hasRespawned = false
+ return GhostState.EATEN
+ }
+
+ if (Object.values(state.players).filter((p: Player) => p.thiccLeft > 0).length > 0) {
+ if (!ghost.hasRespawned) {
+ return GhostState.SCARED
+ }
+ } else {
+ ghost.hasRespawned = false
+ }
+
+ return (Object.keys(state.items).length % 100 < 10 ? GhostState.SCATTER : GhostState.CHASE)
+}
+
+const getGhostPath = (ghost: Ghost, map: Map): Vec2[] => {
+
+ if (ghost.targetQueue.length > 0) { // already pathing
+ return undefined
+ }
+
+ if (ghost.state == GhostState.EATEN) { // dead go back to spawn
+ if (vec2eq(ghost.pos, map.spawns[SpawnIndex.GHOST_SPAWN])) { // returned to spawn, exit the box
+ ghost.state = GhostState.CHASE
+ ghost.hasRespawned = true
+ return path(ghost.pos, map.spawns[SpawnIndex.GHOST_EXIT], map)
+ } else { // go to the box still dead
+ return path(ghost.pos, map.spawns[SpawnIndex.GHOST_SPAWN], map)
+ }
+ }
+
+ let tile = getTile(map, ghost.pos, 0, 0)
+ if (tile != Tile.GHOST_WALL && tile != Tile.GHOST_SPAWN) { // not in the box
+ return undefined
+ }
+
+ let threshold = 0
+ switch (ghost.type) {
+ case GhostType.BLINKY:
+ threshold = 60 * 5
+ break
+ case GhostType.PINKY:
+ threshold = 60 * 10
+ break
+ case GhostType.INKY:
+ threshold = 60 * 15
+ break
+ case GhostType.CLYDE:
+ threshold = 60 * 20
+ break
+ }
+
+ if (ghost.framesInBox < threshold) {
+ return undefined
+ }
+
+ return path(ghost.pos, map.spawns[SpawnIndex.GHOST_EXIT], map)
+
}
const updateGhost = (state: GameState, map: Map, type: GhostType) => {
@@ -115,22 +275,45 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => {
ghost = {
pos: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]),
target: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]),
+ targetQueue: [],
type,
- state: GhostState.SCARED,
+ state: GhostState.SCATTER,
currentDirection: Rotation.EAST,
+ hasRespawned: false,
+ framesInBox: 0,
}
state.ghosts[type] = ghost
}
+
+ let tile = getTile(map, ghost.pos, 0, 0)
+ if (tile == Tile.GHOST_WALL || tile == Tile.GHOST_SPAWN) {
+ ghost.framesInBox++
+ }
+
+ ghost.state = getGhostState(ghost, state)
if (isStablePos(ghost.pos)) {
ghost.pos = roundPos(ghost.pos)
+
+ let newPath = getGhostPath(ghost, map)
+ if (newPath) {
+ ghost.targetQueue = newPath
+ }
- let front = getTileFrontWithRot(map, ghost.pos, ghost.currentDirection) == Tile.WALL
- let north = getTile(map, ghost.pos, 0, -1) != Tile.WALL
- let east = getTile(map, ghost.pos, 1, 0) != Tile.WALL
- let south = getTile(map, ghost.pos, 0, 1) != Tile.WALL
- let west = getTile(map, ghost.pos, -1, 0) != Tile.WALL
+ let frontTile = getTileFrontWithRot(map, ghost.pos, ghost.currentDirection)
+ let northTile = getTile(map, ghost.pos, 0, -1)
+ let eastTile = getTile(map, ghost.pos, 1, 0)
+ let southTile = getTile(map, ghost.pos, 0, 1)
+ let westTile = getTile(map, ghost.pos, -1, 0)
+
+ let isPathing = ghost.targetQueue.length > 0
+
+ let front = canCollide(frontTile, isPathing)
+ let north = canCollide(northTile, isPathing)
+ let east = canCollide(eastTile, isPathing)
+ let south = canCollide(southTile, isPathing)
+ let west = canCollide(westTile, isPathing)
let isIntersection =
(north && east) ||
@@ -138,18 +321,20 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => {
(south && west) ||
(west && north)
- if (!isIntersection && front) {
- ghost.currentDirection = flipRot(ghost.currentDirection)
- } else if (isIntersection) {
+ if (isIntersection || !front || (isPathing && vec2eq(ghost.pos, ghost.target))) {
let target = pickTarget(state, map, type)
ghost.target = target
+
+ if ((!isIntersection && !front) || isPathing) {
+ ghost.currentDirection = undefined
+ }
let newRot = ghost.currentDirection
let min = undefined
if (north && ghost.currentDirection !== Rotation.SOUTH) {
let d = dist({x: ghost.pos.x, y: ghost.pos.y - 1}, target)
- if (!min || min > d) {
+ if (min === undefined || min > d) {
min = d
newRot = Rotation.NORTH
}
@@ -157,7 +342,7 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => {
if (east && ghost.currentDirection !== Rotation.WEST) {
let d = dist({x: ghost.pos.x + 1, y: ghost.pos.y}, target)
- if (!min || min > d) {
+ if (min === undefined || min > d) {
min = d
newRot = Rotation.EAST
}
@@ -165,7 +350,7 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => {
if (south && ghost.currentDirection !== Rotation.NORTH) {
let d = dist({x: ghost.pos.x, y: ghost.pos.y + 1}, target)
- if (!min || min > d) {
+ if (min === undefined || min > d) {
min = d
newRot = Rotation.SOUTH
}
@@ -173,7 +358,7 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => {
if (west && ghost.currentDirection !== Rotation.EAST) {
let d = dist({x: ghost.pos.x - 1, y: ghost.pos.y}, target)
- if (!min || min > d) {
+ if (min === undefined || min > d) {
min = d
newRot = Rotation.WEST
}
@@ -183,7 +368,9 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => {
}
}
- incrementPos(ghost.pos, ghost.currentDirection, MOVE_SPEED)
+ incrementPos(ghost.pos, ghost.currentDirection, MOVE_SPEED)
+ wrapPos(ghost.pos, map)
+ updateKilled(ghost, state)
}
export const updateGhosts = (state: GameState) => {