summaryrefslogtreecommitdiff
path: root/client/src/logic/ai.ts
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/logic/ai.ts')
-rw-r--r--client/src/logic/ai.ts197
1 files changed, 197 insertions, 0 deletions
diff --git a/client/src/logic/ai.ts b/client/src/logic/ai.ts
new file mode 100644
index 0000000..875621a
--- /dev/null
+++ b/client/src/logic/ai.ts
@@ -0,0 +1,197 @@
+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";
+
+const diff = (a: Vec2, b:Vec2): Vec2 => {
+ return {x: a.x - b.x, y: a.y - b.y}
+}
+
+const dist = (a: Vec2, b: Vec2): number => {
+ return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2))
+}
+
+const trans = (pos: Vec2, rot: Rotation, dist: number): Vec2 => {
+ switch (rot) {
+ case Rotation.NORTH:
+ case Rotation.NOTHING:
+ return {x: pos.x - dist, y: pos.y - dist}
+ case Rotation.EAST:
+ return {x: pos.x + dist, y: pos.y}
+ case Rotation.SOUTH:
+ return {x: pos.x, y: pos.y + dist}
+ case Rotation.WEST:
+ return {x: pos.x - dist, y: pos.y}
+ }
+}
+
+const getNearestPlayer = (state: GameState, pos: Vec2): Player => {
+ let min = undefined;
+ let nearest = undefined;
+ for (let id in state.players) {
+ let player = state.players[id];
+ if (!id) continue;
+
+ let d = dist(player.pos, pos)
+ if (!min || min > d) {
+ min = d
+ nearest = player
+ }
+ }
+
+ return nearest
+}
+
+const pickTargetScatter = (state: GameState, type: GhostType): Vec2 => {
+ let map = getMap(state.mapId)
+ switch (type) {
+ case GhostType.BLINKY:
+ return {x: 0, y: -1}
+ case GhostType.PINKY:
+ return {x: map.width - 1, y: -1}
+ case GhostType.INKY:
+ return {x: map.width - 1, y: map.height}
+ case GhostType.CLYDE:
+ return {x: 0, y: map.height}
+ }
+}
+
+const pickTargetChase = (state: GameState, type: GhostType): Vec2 => {
+ let ghost = state.ghosts[type]
+ let player = getNearestPlayer(state, ghost.pos)
+ switch (type) {
+ case GhostType.BLINKY:
+ return {x: player.pos.x, y: player.pos.y}
+ case GhostType.PINKY:
+ return trans(player.pos, player.moveRotation, 2)
+ case GhostType.INKY:
+ let target = trans(player.pos, player.moveRotation, 1)
+ let vec = diff(target, state.ghosts[GhostType.BLINKY].pos)
+ return {x: target.x + vec.x, y: target.y + vec.y}
+ case GhostType.CLYDE:
+ if (dist(ghost.pos, player.pos) > 8)
+ return {x: player.pos.x, y: player.pos.y}
+ else
+ return pickTargetScatter(state, type)
+ }
+}
+
+
+
+const pickTarget = (state: GameState, map: Map, type: GhostType): Vec2 => {
+ let ghost: Ghost = state.ghosts[type]
+ switch (ghost.state) {
+ case GhostState.SCATTER:
+ return pickTargetScatter(state, type)
+ case GhostState.CHASE:
+ return pickTargetChase(state, type)
+ case GhostState.EATEN:
+ return structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN])
+ case GhostState.SCARED:
+ return {
+ x: random(state) % map.width,
+ y: random(state) % map.height
+ }
+ }
+}
+
+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 updateGhost = (state: GameState, map: Map, type: GhostType) => {
+ let ghost: Ghost = state.ghosts[type]
+
+ if (!ghost) {
+ ghost = {
+ pos: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]),
+ target: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]),
+ type,
+ state: GhostState.SCARED,
+ currentDirection: Rotation.EAST,
+ }
+ state.ghosts[type] = ghost
+ }
+
+ if (isStablePos(ghost.pos)) {
+
+ ghost.pos = roundPos(ghost.pos)
+
+ 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 isIntersection =
+ (north && east) ||
+ (east && south) ||
+ (south && west) ||
+ (west && north)
+
+ if (!isIntersection && front) {
+ ghost.currentDirection = flipRot(ghost.currentDirection)
+ } else if (isIntersection) {
+ let target = pickTarget(state, map, type)
+ ghost.target = target
+
+ 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) {
+ min = d
+ newRot = Rotation.NORTH
+ }
+ }
+
+ if (east && ghost.currentDirection !== Rotation.WEST) {
+ let d = dist({x: ghost.pos.x + 1, y: ghost.pos.y}, target)
+ if (!min || min > d) {
+ min = d
+ newRot = Rotation.EAST
+ }
+ }
+
+ if (south && ghost.currentDirection !== Rotation.NORTH) {
+ let d = dist({x: ghost.pos.x, y: ghost.pos.y + 1}, target)
+ if (!min || min > d) {
+ min = d
+ newRot = Rotation.SOUTH
+ }
+ }
+
+ if (west && ghost.currentDirection !== Rotation.EAST) {
+ let d = dist({x: ghost.pos.x - 1, y: ghost.pos.y}, target)
+ if (!min || min > d) {
+ min = d
+ newRot = Rotation.WEST
+ }
+ }
+
+ ghost.currentDirection = newRot
+ }
+ }
+
+ incrementPos(ghost.pos, ghost.currentDirection, MOVE_SPEED)
+}
+
+export const updateGhosts = (state: GameState) => {
+ let map = getMap(state.mapId)
+ if (!map) return
+
+ updateGhost(state, map, GhostType.BLINKY)
+ updateGhost(state, map, GhostType.PINKY)
+ updateGhost(state, map, GhostType.INKY)
+ updateGhost(state, map, GhostType.CLYDE)
+}