From f94a55fd6cdfe89f1c1aa7db85e2e605d9c4587d Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Tue, 15 Oct 2024 20:44:02 +0300 Subject: [PATCH 1/6] Added base simulation functionality --- games/ZombieSurvival/Direction.ts | 6 ++ games/ZombieSurvival/Position.ts | 8 ++ games/ZombieSurvival/ZombieSurvival.ts | 89 ++++++++++++++++++++++ games/ZombieSurvival/entities/Box.ts | 11 +++ games/ZombieSurvival/entities/Entity.ts | 33 ++++++++ games/ZombieSurvival/entities/Player.ts | 22 ++++++ games/ZombieSurvival/entities/Rock.ts | 11 +++ games/ZombieSurvival/entities/Zombie.ts | 60 +++++++++++++++ games/ZombieSurvival/index.ts | 8 ++ games/ZombieSurvival/lib/closest-entity.ts | 30 ++++++++ games/ZombieSurvival/lib/entity-at.ts | 17 +++++ games/ZombieSurvival/lib/path-finder.ts | 7 ++ 12 files changed, 302 insertions(+) create mode 100644 games/ZombieSurvival/Direction.ts create mode 100644 games/ZombieSurvival/Position.ts create mode 100644 games/ZombieSurvival/ZombieSurvival.ts create mode 100644 games/ZombieSurvival/entities/Box.ts create mode 100644 games/ZombieSurvival/entities/Entity.ts create mode 100644 games/ZombieSurvival/entities/Player.ts create mode 100644 games/ZombieSurvival/entities/Rock.ts create mode 100644 games/ZombieSurvival/entities/Zombie.ts create mode 100644 games/ZombieSurvival/index.ts create mode 100644 games/ZombieSurvival/lib/closest-entity.ts create mode 100644 games/ZombieSurvival/lib/entity-at.ts create mode 100644 games/ZombieSurvival/lib/path-finder.ts diff --git a/games/ZombieSurvival/Direction.ts b/games/ZombieSurvival/Direction.ts new file mode 100644 index 0000000..accb6bf --- /dev/null +++ b/games/ZombieSurvival/Direction.ts @@ -0,0 +1,6 @@ +export enum Direction { + Down, + Left, + Right, + Up, +} diff --git a/games/ZombieSurvival/Position.ts b/games/ZombieSurvival/Position.ts new file mode 100644 index 0000000..61a08f9 --- /dev/null +++ b/games/ZombieSurvival/Position.ts @@ -0,0 +1,8 @@ +export interface Position { + x: number; + y: number; +} + +export function positionAsNumber(position: Position): number { + return position.x + position.y; +} diff --git a/games/ZombieSurvival/ZombieSurvival.ts b/games/ZombieSurvival/ZombieSurvival.ts new file mode 100644 index 0000000..9ff7511 --- /dev/null +++ b/games/ZombieSurvival/ZombieSurvival.ts @@ -0,0 +1,89 @@ +import { Box } from "./entities/Box"; +import { Entity } from "./entities/Entity"; +import { Player } from "./entities/Player"; +import { Rock } from "./entities/Rock"; +import { Zombie } from "./entities/Zombie"; + +export class ZombieSurvival { + private readonly boardHeight: number; + private readonly boardWidth: number; + private readonly entities: Entity[]; + private readonly player: Player; + private readonly zombies: Zombie[]; + + public constructor(config: string[][]) { + if (config.length === 0 || config[0].length == 0) { + throw new Error("Config is empty"); + } + + this.boardWidth = config[0].length; + this.boardHeight = config.length; + this.entities = []; + this.zombies = []; + + let player: Player | null = null; + + for (let y = 0; y < this.boardHeight; y++) { + for (let x = 0; x < this.boardWidth; x++) { + const code = config[x][y]; + + switch (code.toLowerCase()) { + case "b": { + this.entities.push(new Box({ x, y })); + break; + } + case "p": { + if (player !== null) { + throw new Error("Config contains multiple players"); + } + + player = new Player(this, { x, y }); + break; + } + case "r": { + this.entities.push(new Rock({ x, y })); + break; + } + case "z": { + this.zombies.push(new Zombie(this, { x, y })); + break; + } + } + } + } + + if (player === null) { + throw new Error("Config has no player"); + } + + this.player = player; + + if (this.zombies.length === 0) { + throw new Error("Config has no zombies"); + } + } + + public finished(): boolean { + return this.player.dead() || this.zombies.every((zombie) => zombie.dead()); + } + + public getAllEntities(): Entity[] { + return [this.zombies, this.player, this.entities].flat(); + } + + public getPlayer(): Player { + return this.player; + } + + public getZombies(): Zombie[] { + return this.zombies; + } + + public step() { + this.player.shoot(); + + for (const zombie of this.zombies) { + zombie.walk(); + } + } +} diff --git a/games/ZombieSurvival/entities/Box.ts b/games/ZombieSurvival/entities/Box.ts new file mode 100644 index 0000000..a2df312 --- /dev/null +++ b/games/ZombieSurvival/entities/Box.ts @@ -0,0 +1,11 @@ +import { Entity } from "./Entity"; +import { Position } from "../Position"; + +export class Box extends Entity { + public static Destructible = true; + public static Health = 2; + + public constructor(position: Position) { + super(Box.Destructible, Box.Health, position); + } +} diff --git a/games/ZombieSurvival/entities/Entity.ts b/games/ZombieSurvival/entities/Entity.ts new file mode 100644 index 0000000..9b7fd62 --- /dev/null +++ b/games/ZombieSurvival/entities/Entity.ts @@ -0,0 +1,33 @@ +import { Position } from "../Position"; + +export class Entity { + protected destructible: boolean; + protected health: number; + protected position: Position; + + public constructor( + destructible: boolean, + health: number, + position: Position, + ) { + this.destructible = destructible; + this.health = health; + this.position = position; + } + + public dead(): boolean { + return this.health === 0; + } + + public getPosition(): Position { + return this.position; + } + + public getPositionAsNumber(): number { + return this.position.x + this.position.y; + } + + public hit() { + this.health--; + } +} diff --git a/games/ZombieSurvival/entities/Player.ts b/games/ZombieSurvival/entities/Player.ts new file mode 100644 index 0000000..74ebf6b --- /dev/null +++ b/games/ZombieSurvival/entities/Player.ts @@ -0,0 +1,22 @@ +import { Entity } from "./Entity"; +import { Position } from "../Position"; +import { ZombieSurvival } from "../ZombieSurvival"; +import { closestEntity } from "../lib/closest-entity"; + +export class Player extends Entity { + public static Destructible = true; + public static Health = 1; + public static ShootDistance = Infinity; + + private game: ZombieSurvival; + + public constructor(game: ZombieSurvival, position: Position) { + super(Player.Destructible, Player.Health, position); + this.game = game; + } + + public shoot() { + const zombie = closestEntity(this, this.game.getZombies()); + zombie.hit(); + } +} diff --git a/games/ZombieSurvival/entities/Rock.ts b/games/ZombieSurvival/entities/Rock.ts new file mode 100644 index 0000000..5ae8c09 --- /dev/null +++ b/games/ZombieSurvival/entities/Rock.ts @@ -0,0 +1,11 @@ +import { Entity } from "./Entity"; +import { Position } from "../Position"; + +export class Rock extends Entity { + public static Destructible = false; + public static Health = -1; + + public constructor(position: Position) { + super(Rock.Destructible, Rock.Health, position); + } +} diff --git a/games/ZombieSurvival/entities/Zombie.ts b/games/ZombieSurvival/entities/Zombie.ts new file mode 100644 index 0000000..c779654 --- /dev/null +++ b/games/ZombieSurvival/entities/Zombie.ts @@ -0,0 +1,60 @@ +import { Direction } from "../Direction"; +import { Entity } from "./Entity"; +import { Position } from "../Position"; +import { ZombieSurvival } from "../ZombieSurvival"; +import { entityAt } from "../lib/entity-at"; +import { pathFinder } from "../lib/path-finder"; + +export class Zombie extends Entity { + public static Destructible = true; + public static Health = 2; + + private game: ZombieSurvival; + private path: Direction[]; + private pathIdx = 0; + + public constructor(game: ZombieSurvival, position: Position) { + super(Zombie.Destructible, Zombie.Health, position); + this.game = game; + this.path = pathFinder(game, this); + } + + public walk() { + const nextDirection = this.path[this.pathIdx++]; + + if (typeof nextDirection === "undefined") { + throw new Error("Zombie out of moves"); + } + + const newPosition: Position = { ...this.position }; + + switch (nextDirection) { + case Direction.Down: { + newPosition.y += 1; + break; + } + case Direction.Left: { + newPosition.x -= 1; + break; + } + case Direction.Right: { + newPosition.x += 1; + break; + } + case Direction.Up: { + newPosition.y -= 1; + break; + } + } + + const entities = this.game.getAllEntities(); + const entity = entityAt(entities, newPosition); + + if (entity !== null) { + entity.hit(); + return; + } + + this.position = newPosition; + } +} diff --git a/games/ZombieSurvival/index.ts b/games/ZombieSurvival/index.ts new file mode 100644 index 0000000..63ab792 --- /dev/null +++ b/games/ZombieSurvival/index.ts @@ -0,0 +1,8 @@ +export * from "./entities/Box"; +export * from "./entities/Entity"; +export * from "./entities/Player"; +export * from "./entities/Rock"; +export * from "./entities/Zombie"; +export * from "./Direction"; +export * from "./Position"; +export * from "./ZombieSurvival"; diff --git a/games/ZombieSurvival/lib/closest-entity.ts b/games/ZombieSurvival/lib/closest-entity.ts new file mode 100644 index 0000000..3a2df7c --- /dev/null +++ b/games/ZombieSurvival/lib/closest-entity.ts @@ -0,0 +1,30 @@ +import { Entity } from "../entities/Entity"; +import { positionAsNumber } from "../Position"; + +export interface ClosestEntityScore { + score: number; + target: Entity; +} + +export function closestEntity(entity: Entity, targets: Entity[]): Entity { + const entityPosition = positionAsNumber(entity.getPosition()); + const scores: ClosestEntityScore[] = []; + + for (const target of targets) { + if (target.dead()) { + continue; + } + + const targetPosition = positionAsNumber(entity.getPosition()); + const score = Math.abs(entityPosition - targetPosition); + + scores.push({ target, score }); + } + + if (scores.length === 0) { + throw new Error("No alive targets found"); + } + + scores.sort((a, b) => a.score - b.score); + return scores[0].target; +} diff --git a/games/ZombieSurvival/lib/entity-at.ts b/games/ZombieSurvival/lib/entity-at.ts new file mode 100644 index 0000000..02d9285 --- /dev/null +++ b/games/ZombieSurvival/lib/entity-at.ts @@ -0,0 +1,17 @@ +import { Entity } from "../entities/Entity"; +import { Position } from "../Position"; + +export function entityAt( + entities: Entity[], + position: Position, +): Entity | null { + for (const entity of entities) { + const entityPosition = entity.getPosition(); + + if (entityPosition.x === position.x && entityPosition.y === position.y) { + return entity; + } + } + + return null; +} diff --git a/games/ZombieSurvival/lib/path-finder.ts b/games/ZombieSurvival/lib/path-finder.ts new file mode 100644 index 0000000..8cb62eb --- /dev/null +++ b/games/ZombieSurvival/lib/path-finder.ts @@ -0,0 +1,7 @@ +import { Direction } from "../Direction"; +import { Zombie } from "../entities/Zombie"; +import { ZombieSurvival } from "../ZombieSurvival"; + +export function pathFinder(game: ZombieSurvival, zombie: Zombie): Direction[] { + return []; +} From e15b7a9591c940b6e1b620cfe042613ea0c76c2e Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Wed, 16 Oct 2024 00:13:38 +0300 Subject: [PATCH 2/6] Implement path finding algorithm --- games/ZombieSurvival/Direction.ts | 63 ++++++++++++++ games/ZombieSurvival/ZombieSurvival.ts | 63 ++++++++++++-- games/ZombieSurvival/entities/Box.ts | 6 +- games/ZombieSurvival/entities/Entity.ts | 38 +++++++++ games/ZombieSurvival/entities/Player.ts | 4 +- games/ZombieSurvival/entities/Rock.ts | 4 +- games/ZombieSurvival/entities/Zombie.ts | 104 +++++++++++++++++------- games/ZombieSurvival/lib/path-finder.ts | 87 +++++++++++++++++++- test2134.ts | 31 +++++++ 9 files changed, 354 insertions(+), 46 deletions(-) create mode 100644 test2134.ts diff --git a/games/ZombieSurvival/Direction.ts b/games/ZombieSurvival/Direction.ts index accb6bf..49177b0 100644 --- a/games/ZombieSurvival/Direction.ts +++ b/games/ZombieSurvival/Direction.ts @@ -1,6 +1,69 @@ +import { Position } from "./Position"; + export enum Direction { Down, Left, Right, Up, } + +export const allDirections = [ + Direction.Down, + Direction.Left, + Direction.Right, + Direction.Up, +]; + +export function directionToString(direction: Direction): string { + switch (direction) { + case Direction.Down: { + return "0"; + } + case Direction.Left: { + return "1"; + } + case Direction.Right: { + return "2"; + } + case Direction.Up: { + return "3"; + } + } +} + +export function directionFromString(val: string): Direction { + switch (val) { + case "0": { + return Direction.Down; + } + case "1": { + return Direction.Left; + } + case "2": { + return Direction.Right; + } + case "3": { + return Direction.Up; + } + default: { + throw new Error("Can't parse direction"); + } + } +} + +export function move(position: Position, direction: Direction): Position { + switch (direction) { + case Direction.Down: { + return { ...position, y: position.y + 1 }; + } + case Direction.Left: { + return { ...position, x: position.x - 1 }; + } + case Direction.Right: { + return { ...position, x: position.x + 1 }; + } + case Direction.Up: { + return { ...position, y: position.y - 1 }; + } + } +} diff --git a/games/ZombieSurvival/ZombieSurvival.ts b/games/ZombieSurvival/ZombieSurvival.ts index 9ff7511..42d5acd 100644 --- a/games/ZombieSurvival/ZombieSurvival.ts +++ b/games/ZombieSurvival/ZombieSurvival.ts @@ -3,13 +3,19 @@ import { Entity } from "./entities/Entity"; import { Player } from "./entities/Player"; import { Rock } from "./entities/Rock"; import { Zombie } from "./entities/Zombie"; +import { entityAt } from "./lib/entity-at"; export class ZombieSurvival { - private readonly boardHeight: number; - private readonly boardWidth: number; - private readonly entities: Entity[]; - private readonly player: Player; - private readonly zombies: Zombie[]; + public readonly boardHeight: number; + public readonly boardWidth: number; + private entities: Entity[]; + private player: Player; + private zombies: Zombie[]; + + public static fromSnapshot(snapshot: string): ZombieSurvival { + const config = snapshot.split(".").map((it) => it.split("")); + return new ZombieSurvival(config); + } public constructor(config: string[][]) { if (config.length === 0 || config[0].length == 0) { @@ -25,7 +31,7 @@ export class ZombieSurvival { for (let y = 0; y < this.boardHeight; y++) { for (let x = 0; x < this.boardWidth; x++) { - const code = config[x][y]; + const code = config[y][x]; switch (code.toLowerCase()) { case "b": { @@ -68,17 +74,60 @@ export class ZombieSurvival { } public getAllEntities(): Entity[] { - return [this.zombies, this.player, this.entities].flat(); + return [this.entities, this.zombies, this.player].flat(); + } + + public getEntities(): Entity[] { + return this.entities; } public getPlayer(): Player { return this.player; } + public getSnapshot(): string { + return this.getState() + .map((it) => it.join("")) + .join("."); + } + + public getState(): string[][] { + const entities = this.getAllEntities(); + let config: string[][] = []; + + for (let y = 0; y < this.boardHeight; y++) { + const item: string[] = []; + + for (let x = 0; x < this.boardWidth; x++) { + const entity = entityAt(entities, { x, y }); + item.push(entity === null ? " " : entity.toConfig()); + } + + config.push(item); + } + + return config; + } + + public getZombie(): Zombie { + const zombie = this.zombies[0]; + + if (typeof zombie === "undefined") { + throw new Error("Tried getting non-existing first zombie"); + } + + return zombie; + } + public getZombies(): Zombie[] { return this.zombies; } + public setZombies(zombies: Zombie[]): this { + this.zombies = zombies; + return this; + } + public step() { this.player.shoot(); diff --git a/games/ZombieSurvival/entities/Box.ts b/games/ZombieSurvival/entities/Box.ts index a2df312..dd51fb1 100644 --- a/games/ZombieSurvival/entities/Box.ts +++ b/games/ZombieSurvival/entities/Box.ts @@ -1,11 +1,11 @@ -import { Entity } from "./Entity"; +import { Entity, EntityType } from "./Entity"; import { Position } from "../Position"; export class Box extends Entity { public static Destructible = true; - public static Health = 2; + public static Health = 1; public constructor(position: Position) { - super(Box.Destructible, Box.Health, position); + super(EntityType.Box, Box.Destructible, Box.Health, position); } } diff --git a/games/ZombieSurvival/entities/Entity.ts b/games/ZombieSurvival/entities/Entity.ts index 9b7fd62..1a8a605 100644 --- a/games/ZombieSurvival/entities/Entity.ts +++ b/games/ZombieSurvival/entities/Entity.ts @@ -1,11 +1,20 @@ import { Position } from "../Position"; +export enum EntityType { + Box, + Player, + Rock, + Zombie, +} + export class Entity { protected destructible: boolean; protected health: number; protected position: Position; + protected type: EntityType; public constructor( + type: EntityType, destructible: boolean, health: number, position: Position, @@ -13,6 +22,7 @@ export class Entity { this.destructible = destructible; this.health = health; this.position = position; + this.type = type; } public dead(): boolean { @@ -27,7 +37,35 @@ export class Entity { return this.position.x + this.position.y; } + public getType(): EntityType { + return this.type; + } + public hit() { + if (!this.destructible) { + return; + } + this.health--; } + + public isDestructible(): boolean { + return this.destructible; + } + + public toConfig(): string { + let letter = " "; + + if (this.type === EntityType.Box) { + letter = "B"; + } else if (this.type === EntityType.Player) { + letter = "P"; + } else if (this.type === EntityType.Rock) { + letter = "R"; + } else if (this.type === EntityType.Zombie) { + letter = "Z"; + } + + return letter; + } } diff --git a/games/ZombieSurvival/entities/Player.ts b/games/ZombieSurvival/entities/Player.ts index 74ebf6b..caa757e 100644 --- a/games/ZombieSurvival/entities/Player.ts +++ b/games/ZombieSurvival/entities/Player.ts @@ -1,4 +1,4 @@ -import { Entity } from "./Entity"; +import { Entity, EntityType } from "./Entity"; import { Position } from "../Position"; import { ZombieSurvival } from "../ZombieSurvival"; import { closestEntity } from "../lib/closest-entity"; @@ -11,7 +11,7 @@ export class Player extends Entity { private game: ZombieSurvival; public constructor(game: ZombieSurvival, position: Position) { - super(Player.Destructible, Player.Health, position); + super(EntityType.Player, Player.Destructible, Player.Health, position); this.game = game; } diff --git a/games/ZombieSurvival/entities/Rock.ts b/games/ZombieSurvival/entities/Rock.ts index 5ae8c09..7a42afc 100644 --- a/games/ZombieSurvival/entities/Rock.ts +++ b/games/ZombieSurvival/entities/Rock.ts @@ -1,4 +1,4 @@ -import { Entity } from "./Entity"; +import { Entity, EntityType } from "./Entity"; import { Position } from "../Position"; export class Rock extends Entity { @@ -6,6 +6,6 @@ export class Rock extends Entity { public static Health = -1; public constructor(position: Position) { - super(Rock.Destructible, Rock.Health, position); + super(EntityType.Rock, Rock.Destructible, Rock.Health, position); } } diff --git a/games/ZombieSurvival/entities/Zombie.ts b/games/ZombieSurvival/entities/Zombie.ts index c779654..362df6e 100644 --- a/games/ZombieSurvival/entities/Zombie.ts +++ b/games/ZombieSurvival/entities/Zombie.ts @@ -1,5 +1,5 @@ -import { Direction } from "../Direction"; -import { Entity } from "./Entity"; +import { Direction, allDirections, move } from "../Direction"; +import { Entity, EntityType } from "./Entity"; import { Position } from "../Position"; import { ZombieSurvival } from "../ZombieSurvival"; import { entityAt } from "../lib/entity-at"; @@ -10,51 +10,97 @@ export class Zombie extends Entity { public static Health = 2; private game: ZombieSurvival; - private path: Direction[]; - private pathIdx = 0; public constructor(game: ZombieSurvival, position: Position) { - super(Zombie.Destructible, Zombie.Health, position); + super(EntityType.Zombie, Zombie.Destructible, Zombie.Health, position); this.game = game; - this.path = pathFinder(game, this); } - public walk() { - const nextDirection = this.path[this.pathIdx++]; - - if (typeof nextDirection === "undefined") { - throw new Error("Zombie out of moves"); - } + public listMoves(): Direction[] { + const entities = this.game.getAllEntities(); + const result: Direction[] = []; - const newPosition: Position = { ...this.position }; + for (const direction of allDirections) { + const position = move(this.position, direction); - switch (nextDirection) { - case Direction.Down: { - newPosition.y += 1; - break; - } - case Direction.Left: { - newPosition.x -= 1; - break; + if ( + position.x < 0 || + position.y < 0 || + position.x >= this.game.boardWidth || + position.y >= this.game.boardHeight + ) { + continue; } - case Direction.Right: { - newPosition.x += 1; - break; + + const entity = entityAt(entities, position); + + if (entity !== null && !entity.isDestructible()) { + continue; } - case Direction.Up: { - newPosition.y -= 1; - break; + + result.push(direction); + } + + return result; + } + + public predictLastMove(): Direction | null { + const entities = this.game.getAllEntities(); + const position = this.position; + + const downEntity = entityAt(entities, move(position, Direction.Down)); + const leftEntity = entityAt(entities, move(position, Direction.Left)); + const rightEntity = entityAt(entities, move(position, Direction.Right)); + const upEntity = entityAt(entities, move(position, Direction.Up)); + + if (downEntity?.getType() === EntityType.Player) { + return Direction.Down; + } + + if (leftEntity?.getType() === EntityType.Player) { + return Direction.Left; + } + + if (rightEntity?.getType() === EntityType.Player) { + return Direction.Right; + } + + if (upEntity?.getType() === EntityType.Player) { + return Direction.Up; + } + + return null; + } + + public walk(direction: Direction | null = null) { + let nextDirection = direction ?? pathFinder(this.game, this)[0]; + + if (typeof nextDirection === "undefined") { + const lastMove = this.predictLastMove(); + + if (lastMove === null) { + throw new Error("Zombie out of moves"); } + + nextDirection = lastMove; } const entities = this.game.getAllEntities(); + const newPosition = move(this.position, nextDirection); const entity = entityAt(entities, newPosition); if (entity !== null) { - entity.hit(); + if (entity.getType() !== EntityType.Zombie) { + entity.hit(); + } + return; } - this.position = newPosition; + this.walkTo(newPosition); + } + + public walkTo(position: Position) { + this.position = position; } } diff --git a/games/ZombieSurvival/lib/path-finder.ts b/games/ZombieSurvival/lib/path-finder.ts index 8cb62eb..aa021a2 100644 --- a/games/ZombieSurvival/lib/path-finder.ts +++ b/games/ZombieSurvival/lib/path-finder.ts @@ -1,7 +1,88 @@ -import { Direction } from "../Direction"; +import { + Direction, + directionFromString, + directionToString, +} from "../Direction"; import { Zombie } from "../entities/Zombie"; import { ZombieSurvival } from "../ZombieSurvival"; -export function pathFinder(game: ZombieSurvival, zombie: Zombie): Direction[] { - return []; +export function pathFinder( + initialGame: ZombieSurvival, + initialZombie: Zombie, +): Direction[] { + const initialSnapshot = ZombieSurvival.fromSnapshot(initialGame.getSnapshot()) + .setZombies([initialZombie]) + .getSnapshot(); + + const graph = new Map>(); + const queue = [initialSnapshot]; + const wins = new Set(); + + while (queue.length > 0) { + const snapshot = queue.shift(); + + if (snapshot === undefined) { + continue; + } + + const game = ZombieSurvival.fromSnapshot(snapshot); + const moves = game.getZombie().listMoves(); + + graph.set(snapshot, {}); + + moves.forEach((move) => { + const game = ZombieSurvival.fromSnapshot(snapshot); + game.getZombie().walk(move); + const newSnapshot = game.getSnapshot(); + + if (game.finished()) { + wins.add(newSnapshot); + return; + } + + if (graph.has(newSnapshot) || queue.includes(newSnapshot)) { + return; + } + + const graphItem = graph.get(snapshot); + + if (graphItem !== undefined) { + graphItem[directionToString(move)] = newSnapshot; + } + + queue.push(newSnapshot); + }); + } + + const bfsQueue: Array<{ + snapshot: string; + moves: Direction[]; + }> = [{ snapshot: initialSnapshot, moves: [] }]; + + while (bfsQueue.length > 0) { + const vertex = bfsQueue.shift(); + + if (vertex === undefined) { + continue; + } + + if (wins.has(vertex.snapshot)) { + return vertex.moves; + } + + const newVertex = graph.get(vertex.snapshot); + + if (newVertex === undefined) { + throw new Error("Tried getting undefined graph item"); + } + + Object.entries(newVertex).forEach(([move, snapshot]) => { + bfsQueue.push({ + snapshot, + moves: [...vertex.moves, directionFromString(move)], + }); + }); + } + + throw new Error("Unable to solve game"); } diff --git a/test2134.ts b/test2134.ts new file mode 100644 index 0000000..b10d44b --- /dev/null +++ b/test2134.ts @@ -0,0 +1,31 @@ +import { ZombieSurvival } from "./games/ZombieSurvival"; + +const config = [ + [" ", " ", " ", " ", " ", " ", " ", "Z", " ", " "], + [" ", " ", " ", "P", "B", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", "Z", " "], + [" ", " ", " ", " ", " ", " ", " ", "Z", " ", " "], + [" ", " ", " ", " ", " ", " ", "Z", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], +]; + +console.time("game"); + +const game = new ZombieSurvival(config); + +while (!game.finished()) { + game.step(); + + console.log( + game + .getState() + .map((it) => it.map((it2) => it2.replace(" ", "-")).join(" ")) + .join("\n"), + ); + console.log(); +} + +console.timeEnd("game"); From 4be523afe3a9b70208910255c7bcc2518a687b0c Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Wed, 16 Oct 2024 01:36:35 +0300 Subject: [PATCH 3/6] Cover game with tests --- games/ZombieSurvival/ZombieSurvival.spec.ts | 361 ++++++++++++++++++++ games/ZombieSurvival/ZombieSurvival.ts | 16 +- games/ZombieSurvival/entities/Zombie.ts | 4 + games/ZombieSurvival/lib/entity-at.ts | 4 + games/ZombieSurvival/lib/path-finder.ts | 14 +- package.json | 4 +- test2134.ts | 31 -- vitest.config.ts | 3 + 8 files changed, 390 insertions(+), 47 deletions(-) create mode 100644 games/ZombieSurvival/ZombieSurvival.spec.ts delete mode 100644 test2134.ts create mode 100644 vitest.config.ts diff --git a/games/ZombieSurvival/ZombieSurvival.spec.ts b/games/ZombieSurvival/ZombieSurvival.spec.ts new file mode 100644 index 0000000..3c696be --- /dev/null +++ b/games/ZombieSurvival/ZombieSurvival.spec.ts @@ -0,0 +1,361 @@ +import { expect, test } from "vitest"; +import { ZombieSurvival } from "./ZombieSurvival"; + +test("fails on invalid config", () => { + expect(() => new ZombieSurvival([])).toThrowError("Config is empty"); + expect(() => new ZombieSurvival([[]])).toThrowError("Config is empty"); + + expect( + () => + new ZombieSurvival([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", "Z", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", "Z", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]), + ).toThrowError("Config has no player"); + + expect( + () => + new ZombieSurvival([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", "B", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", "B", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", "Z", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", "Z", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]), + ).toThrowError("Config contains multiple players"); + + expect( + () => + new ZombieSurvival([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", "B", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]), + ).toThrowError("Config has no zombies"); +}); + +test("fails on impossible to beat config", () => { + const game = new ZombieSurvival([ + [" ", " ", " ", " ", "R", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", "R", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "R", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", "Z", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", "Z", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + expect(() => game.step()).toThrowError("Unable to solve game"); +}); + +test("works with different boards sizes", () => { + expect(() => new ZombieSurvival([["P", "Z"]])).not.toThrow(); + expect(() => new ZombieSurvival([["P"], ["Z"]])).not.toThrow(); + + expect( + () => + new ZombieSurvival([ + [" ", " ", " ", " ", " "], + [" ", " ", "B", " ", " "], + ["Z", " ", "B", "P", " "], + ["R", "R", "R", " ", " "], + ]), + ).not.toThrow(); + + expect( + () => + new ZombieSurvival([["P", "B", "Z", "Z", "Z", " ", " ", " ", " ", " "]]), + ).not.toThrow(); + + expect( + () => + new ZombieSurvival([ + ["P"], + ["B"], + ["Z"], + ["Z"], + ["Z"], + [" "], + [" "], + [" "], + [" "], + ]), + ).not.toThrow(); +}); + +test("kills zombies", () => { + const game = new ZombieSurvival([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", "B", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", "Z", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", "Z", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", "B", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", "Z", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", "Z", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", "B", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "Z", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", "B", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "Z", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", "B", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + expect(game.finished()).toBeTruthy(); +}); + +test("player gets killed instantly", () => { + const game = new ZombieSurvival([ + [" ", " ", " ", "Z", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", "B", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", "Z", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", "Z", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", " ", "Z", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", "Z", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", "Z", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + expect(game.finished()).toBeTruthy(); +}); + +test("player gets killed eventually", () => { + const game = new ZombieSurvival([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", "B", "Z", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", "Z", " ", " ", " ", " "], + ["R", "R", "R", "R", "Z", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", " ", "Z", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", "Z", " ", " ", " ", " "], + ["R", "R", "R", "R", "Z", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "Z", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", "Z", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "Z", "Z", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "P", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "Z", " ", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", "Z", " ", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + expect(game.finished()).toBeTruthy(); +}); + +test("player gets killed with too many zombies", () => { + const game = new ZombieSurvival([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", "Z"], + [" ", " ", " ", "P", "B", " ", " ", " ", " ", "Z"], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", "Z"], + ["R", "R", "R", "R", " ", " ", " ", " ", " ", "Z"], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", "Z"], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", "Z"], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", "Z"], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", "Z"], + ["Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z"], + ]); + + game.step(); + game.step(); + game.step(); + game.step(); + game.step(); + game.step(); + game.step(); + game.step(); + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], + [" ", " ", " ", "Z", "Z", " ", " ", " ", " ", " "], + ["R", "R", "R", "R", "Z", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", "Z", "Z", " ", " ", " ", " "], + [" ", " ", " ", " ", "Z", "Z", " ", " ", " ", " "], + [" ", " ", " ", " ", "Z", "Z", " ", " ", " ", " "], + [" ", " ", " ", " ", "Z", " ", " ", " ", " ", " "], + ["Z", "Z", " ", "Z", "Z", " ", " ", " ", " ", " "], + ]); + + expect(game.finished()).toBeTruthy(); +}); + +test("player gets killed behind walls", () => { + const game = new ZombieSurvival([ + ["P", "B", "Z", " ", " ", " ", " ", " ", " ", " "], + ["B", "B", "Z", " ", " ", " ", " ", " ", " ", " "], + ["Z", "Z", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + game.step(); + game.step(); + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", "Z", " ", " ", " ", " ", " ", " ", " ", " "], + ["Z", "B", " ", " ", " ", " ", " ", " ", " ", " "], + ["Z", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + ]); + + expect(game.finished()).toBeTruthy(); +}); diff --git a/games/ZombieSurvival/ZombieSurvival.ts b/games/ZombieSurvival/ZombieSurvival.ts index 42d5acd..2d0efe6 100644 --- a/games/ZombieSurvival/ZombieSurvival.ts +++ b/games/ZombieSurvival/ZombieSurvival.ts @@ -110,13 +110,7 @@ export class ZombieSurvival { } public getZombie(): Zombie { - const zombie = this.zombies[0]; - - if (typeof zombie === "undefined") { - throw new Error("Tried getting non-existing first zombie"); - } - - return zombie; + return this.zombies[0]; } public getZombies(): Zombie[] { @@ -124,6 +118,10 @@ export class ZombieSurvival { } public setZombies(zombies: Zombie[]): this { + if (zombies.length === 0) { + throw new Error("Tried setting zero zombies"); + } + this.zombies = zombies; return this; } @@ -132,6 +130,10 @@ export class ZombieSurvival { this.player.shoot(); for (const zombie of this.zombies) { + if (this.player.dead()) { + break; + } + zombie.walk(); } } diff --git a/games/ZombieSurvival/entities/Zombie.ts b/games/ZombieSurvival/entities/Zombie.ts index 362df6e..0097eca 100644 --- a/games/ZombieSurvival/entities/Zombie.ts +++ b/games/ZombieSurvival/entities/Zombie.ts @@ -73,6 +73,10 @@ export class Zombie extends Entity { } public walk(direction: Direction | null = null) { + if (this.dead()) { + return; + } + let nextDirection = direction ?? pathFinder(this.game, this)[0]; if (typeof nextDirection === "undefined") { diff --git a/games/ZombieSurvival/lib/entity-at.ts b/games/ZombieSurvival/lib/entity-at.ts index 02d9285..5296f56 100644 --- a/games/ZombieSurvival/lib/entity-at.ts +++ b/games/ZombieSurvival/lib/entity-at.ts @@ -6,6 +6,10 @@ export function entityAt( position: Position, ): Entity | null { for (const entity of entities) { + if (entity.dead()) { + continue; + } + const entityPosition = entity.getPosition(); if (entityPosition.x === position.x && entityPosition.y === position.y) { diff --git a/games/ZombieSurvival/lib/path-finder.ts b/games/ZombieSurvival/lib/path-finder.ts index aa021a2..eec8da5 100644 --- a/games/ZombieSurvival/lib/path-finder.ts +++ b/games/ZombieSurvival/lib/path-finder.ts @@ -25,21 +25,15 @@ export function pathFinder( continue; } + graph.set(snapshot, {}); const game = ZombieSurvival.fromSnapshot(snapshot); const moves = game.getZombie().listMoves(); - graph.set(snapshot, {}); - moves.forEach((move) => { const game = ZombieSurvival.fromSnapshot(snapshot); game.getZombie().walk(move); const newSnapshot = game.getSnapshot(); - if (game.finished()) { - wins.add(newSnapshot); - return; - } - if (graph.has(newSnapshot) || queue.includes(newSnapshot)) { return; } @@ -50,7 +44,11 @@ export function pathFinder( graphItem[directionToString(move)] = newSnapshot; } - queue.push(newSnapshot); + if (game.finished()) { + wins.add(newSnapshot); + } else { + queue.push(newSnapshot); + } }); } diff --git a/package.json b/package.json index 12e5420..fe7e049 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "predev": "convex dev --until-success && node setup.mjs --once && convex dashboard", "build": "next build", "start": "next start", + "test": "vitest", "lint": "next lint" }, "dependencies": { @@ -41,6 +42,7 @@ "postcss": "^8", "prettier": "3.3.2", "tailwindcss": "^3.4.1", - "typescript": "^5" + "typescript": "^5", + "vitest": "^2.1.3" } } diff --git a/test2134.ts b/test2134.ts deleted file mode 100644 index b10d44b..0000000 --- a/test2134.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ZombieSurvival } from "./games/ZombieSurvival"; - -const config = [ - [" ", " ", " ", " ", " ", " ", " ", "Z", " ", " "], - [" ", " ", " ", "P", "B", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], - ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", " ", " ", " ", " ", "Z", " "], - [" ", " ", " ", " ", " ", " ", " ", "Z", " ", " "], - [" ", " ", " ", " ", " ", " ", "Z", " ", " ", " "], - [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], -]; - -console.time("game"); - -const game = new ZombieSurvival(config); - -while (!game.finished()) { - game.step(); - - console.log( - game - .getState() - .map((it) => it.map((it2) => it2.replace(" ", "-")).join(" ")) - .join("\n"), - ); - console.log(); -} - -console.timeEnd("game"); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..8fb6f2d --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({}); From 4dd82cfacbcb7004241124f4918a5a2af72f0d45 Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Wed, 16 Oct 2024 01:44:55 +0300 Subject: [PATCH 4/6] Rename pathfinder function and file --- games/ZombieSurvival/entities/Zombie.ts | 4 ++-- games/ZombieSurvival/lib/{path-finder.ts => pathfinder.ts} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename games/ZombieSurvival/lib/{path-finder.ts => pathfinder.ts} (98%) diff --git a/games/ZombieSurvival/entities/Zombie.ts b/games/ZombieSurvival/entities/Zombie.ts index 0097eca..0b76524 100644 --- a/games/ZombieSurvival/entities/Zombie.ts +++ b/games/ZombieSurvival/entities/Zombie.ts @@ -3,7 +3,7 @@ import { Entity, EntityType } from "./Entity"; import { Position } from "../Position"; import { ZombieSurvival } from "../ZombieSurvival"; import { entityAt } from "../lib/entity-at"; -import { pathFinder } from "../lib/path-finder"; +import { pathfinder } from "../lib/pathfinder"; export class Zombie extends Entity { public static Destructible = true; @@ -77,7 +77,7 @@ export class Zombie extends Entity { return; } - let nextDirection = direction ?? pathFinder(this.game, this)[0]; + let nextDirection = direction ?? pathfinder(this.game, this)[0]; if (typeof nextDirection === "undefined") { const lastMove = this.predictLastMove(); diff --git a/games/ZombieSurvival/lib/path-finder.ts b/games/ZombieSurvival/lib/pathfinder.ts similarity index 98% rename from games/ZombieSurvival/lib/path-finder.ts rename to games/ZombieSurvival/lib/pathfinder.ts index eec8da5..1135896 100644 --- a/games/ZombieSurvival/lib/path-finder.ts +++ b/games/ZombieSurvival/lib/pathfinder.ts @@ -6,7 +6,7 @@ import { import { Zombie } from "../entities/Zombie"; import { ZombieSurvival } from "../ZombieSurvival"; -export function pathFinder( +export function pathfinder( initialGame: ZombieSurvival, initialZombie: Zombie, ): Direction[] { From b45f0e798dac414fdf1e3b84ebb8c0b1144c784a Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Wed, 16 Oct 2024 02:15:53 +0300 Subject: [PATCH 5/6] Fix typo in closest target algorithm --- games/ZombieSurvival/ZombieSurvival.spec.ts | 5 +++-- games/ZombieSurvival/lib/closest-entity.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/games/ZombieSurvival/ZombieSurvival.spec.ts b/games/ZombieSurvival/ZombieSurvival.spec.ts index 3c696be..44e0ed8 100644 --- a/games/ZombieSurvival/ZombieSurvival.spec.ts +++ b/games/ZombieSurvival/ZombieSurvival.spec.ts @@ -136,9 +136,9 @@ test("kills zombies", () => { [" ", " ", " ", "P", "B", " ", " ", " ", " ", " "], [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", "Z", " ", " ", " "], [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], - [" ", " ", " ", "Z", " ", " ", " ", " ", " ", " "], [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], ]); @@ -150,9 +150,9 @@ test("kills zombies", () => { [" ", " ", " ", "P", "B", " ", " ", " ", " ", " "], [" ", " ", " ", " ", "B", " ", " ", " ", " ", " "], ["R", "R", "R", "R", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", "Z", " ", " ", " ", " "], [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", "Z", " ", " ", " ", " ", " "], [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], ]); @@ -312,6 +312,7 @@ test("player gets killed with too many zombies", () => { game.step(); game.step(); game.step(); + game.step(); expect(game.getState()).toStrictEqual([ [" ", " ", " ", " ", " ", " ", " ", " ", " ", " "], diff --git a/games/ZombieSurvival/lib/closest-entity.ts b/games/ZombieSurvival/lib/closest-entity.ts index 3a2df7c..2facd5e 100644 --- a/games/ZombieSurvival/lib/closest-entity.ts +++ b/games/ZombieSurvival/lib/closest-entity.ts @@ -15,7 +15,7 @@ export function closestEntity(entity: Entity, targets: Entity[]): Entity { continue; } - const targetPosition = positionAsNumber(entity.getPosition()); + const targetPosition = positionAsNumber(target.getPosition()); const score = Math.abs(entityPosition - targetPosition); scores.push({ target, score }); From 6044972014c70cc961c5ce55a5743409bfda9733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20R=C3=B6ssner?= Date: Wed, 16 Oct 2024 10:41:18 +1100 Subject: [PATCH 6/6] Changed logo to the real logo --- app/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/layout.tsx b/app/layout.tsx index a0e0ce0..f221e13 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -11,7 +11,7 @@ export const metadata: Metadata = { title: "Convex + Next.js + Convex Auth", description: "Generated by npm create convex", icons: { - icon: "/convex.svg", + icon: "/logo.png", }, };