Add spiky box variants

This commit is contained in:
Your Name 2026-08-11 20:08:58 -04:00
parent 761214fe3e
commit c3a2692351
10 changed files with 340 additions and 19 deletions

View file

@ -29,17 +29,19 @@ local function assertNotIn(group, entity, message)
if group[entity] then error(message or "expected entity outside movement group", 2) end
end
local function movable(x, y, sticky, shape, skewer)
local function movable(x, y, sticky, shape, skewer, spiky)
local entity = {
x = x, y = y,
sticky = sticky or false,
skewer = skewer or false,
spiky = spiky or false,
shape = shape or {{x = 0, y = 0}},
}
function entity:canMove() return true end
function entity:canPassOver() return false end
function entity:isSticky() return self.sticky end
function entity:isSkewer() return self.skewer end
function entity:isSpiky() return self.spiky end
function entity:bump() end
function entity:move(axis, amount) self[axis] = self[axis] + amount end
function entity:getGridPos() return {x = self.x, y = self.y} end
@ -100,7 +102,7 @@ test("a trailing sticky block joins a horizontal pull", function()
assertIn(group, sticky)
end)
test("a sticky block touching the player pulls a regular block behind it", function()
test("a moving sticky block pulls a regular block behind it", function()
-- regular, sticky, player; moving right
local regular = movable(2, 3)
local sticky = movable(3, 3, true)
@ -176,6 +178,36 @@ test("a blocked skewer branch stops its rigid movement group", function()
assertFalse(possible)
end)
test("a spiky block pulls both orthogonal side blocks", function()
local spiky = movable(4, 4, false, nil, false, true)
local upper = movable(4, 3)
local lower = movable(4, 5)
local possible, group = plan(
{spiky, upper, lower}, {x = 4, y = 4}, {x = 1, y = 0}
)
assertTrue(possible)
assertIn(group, spiky)
assertIn(group, upper)
assertIn(group, lower)
end)
test("a spiky block releases one blocked side while pulling the other", function()
local spiky = movable(4, 4, false, nil, false, true)
local blockedUpper = movable(4, 3)
local freeLower = movable(4, 5)
local blockingWall = wall(5, 3)
local possible, group = plan(
{spiky, blockedUpper, freeLower, blockingWall},
{x = 4, y = 4}, {x = 1, y = 0}
)
assertTrue(possible)
assertIn(group, spiky)
assertNotIn(group, blockedUpper)
assertIn(group, freeLower)
end)
test("ordinary pushing still propagates through regular blocks", function()
local first = movable(3, 3)
local second = movable(4, 3)
@ -203,7 +235,88 @@ test("multi-cell sticky blocks pull from every trailing occupied cell", function
assertIn(group, sticky)
end)
test("production sticky and skewer blocks advertise distinct behavior", function()
test("an L-shaped sticky pulls from its trailing face in every direction", function()
local lShape = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1}}
local cases = {
{direction = {x = 1, y = 0}, trailing = {x = 2, y = 3}},
{direction = {x = -1, y = 0}, trailing = {x = 5, y = 3}},
{direction = {x = 0, y = 1}, trailing = {x = 3, y = 2}},
{direction = {x = 0, y = -1}, trailing = {x = 3, y = 5}},
}
for _, case in ipairs(cases) do
local stickyL = movable(3, 3, true, lShape)
local trailing = movable(case.trailing.x, case.trailing.y)
local possible, group = plan(
{stickyL, trailing}, {x = 3, y = 3}, case.direction
)
assertTrue(possible)
assertIn(group, stickyL)
assertIn(group, trailing)
end
end)
test("an L-shaped sticky can release one blocked pull while completing another", function()
-- Moving right, the two exposed trailing cells touch separate objects. The
-- upper object is pinned against a wall; the lower one is free to follow.
local stickyL = movable(3, 3, true, {
{x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1},
})
local pinned = movable(2, 2, false, {{x = 0, y = 0}, {x = 0, y = 1}})
local free = movable(2, 4)
local blockingWall = wall(3, 2)
local possible, group = plan(
{stickyL, pinned, free, blockingWall},
{x = 3, y = 3}, {x = 1, y = 0}
)
assertTrue(possible)
assertIn(group, stickyL)
assertIn(group, free)
assertNotIn(group, pinned)
end)
test("a T-shaped sticky pulls independently across a notched trailing face", function()
-- Moving up, three exposed lower faces pull into cells vacated by the T.
local stickyT = movable(3, 3, true, {
{x = 0, y = 0}, {x = 1, y = 0}, {x = 2, y = 0},
{x = 1, y = 1},
})
local trailingLeft = movable(3, 4)
local trailingRight = movable(5, 4)
local trailingStem = movable(4, 5)
local possible, group = plan(
{stickyT, trailingLeft, trailingRight, trailingStem},
{x = 3, y = 3}, {x = 0, y = -1}
)
assertTrue(possible)
assertIn(group, stickyT)
assertIn(group, trailingLeft)
assertIn(group, trailingRight)
assertIn(group, trailingStem)
end)
test("an L-shaped spiky block grabs moveables across several exposed faces", function()
local spikyL = movable(3, 3, false, {
{x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1},
}, false, true)
local above = movable(3, 2)
local below = movable(3, 5)
local behind = movable(2, 3)
local possible, group = plan(
{spikyL, above, below, behind},
{x = 3, y = 3}, {x = 1, y = 0}
)
assertTrue(possible)
assertIn(group, spikyL)
assertIn(group, above)
assertIn(group, below)
assertIn(group, behind)
end)
test("production sticky, skewer, and spiky blocks advertise distinct behavior", function()
class = require "libs/middleclass"
Color = class("TestColor")
function Color:initialize(name) self.name = name end
@ -212,10 +325,12 @@ test("production sticky and skewer blocks advertise distinct behavior", function
require "generic_moveable"
require "generic_sticky"
require "generic_skewer"
require "generic_spiky"
local sprite = {getWidth = function() return 16 end, getHeight = function() return 16 end}
local regular = GenericMoveable:new(2, 2, "red", sprite, "box")
local sticky = GenericSticky:new(3, 2, "red", sprite, "sticky_box")
local skewer = GenericSkewer:new(4, 2, "red", sprite, "skewer_box")
local spiky = GenericSpiky:new(5, 2, "red", sprite, "spiky_box")
assertTrue(regular:canMove())
assertFalse(regular:isSticky())
@ -225,6 +340,11 @@ test("production sticky and skewer blocks advertise distinct behavior", function
assertTrue(skewer:canMove())
assertFalse(skewer:isSticky())
assertTrue(skewer:isSkewer())
assertFalse(skewer:isSpiky())
assertTrue(spiky:canMove())
assertFalse(spiky:isSticky())
assertFalse(spiky:isSkewer())
assertTrue(spiky:isSpiky())
end)
local function loadRoomClass()
@ -276,6 +396,47 @@ test("Room applies the sticky-regular-player pull", function()
assertTrue(player.x == 5, "player should move one cell right")
end)
test("Room pulls sticky out of a pinned concave moveable", function()
loadRoomClass()
-- Before: ___ After: p__
-- px_ sx_
-- sm_ _m_
-- mm_ mm_
local player = movable(1, 2)
local wall = wall(2, 2)
local sticky = movable(1, 3, true)
local concave = movable(1, 3, false, {
{x = 1, y = 0},
{x = 0, y = 1}, {x = 1, y = 1},
})
local room = roomWith(player, {wall, sticky, concave})
room:movePlayer("up")
assertTrue(player.x == 1 and player.y == 1)
assertTrue(sticky.x == 1 and sticky.y == 2, "sticky should peel away from the concave block")
assertTrue(concave.x == 1 and concave.y == 3, "pinned concave block should stay put")
assertTrue(wall.x == 2 and wall.y == 2)
end)
test("Room moves an L-shaped sticky and both available trailing pulls", function()
loadRoomClass()
local player = movable(3, 2)
local stickyL = movable(3, 3, true, {
{x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1},
})
local trailingLeft = movable(3, 5)
local trailingRight = movable(4, 4)
local room = roomWith(player, {stickyL, trailingLeft, trailingRight})
room:movePlayer("up")
assertTrue(player.x == 3 and player.y == 1)
assertTrue(stickyL.x == 3 and stickyL.y == 2)
assertTrue(trailingLeft.x == 3 and trailingLeft.y == 4)
assertTrue(trailingRight.x == 4 and trailingRight.y == 3)
end)
test("Room shears a lateral sticky contact", function()
loadRoomClass()
local sticky = movable(3, 2, true)
@ -314,15 +475,36 @@ test("Room never moves a component found by push and pull twice", function()
assertTrue(player.x == 5)
end)
test("every moveable base box gets a synthetic sticky variant", function()
test("Room pushes spiky onward while a blocked side shears off", function()
loadRoomClass()
local player = movable(3, 4)
local spiky = movable(4, 4, false, nil, false, true)
local blockedUpper = movable(4, 3)
local freeLower = movable(4, 5)
local blockingWall = wall(5, 3)
local room = roomWith(player, {spiky, blockedUpper, freeLower, blockingWall})
room:movePlayer("right")
assertTrue(player.x == 4 and player.y == 4)
assertTrue(spiky.x == 5 and spiky.y == 4)
assertTrue(blockedUpper.x == 4 and blockedUpper.y == 3)
assertTrue(freeLower.x == 5 and freeLower.y == 5)
assertTrue(blockingWall.x == 5 and blockingWall.y == 3)
end)
test("every moveable base box gets all three synthetic behavior variants", function()
local BoxVariants = require "box_variants"
local boxSprites = {red = "box_r.png"}
local boxCells = {red = {{x = 1, y = 1}}}
local shapeSprites = {blue = "box_shape_b.png"}
local shapeCells = {blue = {{x = 1, y = 1}, {x = 2, y = 1}}}
local lSprites = {green = "box_l_g.png"}
local lCells = {green = {{x = 1, y = 1}, {x = 1, y = 2}, {x = 2, y = 2}}}
local assets = {
box = {class = "moveable", sprites = boxSprites, cells = boxCells},
["box_2x1_11"] = {class = "moveable", sprites = shapeSprites, cells = shapeCells},
["box_2x2_10-11"] = {class = "moveable", sprites = lSprites, cells = lCells},
box_statue = {class = "immoveable", sprites = {}},
beer_mug = {class = "moveable", sprites = {}},
}
@ -337,6 +519,8 @@ test("every moveable base box gets a synthetic sticky variant", function()
assertTrue(assets.sticky_box.cells == boxCells, "variant should share collision cells")
assertTrue(assets["sticky_box_2x1_11"].sprites == shapeSprites)
assertTrue(assets["sticky_box_2x1_11"].cells == shapeCells)
assertTrue(assets["sticky_box_2x2_10-11"].sprites == lSprites)
assertTrue(assets["sticky_box_2x2_10-11"].cells == lCells)
assertTrue(assets.skewer_box.class == "skewer")
assertTrue(assets.skewer_box.directory == "skewer")
assertTrue(assets.skewer_box.synthetic)
@ -344,10 +528,21 @@ test("every moveable base box gets a synthetic sticky variant", function()
assertTrue(assets.skewer_box.sprites == boxSprites)
assertTrue(assets.skewer_box.cells == boxCells)
assertTrue(assets["skewer_box_2x1_11"].sprites == shapeSprites)
assertTrue(assets["skewer_box_2x2_10-11"].cells == lCells)
assertTrue(assets.spiky_box.class == "spiky")
assertTrue(assets.spiky_box.directory == "spiky")
assertTrue(assets.spiky_box.synthetic)
assertTrue(assets.spiky_box.spiky)
assertTrue(assets.spiky_box.sprites == boxSprites)
assertTrue(assets.spiky_box.cells == boxCells)
assertTrue(assets["spiky_box_2x1_11"].sprites == shapeSprites)
assertTrue(assets["spiky_box_2x2_10-11"].cells == lCells)
assertFalse(assets.sticky_box_statue)
assertFalse(assets.sticky_beer_mug)
assertFalse(assets.skewer_box_statue)
assertFalse(assets.skewer_beer_mug)
assertFalse(assets.spiky_box_statue)
assertFalse(assets.spiky_beer_mug)
end)
print("movement/block-variant tests passed: " .. passed)