diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..2ca7a99 --- /dev/null +++ b/conftest.py @@ -0,0 +1,4 @@ +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) diff --git a/tests/support.py b/tests/support.py new file mode 100644 index 0000000..719ad92 --- /dev/null +++ b/tests/support.py @@ -0,0 +1,28 @@ +from game.sim.level import parseAscii +from game.sim.world import World +from game.sim.inputCommand import InputCommand + + +def makeLevel(lines, name="test"): + return parseAscii(name, lines) + + +def makeWorld(lines, name="test"): + return World(parseAscii(name, lines)) + + +def command(**kwargs): + return InputCommand(**kwargs) + + +def stepWorld(world, times=1, **kwargs): + cmd = InputCommand(**kwargs) + for _ in range(times): + world.step(cmd) + return world + + +def settle(world, times=30): + for _ in range(times): + world.step(InputCommand()) + return world diff --git a/tests/test_geometry.py b/tests/test_geometry.py new file mode 100644 index 0000000..67bb729 --- /dev/null +++ b/tests/test_geometry.py @@ -0,0 +1,71 @@ +import pytest + +from game.sim.geometry import Vec2, Aabb, clamp, sign, approach + + +def test_aabbIntersectsOverlapping(): + a = Aabb(0, 0, 10, 10) + b = Aabb(5, 5, 10, 10) + assert a.intersects(b) + assert b.intersects(a) + + +def test_aabbDoesNotIntersectSeparated(): + a = Aabb(0, 0, 10, 10) + b = Aabb(20, 0, 10, 10) + assert not a.intersects(b) + + +def test_aabbTouchingEdgeIsNotOverlap(): + a = Aabb(0, 0, 10, 10) + b = Aabb(10, 0, 10, 10) + assert not a.intersects(b) + assert a.touches(b) + + +def test_aabbOverlapArea(): + a = Aabb(0, 0, 10, 10) + b = Aabb(5, 5, 10, 10) + assert a.overlapArea(b) == pytest.approx(25.0) + assert a.overlapArea(Aabb(100, 100, 5, 5)) == 0.0 + + +def test_aabbContainsAndEdges(): + box = Aabb(4, 6, 8, 10) + assert box.left == 4 and box.right == 12 + assert box.top == 6 and box.bottom == 16 + assert box.centerX == pytest.approx(8.0) + assert box.containsPoint(8, 10) + assert not box.containsPoint(100, 10) + + +def test_aabbInflated(): + box = Aabb(10, 10, 4, 4) + grown = box.inflated(2, 3) + assert grown.left == 8 and grown.right == 16 + assert grown.top == 7 and grown.bottom == 17 + + +def test_vecArithmetic(): + a = Vec2(3, 4) + assert a.length() == pytest.approx(5.0) + assert a.add(Vec2(1, 1)).asTuple() == (4.0, 5.0) + assert a.sub(Vec2(3, 4)).asTuple() == (0.0, 0.0) + assert a.scale(2).asTuple() == (6.0, 8.0) + normalized = a.normalized() + assert normalized.length() == pytest.approx(1.0) + + +def test_vecDistanceAndZeroNormalize(): + assert Vec2(0, 0).distanceTo(Vec2(3, 4)) == pytest.approx(5.0) + assert Vec2(0, 0).normalized().asTuple() == (0.0, 0.0) + + +def test_helpers(): + assert clamp(5, 0, 3) == 3 + assert clamp(-1, 0, 3) == 0 + assert clamp(2, 0, 3) == 2 + assert sign(-4) == -1 and sign(4) == 1 and sign(0) == 0 + assert approach(0, 10, 3) == 3 + assert approach(10, 0, 3) == 7 + assert approach(1, 2, 5) == 2 diff --git a/tests/test_slopes.py b/tests/test_slopes.py new file mode 100644 index 0000000..24bb2db --- /dev/null +++ b/tests/test_slopes.py @@ -0,0 +1,58 @@ +import pytest + +from game.sim import physics +from game.sim.entity import Actor + + +class FakeSlopeTerrain: + def __init__(self, slopes): + self.slopes = slopes + + def slopeAt(self, col, row): + return self.slopes.get((col, row)) + + +def test_slopeSurfaceAscendRight(): + assert physics.slopeSurfaceY(0, 0, 0, "ascendRight") == pytest.approx(16.0) + assert physics.slopeSurfaceY(0, 0, 16, "ascendRight") == pytest.approx(0.0) + assert physics.slopeSurfaceY(0, 0, 8, "ascendRight") == pytest.approx(8.0) + + +def test_slopeSurfaceAscendLeft(): + assert physics.slopeSurfaceY(0, 0, 0, "ascendLeft") == pytest.approx(0.0) + assert physics.slopeSurfaceY(0, 0, 16, "ascendLeft") == pytest.approx(16.0) + + +def test_resolveSlopeSnapsPenetratingActor(): + terrain = FakeSlopeTerrain({(0, 0): "ascendRight"}) + actor = Actor(2, -6, 12, 16) + actor.vy = 1.0 + snapped = physics.resolveSlope(actor, terrain, wasOnGround=False) + assert snapped + assert actor.onGround + assert actor.bottom == pytest.approx(physics.slopeSurfaceY(0, 0, actor.centerX, "ascendRight")) + assert actor.vy == 0.0 + + +def test_walkingUpSlopeGainsHeight(): + terrain = FakeSlopeTerrain({(0, 0): "ascendRight"}) + actor = Actor(0, 0, 12, 16) + actor.vy = 1.0 + physics.resolveSlope(actor, terrain, wasOnGround=False) + lowBottom = actor.bottom + actor.x = 6 + actor.vy = 1.0 + physics.resolveSlope(actor, terrain, wasOnGround=True) + assert actor.bottom < lowBottom + + +def test_resolveSlopeClingsWhenDescending(): + terrain = FakeSlopeTerrain({(0, 0): "ascendLeft"}) + actor = Actor(2, 0, 12, 16) + surface = physics.slopeSurfaceY(0, 0, actor.centerX, "ascendLeft") + actor.y = surface - actor.h - 3.0 + actor.vy = 0.5 + snapped = physics.resolveSlope(actor, terrain, wasOnGround=True) + assert snapped + assert actor.onGround + assert actor.bottom == pytest.approx(surface)