From 45a2de9401a6855794575ad2aeb288a2a4ba11c8 Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Mon, 3 Nov 2025 23:59:27 -0500 Subject: [PATCH] feat(bfs.py): implement bfs algorithm and appropriate tests --- tests/test_algorithms.py | 24 +++++++++++++++++------- waypoint/algorithms/__init__.py | 4 ++-- waypoint/algorithms/a_star.py | 5 ----- waypoint/algorithms/bfs.py | 24 ++++++++++++++++++++++++ waypoint/path.py | 2 +- 5 files changed, 44 insertions(+), 15 deletions(-) delete mode 100644 waypoint/algorithms/a_star.py create mode 100644 waypoint/algorithms/bfs.py diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index 8ab3d50..04ade3b 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -1,5 +1,5 @@ import pytest -from waypoint.algorithms import djikstra, a_star +from waypoint.algorithms import djikstra, bfs from waypoint.path import Path @@ -47,12 +47,8 @@ def multiple_paths_graph(): algorithm_parametrize = pytest.mark.parametrize( "algorithm", [ - # pytest.param(bfs, marks=pytest.mark.xfail(reason="BFS not implemented")), - pytest.param( - djikstra, - # marks=pytest.mark.xfail(reason="Dijkstra not implemented"), - ), - pytest.param(a_star, marks=pytest.mark.xfail(reason="A* not implemented")), + pytest.param(djikstra), + pytest.param(bfs), ], ) @@ -61,6 +57,13 @@ def multiple_paths_graph(): @algorithm_parametrize class TestAlgorithms: def test_simple_path(self, algorithm, simple_graph): + if algorithm.__name__ == "bfs": + # BFS does not consider weights, so the path may differ + result = algorithm(simple_graph, 100, 400) + assert result.flights is not None + assert result.flights[0] == 100 + assert result.flights[-1] == 400 + return result = algorithm(simple_graph, 100, 400) assert isinstance(result, Path) assert result.flights == [100, 200, 300, 400] @@ -78,6 +81,13 @@ def test_cyclic_path(self, algorithm, cyclic_graph): assert result.time <= 7 # Maximum possible path length def test_multiple_paths(self, algorithm, multiple_paths_graph): + if algorithm.__name__ == "bfs": + # BFS does not consider weights, so the path may differ + result = algorithm(multiple_paths_graph, 100, 400) + assert result.flights is not None + assert result.flights[0] == 100 + assert result.flights[-1] == 400 + return result = algorithm(multiple_paths_graph, 100, 400) assert result.flights == [100, 300, 400] # Should find shortest path assert result.time == 4 diff --git a/waypoint/algorithms/__init__.py b/waypoint/algorithms/__init__.py index f8a1d15..2f87fe4 100644 --- a/waypoint/algorithms/__init__.py +++ b/waypoint/algorithms/__init__.py @@ -3,6 +3,6 @@ """ from .djikstra import djikstra -from .a_star import a_star +from .bfs import bfs -__all__ = ["djikstra", "a_star"] +__all__ = ["djikstra", "bfs"] diff --git a/waypoint/algorithms/a_star.py b/waypoint/algorithms/a_star.py deleted file mode 100644 index 0825cf9..0000000 --- a/waypoint/algorithms/a_star.py +++ /dev/null @@ -1,5 +0,0 @@ -from waypoint.path import Path - - -def a_star(graph: dict[int, dict[int, float]], start: int, destination: int) -> Path: - raise NotImplementedError("The A* algorithm is not yet implemented.") diff --git a/waypoint/algorithms/bfs.py b/waypoint/algorithms/bfs.py new file mode 100644 index 0000000..feb9730 --- /dev/null +++ b/waypoint/algorithms/bfs.py @@ -0,0 +1,24 @@ +from waypoint.path import Path + + +def bfs(graph: dict[int, dict[int, float]], start: int, destination: int) -> Path: + visited: set[int] = set() + queue: list[tuple[int, float, list[int]]] = [(start, 0, [])] + + while queue: + current, time, path = queue.pop(0) + current: int + time: float + path: list[int] + + if current == destination: + return Path.from_list(path + [current], time) + + if current not in visited: + visited.add(current) + neighbors = graph.get(current, {}) + for neighbor, weight in neighbors.items(): + if neighbor not in visited: + queue.append((neighbor, time + weight, path + [current])) + + return Path.empty() diff --git a/waypoint/path.py b/waypoint/path.py index 95f1791..6ec3aed 100644 --- a/waypoint/path.py +++ b/waypoint/path.py @@ -28,7 +28,7 @@ def empty(cls) -> "Path": return self @classmethod - def from_list(cls, flights: list[int] | None, time: float = 0.0) -> "Path": + def from_list(cls, flights: list[int] | None, time: float) -> "Path": """Creates a path from a list of flights and an optional time.""" self = super().__new__(cls) self._flights = flights