class Undead: life_state = LifeState.UNDEAD movement_range = BoundingBox.range(1) attack_range = BoundingBox.range(1) next_state = None def attack(self, target_vectors: SupportsNearestHuman) -> Optional[Vector]: nearest_human = target_vectors.nearest_human if nearest_human is not None and nearest_human in self.attack_range: return nearest_human else: return None def best_move( self, target_vectors: SupportsNearestHuman, available_moves: Iterable[Vector] ) -> Vector: nearest_human = target_vectors.nearest_human if nearest_human: def move_rank(move: Vector) -> Tuple[float, float]: assert nearest_human is not None return ((nearest_human - move).distance, move.distance) return min(available_moves, key=move_rank) else: return shortest(available_moves) def __eq__(self, other: object) -> bool: return isinstance(other, Undead)
def test_runs_away_from_zombie(self, human, environment_and_limits): environment, limits = environment_and_limits # Make sure there's actually room to run away assume(limits.intersect(BoundingBox.range(1)) == BoundingBox.range(1)) move = human.move(FakeViewpoint(environment), limits) zombie_vector = environment[0][0] assert (zombie_vector - move).distance > zombie_vector.distance
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_state_change_action(self): character = Character(state=Dead(age=20)) viewpoint = FakeViewpoint([]) next_action = character.next_action(viewpoint, BoundingBox.range(5), FakeActions()) assert next_action == StateChange(Undead())
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))
class Dead: def __init__(self, age: int = 0): self._age = age life_state = LifeState.DEAD movement_range = BoundingBox.range(0) _resurrection_age: ClassVar[int] = 20 def attack(self, target_vectors: TargetVectors) -> Optional[Vector]: return None def best_move( self, target_vectors: TargetVectors, available_moves: Iterable[Vector] ) -> Vector: if Vector.ZERO not in available_moves: raise ValueError("Zero move unavailable for dead character") return Vector.ZERO @property def next_state(self) -> State: if self._age >= self._resurrection_age: return Undead() else: return Dead(self._age + 1) def __eq__(self, other: object) -> bool: return isinstance(other, Dead) and self._age == other._age
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 occupied_points_in(self, box: BoundingBox) -> Set[Vector]: area = box.to_area(self._origin) occupied_by_character = { m.position - self._origin for m in self._roster.characters_in(area) } occupied_by_barrier = { p - self._origin for p in self._barriers.occupied_points_in(area) } return occupied_by_character | occupied_by_barrier
class Living: life_state = LifeState.LIVING movement_range = BoundingBox.range(2) next_state = None def attack(self, target_vectors: TargetVectors) -> Optional[Vector]: return None def best_move( self, target_vectors: TargetVectors, available_moves: Iterable[Vector] ) -> Vector: def nearest_zombie(move: Vector) -> Optional[Vector]: if (nearest := target_vectors.nearest_zombie_to(move)) is not None: return nearest - move else:
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_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_invalid_range(self): with pytest.raises(ValueError): BoundingBox.range(-1)
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_vector_containment(self, vector): box = BoundingBox(Vector.ZERO, Vector(1, 1)) assert (vector in box) == (vector == vector.ZERO)
def test_negative_box(self): box = BoundingBox(Vector.ZERO, Vector(-1, -1)) assert Vector.ZERO not in box
def test_empty_box(self): box = BoundingBox(Vector.ZERO, Vector.ZERO) assert Vector(1, 1) not in box
def test_takes_two_vectors(self): BoundingBox(Vector.ZERO, Vector(1, 1))
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_empty_viewpoint(self): characters: Mapping[Point, Any] = {} roster = Roster.for_mapping(characters, area=Area(Point(0, 0), Point(2, 2))) viewpoint = Viewpoint(Point(1, 1), roster) assert viewpoint.occupied_points_in(BoundingBox.range(2)) == set()
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
def test_viewpoint_single_character(self, character): roster = Roster.for_mapping({Point(1, 1): character}, area=Area(Point(0, 0), Point(2, 2))) viewpoint = Viewpoint(Point(1, 1), roster) assert viewpoint.occupied_points_in( BoundingBox.range(2)) == {Vector.ZERO}
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