2026-08-11 15:07:11 -04:00
|
|
|
package.path = "./?.lua;" .. package.path
|
|
|
|
|
|
|
|
|
|
local Movement = require "movement"
|
|
|
|
|
|
|
|
|
|
local passed = 0
|
|
|
|
|
|
|
|
|
|
local function test(name, fn)
|
|
|
|
|
local ok, err = pcall(fn)
|
|
|
|
|
if not ok then
|
|
|
|
|
io.stderr:write("FAIL: " .. name .. "\n" .. err .. "\n")
|
|
|
|
|
os.exit(1)
|
|
|
|
|
end
|
|
|
|
|
passed = passed + 1
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function assertTrue(value, message)
|
|
|
|
|
if not value then error(message or "expected true", 2) end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function assertFalse(value, message)
|
|
|
|
|
if value then error(message or "expected false", 2) end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function assertIn(group, entity, message)
|
|
|
|
|
if group[entity] ~= entity then error(message or "expected entity in movement group", 2) end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function assertNotIn(group, entity, message)
|
|
|
|
|
if group[entity] then error(message or "expected entity outside movement group", 2) end
|
|
|
|
|
end
|
|
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
local function movable(x, y, adhesion, shape)
|
2026-08-11 15:07:11 -04:00
|
|
|
local entity = {
|
|
|
|
|
x = x, y = y,
|
2026-08-21 16:34:01 -04:00
|
|
|
adhesion = adhesion,
|
2026-08-11 15:07:11 -04:00
|
|
|
shape = shape or {{x = 0, y = 0}},
|
|
|
|
|
}
|
|
|
|
|
function entity:canMove() return true end
|
|
|
|
|
function entity:canPassOver() return false end
|
2026-08-21 16:34:01 -04:00
|
|
|
function entity:isSticky() return self.adhesion == "sticky" end
|
|
|
|
|
function entity:isGluey() return self.adhesion == "gluey" end
|
2026-08-11 15:07:11 -04:00
|
|
|
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
|
|
|
|
|
function entity:getOccupiedCells(offset)
|
|
|
|
|
offset = offset or {x = 0, y = 0}
|
|
|
|
|
local cells = {}
|
|
|
|
|
for _, cell in ipairs(self.shape) do
|
|
|
|
|
table.insert(cells, {x = self.x + cell.x + offset.x, y = self.y + cell.y + offset.y})
|
|
|
|
|
end
|
|
|
|
|
return cells
|
|
|
|
|
end
|
|
|
|
|
return entity
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function wall(x, y)
|
|
|
|
|
local entity = movable(x, y)
|
|
|
|
|
function entity:canMove() return false end
|
|
|
|
|
return entity
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function world(entities, width, height)
|
|
|
|
|
width, height = width or 8, height or 8
|
|
|
|
|
local cells = {}
|
|
|
|
|
for _, entity in ipairs(entities) do
|
|
|
|
|
for _, cell in ipairs(entity:getOccupiedCells()) do
|
|
|
|
|
cells[cell.y .. ":" .. cell.x] = entity
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return function(cell)
|
|
|
|
|
return cells[cell.y .. ":" .. cell.x]
|
|
|
|
|
end, function(cell)
|
|
|
|
|
return cell.x >= 1 and cell.x <= width and cell.y >= 1 and cell.y <= height
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function plan(entities, startCell, direction, width, height)
|
|
|
|
|
local entityAt, inBounds = world(entities, width, height)
|
|
|
|
|
return Movement.findMovableGroup({startCell}, direction, entityAt, inBounds)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test("ordinary pull only drags the block touching the player", function()
|
|
|
|
|
-- regular, regular, player; moving right
|
|
|
|
|
local rear = movable(2, 3)
|
|
|
|
|
local touching = movable(3, 3)
|
|
|
|
|
local possible, group = plan({rear, touching}, {x = 3, y = 3}, {x = 1, y = 0})
|
|
|
|
|
assertTrue(possible)
|
|
|
|
|
assertIn(group, touching)
|
|
|
|
|
assertNotIn(group, rear)
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
test("a trailing sticky block joins a horizontal pull", function()
|
|
|
|
|
-- sticky, regular, player; moving right
|
2026-08-21 16:34:01 -04:00
|
|
|
local sticky = movable(2, 3, "sticky")
|
2026-08-11 15:07:11 -04:00
|
|
|
local regular = movable(3, 3)
|
|
|
|
|
local possible, group = plan({sticky, regular}, {x = 3, y = 3}, {x = 1, y = 0})
|
|
|
|
|
assertTrue(possible)
|
|
|
|
|
assertIn(group, regular)
|
|
|
|
|
assertIn(group, sticky)
|
|
|
|
|
end)
|
|
|
|
|
|
2026-08-11 20:08:58 -04:00
|
|
|
test("a moving sticky block pulls a regular block behind it", function()
|
2026-08-11 15:07:11 -04:00
|
|
|
-- regular, sticky, player; moving right
|
|
|
|
|
local regular = movable(2, 3)
|
2026-08-21 16:34:01 -04:00
|
|
|
local sticky = movable(3, 3, "sticky")
|
2026-08-11 15:07:11 -04:00
|
|
|
local possible, group = plan({regular, sticky}, {x = 3, y = 3}, {x = 1, y = 0})
|
|
|
|
|
assertTrue(possible)
|
|
|
|
|
assertIn(group, sticky)
|
|
|
|
|
assertIn(group, regular)
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
test("sticky pull chains work vertically", function()
|
|
|
|
|
-- player moves down, pulling the two blocks above it
|
2026-08-21 16:34:01 -04:00
|
|
|
local sticky = movable(4, 2, "sticky")
|
2026-08-11 15:07:11 -04:00
|
|
|
local regular = movable(4, 3)
|
|
|
|
|
local possible, group = plan({sticky, regular}, {x = 4, y = 3}, {x = 0, y = 1})
|
|
|
|
|
assertTrue(possible)
|
|
|
|
|
assertIn(group, regular)
|
|
|
|
|
assertIn(group, sticky)
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
test("sticky pulls are symmetric in all four directions", function()
|
|
|
|
|
for _, direction in ipairs({
|
|
|
|
|
{x = 1, y = 0}, {x = -1, y = 0},
|
|
|
|
|
{x = 0, y = 1}, {x = 0, y = -1},
|
|
|
|
|
}) do
|
|
|
|
|
local player = {x = 4, y = 4}
|
|
|
|
|
local regular = movable(player.x - direction.x, player.y - direction.y)
|
2026-08-21 16:34:01 -04:00
|
|
|
local sticky = movable(player.x - direction.x * 2, player.y - direction.y * 2, "sticky")
|
2026-08-11 15:07:11 -04:00
|
|
|
local possible, group = plan(
|
|
|
|
|
{sticky, regular},
|
|
|
|
|
{x = regular.x, y = regular.y},
|
|
|
|
|
direction
|
|
|
|
|
)
|
|
|
|
|
assertTrue(possible)
|
|
|
|
|
assertIn(group, regular)
|
|
|
|
|
assertIn(group, sticky)
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
test("a sticky block grabs a free lateral block during orthogonal movement", function()
|
|
|
|
|
-- The regular block moves right; the sticky block above it is soft-grabbed on
|
|
|
|
|
-- that lateral face (sticky pulls on every edge) and follows along.
|
2026-08-11 15:07:11 -04:00
|
|
|
local regular = movable(3, 4)
|
2026-08-21 16:34:01 -04:00
|
|
|
local stickyBranch = movable(3, 3, "sticky")
|
2026-08-11 15:07:11 -04:00
|
|
|
local possible, group = plan({regular, stickyBranch}, {x = 3, y = 4}, {x = 1, y = 0})
|
|
|
|
|
assertTrue(possible)
|
|
|
|
|
assertIn(group, regular)
|
2026-08-21 16:34:01 -04:00
|
|
|
assertIn(group, stickyBranch)
|
2026-08-11 15:07:11 -04:00
|
|
|
end)
|
|
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
test("a blocked sticky lateral grab does not stop movement", function()
|
2026-08-11 15:07:11 -04:00
|
|
|
local regular = movable(3, 4)
|
2026-08-21 16:34:01 -04:00
|
|
|
local stickyBranch = movable(3, 3, "sticky")
|
2026-08-11 15:07:11 -04:00
|
|
|
local blockingWall = wall(4, 3)
|
|
|
|
|
local possible, group = plan({regular, stickyBranch, blockingWall}, {x = 3, y = 4}, {x = 1, y = 0})
|
|
|
|
|
assertTrue(possible)
|
|
|
|
|
assertIn(group, regular)
|
|
|
|
|
assertNotIn(group, stickyBranch)
|
|
|
|
|
end)
|
|
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
test("a gluey block welds an orthogonal branch", function()
|
2026-08-11 15:07:11 -04:00
|
|
|
local regular = movable(3, 4)
|
2026-08-21 16:34:01 -04:00
|
|
|
local glueyBranch = movable(3, 3, "gluey")
|
|
|
|
|
local possible, group = plan({regular, glueyBranch}, {x = 3, y = 4}, {x = 1, y = 0})
|
2026-08-11 15:07:11 -04:00
|
|
|
assertTrue(possible)
|
|
|
|
|
assertIn(group, regular)
|
2026-08-21 16:34:01 -04:00
|
|
|
assertIn(group, glueyBranch)
|
2026-08-11 15:07:11 -04:00
|
|
|
end)
|
|
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
test("a blocked gluey branch stops its rigid movement group", function()
|
2026-08-11 15:07:11 -04:00
|
|
|
local regular = movable(3, 4)
|
2026-08-21 16:34:01 -04:00
|
|
|
local glueyBranch = movable(3, 3, "gluey")
|
2026-08-11 15:07:11 -04:00
|
|
|
local blockingWall = wall(4, 3)
|
2026-08-21 16:34:01 -04:00
|
|
|
local possible = plan({regular, glueyBranch, blockingWall}, {x = 3, y = 4}, {x = 1, y = 0})
|
2026-08-11 15:07:11 -04:00
|
|
|
assertFalse(possible)
|
|
|
|
|
end)
|
|
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
test("a sticky block pulls both orthogonal side blocks", function()
|
|
|
|
|
local sticky = movable(4, 4, "sticky")
|
2026-08-11 20:08:58 -04:00
|
|
|
local upper = movable(4, 3)
|
|
|
|
|
local lower = movable(4, 5)
|
|
|
|
|
local possible, group = plan(
|
2026-08-21 16:34:01 -04:00
|
|
|
{sticky, upper, lower}, {x = 4, y = 4}, {x = 1, y = 0}
|
2026-08-11 20:08:58 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assertTrue(possible)
|
2026-08-21 16:34:01 -04:00
|
|
|
assertIn(group, sticky)
|
2026-08-11 20:08:58 -04:00
|
|
|
assertIn(group, upper)
|
|
|
|
|
assertIn(group, lower)
|
|
|
|
|
end)
|
|
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
test("a sticky block releases one blocked side while pulling the other", function()
|
|
|
|
|
local sticky = movable(4, 4, "sticky")
|
2026-08-11 20:08:58 -04:00
|
|
|
local blockedUpper = movable(4, 3)
|
|
|
|
|
local freeLower = movable(4, 5)
|
|
|
|
|
local blockingWall = wall(5, 3)
|
|
|
|
|
local possible, group = plan(
|
2026-08-21 16:34:01 -04:00
|
|
|
{sticky, blockedUpper, freeLower, blockingWall},
|
2026-08-11 20:08:58 -04:00
|
|
|
{x = 4, y = 4}, {x = 1, y = 0}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assertTrue(possible)
|
2026-08-21 16:34:01 -04:00
|
|
|
assertIn(group, sticky)
|
2026-08-11 20:08:58 -04:00
|
|
|
assertNotIn(group, blockedUpper)
|
|
|
|
|
assertIn(group, freeLower)
|
|
|
|
|
end)
|
|
|
|
|
|
2026-08-11 15:07:11 -04:00
|
|
|
test("ordinary pushing still propagates through regular blocks", function()
|
|
|
|
|
local first = movable(3, 3)
|
|
|
|
|
local second = movable(4, 3)
|
|
|
|
|
local possible, group = plan({first, second}, {x = 3, y = 3}, {x = 1, y = 0})
|
|
|
|
|
assertTrue(possible)
|
|
|
|
|
assertIn(group, first)
|
|
|
|
|
assertIn(group, second)
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
test("diagonal blocks do not stick", function()
|
|
|
|
|
local regular = movable(3, 3)
|
2026-08-21 16:34:01 -04:00
|
|
|
local diagonalSticky = movable(2, 2, "sticky")
|
2026-08-11 15:07:11 -04:00
|
|
|
local possible, group = plan({regular, diagonalSticky}, {x = 3, y = 3}, {x = 1, y = 0})
|
|
|
|
|
assertTrue(possible)
|
|
|
|
|
assertIn(group, regular)
|
|
|
|
|
assertNotIn(group, diagonalSticky)
|
|
|
|
|
end)
|
|
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
test("multi-cell sticky blocks pull from every occupied cell", function()
|
|
|
|
|
local tallRegular = movable(3, 3, nil, {{x = 0, y = 0}, {x = 0, y = 1}})
|
|
|
|
|
local sticky = movable(2, 4, "sticky")
|
2026-08-11 15:07:11 -04:00
|
|
|
local possible, group = plan({tallRegular, sticky}, {x = 3, y = 3}, {x = 1, y = 0})
|
|
|
|
|
assertTrue(possible)
|
|
|
|
|
assertIn(group, tallRegular)
|
|
|
|
|
assertIn(group, sticky)
|
|
|
|
|
end)
|
|
|
|
|
|
2026-08-11 20:08:58 -04:00
|
|
|
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
|
2026-08-21 16:34:01 -04:00
|
|
|
local stickyL = movable(3, 3, "sticky", lShape)
|
2026-08-11 20:08:58 -04:00
|
|
|
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.
|
2026-08-21 16:34:01 -04:00
|
|
|
local stickyL = movable(3, 3, "sticky", {
|
2026-08-11 20:08:58 -04:00
|
|
|
{x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1},
|
|
|
|
|
})
|
2026-08-21 16:34:01 -04:00
|
|
|
local pinned = movable(2, 2, nil, {{x = 0, y = 0}, {x = 0, y = 1}})
|
2026-08-11 20:08:58 -04:00
|
|
|
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.
|
2026-08-21 16:34:01 -04:00
|
|
|
local stickyT = movable(3, 3, "sticky", {
|
2026-08-11 20:08:58 -04:00
|
|
|
{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)
|
|
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
test("an L-shaped sticky block grabs moveables across several exposed faces", function()
|
|
|
|
|
local stickyL = movable(3, 3, "sticky", {
|
2026-08-11 20:08:58 -04:00
|
|
|
{x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1},
|
2026-08-21 16:34:01 -04:00
|
|
|
})
|
2026-08-11 20:08:58 -04:00
|
|
|
local above = movable(3, 2)
|
|
|
|
|
local below = movable(3, 5)
|
|
|
|
|
local behind = movable(2, 3)
|
|
|
|
|
local possible, group = plan(
|
2026-08-21 16:34:01 -04:00
|
|
|
{stickyL, above, below, behind},
|
2026-08-11 20:08:58 -04:00
|
|
|
{x = 3, y = 3}, {x = 1, y = 0}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assertTrue(possible)
|
2026-08-21 16:34:01 -04:00
|
|
|
assertIn(group, stickyL)
|
2026-08-11 20:08:58 -04:00
|
|
|
assertIn(group, above)
|
|
|
|
|
assertIn(group, below)
|
|
|
|
|
assertIn(group, behind)
|
|
|
|
|
end)
|
|
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
test("production moveables advertise adhesion (regular, sticky, gluey)", function()
|
2026-08-11 15:07:11 -04:00
|
|
|
class = require "libs/middleclass"
|
|
|
|
|
Color = class("TestColor")
|
|
|
|
|
function Color:initialize(name) self.name = name end
|
|
|
|
|
function Color:getname() return self.name end
|
|
|
|
|
|
|
|
|
|
require "generic_moveable"
|
|
|
|
|
local sprite = {getWidth = function() return 16 end, getHeight = function() return 16 end}
|
|
|
|
|
local regular = GenericMoveable:new(2, 2, "red", sprite, "box")
|
2026-08-21 16:34:01 -04:00
|
|
|
local sticky = GenericMoveable:new(3, 2, "red", sprite, "box", nil, "sticky")
|
|
|
|
|
local gluey = GenericMoveable:new(4, 2, "red", sprite, "box", nil, "gluey")
|
2026-08-11 15:07:11 -04:00
|
|
|
|
|
|
|
|
assertTrue(regular:canMove())
|
|
|
|
|
assertFalse(regular:isSticky())
|
2026-08-21 16:34:01 -04:00
|
|
|
assertFalse(regular:isGluey())
|
2026-08-11 15:07:11 -04:00
|
|
|
assertTrue(sticky:canMove())
|
|
|
|
|
assertTrue(sticky:isSticky())
|
2026-08-21 16:34:01 -04:00
|
|
|
assertFalse(sticky:isGluey())
|
|
|
|
|
assertTrue(gluey:canMove())
|
|
|
|
|
assertFalse(gluey:isSticky())
|
|
|
|
|
assertTrue(gluey:isGluey())
|
2026-08-11 15:07:11 -04:00
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
local function loadRoomClass()
|
|
|
|
|
love = {
|
|
|
|
|
audio = {
|
|
|
|
|
newSource = function()
|
|
|
|
|
return {stop = function() end, play = function() end}
|
|
|
|
|
end,
|
|
|
|
|
},
|
|
|
|
|
}
|
2026-08-21 15:56:53 -04:00
|
|
|
-- room pulls in note.lua/mark.lua, which call loadSprite at module-load time.
|
|
|
|
|
-- Provide a default stub so requiring room works; individual tests still
|
|
|
|
|
-- override loadSprite as needed.
|
|
|
|
|
loadSprite = loadSprite or function()
|
|
|
|
|
return {getWidth = function() return 16 end, getHeight = function() return 16 end}
|
|
|
|
|
end
|
2026-08-11 15:07:11 -04:00
|
|
|
require "room"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function roomWith(player, entities, width, height)
|
|
|
|
|
gridWidth, gridHeight = width or 8, height or 8
|
|
|
|
|
local matrix = {}
|
2026-08-11 20:29:35 -04:00
|
|
|
local blockerMatrix = {}
|
2026-08-11 15:07:11 -04:00
|
|
|
for y = 1, gridHeight do
|
|
|
|
|
matrix[y] = {}
|
2026-08-11 20:29:35 -04:00
|
|
|
blockerMatrix[y] = {}
|
2026-08-11 15:07:11 -04:00
|
|
|
for x = 1, gridWidth do matrix[y][x] = false end
|
2026-08-11 20:29:35 -04:00
|
|
|
for x = 1, gridWidth do blockerMatrix[y][x] = false end
|
2026-08-11 15:07:11 -04:00
|
|
|
end
|
|
|
|
|
for _, entity in ipairs(entities) do
|
|
|
|
|
for _, cell in ipairs(entity:getOccupiedCells()) do
|
|
|
|
|
matrix[cell.y][cell.x] = entity
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local room = setmetatable({
|
|
|
|
|
activeColors = {red = true},
|
|
|
|
|
colorsPlayerHas = {"red"},
|
|
|
|
|
player = {red = player},
|
|
|
|
|
collidableMatrices = {red = matrix},
|
2026-08-11 20:29:35 -04:00
|
|
|
playerBlockerMatrices = {red = blockerMatrix},
|
2026-08-11 15:07:11 -04:00
|
|
|
}, {__index = Room})
|
|
|
|
|
room.playerJoinCheck = function() end
|
|
|
|
|
room.tick = function() end
|
|
|
|
|
return room
|
|
|
|
|
end
|
|
|
|
|
|
2026-08-11 20:29:35 -04:00
|
|
|
test("Room keeps player-only blockers out of block collision", function()
|
|
|
|
|
loadRoomClass()
|
|
|
|
|
local oldAssets, oldLoadSprite = globalAssetProperties, loadSprite
|
|
|
|
|
local sprite = {getWidth = function() return 16 end, getHeight = function() return 16 end}
|
|
|
|
|
globalAssetProperties = {
|
|
|
|
|
player_blocker = {
|
|
|
|
|
class = "player_blocker",
|
|
|
|
|
sprites = {red = "player_blocker_r.png"},
|
|
|
|
|
cells = {red = {{x = 1, y = 1}}},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
loadSprite = function() return sprite end
|
|
|
|
|
local room = setmetatable({
|
|
|
|
|
collideables = {red = {}, green = {}, blue = {}},
|
|
|
|
|
playerBlockers = {red = {}, green = {}, blue = {}},
|
|
|
|
|
}, {__index = Room})
|
|
|
|
|
|
|
|
|
|
local blocker = room:createEntity("player_blocker", "red", 4, 3)
|
|
|
|
|
|
|
|
|
|
assertTrue(blocker:blocksPlayer())
|
|
|
|
|
assertTrue(#room.playerBlockers.red == 1)
|
|
|
|
|
assertTrue(#room.collideables.red == 0, "blockers must not enter block collision")
|
|
|
|
|
globalAssetProperties, loadSprite = oldAssets, oldLoadSprite
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
test("Room player-only blockers stop the player but do not stop a box", function()
|
|
|
|
|
loadRoomClass()
|
|
|
|
|
local player = movable(2, 3)
|
|
|
|
|
local box = movable(3, 3)
|
|
|
|
|
local blocker = movable(4, 3)
|
|
|
|
|
function blocker:blocksPlayer() return true end
|
|
|
|
|
local room = roomWith(player, {box}, 8, 8)
|
|
|
|
|
local blockerMatrix = {}
|
|
|
|
|
for y = 1, gridHeight do
|
|
|
|
|
blockerMatrix[y] = {}
|
|
|
|
|
for x = 1, gridWidth do blockerMatrix[y][x] = false end
|
|
|
|
|
end
|
|
|
|
|
blockerMatrix[3][4] = blocker
|
|
|
|
|
room.playerBlockerMatrices.red = blockerMatrix
|
|
|
|
|
|
|
|
|
|
room:movePlayer("right")
|
|
|
|
|
|
|
|
|
|
assertTrue(box.x == 4, "box should move onto the player-only blocker")
|
|
|
|
|
assertTrue(player.x == 3, "player should move while pushing the box through it")
|
|
|
|
|
room:movePlayer("right")
|
|
|
|
|
assertTrue(player.x == 3, "player should be stopped by the player-only blocker")
|
|
|
|
|
end)
|
|
|
|
|
|
2026-08-11 15:07:11 -04:00
|
|
|
test("Room applies the sticky-regular-player pull", function()
|
|
|
|
|
loadRoomClass()
|
2026-08-21 16:34:01 -04:00
|
|
|
local sticky = movable(2, 3, "sticky")
|
2026-08-11 15:07:11 -04:00
|
|
|
local regular = movable(3, 3)
|
|
|
|
|
local player = movable(4, 3)
|
|
|
|
|
local room = roomWith(player, {sticky, regular})
|
|
|
|
|
|
|
|
|
|
room:movePlayer("right")
|
|
|
|
|
|
|
|
|
|
assertTrue(sticky.x == 3, "sticky block should move one cell right")
|
|
|
|
|
assertTrue(regular.x == 4, "regular block should move one cell right")
|
|
|
|
|
assertTrue(player.x == 5, "player should move one cell right")
|
|
|
|
|
end)
|
|
|
|
|
|
2026-08-11 20:08:58 -04:00
|
|
|
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)
|
2026-08-21 16:34:01 -04:00
|
|
|
local sticky = movable(1, 3, "sticky")
|
|
|
|
|
local concave = movable(1, 3, nil, {
|
2026-08-11 20:08:58 -04:00
|
|
|
{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)
|
2026-08-21 16:34:01 -04:00
|
|
|
local stickyL = movable(3, 3, "sticky", {
|
2026-08-11 20:08:58 -04:00
|
|
|
{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)
|
|
|
|
|
|
2026-08-11 15:07:11 -04:00
|
|
|
test("Room never moves a component found by push and pull twice", function()
|
|
|
|
|
loadRoomClass()
|
|
|
|
|
local player = movable(4, 4)
|
|
|
|
|
local front = movable(5, 4)
|
2026-08-21 16:34:01 -04:00
|
|
|
local bridgeRight = movable(5, 3, "gluey")
|
|
|
|
|
local bridgeMiddle = movable(4, 3, "gluey")
|
|
|
|
|
local bridgeLeft = movable(3, 3, "gluey")
|
2026-08-11 15:07:11 -04:00
|
|
|
local rear = movable(3, 4)
|
|
|
|
|
local component = {front, bridgeRight, bridgeMiddle, bridgeLeft, rear}
|
|
|
|
|
local room = roomWith(player, component)
|
|
|
|
|
|
|
|
|
|
room:movePlayer("right")
|
|
|
|
|
|
|
|
|
|
for _, entity in ipairs(component) do
|
|
|
|
|
assertTrue(entity.x <= 6, "an entity in the shared component moved more than once")
|
|
|
|
|
end
|
|
|
|
|
assertTrue(front.x == 6)
|
|
|
|
|
assertTrue(bridgeRight.x == 6)
|
|
|
|
|
assertTrue(bridgeMiddle.x == 5)
|
|
|
|
|
assertTrue(bridgeLeft.x == 4)
|
|
|
|
|
assertTrue(rear.x == 4)
|
|
|
|
|
assertTrue(player.x == 5)
|
|
|
|
|
end)
|
|
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
test("Room pushes sticky onward while a blocked side shears off", function()
|
2026-08-11 20:08:58 -04:00
|
|
|
loadRoomClass()
|
|
|
|
|
local player = movable(3, 4)
|
2026-08-21 16:34:01 -04:00
|
|
|
local sticky = movable(4, 4, "sticky")
|
2026-08-11 20:08:58 -04:00
|
|
|
local blockedUpper = movable(4, 3)
|
|
|
|
|
local freeLower = movable(4, 5)
|
|
|
|
|
local blockingWall = wall(5, 3)
|
2026-08-21 16:34:01 -04:00
|
|
|
local room = roomWith(player, {sticky, blockedUpper, freeLower, blockingWall})
|
2026-08-11 20:08:58 -04:00
|
|
|
|
|
|
|
|
room:movePlayer("right")
|
|
|
|
|
|
|
|
|
|
assertTrue(player.x == 4 and player.y == 4)
|
2026-08-21 16:34:01 -04:00
|
|
|
assertTrue(sticky.x == 5 and sticky.y == 4)
|
2026-08-11 20:08:58 -04:00
|
|
|
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)
|
|
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
test("legacy box-variant classes load as adhesion on a plain moveable", function()
|
|
|
|
|
loadRoomClass()
|
|
|
|
|
local oldAssets, oldLoadSprite = globalAssetProperties, loadSprite
|
|
|
|
|
local sprite = {getWidth = function() return 16 end, getHeight = function() return 16 end}
|
|
|
|
|
globalAssetProperties = {
|
|
|
|
|
box = { class = "moveable", sprites = {red = "box_r.png"}, cells = {red = {{x = 1, y = 1}}} },
|
2026-08-11 15:07:11 -04:00
|
|
|
}
|
2026-08-21 16:34:01 -04:00
|
|
|
loadSprite = function() return sprite end
|
|
|
|
|
local room = setmetatable({
|
|
|
|
|
collideables = {red = {}, green = {}, blue = {}},
|
|
|
|
|
playerBlockers = {red = {}, green = {}, blue = {}},
|
|
|
|
|
}, {__index = Room})
|
2026-08-11 15:07:11 -04:00
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
-- Old .sav files baked adhesion into the class name; loading must collapse them
|
|
|
|
|
-- onto the base box carrying the new adhesion property.
|
|
|
|
|
local fromSpiky = room:createEntity("spiky_box", "red", 2, 2)
|
|
|
|
|
local fromSkewer = room:createEntity("skewer_box", "red", 3, 2)
|
|
|
|
|
local fromSticky = room:createEntity("sticky_box", "red", 4, 2)
|
|
|
|
|
|
|
|
|
|
assertTrue(fromSpiky:isSticky(), "legacy spiky_ should load as sticky")
|
|
|
|
|
assertTrue(fromSkewer:isGluey(), "legacy skewer_ should load as gluey")
|
|
|
|
|
assertTrue(fromSticky:isSticky(), "legacy sticky_ should load as sticky")
|
|
|
|
|
assertTrue(fromSpiky.saveClass == "box", "class should normalize to the base box")
|
|
|
|
|
|
|
|
|
|
-- An explicit adhesion in the object data wins and rides through on a plain box.
|
|
|
|
|
local placed = room:createEntity("box", "red", 5, 2, { adhesion = "gluey" })
|
|
|
|
|
assertTrue(placed:isGluey())
|
|
|
|
|
assertTrue(placed.saveClass == "box")
|
|
|
|
|
globalAssetProperties, loadSprite = oldAssets, oldLoadSprite
|
2026-08-11 15:07:11 -04:00
|
|
|
end)
|
|
|
|
|
|
2026-08-21 16:34:01 -04:00
|
|
|
print("movement tests passed: " .. passed)
|