test: add generation, dungeon and monster ai tests
This commit is contained in:
1 parent
7b06f9ef87
commit
309c42dda7
1 file changed
+205
+205
@@ -211,9 +211,214 @@ function tests.statusEffects()
|
||||
eq(strength.duration, 99, "re-applying a status refreshes to the longer duration")
|
||||
end
|
||||
|
||||
function tests.scheduler()
|
||||
local player = Entity.new({ id = 0, x = 1, y = 1, glyph = "@", name = "you", kind = "player", hp = 10, attack = 1, defense = 0, speed = 100 })
|
||||
local slow = Entity.new({ id = 1, x = 2, y = 1, glyph = "o", name = "orc", kind = "monster", hp = 10, attack = 1, defense = 0, speed = 100 })
|
||||
local fast = Entity.new({ id = 2, x = 3, y = 1, glyph = "v", name = "bat", kind = "monster", hp = 10, attack = 1, defense = 0, speed = 200 })
|
||||
local stub = { player = player }
|
||||
function stub:currentEntities() return { slow, fast } end
|
||||
local function run()
|
||||
player.energy, slow.energy, fast.energy = 0, 0, 0
|
||||
local tally = { [0] = 0, [1] = 0, [2] = 0 }
|
||||
local firstId
|
||||
for step = 1, 60 do
|
||||
local actor = Scheduler.next(stub)
|
||||
if step == 1 then firstId = actor.id end
|
||||
tally[actor.id] = tally[actor.id] + 1
|
||||
Scheduler.spend(actor)
|
||||
end
|
||||
return tally, firstId
|
||||
end
|
||||
local tally, firstId = run()
|
||||
eq(firstId, 2, "the fastest actor acts first")
|
||||
ok(tally[2] > tally[0], "a double-speed actor acts more often than the player")
|
||||
eq(tally[0], tally[1], "equal-speed actors act equally often")
|
||||
local tally2 = run()
|
||||
ok(tally[0] == tally2[0] and tally[2] == tally2[2], "scheduling is deterministic")
|
||||
end
|
||||
|
||||
function tests.generators()
|
||||
for _, name in ipairs(Gen.order) do
|
||||
local m1, reach1 = Gen.build(name, 60, 24, Rng.new(2024))
|
||||
local m2 = Gen.build(name, 60, 24, Rng.new(2024))
|
||||
eq(table.concat(m1.tiles), table.concat(m2.tiles), name .. " is deterministic")
|
||||
local other = Gen.build(name, 60, 24, Rng.new(2025))
|
||||
ok(table.concat(m1.tiles) ~= table.concat(other.tiles), name .. " differs across seeds")
|
||||
local walkable = 0
|
||||
for y = 1, m1.height do for x = 1, m1.width do if m1:isWalkable(x, y) then walkable = walkable + 1 end end end
|
||||
ok(walkable > 0, name .. " carves open floor")
|
||||
eq(#reach1, walkable, name .. " leaves every floor cell reachable")
|
||||
end
|
||||
end
|
||||
|
||||
function tests.connectivityRepair()
|
||||
local map = Map.new(15, 7)
|
||||
for y = 2, 4 do for x = 2, 4 do map:set(x, y, Map.FLOOR) end end
|
||||
for y = 2, 4 do for x = 10, 12 do map:set(x, y, Map.FLOOR) end end
|
||||
local function walkableCount()
|
||||
local total = 0
|
||||
for y = 1, map.height do for x = 1, map.width do
|
||||
if map:isWalkable(x, y) then total = total + 1 end
|
||||
end end
|
||||
return total
|
||||
end
|
||||
eq(walkableCount(), 18, "the two rooms start disconnected")
|
||||
local reachable = Connect.ensure(map, Rng.new(1))
|
||||
ok(walkableCount() > 18, "connect carves new corridor to join regions")
|
||||
eq(#reachable, walkableCount(), "after repair every floor cell is reachable")
|
||||
end
|
||||
|
||||
function tests.dungeonPersistence()
|
||||
local dungeon = Dungeon.new(4242)
|
||||
local level3 = dungeon:get(3)
|
||||
local tiles3 = table.concat(level3.map.tiles)
|
||||
local fresh = Dungeon.new(4242)
|
||||
eq(table.concat(fresh:get(3).map.tiles), tiles3, "layout depends only on world seed and depth")
|
||||
ok(dungeon:get(3) == level3, "revisiting returns the cached level object")
|
||||
local before = #level3.entities
|
||||
ok(before > 0, "levels spawn monsters")
|
||||
table.remove(level3.entities)
|
||||
eq(#dungeon:get(3).entities, before - 1, "level state persists across revisits")
|
||||
ok(table.concat(dungeon:get(4).map.tiles) ~= tiles3, "different depth yields a different layout")
|
||||
ok(table.concat(Dungeon.new(9999):get(3).map.tiles) ~= tiles3, "different world seed yields a different layout")
|
||||
eq(level3.map:get(level3.upX, level3.upY), Tiles.STAIRS_UP, "up stairs are cut")
|
||||
eq(level3.map:get(level3.downX, level3.downY), Tiles.STAIRS_DOWN, "down stairs are cut")
|
||||
end
|
||||
|
||||
local function arena(w, h)
|
||||
local map = Map.new(w, h)
|
||||
for y = 2, h - 1 do for x = 2, w - 1 do map:set(x, y, Map.FLOOR) end end
|
||||
return Level.new({
|
||||
depth = 1, generator = "arena", map = map,
|
||||
entities = {}, floorItems = {}, traps = {}, lights = {}, dark = false, explored = {},
|
||||
upX = 2, upY = 2, downX = w - 1, downY = h - 1,
|
||||
})
|
||||
end
|
||||
|
||||
local function stagedGame(seed, level, px, py)
|
||||
local game = Game.new(seed)
|
||||
game.level = level
|
||||
game.player.x, game.player.y = px, py
|
||||
game:computeVisible()
|
||||
return game
|
||||
end
|
||||
|
||||
function tests.monsterAiChase()
|
||||
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||
local orc = Monsters.create(Monsters.byKey.orc, 1, 12, 6, 1)
|
||||
game.level.entities = { orc }
|
||||
Ai.act(game, orc)
|
||||
eq(orc.x, 11, "a chaser steps one cell toward a visible player")
|
||||
end
|
||||
|
||||
function tests.monsterAiRanged()
|
||||
local shootGame = stagedGame(1, arena(21, 11), 3, 6)
|
||||
local archer = Monsters.create(Monsters.byKey.archer, 1, 8, 6, 1)
|
||||
shootGame.level.entities = { archer }
|
||||
local hpBefore = shootGame.player.hp
|
||||
Ai.act(shootGame, archer)
|
||||
ok(shootGame.player.hp < hpBefore, "an archer in range and line of sight shoots")
|
||||
eq(archer.x, 8, "an archer that can shoot holds its ground")
|
||||
|
||||
local kiteGame = stagedGame(1, arena(21, 11), 3, 6)
|
||||
local adjacent = Monsters.create(Monsters.byKey.archer, 1, 4, 6, 1)
|
||||
kiteGame.level.entities = { adjacent }
|
||||
Ai.act(kiteGame, adjacent)
|
||||
eq(adjacent.x, 5, "an archer kites away when the player closes in")
|
||||
end
|
||||
|
||||
function tests.monsterAiFlee()
|
||||
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||
local wretch = Monsters.create(Monsters.byKey.wretch, 1, 5, 6, 1)
|
||||
game.level.entities = { wretch }
|
||||
Ai.act(game, wretch)
|
||||
eq(wretch.x, 6, "a coward flees away from the player")
|
||||
end
|
||||
|
||||
function tests.monsterAiDeterministic()
|
||||
local function batRun()
|
||||
local game = stagedGame(55, arena(21, 11), 3, 6)
|
||||
local bat = Monsters.create(Monsters.byKey.bat, 1, 10, 6, 1)
|
||||
game.level.entities = { bat }
|
||||
game.rng = Rng.new(55)
|
||||
Ai.act(game, bat)
|
||||
return bat.x, bat.y
|
||||
end
|
||||
local x1, y1 = batRun()
|
||||
local x2, y2 = batRun()
|
||||
ok(x1 >= 2 and x1 <= 20 and y1 >= 2 and y1 <= 10, "erratic bat lands on a valid cell")
|
||||
ok(x1 == x2 and y1 == y2, "erratic behaviour is deterministic per seed")
|
||||
end
|
||||
|
||||
function tests.doorsAndKeys()
|
||||
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||
game.player.inventory = {}
|
||||
game.level.map:set(4, 6, Tiles.DOOR_CLOSED)
|
||||
game:movePlayer(1, 0)
|
||||
eq(game.level.map:get(4, 6), Tiles.DOOR_OPEN, "bumping a closed door opens it")
|
||||
eq(game.player.x, 3, "opening a door does not move the player")
|
||||
|
||||
game.level.map:set(2, 6, Tiles.FLOOR)
|
||||
game.level.map:set(4, 6, Tiles.DOOR_LOCKED)
|
||||
game:movePlayer(1, 0)
|
||||
eq(game.level.map:get(4, 6), Tiles.DOOR_LOCKED, "a locked door stays shut without a key")
|
||||
eq(game.log:last(), "the door is locked", "the game reports a locked door")
|
||||
game.player.inventory[#game.player.inventory + 1] = Items.key()
|
||||
game:movePlayer(1, 0)
|
||||
eq(game.level.map:get(4, 6), Tiles.DOOR_OPEN, "a key unlocks the door")
|
||||
eq(#game.player.inventory, 0, "unlocking consumes the key")
|
||||
end
|
||||
|
||||
function tests.traps()
|
||||
local dartGame = stagedGame(1, arena(21, 11), 3, 6)
|
||||
dartGame.level.traps = { { x = 4, y = 6, effect = "dart", revealed = false } }
|
||||
local hpBefore = dartGame.player.hp
|
||||
dartGame:movePlayer(1, 0)
|
||||
ok(dartGame.player.hp < hpBefore, "a dart trap wounds the stepper")
|
||||
eq(dartGame.level.traps[1].revealed, true, "a sprung trap becomes revealed")
|
||||
eq(dartGame.level.map:get(4, 6), Tiles.TRAP, "a sprung trap shows on the map")
|
||||
|
||||
local poisonGame = stagedGame(1, arena(21, 11), 3, 6)
|
||||
poisonGame.level.traps = { { x = 4, y = 6, effect = "poison", revealed = false } }
|
||||
poisonGame:movePlayer(1, 0)
|
||||
eq(Status.has(poisonGame.player, Status.POISON), true, "a poison trap poisons the stepper")
|
||||
end
|
||||
|
||||
function tests.lightAndDark()
|
||||
local map = Map.new(13, 3)
|
||||
for x = 2, 12 do map:set(x, 2, Map.FLOOR) end
|
||||
local dim = Light.compute(map, 2, 2, 2, {})
|
||||
local bright = Light.compute(map, 2, 2, 8, {})
|
||||
local dimCount, brightCount = 0, 0
|
||||
for _ in pairs(dim) do dimCount = dimCount + 1 end
|
||||
for _ in pairs(bright) do brightCount = brightCount + 1 end
|
||||
ok(dimCount < brightCount, "a smaller light radius reveals fewer cells")
|
||||
ok(not dim[map:index(9, 2)], "a far cell is dark without light")
|
||||
local lit = Light.compute(map, 2, 2, 3, { { x = 5, y = 2, radius = 3 } })
|
||||
ok(lit[map:index(8, 2)], "a visible emitter lights cells beyond the player's own radius")
|
||||
end
|
||||
|
||||
function tests.identification()
|
||||
local a = Identify.new(Rng.new(7))
|
||||
local b = Identify.new(Rng.new(7))
|
||||
local healing = Items.potionOf("heal")
|
||||
eq(Identify.label(a, healing), Identify.label(b, healing), "appearances are deterministic per seed")
|
||||
ok(not contains(Identify.label(a, healing), "healing"), "an unidentified potion hides its true name")
|
||||
ok(contains(Identify.label(a, healing), "potion"), "an unidentified potion still reads as a potion")
|
||||
local c = Identify.new(Rng.new(8))
|
||||
local differs = Identify.label(a, healing) ~= Identify.label(c, healing)
|
||||
or Identify.label(a, Items.potionOf("poison")) ~= Identify.label(c, Items.potionOf("poison"))
|
||||
ok(differs, "a different world scrambles appearances differently")
|
||||
Identify.reveal(a, healing)
|
||||
eq(Identify.label(a, healing), "potion of healing", "identifying reveals the true name")
|
||||
end
|
||||
|
||||
local order = {
|
||||
"rngDeterminism", "mapDeterminism", "tilesRegistry", "fovKnownScene", "losSightLine",
|
||||
"astarPathing", "combatResolution", "itemsAndEquipment", "affixes", "statusEffects",
|
||||
"scheduler", "generators", "connectivityRepair", "dungeonPersistence", "monsterAiChase",
|
||||
"monsterAiRanged", "monsterAiFlee", "monsterAiDeterministic", "doorsAndKeys", "traps",
|
||||
"lightAndDark", "identification",
|
||||
}
|
||||
|
||||
for _, name in ipairs(order) do
|
||||
|
||||
Reference in new issue
Block a user