Refactor codebase

This commit is contained in:
Peter Stockings
2026-01-04 15:56:18 +11:00
parent 3785885abe
commit bfe5ebae8c
18 changed files with 380 additions and 191 deletions

View File

@@ -0,0 +1,136 @@
import { describe, it, expect } from 'vitest';
import { generateWorld } from '../world/generator';
import { isWall, inBounds } from '../world/world-logic';
describe('World Generator', () => {
describe('generateWorld', () => {
it('should generate a world with correct dimensions', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world } = generateWorld(1, runState);
expect(world.width).toBe(60);
expect(world.height).toBe(40);
expect(world.tiles.length).toBe(60 * 40);
});
it('should place player actor', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world, playerId } = generateWorld(1, runState);
expect(playerId).toBe(1);
const player = world.actors.get(playerId);
expect(player).toBeDefined();
expect(player?.isPlayer).toBe(true);
expect(player?.stats).toEqual(runState.stats);
});
it('should create walkable rooms', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world, playerId } = generateWorld(1, runState);
const player = world.actors.get(playerId)!;
// Player should spawn in a walkable area
expect(isWall(world, player.pos.x, player.pos.y)).toBe(false);
});
it('should place exit in valid location', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world } = generateWorld(1, runState);
expect(inBounds(world, world.exit.x, world.exit.y)).toBe(true);
// Exit should be on a floor tile
expect(isWall(world, world.exit.x, world.exit.y)).toBe(false);
});
it('should create enemies', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world } = generateWorld(1, runState);
// Should have player + enemies
expect(world.actors.size).toBeGreaterThan(1);
// All non-player actors should be enemies
const enemies = Array.from(world.actors.values()).filter(a => !a.isPlayer);
expect(enemies.length).toBeGreaterThan(0);
// Enemies should have stats
enemies.forEach(enemy => {
expect(enemy.stats).toBeDefined();
expect(enemy.stats!.hp).toBeGreaterThan(0);
expect(enemy.stats!.attack).toBeGreaterThan(0);
});
});
it('should generate deterministic maps for same level', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world: world1, playerId: player1 } = generateWorld(1, runState);
const { world: world2, playerId: player2 } = generateWorld(1, runState);
// Same level should generate identical layouts
expect(world1.tiles).toEqual(world2.tiles);
expect(world1.exit).toEqual(world2.exit);
const player1Pos = world1.actors.get(player1)!.pos;
const player2Pos = world2.actors.get(player2)!.pos;
expect(player1Pos).toEqual(player2Pos);
});
it('should generate different maps for different levels', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world: world1 } = generateWorld(1, runState);
const { world: world2 } = generateWorld(2, runState);
// Different levels should have different layouts
expect(world1.tiles).not.toEqual(world2.tiles);
});
it('should scale enemy difficulty with level', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world: world1 } = generateWorld(1, runState);
const { world: world5 } = generateWorld(5, runState);
const enemies1 = Array.from(world1.actors.values()).filter(a => !a.isPlayer);
const enemies5 = Array.from(world5.actors.values()).filter(a => !a.isPlayer);
// Higher level should have more enemies
expect(enemies5.length).toBeGreaterThan(enemies1.length);
// Higher level enemies should have higher stats
const avgHp1 = enemies1.reduce((sum, e) => sum + (e.stats?.hp || 0), 0) / enemies1.length;
const avgHp5 = enemies5.reduce((sum, e) => sum + (e.stats?.hp || 0), 0) / enemies5.length;
expect(avgHp5).toBeGreaterThan(avgHp1);
});
});
});

View File

@@ -0,0 +1,125 @@
import { describe, it, expect } from 'vitest';
import { applyAction } from '../simulation/simulation';
import { type World, type Actor, type EntityId } from '../../core/types';
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 }
});
describe('applyAction - attack', () => {
it('should deal damage when player attacks enemy', () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1,
isPlayer: true,
pos: { x: 3, y: 3 },
speed: 100,
energy: 0,
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 }
});
actors.set(2, {
id: 2,
isPlayer: false,
pos: { x: 4, y: 3 },
speed: 100,
energy: 0,
stats: { maxHp: 10, hp: 10, attack: 3, defense: 1 }
});
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: 'attack', targetId: 2 });
const enemy = world.actors.get(2)!;
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', () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1,
isPlayer: true,
pos: { x: 3, y: 3 },
speed: 100,
energy: 0,
stats: { maxHp: 20, hp: 20, attack: 50, defense: 2 }
});
actors.set(2, {
id: 2,
isPlayer: false,
pos: { x: 4, y: 3 },
speed: 100,
energy: 0,
stats: { maxHp: 10, hp: 10, attack: 3, defense: 1 }
});
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: 'attack', targetId: 2 });
// Enemy should be removed from world
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,
isPlayer: true,
pos: { x: 3, y: 3 },
speed: 100,
energy: 0,
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 }
});
actors.set(2, {
id: 2,
isPlayer: false,
pos: { x: 4, y: 3 },
speed: 100,
energy: 0,
stats: { 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)!;
const damage = 10 - enemy.stats!. hp;
// Damage should be reduced by defense (5 attack - 3 defense = 2 damage)
expect(damage).toBe(2);
});
});
describe('applyAction - move', () => {
it('should move actor to new position', () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1,
isPlayer: true,
pos: { x: 3, y: 3 },
speed: 100,
energy: 0,
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 }
});
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: 'move', dx: 1, dy: 0 });
const player = world.actors.get(1)!;
expect(player.pos).toEqual({ x: 4, y: 3 });
// Should have moved event
expect(events.some(e => e.type === 'moved')).toBe(true);
});
});
});

View File

@@ -0,0 +1,110 @@
import { describe, it, expect } from 'vitest';
import { idx, inBounds, isWall, isBlocked } from '../world/world-logic';
import { type World, type Tile } from '../../core/types';
describe('World Utilities', () => {
const createTestWorld = (width: number, height: number, tiles: Tile[]): World => ({
width,
height,
tiles,
actors: new Map(),
exit: { x: 0, y: 0 }
});
describe('idx', () => {
it('should calculate correct index for 2D coordinates', () => {
const world = createTestWorld(10, 10, []);
expect(idx(world, 0, 0)).toBe(0);
expect(idx(world, 5, 0)).toBe(5);
expect(idx(world, 0, 1)).toBe(10);
expect(idx(world, 5, 3)).toBe(35);
});
});
describe('inBounds', () => {
it('should return true for coordinates within bounds', () => {
const world = createTestWorld(10, 10, []);
expect(inBounds(world, 0, 0)).toBe(true);
expect(inBounds(world, 5, 5)).toBe(true);
expect(inBounds(world, 9, 9)).toBe(true);
});
it('should return false for coordinates outside bounds', () => {
const world = createTestWorld(10, 10, []);
expect(inBounds(world, -1, 0)).toBe(false);
expect(inBounds(world, 0, -1)).toBe(false);
expect(inBounds(world, 10, 0)).toBe(false);
expect(inBounds(world, 0, 10)).toBe(false);
expect(inBounds(world, 11, 11)).toBe(false);
});
});
describe('isWall', () => {
it('should return true for wall tiles', () => {
const tiles: Tile[] = new Array(100).fill(0);
tiles[0] = 1; // wall at 0,0
tiles[55] = 1; // wall at 5,5
const world = createTestWorld(10, 10, tiles);
expect(isWall(world, 0, 0)).toBe(true);
expect(isWall(world, 5, 5)).toBe(true);
});
it('should return false for floor tiles', () => {
const tiles: Tile[] = new Array(100).fill(0);
const world = createTestWorld(10, 10, tiles);
expect(isWall(world, 3, 3)).toBe(false);
expect(isWall(world, 7, 7)).toBe(false);
});
it('should return false for out of bounds coordinates', () => {
const world = createTestWorld(10, 10, new Array(100).fill(0));
expect(isWall(world, -1, 0)).toBe(false);
expect(isWall(world, 10, 10)).toBe(false);
});
});
describe('isBlocked', () => {
it('should return true for walls', () => {
const tiles: Tile[] = new Array(100).fill(0);
tiles[55] = 1; // wall at 5,5
const world = createTestWorld(10, 10, tiles);
expect(isBlocked(world, 5, 5)).toBe(true);
});
it('should return true for actor positions', () => {
const world = createTestWorld(10, 10, new Array(100).fill(0));
world.actors.set(1, {
id: 1,
isPlayer: true,
pos: { x: 3, y: 3 },
speed: 100,
energy: 0
});
expect(isBlocked(world, 3, 3)).toBe(true);
});
it('should return false for empty floor tiles', () => {
const world = createTestWorld(10, 10, new Array(100).fill(0));
expect(isBlocked(world, 3, 3)).toBe(false);
expect(isBlocked(world, 7, 7)).toBe(false);
});
it('should return true for out of bounds', () => {
const world = createTestWorld(10, 10, new Array(100).fill(0));
expect(isBlocked(world, -1, 0)).toBe(true);
expect(isBlocked(world, 10, 10)).toBe(true);
});
});
});