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 local function movable(x, y, sticky, shape, skewer) local entity = { x = x, y = y, sticky = sticky or false, skewer = skewer 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: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 local sticky = movable(2, 3, true) 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) test("a sticky block touching the player pulls a regular block behind it", function() -- regular, sticky, player; moving right local regular = movable(2, 3) local sticky = movable(3, 3, true) 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 local sticky = movable(4, 2, true) 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) local sticky = movable(player.x - direction.x * 2, player.y - direction.y * 2, true) local possible, group = plan( {sticky, regular}, {x = regular.x, y = regular.y}, direction ) assertTrue(possible) assertIn(group, regular) assertIn(group, sticky) end end) test("a sticky block shears off during orthogonal movement", function() -- The regular block moves right; the sticky block above it stays behind. local regular = movable(3, 4) local stickyBranch = movable(3, 3, true) local possible, group = plan({regular, stickyBranch}, {x = 3, y = 4}, {x = 1, y = 0}) assertTrue(possible) assertIn(group, regular) assertNotIn(group, stickyBranch) end) test("a blocked sheared sticky block does not stop movement", function() local regular = movable(3, 4) local stickyBranch = movable(3, 3, true) 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) test("a skewer carries an orthogonal branch", function() local regular = movable(3, 4) local skewerBranch = movable(3, 3, false, nil, true) local possible, group = plan({regular, skewerBranch}, {x = 3, y = 4}, {x = 1, y = 0}) assertTrue(possible) assertIn(group, regular) assertIn(group, skewerBranch) end) test("a blocked skewer branch stops its rigid movement group", function() local regular = movable(3, 4) local skewerBranch = movable(3, 3, false, nil, true) local blockingWall = wall(4, 3) local possible = plan({regular, skewerBranch, blockingWall}, {x = 3, y = 4}, {x = 1, y = 0}) assertFalse(possible) end) 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) local diagonalSticky = movable(2, 2, true) local possible, group = plan({regular, diagonalSticky}, {x = 3, y = 3}, {x = 1, y = 0}) assertTrue(possible) assertIn(group, regular) assertNotIn(group, diagonalSticky) end) test("multi-cell sticky blocks pull from every trailing occupied cell", function() local tallRegular = movable(3, 3, false, {{x = 0, y = 0}, {x = 0, y = 1}}) local sticky = movable(2, 4, true) local possible, group = plan({tallRegular, sticky}, {x = 3, y = 3}, {x = 1, y = 0}) assertTrue(possible) assertIn(group, tallRegular) assertIn(group, sticky) end) test("production sticky and skewer blocks advertise distinct behavior", function() 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" require "generic_sticky" require "generic_skewer" 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") assertTrue(regular:canMove()) assertFalse(regular:isSticky()) assertTrue(sticky:canMove()) assertTrue(sticky:isSticky()) assertFalse(sticky:isSkewer()) assertTrue(skewer:canMove()) assertFalse(skewer:isSticky()) assertTrue(skewer:isSkewer()) end) local function loadRoomClass() love = { audio = { newSource = function() return {stop = function() end, play = function() end} end, }, } require "room" end local function roomWith(player, entities, width, height) gridWidth, gridHeight = width or 8, height or 8 local matrix = {} for y = 1, gridHeight do matrix[y] = {} for x = 1, gridWidth do matrix[y][x] = false end 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}, }, {__index = Room}) room.playerJoinCheck = function() end room.tick = function() end return room end test("Room applies the sticky-regular-player pull", function() loadRoomClass() local sticky = movable(2, 3, true) 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) test("Room shears a lateral sticky contact", function() loadRoomClass() local sticky = movable(3, 2, true) local regular = movable(3, 3) local player = movable(4, 3) local room = roomWith(player, {sticky, regular}) room:movePlayer("right") assertTrue(sticky.x == 3, "lateral sticky block should shear and stay behind") assertTrue(regular.x == 4) assertTrue(player.x == 5) end) test("Room never moves a component found by push and pull twice", function() loadRoomClass() local player = movable(4, 4) local front = movable(5, 4) local bridgeRight = movable(5, 3, false, nil, true) local bridgeMiddle = movable(4, 3, false, nil, true) local bridgeLeft = movable(3, 3, false, nil, true) 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) test("every moveable base box gets a synthetic sticky variant", 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 assets = { box = {class = "moveable", sprites = boxSprites, cells = boxCells}, ["box_2x1_11"] = {class = "moveable", sprites = shapeSprites, cells = shapeCells}, box_statue = {class = "immoveable", sprites = {}}, beer_mug = {class = "moveable", sprites = {}}, } BoxVariants.addTo(assets) assertTrue(assets.sticky_box.class == "sticky") assertTrue(assets.sticky_box.directory == "sticky") assertTrue(assets.sticky_box.synthetic) assertTrue(assets.sticky_box.goopy) assertTrue(assets.sticky_box.sprites == boxSprites, "variant should share base sprites") 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.skewer_box.class == "skewer") assertTrue(assets.skewer_box.directory == "skewer") assertTrue(assets.skewer_box.synthetic) assertTrue(assets.skewer_box.skewer) assertTrue(assets.skewer_box.sprites == boxSprites) assertTrue(assets.skewer_box.cells == boxCells) assertTrue(assets["skewer_box_2x1_11"].sprites == shapeSprites) assertFalse(assets.sticky_box_statue) assertFalse(assets.sticky_beer_mug) assertFalse(assets.skewer_box_statue) assertFalse(assets.skewer_beer_mug) end) print("movement/block-variant tests passed: " .. passed)