Add more test coverage

This commit is contained in:
Peter Stockings
2026-01-05 14:03:25 +11:00
parent ce68470ab1
commit f86daac9ac
5 changed files with 260 additions and 189 deletions

View File

@@ -1,227 +1,99 @@
import { describe, it, expect } from 'vitest';
import { applyAction } from '../simulation/simulation';
import { applyAction, decideEnemyAction } from '../simulation/simulation';
import { type World, type Actor, type EntityId, type CombatantActor } from '../../core/types';
import { EntityManager } from '../EntityManager';
describe('Combat Simulation', () => {
const createTestWorld = (actors: Map<EntityId, Actor>): World => ({
width: 10,
height: 10,
tiles: new Array(100).fill(0),
actors,
exit: { x: 9, y: 9 }
});
let entityManager: EntityManager;
const createTestWorld = (actors: Map<EntityId, Actor>): World => {
return {
width: 10,
height: 10,
tiles: new Array(100).fill(0),
actors,
exit: { x: 9, y: 9 }
};
};
const createTestStats = (overrides: Partial<any> = {}) => ({
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: [],
// New stats (defaults for tests to keep them deterministic)
critChance: 0,
critMultiplier: 100,
accuracy: 100, // Always hit in tests unless specified
lifesteal: 0,
evasion: 0,
blockChance: 0,
luck: 0,
critChance: 0, critMultiplier: 100, accuracy: 100, lifesteal: 0, evasion: 0, blockChance: 0, luck: 0,
...overrides
});
describe('applyAction - attack', () => {
describe('applyAction - success paths', () => {
it('should deal damage when player attacks enemy', () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1,
category: "combatant",
isPlayer: true,
type: "player",
pos: { x: 3, y: 3 },
speed: 100,
energy: 0,
stats: createTestStats()
});
id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, energy: 0, stats: createTestStats()
} as any);
actors.set(2, {
id: 2,
category: "combatant",
isPlayer: false,
type: "rat",
pos: { x: 4, y: 3 },
speed: 100,
energy: 0,
stats: createTestStats({ maxHp: 10, hp: 10, attack: 3, defense: 1 })
});
id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, energy: 0, stats: createTestStats({ maxHp: 10, hp: 10, attack: 3, defense: 1 })
} as any);
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: "attack", targetId: 2 });
entityManager = new EntityManager(world);
const events = applyAction(world, 1, { type: "attack", targetId: 2 }, entityManager);
const enemy = world.actors.get(2) as CombatantActor;
expect(enemy.stats.hp).toBeLessThan(10);
// Should have attack event
expect(events.some(e => e.type === "attacked")).toBe(true);
});
it("should kill enemy when damage exceeds hp", () => {
it("should kill enemy and spawn EXP orb without ID reuse collision", () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1,
category: "combatant",
isPlayer: true,
type: "player",
pos: { x: 3, y: 3 },
speed: 100,
energy: 0,
stats: createTestStats({ attack: 50 })
});
id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, energy: 0, stats: createTestStats({ attack: 50 })
} as any);
actors.set(2, {
id: 2,
category: "combatant",
isPlayer: false,
type: "rat",
pos: { x: 4, y: 3 },
speed: 100,
energy: 0,
stats: createTestStats({ maxHp: 10, hp: 10, attack: 3, defense: 1 })
});
id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, energy: 0, stats: createTestStats({ maxHp: 10, hp: 10, attack: 3, defense: 1 })
} as any);
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: "attack", targetId: 2 });
entityManager = new EntityManager(world);
applyAction(world, 1, { type: "attack", targetId: 2 }, entityManager);
// Enemy should be removed from world
// Enemy (id 2) should be gone
expect(world.actors.has(2)).toBe(false);
// Should have killed event
expect(events.some(e => e.type === "killed")).toBe(true);
});
it("should apply defense to reduce damage", () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1,
category: "combatant",
isPlayer: true,
type: "player",
pos: { x: 3, y: 3 },
speed: 100,
energy: 0,
stats: createTestStats()
});
actors.set(2, {
id: 2,
category: "combatant",
isPlayer: false,
type: "rat",
pos: { x: 4, y: 3 },
speed: 100,
energy: 0,
stats: createTestStats({ maxHp: 10, hp: 10, attack: 3, defense: 3 })
});
const world = createTestWorld(actors);
applyAction(world, 1, { type: "attack", targetId: 2 });
const enemy = world.actors.get(2) as CombatantActor;
const damage = 10 - enemy.stats.hp;
// Damage should be reduced by defense (5 attack - 3 defense = 2 damage)
expect(damage).toBe(2);
});
it("should emit dodged event when attack misses", () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, energy: 0,
stats: createTestStats({ accuracy: 0 }) // Force miss
});
actors.set(2, {
id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, energy: 0,
stats: createTestStats({ evasion: 0 })
});
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: "attack", targetId: 2 });
expect(events.some(e => e.type === "dodged")).toBe(true);
expect((world.actors.get(2) as CombatantActor).stats.hp).toBe(20);
});
it("should emit crit flag when critical strike occurs", () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, energy: 0,
stats: createTestStats({ critChance: 100, critMultiplier: 200, attack: 10 })
});
actors.set(2, {
id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, energy: 0,
stats: createTestStats({ defense: 0, hp: 100 })
});
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: "attack", targetId: 2 });
const damagedEvent = events.find(e => e.type === "damaged") as any;
expect(damagedEvent.isCrit).toBe(true);
expect(damagedEvent.amount).toBe(20); // 10 * 2.0
});
it("should emit block flag and reduce damage", () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, energy: 0,
stats: createTestStats({ attack: 10 })
});
actors.set(2, {
id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, energy: 0,
stats: createTestStats({ defense: 0, blockChance: 100, hp: 100 }) // Force block
});
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: "attack", targetId: 2 });
const damagedEvent = events.find(e => e.type === "damaged") as any;
expect(damagedEvent.isBlock).toBe(true);
expect(damagedEvent.amount).toBe(5); // 10 * 0.5
});
it("should heal attacker via lifesteal", () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, energy: 0,
stats: createTestStats({ attack: 10, lifesteal: 50, hp: 5, maxHp: 20 })
});
actors.set(2, {
id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, energy: 0,
stats: createTestStats({ defense: 0 })
});
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: "attack", targetId: 2 });
expect(events.some(e => e.type === "healed")).toBe(true);
expect((world.actors.get(1) as CombatantActor).stats.hp).toBe(10); // 5 + (10 * 0.5)
// A new ID should be generated for the orb (should be 3)
const orb = [...world.actors.values()].find(a => a.type === "exp_orb");
expect(orb).toBeDefined();
expect(orb!.id).toBe(3);
});
});
describe("applyAction - move", () => {
it("should move actor to new position", () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1,
category: "combatant",
isPlayer: true,
type: "player",
pos: { x: 3, y: 3 },
speed: 100,
energy: 0,
stats: createTestStats()
describe("decideEnemyAction - AI Logic", () => {
it("should path around walls", () => {
const actors = new Map<EntityId, Actor>();
const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 5, y: 3 }, stats: createTestStats() } as any;
const enemy = { id: 2, category: "combatant", isPlayer: false, pos: { x: 3, y: 3 }, stats: createTestStats() } as any;
actors.set(1, player);
actors.set(2, enemy);
const world = createTestWorld(actors);
world.tiles[3 * 10 + 4] = 4; // Wall
entityManager = new EntityManager(world);
const action = decideEnemyAction(world, enemy, player, entityManager);
expect(action.type).toBe("move");
});
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: "move", dx: 1, dy: 0 });
it("should attack if player is adjacent", () => {
const actors = new Map<EntityId, Actor>();
const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 4, y: 3 }, stats: createTestStats() } as any;
const enemy = { id: 2, category: "combatant", isPlayer: false, pos: { x: 3, y: 3 }, stats: createTestStats() } as any;
actors.set(1, player);
actors.set(2, enemy);
const player = world.actors.get(1) as CombatantActor;
expect(player.pos).toEqual({ x: 4, y: 3 });
// Should have moved event
expect(events.some(e => e.type === "moved")).toBe(true);
});
const world = createTestWorld(actors);
entityManager = new EntityManager(world);
const action = decideEnemyAction(world, enemy, player, entityManager);
expect(action).toEqual({ type: "attack", targetId: 1 });
});
});
});