def test_attack_action(self): character = Character(state=Undead()) target = default_human() next_action = character.next_action( FakeViewpoint([(Vector(1, 1), target)]), BoundingBox.range(5), FakeActions()) assert next_action == Attack(Vector(1, 1))
def test_move_action(self): character = Character(state=Undead()) environment = FakeViewpoint([(Vector(3, 3), default_human())]) next_action: Action = character.next_action(environment, BoundingBox.range(5), FakeActions()) assert next_action == Move(Vector(1, 1))
def test_chooses_shortest_best_move(self): target_vectors = TargetVectors( FakeViewpoint([(Vector(1, 2), default_zombie())])) moves = [Vector.ZERO, Vector(1, 0), Vector(2, 0)] assert Living().best_move(target_vectors, moves) == Vector.ZERO # To make sure we're not lucking out based on the order assert Living().best_move(target_vectors, list(reversed(moves))) == Vector.ZERO
class TestVector: def test_no_arg_constructor(self): with pytest.raises(TypeError) as exc: Vector() # type: ignore def test_single_arg_constructor(self): with pytest.raises(TypeError): Vector(3) # type: ignore @given(st.integers(), st.integers()) def test_accepts_two_coordinates(self, dx, dy): vector = Vector(dx, dy) assert vector.dx == dx assert vector.dy == dy def test_zero_distance(self): assert Vector.ZERO.distance == 0 @given(vectors()) def test_vector_distance(self, vector): assert vector.distance >= 0 @given(vectors()) @example(Vector.ZERO) def test_truthiness(self, vector): assert bool(vector) == (vector.distance > 0) @pytest.mark.parametrize("dx,dy", [(2, 1), (-2, 1), (2, -1), (-2, -1)]) def test_non_zero_distance(self, dx, dy): assert Vector(dx, dy).distance == math.sqrt(5) @given(vectors(), vectors()) @example(Vector(1, 1), Vector(3, 3)) def test_triangle_inequality(self, a, b): sum_distance = (a + b).distance distance_sum = a.distance + b.distance assert sum_distance < distance_sum or sum_distance == approx( distance_sum) def test_value_equality(self): assert Vector(2, 5) == Vector(2, 5) def test_value_inequality(self): assert Vector(2, 5) != Vector(2, 3) @given(vectors(), vectors()) def test_addition(self, vector_a, vector_b): vector_sum = vector_a + vector_b assert vector_sum.dx == vector_a.dx + vector_b.dx assert vector_sum.dy == vector_a.dy + vector_b.dy @given(vectors(), vectors()) def test_addition_then_subtraction(self, vector_a, vector_b): assert vector_a + vector_b - vector_b == vector_a assert vector_a + vector_b - vector_a == vector_b
class TestLivingState: def test_life_state(self): assert Living().life_state == LifeState.LIVING @given(moves=st.lists(vectors(), min_size=1)) def test_movement_without_zombies(self, moves): target_vectors = TargetVectors(FakeViewpoint([])) assert Living().best_move(target_vectors, moves) == min(moves, key=lambda v: v.distance) @given( zombie_vectors=st.lists(vectors(), min_size=1), moves=st.lists(vectors(), min_size=1), ) @example( zombie_vectors=[Vector(dx=0, dy=130), Vector(dx=0, dy=-128)], moves=[Vector(dx=0, dy=0), Vector(dx=0, dy=1), Vector(dx=0, dy=2)], ) def test_movement_with_zombies(self, zombie_vectors, moves): target_vectors = TargetVectors( FakeViewpoint([(v, default_zombie()) for v in zombie_vectors])) best_move = Living().best_move(target_vectors, moves) def distance_after_move(move): return min((zombie - move).distance for zombie in zombie_vectors) best_move_distance = distance_after_move(best_move) note(f"Best distance, after {best_move}: {best_move_distance}") for move in moves: move_distance = distance_after_move(move) note(f"Distance after {move}: {move_distance}") assert all(best_move_distance >= distance_after_move(move) for move in moves) def test_chooses_shortest_best_move(self): target_vectors = TargetVectors( FakeViewpoint([(Vector(1, 2), default_zombie())])) moves = [Vector.ZERO, Vector(1, 0), Vector(2, 0)] assert Living().best_move(target_vectors, moves) == Vector.ZERO # To make sure we're not lucking out based on the order assert Living().best_move(target_vectors, list(reversed(moves))) == Vector.ZERO @given(environments()) def test_never_attacks(self, environment): assert Living().attack(environment) is None def test_no_next_state(self): assert Living().next_state is None
def test_viewpoint_multiple_characters(self, char1, char2): roster = Roster.for_mapping( { Point(1, 1): char1, Point(2, 0): char2 }, area=Area(Point(0, 0), Point(3, 3)), ) viewpoint = Viewpoint(Point(0, 1), roster) assert viewpoint.occupied_points_in( BoundingBox.range(1)) == {Vector(1, 0)} assert viewpoint.occupied_points_in(BoundingBox.range(5)) == { Vector(1, 0), Vector(2, -1), }
def test_all_paths_blocked(self, zombie): """Test that zombies stay still when surrounded by other zombies. This effectively functions as a last check that zombies always have their own position as a fall-back, and don't register as blocking their own non-movement. """ def env_contents(vector): return default_zombie() if vector else zombie vectors = [Vector(dx, dy) for dx in [-1, 0, 1] for dy in [-1, 0, 1]] distant_human = [(Vector(2, 2), default_human())] zombies_all_around = [(v, env_contents(v)) for v in vectors] viewpoint = FakeViewpoint(distant_human + zombies_all_around) limits = BoundingBox(Vector(-100, -100), Vector(100, 100)) assert zombie.move(viewpoint, limits) == Vector.ZERO
def test_alternate_path(self, zombie): environment = [ (Vector(2, 2), default_human()), (Vector(1, 1), default_zombie()), (Vector(1, 0), default_zombie()), ] limits = BoundingBox(Vector(-100, -100), Vector(100, 100)) assert zombie.move(FakeViewpoint(environment), limits) == Vector(0, 1)
def test_nearest_human(self, zombie): environment = [ (Vector(3, -3), default_human()), (Vector(2, 2), default_human()), (Vector(-3, 3), default_human()), ] limits = BoundingBox(Vector(-100, -100), Vector(100, 100)) assert zombie.move(FakeViewpoint(environment), limits) == Vector(1, 1)
def test_partial_barrier(self): # +---+---+---+ # | m | m | | # +---+---+---+ # | m | x | | # +---+---+---+ # | 0 | x | | # +---+---+---+ character_range = BoundingBox(Vector(0, 0), Vector(3, 3)) obstacles = {Vector(1, 1), Vector(1, 0)} available = available_moves(character_range, obstacles) assert available == { Vector(0, 0), Vector(0, 1), Vector(0, 2), Vector(1, 2) }
def test_vector_containment(self, vector): box = BoundingBox(Vector.ZERO, Vector(1, 1)) assert (vector in box) == (vector == vector.ZERO)
def test_empty_box(self): box = BoundingBox(Vector.ZERO, Vector.ZERO) assert Vector(1, 1) not in box
def test_negative_box(self): box = BoundingBox(Vector.ZERO, Vector(-1, -1)) assert Vector.ZERO not in box
class TestBoundingBox: def test_takes_two_vectors(self): BoundingBox(Vector.ZERO, Vector(1, 1)) def test_empty_box(self): box = BoundingBox(Vector.ZERO, Vector.ZERO) assert Vector(1, 1) not in box def test_negative_box(self): box = BoundingBox(Vector.ZERO, Vector(-1, -1)) assert Vector.ZERO not in box @given(vectors()) def test_vector_containment(self, vector): box = BoundingBox(Vector.ZERO, Vector(1, 1)) assert (vector in box) == (vector == vector.ZERO) @given( st.builds(BoundingBox, vectors(bound=ITERATION_BOUND), vectors(bound=ITERATION_BOUND))) def test_iteration_covers_box(self, box): box_vectors = list(box) for vector in box: assert vector in box_vectors @given( box=st.builds(BoundingBox, vectors(bound=ITERATION_BOUND), vectors(bound=ITERATION_BOUND)), vector=vectors(), ) @example(BoundingBox(Vector(-2, -2), Vector(3, 3)), Vector(2, 3)) def test_iteration_is_limited_to_box(self, box, vector): assume(vector not in box) assert vector not in list(box) @given( boxes=st.lists(st.builds(BoundingBox, vectors(), vectors()), min_size=2), vector=vectors(), ) def test_vector_outside_intersection(self, boxes, vector): assume(any(vector not in box for box in boxes)) intersection = reduce(lambda a, b: a.intersect(b), boxes) assert vector not in intersection @given(vectors_and_containing_boxes()) def test_vector_inside_intersection(self, vector_and_boxes): vector, boxes = vector_and_boxes intersection = reduce(lambda a, b: a.intersect(b), boxes) assert vector in intersection @given(st.integers(min_value=0), vectors()) @example(radius=10, vector=Vector(10, 10)) @example(radius=0, vector=Vector(0, 0)) def test_range(self, radius, vector): bounding_box = BoundingBox.range(radius) if abs(vector.dx) <= radius and abs(vector.dy) <= radius: assert vector in bounding_box else: assert vector not in bounding_box def test_invalid_range(self): with pytest.raises(ValueError): BoundingBox.range(-1)
def test_takes_two_vectors(self): BoundingBox(Vector.ZERO, Vector(1, 1))
def test_value_equality(self): assert Vector(2, 5) == Vector(2, 5)
def test_value_inequality(self): assert Vector(2, 5) != Vector(2, 3)
def test_accepts_two_coordinates(self, dx, dy): vector = Vector(dx, dy) assert vector.dx == dx assert vector.dy == dy
def test_non_zero_distance(self, dx, dy): assert Vector(dx, dy).distance == math.sqrt(5)
def test_no_arg_constructor(self): with pytest.raises(TypeError) as exc: Vector() # type: ignore
def test_single_arg_constructor(self): with pytest.raises(TypeError): Vector(3) # type: ignore
class TestZombie: @pytest.fixture(scope="session") def zombie(self): return default_zombie() @given(environments_and_limits()) def test_move_returns_a_vector(self, zombie, environment_and_limits): environment, limits = environment_and_limits assert isinstance(zombie.move(FakeViewpoint(environment), limits), Vector) @given(environments(characters=humans, min_size=1, max_size=1), containing_boxes) def test_never_moves_away_from_human(self, zombie, environment, limits): viewpoint = FakeViewpoint(environment) move = zombie.move(viewpoint, limits) assert (environment[0][0] - move).distance <= environment[0][0].distance @given(environments_and_limits(characters=humans, min_chars=1, max_chars=1)) def test_move_approaches_single_human(self, zombie, environment_and_limits): environment, limits = environment_and_limits assume(environment[0][0].distance > 1) move = zombie.move(FakeViewpoint(environment), limits) assert (environment[0][0] - move).distance < environment[0][0].distance @given(environments_and_limits()) def test_does_not_move_onto_occupied_space(self, zombie, environment_and_limits): environment, limits = environment_and_limits move = zombie.move(FakeViewpoint(environment), limits) assert move not in [e[0] for e in environment] @given(environments_and_limits()) def test_moves_up_to_one_space(self, zombie, environment_and_limits): environment, limits = environment_and_limits move = zombie.move(FakeViewpoint(environment), limits) assert abs(move.dx) <= 1 assert abs(move.dy) <= 1 @given(environments_and_limits(characters=zombies)) def test_ignores_zombies(self, zombie, environment_and_limits): environment, limits = environment_and_limits assert zombie.move(FakeViewpoint(environment), limits) == Vector.ZERO @given(environments_and_limits()) def test_respects_limits(self, zombie, environment_and_limits): environment, limits = environment_and_limits move = zombie.move(FakeViewpoint(environment), limits) assert move in limits @given(containing_boxes) def test_nothing_nearby(self, zombie, limits): assert zombie.move(FakeViewpoint([]), limits) == Vector.ZERO def test_nearest_human(self, zombie): environment = [ (Vector(3, -3), default_human()), (Vector(2, 2), default_human()), (Vector(-3, 3), default_human()), ] limits = BoundingBox(Vector(-100, -100), Vector(100, 100)) assert zombie.move(FakeViewpoint(environment), limits) == Vector(1, 1) def test_blocked_path(self, zombie): environment = [ (Vector(2, 2), default_human()), (Vector(1, 1), default_zombie()), (Vector(1, 0), default_zombie()), (Vector(0, 1), default_zombie()), ] limits = BoundingBox(Vector(-100, -100), Vector(100, 100)) assert zombie.move(FakeViewpoint(environment), limits) == Vector.ZERO def test_all_paths_blocked(self, zombie): """Test that zombies stay still when surrounded by other zombies. This effectively functions as a last check that zombies always have their own position as a fall-back, and don't register as blocking their own non-movement. """ def env_contents(vector): return default_zombie() if vector else zombie vectors = [Vector(dx, dy) for dx in [-1, 0, 1] for dy in [-1, 0, 1]] distant_human = [(Vector(2, 2), default_human())] zombies_all_around = [(v, env_contents(v)) for v in vectors] viewpoint = FakeViewpoint(distant_human + zombies_all_around) limits = BoundingBox(Vector(-100, -100), Vector(100, 100)) assert zombie.move(viewpoint, limits) == Vector.ZERO def test_alternate_path(self, zombie): environment = [ (Vector(2, 2), default_human()), (Vector(1, 1), default_zombie()), (Vector(1, 0), default_zombie()), ] limits = BoundingBox(Vector(-100, -100), Vector(100, 100)) assert zombie.move(FakeViewpoint(environment), limits) == Vector(0, 1) @given( st.lists(st.tuples(vectors(max_offset=1), humans), min_size=1, max_size=1)) def test_attack(self, zombie, environment): vector = environment[0][0] assert zombie.attack(FakeViewpoint(environment)) == vector @given(environments(characters=humans)) @example([(Vector(2, 0), default_human())]) def test_targets_out_of_range(self, zombie, environment): biting_range = BoundingBox(Vector(-1, -1), Vector(2, 2)) assume(all(e[0] not in biting_range for e in environment)) assert zombie.attack(FakeViewpoint(environment)) is None
def test_targets_out_of_range(self, zombie, environment): biting_range = BoundingBox(Vector(-1, -1), Vector(2, 2)) assume(all(e[0] not in biting_range for e in environment)) assert zombie.attack(FakeViewpoint(environment)) is None
def test_total_barrier(self): obstacles = {Vector(1, y) for y in range(-2, 3)} available = available_moves(BoundingBox.range(2), obstacles) assert available == set(BoundingBox(Vector(-2, -2), Vector(1, 3)))
def test_does_not_obstruct_self(self, human): environment = FakeViewpoint([(Vector.ZERO, human)]) limits = BoundingBox(Vector(-100, -100), Vector(100, 100)) assert human.move(environment, limits) == Vector.ZERO
def test_does_not_move_in_empty_environment(self, human): limits = BoundingBox(Vector(-100, -100), Vector(100, 100)) assert human.move(FakeViewpoint([]), limits) == Vector.ZERO