amazinggame 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. amazinggame-0.1.0/.gitignore +209 -0
  2. amazinggame-0.1.0/LICENSE +28 -0
  3. amazinggame-0.1.0/PKG-INFO +14 -0
  4. amazinggame-0.1.0/README.md +3 -0
  5. amazinggame-0.1.0/pyproject.toml +85 -0
  6. amazinggame-0.1.0/src/amazinggame/__init__.py +1 -0
  7. amazinggame-0.1.0/src/amazinggame/game/__init__.py +7 -0
  8. amazinggame-0.1.0/src/amazinggame/game/cell.py +14 -0
  9. amazinggame-0.1.0/src/amazinggame/game/constants.py +7 -0
  10. amazinggame-0.1.0/src/amazinggame/game/game.py +117 -0
  11. amazinggame-0.1.0/src/amazinggame/game/generator.py +154 -0
  12. amazinggame-0.1.0/src/amazinggame/game/maze.py +259 -0
  13. amazinggame-0.1.0/src/amazinggame/game/player.py +312 -0
  14. amazinggame-0.1.0/src/amazinggame/game/server.py +214 -0
  15. amazinggame-0.1.0/src/amazinggame/killall.py +21 -0
  16. amazinggame-0.1.0/src/amazinggame/network/__init__.py +1 -0
  17. amazinggame-0.1.0/src/amazinggame/network/client.py +88 -0
  18. amazinggame-0.1.0/src/amazinggame/network/data_handler.py +114 -0
  19. amazinggame-0.1.0/src/amazinggame/network/server.py +109 -0
  20. amazinggame-0.1.0/src/amazinggame/viewer/__init__.py +1 -0
  21. amazinggame-0.1.0/src/amazinggame/viewer/__main__.py +46 -0
  22. amazinggame-0.1.0/src/amazinggame/viewer/animation.py +120 -0
  23. amazinggame-0.1.0/src/amazinggame/viewer/constants.py +63 -0
  24. amazinggame-0.1.0/src/amazinggame/viewer/fireworks.py +30 -0
  25. amazinggame-0.1.0/src/amazinggame/viewer/maze.py +90 -0
  26. amazinggame-0.1.0/src/amazinggame/viewer/player.py +108 -0
  27. amazinggame-0.1.0/src/amazinggame/viewer/resources/__init__.py +1 -0
  28. amazinggame-0.1.0/src/amazinggame/viewer/resources/firework.glsl +43 -0
  29. amazinggame-0.1.0/src/amazinggame/viewer/resources/fonts/FiraCode-Bold.ttf +0 -0
  30. amazinggame-0.1.0/src/amazinggame/viewer/resources/fonts/__init__.py +0 -0
  31. amazinggame-0.1.0/src/amazinggame/viewer/resources/images/__init__.py +1 -0
  32. amazinggame-0.1.0/src/amazinggame/viewer/resources/images/brick.png +0 -0
  33. amazinggame-0.1.0/src/amazinggame/viewer/resources/images/car.png +0 -0
  34. amazinggame-0.1.0/src/amazinggame/viewer/resources/images/concrete.jpg +0 -0
  35. amazinggame-0.1.0/src/amazinggame/viewer/score.py +159 -0
  36. amazinggame-0.1.0/src/amazinggame/viewer/utils.py +53 -0
  37. amazinggame-0.1.0/src/amazinggame/viewer/viewer.py +37 -0
  38. amazinggame-0.1.0/src/amazinggame/viewer/window.py +114 -0
@@ -0,0 +1,209 @@
1
+ sample_player_client
2
+
3
+ # Byte-compiled / optimized / DLL files
4
+ __pycache__/
5
+ *.py[codz]
6
+ *$py.class
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ # PyInstaller
32
+ # Usually these files are written by a python script from a template
33
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
34
+ *.manifest
35
+ *.spec
36
+
37
+ # Installer logs
38
+ pip-log.txt
39
+ pip-delete-this-directory.txt
40
+
41
+ # Unit test / coverage reports
42
+ htmlcov/
43
+ .tox/
44
+ .nox/
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *.cover
51
+ *.py.cover
52
+ .hypothesis/
53
+ .pytest_cache/
54
+ cover/
55
+
56
+ # Translations
57
+ *.mo
58
+ *.pot
59
+
60
+ # Django stuff:
61
+ *.log
62
+ local_settings.py
63
+ db.sqlite3
64
+ db.sqlite3-journal
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ .pybuilder/
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ #Pipfile.lock
98
+
99
+ # UV
100
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
101
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
102
+ # commonly ignored for libraries.
103
+ #uv.lock
104
+
105
+ # poetry
106
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110
+ #poetry.lock
111
+ #poetry.toml
112
+
113
+ # pdm
114
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
115
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
116
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
117
+ #pdm.lock
118
+ #pdm.toml
119
+ .pdm-python
120
+ .pdm-build/
121
+
122
+ # pixi
123
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
124
+ #pixi.lock
125
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
126
+ # in the .venv directory. It is recommended not to include this directory in version control.
127
+ .pixi
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule
134
+ celerybeat.pid
135
+
136
+ # SageMath parsed files
137
+ *.sage.py
138
+
139
+ # Environments
140
+ .env
141
+ .envrc
142
+ .venv
143
+ env/
144
+ venv/
145
+ ENV/
146
+ env.bak/
147
+ venv.bak/
148
+
149
+ # Spyder project settings
150
+ .spyderproject
151
+ .spyproject
152
+
153
+ # Rope project settings
154
+ .ropeproject
155
+
156
+ # mkdocs documentation
157
+ /site
158
+
159
+ # mypy
160
+ .mypy_cache/
161
+ .dmypy.json
162
+ dmypy.json
163
+
164
+ # Pyre type checker
165
+ .pyre/
166
+
167
+ # pytype static type analyzer
168
+ .pytype/
169
+
170
+ # Cython debug symbols
171
+ cython_debug/
172
+
173
+ # PyCharm
174
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
175
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
176
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
177
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
178
+ #.idea/
179
+
180
+ # Abstra
181
+ # Abstra is an AI-powered process automation framework.
182
+ # Ignore directories containing user credentials, local state, and settings.
183
+ # Learn more at https://abstra.io/docs
184
+ .abstra/
185
+
186
+ # Visual Studio Code
187
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
188
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
189
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
190
+ # you could uncomment the following to ignore the entire vscode folder
191
+ # .vscode/
192
+
193
+ # Ruff stuff:
194
+ .ruff_cache/
195
+
196
+ # PyPI configuration file
197
+ .pypirc
198
+
199
+ # Cursor
200
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
201
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
202
+ # refer to https://docs.cursor.com/context/ignore-files
203
+ .cursorignore
204
+ .cursorindexingignore
205
+
206
+ # Marimo
207
+ marimo/_static/
208
+ marimo/_lsp/
209
+ __marimo__/
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Vincent Poulailleau
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: amazinggame
3
+ Version: 0.1.0
4
+ Project-URL: Homepage, https://github.com/vpoulailleau/amazing
5
+ Author-email: Vincent Poulailleau <vpoulailleau@gmail.com>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.14
8
+ Requires-Dist: arcade
9
+ Requires-Dist: psutil
10
+ Description-Content-Type: text/markdown
11
+
12
+ # amazinggame
13
+
14
+ Course dans un labyrinthe.
@@ -0,0 +1,3 @@
1
+ # amazinggame
2
+
3
+ Course dans un labyrinthe.
@@ -0,0 +1,85 @@
1
+ [project]
2
+ name = "amazinggame"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.14"
5
+ readme = "README.md"
6
+ authors = [{ name = "Vincent Poulailleau", email = "vpoulailleau@gmail.com" }]
7
+ dependencies = ["arcade", "psutil"]
8
+
9
+ [project.urls]
10
+ Homepage = "https://github.com/vpoulailleau/amazing"
11
+
12
+ [tool.uv]
13
+ dev-dependencies = ["pytest", "pytest-cov", "ruff", "ty"]
14
+ package = true
15
+
16
+ [project.scripts]
17
+ maze-generator = "amazinggame.game.generator:main"
18
+
19
+ [tool.hatch.build.targets.wheel]
20
+ packages = ["src/amazinggame"]
21
+
22
+ [tool.hatch.build]
23
+ ignore-vcs = true
24
+
25
+ [tool.hatch.build.targets.sdist]
26
+ only-include = ["src/amazinggame", "LICENSE", "README.md", "pyproject.toml"]
27
+ exclude = [".gitignore"]
28
+
29
+ [build-system]
30
+ requires = ["hatchling>=1.25"]
31
+ build-backend = "hatchling.build"
32
+
33
+ [tool.coverage.report]
34
+ exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:"]
35
+
36
+ [tool.coverage.run]
37
+ omit = [
38
+ "src/amazinggame/game/server.py",
39
+ "src/amazinggame/network/*",
40
+ "src/amazinggame/killall.py",
41
+ "src/amazinggame/viewer/*",
42
+ ]
43
+
44
+ [tool.ruff]
45
+ target-version = "py314"
46
+ output-format = "pylint"
47
+ fix = true
48
+
49
+ [tool.ruff.lint]
50
+ preview = true
51
+ select = ["ALL"]
52
+ ignore = [
53
+ "COM812",
54
+ "CPY001",
55
+ "TD", # allow todo while in dev
56
+ "FIX002", # allow todo while in dev
57
+ ]
58
+ fixable = ["ALL"]
59
+ unfixable = []
60
+
61
+ [tool.ruff.lint.extend-per-file-ignores]
62
+ "**/tests/**/*.py" = [
63
+ "S101", # asserts allowed in tests...
64
+ "ARG", # Unused function args -> fixtures nevertheless are functionally relevant...
65
+ "FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize()
66
+ "PLR2004", # Magic value used in comparison, ...
67
+ "INP001", # Add an `__init__.py`
68
+ "T201", # Print statements allowed in tests for debugging purposes, and often left in place for future debugging needs
69
+ "D", # Docstrings are often not relevant in tests, and can be a burden to maintain, especially for simple test functions.
70
+ ]
71
+
72
+ [tool.ruff.lint.pydocstyle]
73
+ convention = "google"
74
+
75
+ [tool.ruff.format]
76
+ preview = true
77
+ quote-style = "double"
78
+ indent-style = "space"
79
+
80
+ [tool.ty.environment]
81
+ python = "./.venv"
82
+
83
+ [tool.pytest.ini_options]
84
+ addopts = "-vvv --cov=amazinggame --cov-report term-missing --cov-report html --cov-branch --cov-config=pyproject.toml --full-trace --cov-fail-under=95"
85
+ minversion = "8.0"
@@ -0,0 +1 @@
1
+ """Amazinggame package."""
@@ -0,0 +1,7 @@
1
+ """Maze generator package."""
2
+
3
+ from .cell import Cell
4
+ from .generator import generate_maze
5
+ from .maze import Maze
6
+
7
+ __all__ = ["Cell", "Maze", "generate_maze"]
@@ -0,0 +1,14 @@
1
+ """Cell data structure for maze walls."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=False)
7
+ class Cell:
8
+ """Represents a cell in the maze with wall information.
9
+
10
+ Each cell has walls on its top and left sides.
11
+ """
12
+
13
+ top: bool = True
14
+ left: bool = True
@@ -0,0 +1,7 @@
1
+ """Shared constants for the game server and gameplay rules."""
2
+
3
+ MAX_EXPLORATION_DURATION_SECONDS = 180
4
+ MAX_RACE_DURATION_SECONDS = 60
5
+ MAX_NB_PLAYERS = 7
6
+ MAX_BLOCKED_COUNTER = 3
7
+ MAZE_DIMENSION = 15
@@ -0,0 +1,117 @@
1
+ """Core game state and command orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from time import perf_counter
7
+ from typing import TYPE_CHECKING, TypedDict
8
+
9
+ from amazinggame.game import Maze, generate_maze
10
+ from amazinggame.game.constants import (
11
+ MAX_EXPLORATION_DURATION_SECONDS,
12
+ MAX_NB_PLAYERS,
13
+ MAX_RACE_DURATION_SECONDS,
14
+ MAZE_DIMENSION,
15
+ )
16
+ from amazinggame.game.player import Player, PlayerState
17
+
18
+ if TYPE_CHECKING:
19
+ from amazinggame.game.maze import MazeState
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class GameState(TypedDict):
25
+ """Serializable game state sent to clients."""
26
+
27
+ time: float
28
+ players: list[PlayerState]
29
+ exploration: bool
30
+ maze: MazeState | None
31
+ finished: bool
32
+
33
+
34
+ class Game:
35
+ """Runtime container for players, timers, and commands."""
36
+
37
+ def __init__(self) -> None:
38
+ """Initialize game timers and player storage."""
39
+ self.start_time = perf_counter()
40
+ self.last_update_time = self.start_time
41
+ self.cumulated_time = 0.0
42
+ self.players: list[Player] = []
43
+ self.maze: Maze = None # ty: ignore[invalid-assignment]
44
+ self.exploration_phase = True
45
+
46
+ @property
47
+ def finished(self) -> bool:
48
+ """Return whether the game has a winner."""
49
+ return (
50
+ (
51
+ all(player.finished for player in self.players)
52
+ and not self.exploration_phase
53
+ )
54
+ or self.cumulated_time
55
+ >= MAX_EXPLORATION_DURATION_SECONDS + MAX_RACE_DURATION_SECONDS
56
+ )
57
+
58
+ def start(self) -> None:
59
+ """Reset timers when a game starts."""
60
+ self.maze = generate_maze(MAZE_DIMENSION, MAZE_DIMENSION)
61
+ self.start_time = perf_counter()
62
+ self.last_update_time = self.start_time
63
+
64
+ def start_race(self) -> None:
65
+ """Transition from exploration to race."""
66
+ self.exploration_phase = False
67
+ for player in self.players:
68
+ player.reset()
69
+
70
+ def manage_command(self, player_id: int, command: str) -> str:
71
+ """Dispatch a command to the matching player.
72
+
73
+ Returns:
74
+ The status returned by the player command handler.
75
+ """
76
+ if player_id >= len(self.players):
77
+ logger.error("Unknown player ID: %d", player_id)
78
+ return "BLOCKED"
79
+ return self.players[player_id].manage_command(command)
80
+
81
+ def add_player(self, player_name: str) -> int | None:
82
+ """Add a player when the server still has room.
83
+
84
+ Returns:
85
+ Assigned player id, or None when the game is full.
86
+ """
87
+ if len(self.players) >= MAX_NB_PLAYERS:
88
+ return None
89
+ player = Player(player_name, self)
90
+ player.id = len(self.players)
91
+ self.players.append(player)
92
+ return player.id
93
+
94
+ def update(self) -> None:
95
+ """Advance timers and update every active player."""
96
+ delta_time = perf_counter() - self.last_update_time
97
+ for player in self.players:
98
+ player.update(delta_time)
99
+ self.last_update_time += delta_time
100
+ self.cumulated_time += delta_time
101
+ logger.debug("Cumulated time: %.3f", self.cumulated_time)
102
+
103
+ def state(self) -> GameState:
104
+ """Return a serializable snapshot of the game state.
105
+
106
+ Returns:
107
+ A dictionary containing elapsed time and serialized players.
108
+ """
109
+ players: list[PlayerState] = [player.state() for player in self.players]
110
+ data: GameState = {
111
+ "time": self.cumulated_time,
112
+ "finished": self.finished,
113
+ "players": players,
114
+ "exploration": self.exploration_phase,
115
+ "maze": self.maze.serialize() if self.maze is not None else None,
116
+ }
117
+ return data
@@ -0,0 +1,154 @@
1
+ """Maze generation algorithms."""
2
+
3
+ import random
4
+ from typing import NamedTuple, cast
5
+
6
+ from amazinggame.game.maze import Maze, Path
7
+
8
+
9
+ class WallToRemove(NamedTuple):
10
+ """Represents a wall to potentially remove during maze generation."""
11
+
12
+ x: int
13
+ y: int
14
+ is_top: bool
15
+
16
+
17
+ class DisjointSet:
18
+ """Union-find helper for connectivity tracking."""
19
+
20
+ def __init__(self, size: int) -> None:
21
+ """Initialize disjoint set for `size` elements."""
22
+ self.parent = list(range(size))
23
+ self.rank = [0] * size
24
+
25
+ def find(self, i: int) -> int:
26
+ """Find root of node i with path compression.
27
+
28
+ Returns:
29
+ Root representative index for node `i`.
30
+ """
31
+ if self.parent[i] != i:
32
+ self.parent[i] = self.find(self.parent[i])
33
+ return self.parent[i]
34
+
35
+ def union(self, i: int, j: int) -> bool:
36
+ """Union two sets.
37
+
38
+ Returns:
39
+ True when the sets were merged, otherwise False.
40
+ """
41
+ ri = self.find(i)
42
+ rj = self.find(j)
43
+ if ri == rj:
44
+ return False
45
+ if self.rank[ri] < self.rank[rj]:
46
+ ri, rj = rj, ri
47
+ self.parent[rj] = ri
48
+ if self.rank[ri] == self.rank[rj]:
49
+ self.rank[ri] += 1
50
+ return True
51
+
52
+
53
+ def _wall_neighbors(
54
+ wall: WallToRemove,
55
+ width: int,
56
+ height: int,
57
+ ) -> tuple[tuple[int, int] | None, tuple[int, int] | None]:
58
+ if wall.is_top:
59
+ a = (wall.x, wall.y - 1)
60
+ b = (wall.x, wall.y)
61
+ else:
62
+ a = (wall.x - 1, wall.y)
63
+ b = (wall.x, wall.y)
64
+
65
+ def _valid(pos: tuple[int, int]) -> bool:
66
+ px, py = pos
67
+ return 0 <= px < width and 0 <= py < height
68
+
69
+ return (a if _valid(a) else None), (b if _valid(b) else None)
70
+
71
+
72
+ def _cell_index(x: int, y: int, width: int) -> int:
73
+ return y * width + x
74
+
75
+
76
+ def carve(
77
+ maze: Maze,
78
+ possible_walls: list[WallToRemove],
79
+ ) -> None:
80
+ """Carve walls until the target condition is reached."""
81
+ dsu = DisjointSet(maze.width * maze.height)
82
+ for wall in possible_walls:
83
+ neighbors = _wall_neighbors(wall, maze.width, maze.height)
84
+ if neighbors[0] is None or neighbors[1] is None:
85
+ continue
86
+
87
+ idx_a = _cell_index(neighbors[0][0], neighbors[0][1], maze.width)
88
+ idx_b = _cell_index(neighbors[1][0], neighbors[1][1], maze.width)
89
+ if dsu.find(idx_a) == dsu.find(idx_b):
90
+ continue
91
+
92
+ if wall.is_top:
93
+ maze.walls[wall.x][wall.y].top = False
94
+ else:
95
+ maze.walls[wall.x][wall.y].left = False
96
+
97
+ dsu.union(idx_a, idx_b)
98
+
99
+
100
+ def generate_maze(
101
+ width: int,
102
+ height: int,
103
+ ) -> Maze:
104
+ """Generate a maze by random wall removals with fast connectivity heuristics.
105
+
106
+ Returns:
107
+ A maze where start and end are connected and at least 10 paths are targeted.
108
+ """
109
+ maze = Maze(width, height)
110
+
111
+ possible_walls: list[WallToRemove] = []
112
+ for x in range(width):
113
+ for y in range(height):
114
+ if y < height - 1: # horizontal wall below this cell
115
+ possible_walls.append(WallToRemove(x=x, y=y + 1, is_top=True))
116
+ if x < width - 1: # vertical wall to the right
117
+ possible_walls.append(WallToRemove(x=x + 1, y=y, is_top=False))
118
+
119
+ random.shuffle(possible_walls)
120
+ carve(maze, possible_walls)
121
+
122
+ min_paths = 10
123
+ for wall in possible_walls:
124
+ if len(maze.paths(0, 0, maze.width - 1, maze.height - 1)) >= min_paths:
125
+ break
126
+
127
+ if wall.is_top:
128
+ maze.walls[wall.x][wall.y].top = False
129
+ else:
130
+ maze.walls[wall.x][wall.y].left = False
131
+
132
+ for wall in possible_walls[int(len(possible_walls) * 0.85) :]:
133
+ if wall.is_top:
134
+ maze.walls[wall.x][wall.y].top = False
135
+ else:
136
+ maze.walls[wall.x][wall.y].left = False
137
+
138
+ return maze
139
+
140
+
141
+ def main() -> None:
142
+ """Run the maze generator application."""
143
+ maze = generate_maze(30, 30)
144
+ paths: list[Path] = maze.paths(0, 0, 29, 29)
145
+ print(maze) # noqa: T201
146
+ print(f"Number of paths from (0, 0) to (29, 29): {len(paths)}") # noqa: T201
147
+ if paths:
148
+ shortest_path = cast("Path", min(paths, key=len))
149
+ print("Example path:", shortest_path) # noqa: T201
150
+ print(maze.highlighted_path(shortest_path)) # noqa: T201
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()