pathviz 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.
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
pathviz-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Matej Jelisavac
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pathviz-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.5
2
+ Name: pathviz
3
+ Version: 0.1.0
4
+ Summary: Maze generation, pathfinding and visualisation connected only by a plain adjacency list
5
+ Project-URL: Source, https://github.com/matejjelisavac/PathfindingVisualizer
6
+ Project-URL: Issues, https://github.com/matejjelisavac/PathfindingVisualizer/issues
7
+ Author-email: Matej Jelisavac <matej.jelisavac@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: astar,dijkstra,maze,pathfinding,visualisation
11
+ Classifier: Intended Audience :: Education
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Visualization
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: pygame>=2.6
17
+ Description-Content-Type: text/markdown
18
+
19
+ # pathviz
20
+
21
+ Generate mazes, run pathfinding algorithms on them, and watch the search happen.
22
+
23
+ Maze generation, search and drawing are three independent modules. The only thing
24
+ they share is a plain adjacency list, `dict[(x, y), set[(x, y)]]`, so you can use
25
+ any one of them without the other two, and drop your own algorithm in anywhere.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install pathviz
31
+ ```
32
+
33
+ pygame comes with it, for the visualiser. Everything else is standard library.
34
+
35
+ ## A path
36
+
37
+ ```python
38
+ from pathviz.maze import recursive_backtracker
39
+ from pathviz.search import astar
40
+
41
+ maze = recursive_backtracker(40, 40, braid=0.2)
42
+ path = astar((0, 0), (39, 39), maze)
43
+ ```
44
+
45
+ `braid` is the fraction of dead ends removed, so `0` is a perfect maze with exactly
46
+ one route between any two cells, and `1` is a maze with no dead ends at all.
47
+
48
+ ## Watching it search
49
+
50
+ Every algorithm has a `_steps` twin that yields one `Step` per expansion instead of
51
+ returning a path. You write the loop, so what gets drawn is entirely up to you:
52
+
53
+ ```python
54
+ from pathviz.maze import recursive_backtracker
55
+ from pathviz.search import astar_steps, trace_path
56
+ from pathviz.visualizer import Visualizer
57
+
58
+ maze = recursive_backtracker(60, 60, braid=0.2)
59
+ start, end = (0, 0), (59, 59)
60
+
61
+ vis = Visualizer(maze)
62
+ vis.draw_maze()
63
+
64
+ for step in astar_steps(start, end, maze):
65
+ vis.fill_cell(step.current, "Blue")
66
+ vis.present()
67
+
68
+ for coord in trace_path(step.predecessors, end):
69
+ vis.fill_cell(coord, "Orange")
70
+ vis.present()
71
+ vis.sleep(2000)
72
+ ```
73
+
74
+ Drawing writes to a surface and nothing appears until `present()`, so one frame can
75
+ be built from as many `fill_cell` calls as you like.
76
+
77
+ A `Step` carries `current`, `discovered` (the cells first reached this expansion),
78
+ and live references to the search's own `distances` and `predecessors`. Reading them
79
+ is free, but they mutate as the search runs, so copy anything you want to keep.
80
+
81
+ ## Your own algorithm
82
+
83
+ Nothing in pathviz requires a `Step`. Yield whatever you measure and draw it however
84
+ you want. The visualiser only ever sees coordinates and colours:
85
+
86
+ ```python
87
+ from collections import deque
88
+
89
+ def bfs(start, end, adjacency_list):
90
+ queue, seen = deque([start]), {start}
91
+ while queue:
92
+ current = queue.popleft()
93
+ yield current, len(queue) # your own shape
94
+ if current == end:
95
+ return
96
+ for neighbor in adjacency_list[current]:
97
+ if neighbor not in seen:
98
+ seen.add(neighbor)
99
+ queue.append(neighbor)
100
+
101
+ for current, fringe_size in bfs(start, end, maze):
102
+ vis.fill_cell(current, (0, min(fringe_size * 8, 255), 120))
103
+ vis.present()
104
+ ```
105
+
106
+ The same is true going the other way: any `dict[node, set[node]]` works as input, so
107
+ `astar` and `dijkstra` will run on a graph that has nothing to do with a grid.
108
+
109
+ ## Modules
110
+
111
+ | Module | What it does | Depends on |
112
+ | --- | --- | --- |
113
+ | `pathviz.graph` | `GridGraph`, used to build an adjacency list | stdlib only |
114
+ | `pathviz.maze` | `recursive_backtracker`, `fully_connected` | `pathviz.graph` |
115
+ | `pathviz.search` | `dijkstra`, `astar`, their `_steps` twins, `trace_path` | stdlib only |
116
+ | `pathviz.visualizer` | `Visualizer`, draws mazes, cells and numbers | `pygame` |
117
+
118
+ Nothing is re-exported from the top level, so importing one module doesn't drag in
119
+ the others. `pathviz.search` imports nothing but the standard library, and code that
120
+ never touches `pathviz.visualizer` never loads pygame.
121
+
122
+ Both import forms work, whichever reads better where you are:
123
+
124
+ ```python
125
+ from pathviz.search import astar # astar(...)
126
+ from pathviz import search # search.astar(...)
127
+ ```
128
+
129
+ ## Examples
130
+
131
+ ```bash
132
+ python examples/animate.py
133
+ ```
@@ -0,0 +1,115 @@
1
+ # pathviz
2
+
3
+ Generate mazes, run pathfinding algorithms on them, and watch the search happen.
4
+
5
+ Maze generation, search and drawing are three independent modules. The only thing
6
+ they share is a plain adjacency list, `dict[(x, y), set[(x, y)]]`, so you can use
7
+ any one of them without the other two, and drop your own algorithm in anywhere.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install pathviz
13
+ ```
14
+
15
+ pygame comes with it, for the visualiser. Everything else is standard library.
16
+
17
+ ## A path
18
+
19
+ ```python
20
+ from pathviz.maze import recursive_backtracker
21
+ from pathviz.search import astar
22
+
23
+ maze = recursive_backtracker(40, 40, braid=0.2)
24
+ path = astar((0, 0), (39, 39), maze)
25
+ ```
26
+
27
+ `braid` is the fraction of dead ends removed, so `0` is a perfect maze with exactly
28
+ one route between any two cells, and `1` is a maze with no dead ends at all.
29
+
30
+ ## Watching it search
31
+
32
+ Every algorithm has a `_steps` twin that yields one `Step` per expansion instead of
33
+ returning a path. You write the loop, so what gets drawn is entirely up to you:
34
+
35
+ ```python
36
+ from pathviz.maze import recursive_backtracker
37
+ from pathviz.search import astar_steps, trace_path
38
+ from pathviz.visualizer import Visualizer
39
+
40
+ maze = recursive_backtracker(60, 60, braid=0.2)
41
+ start, end = (0, 0), (59, 59)
42
+
43
+ vis = Visualizer(maze)
44
+ vis.draw_maze()
45
+
46
+ for step in astar_steps(start, end, maze):
47
+ vis.fill_cell(step.current, "Blue")
48
+ vis.present()
49
+
50
+ for coord in trace_path(step.predecessors, end):
51
+ vis.fill_cell(coord, "Orange")
52
+ vis.present()
53
+ vis.sleep(2000)
54
+ ```
55
+
56
+ Drawing writes to a surface and nothing appears until `present()`, so one frame can
57
+ be built from as many `fill_cell` calls as you like.
58
+
59
+ A `Step` carries `current`, `discovered` (the cells first reached this expansion),
60
+ and live references to the search's own `distances` and `predecessors`. Reading them
61
+ is free, but they mutate as the search runs, so copy anything you want to keep.
62
+
63
+ ## Your own algorithm
64
+
65
+ Nothing in pathviz requires a `Step`. Yield whatever you measure and draw it however
66
+ you want. The visualiser only ever sees coordinates and colours:
67
+
68
+ ```python
69
+ from collections import deque
70
+
71
+ def bfs(start, end, adjacency_list):
72
+ queue, seen = deque([start]), {start}
73
+ while queue:
74
+ current = queue.popleft()
75
+ yield current, len(queue) # your own shape
76
+ if current == end:
77
+ return
78
+ for neighbor in adjacency_list[current]:
79
+ if neighbor not in seen:
80
+ seen.add(neighbor)
81
+ queue.append(neighbor)
82
+
83
+ for current, fringe_size in bfs(start, end, maze):
84
+ vis.fill_cell(current, (0, min(fringe_size * 8, 255), 120))
85
+ vis.present()
86
+ ```
87
+
88
+ The same is true going the other way: any `dict[node, set[node]]` works as input, so
89
+ `astar` and `dijkstra` will run on a graph that has nothing to do with a grid.
90
+
91
+ ## Modules
92
+
93
+ | Module | What it does | Depends on |
94
+ | --- | --- | --- |
95
+ | `pathviz.graph` | `GridGraph`, used to build an adjacency list | stdlib only |
96
+ | `pathviz.maze` | `recursive_backtracker`, `fully_connected` | `pathviz.graph` |
97
+ | `pathviz.search` | `dijkstra`, `astar`, their `_steps` twins, `trace_path` | stdlib only |
98
+ | `pathviz.visualizer` | `Visualizer`, draws mazes, cells and numbers | `pygame` |
99
+
100
+ Nothing is re-exported from the top level, so importing one module doesn't drag in
101
+ the others. `pathviz.search` imports nothing but the standard library, and code that
102
+ never touches `pathviz.visualizer` never loads pygame.
103
+
104
+ Both import forms work, whichever reads better where you are:
105
+
106
+ ```python
107
+ from pathviz.search import astar # astar(...)
108
+ from pathviz import search # search.astar(...)
109
+ ```
110
+
111
+ ## Examples
112
+
113
+ ```bash
114
+ python examples/animate.py
115
+ ```
@@ -0,0 +1,32 @@
1
+ from pathviz import maze, search
2
+ from pathviz.visualizer import Visualizer
3
+
4
+
5
+ def animate(adjacency_list, start, end, steps):
6
+ vis = Visualizer(adjacency_list, 800, 1000)
7
+ vis.draw_maze()
8
+ vis.fill_cell(start, "Orange")
9
+ vis.fill_cell(end, "Orange")
10
+ vis.present()
11
+
12
+ for step in steps:
13
+ vis.fill_cell(step.current, (100, min(step.distances[step.current], 255), 100))
14
+ vis.present()
15
+
16
+ # predecessors is live, so the last step already holds the finished search
17
+ for coord in search.trace_path(step.predecessors, end):
18
+ vis.fill_cell(coord, "Orange")
19
+ vis.present()
20
+
21
+ vis.sleep(250)
22
+
23
+
24
+ if __name__ == "__main__":
25
+ width = 120
26
+ height = 120
27
+ start, end = (0,0), (width-1, height-1)
28
+
29
+ adjacency_list = maze.recursive_backtracker(width, height, 1)
30
+
31
+ animate(adjacency_list, start, end, search.astar_steps(start, end, adjacency_list))
32
+ animate(adjacency_list, start, end, search.dijkstra_steps(start, end, adjacency_list))
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pathviz"
7
+ dynamic = ["version"]
8
+ description = "Maze generation, pathfinding and visualisation connected only by a plain adjacency list"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Matej Jelisavac", email = "matej.jelisavac@gmail.com" }]
14
+ keywords = ["pathfinding", "astar", "dijkstra", "maze", "visualisation"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Scientific/Engineering :: Visualization",
19
+ "Intended Audience :: Education",
20
+ ]
21
+
22
+ dependencies = ["pygame>=2.6"]
23
+
24
+ [project.urls]
25
+ Source = "https://github.com/matejjelisavac/PathfindingVisualizer"
26
+ Issues = "https://github.com/matejjelisavac/PathfindingVisualizer/issues"
27
+
28
+ [tool.hatch.version]
29
+ path = "src/pathviz/__init__.py"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/pathviz"]
@@ -0,0 +1,11 @@
1
+ """Maze generation, pathfinding and visualisation, connected only by a plain
2
+ adjacency list: dict[Coord, set[Coord]].
3
+
4
+ Nothing is re-exported here on purpose. Import the module you want and you pay
5
+ for that one alone:
6
+
7
+ from pathviz import maze, search
8
+ from pathviz.visualizer import Visualizer
9
+ """
10
+
11
+ __version__ = "0.1.0"
@@ -0,0 +1,25 @@
1
+ Coord = tuple[int, int]
2
+
3
+ class GridGraph:
4
+ width: int
5
+ height: int
6
+ adjacency_list: dict[Coord, set[Coord]]
7
+
8
+
9
+ def __init__(self, width, height, adjacency_list):
10
+ self.width, self.height, self.adjacency_list = width, height, adjacency_list
11
+
12
+ @classmethod
13
+ def empty(cls, width, height):
14
+ nodes = [(x, y) for x in range(width) for y in range(height)]
15
+ return cls(width, height, {n: set() for n in nodes})
16
+
17
+ def add_edge(self, a, b):
18
+ # sets make a repeated edge a no-op rather than a duplicate
19
+ self.adjacency_list[a].add(b)
20
+ self.adjacency_list[b].add(a)
21
+
22
+ def grid_neighbors(self, node): # adjacent in space — what generators consider
23
+ x, y = node
24
+ return [(nx, ny) for nx, ny in ((x, y-1), (x-1, y), (x+1, y), (x, y+1))
25
+ if 0 <= nx < self.width and 0 <= ny < self.height]
@@ -0,0 +1,45 @@
1
+ from random import choice, sample
2
+
3
+ from . import graph
4
+
5
+ def recursive_backtracker(width, height, braid=0):
6
+
7
+ gr = graph.GridGraph.empty(width, height)
8
+
9
+ current = choice(list(gr.adjacency_list.keys()))
10
+ path, visited = [current], set([current])
11
+
12
+ while path:
13
+ neighbors = gr.grid_neighbors(current)
14
+ candidates = [neighbor for neighbor in neighbors if neighbor not in visited]
15
+
16
+ # If all neighbors already visited then backtrack
17
+ if not candidates:
18
+ visited.add(current)
19
+ current = path.pop()
20
+
21
+ # Otherwise, if at least 1 neighbor not visited
22
+ else:
23
+ next = choice(candidates)
24
+ gr.add_edge(current, next)
25
+ visited.add(current)
26
+ path.append(current)
27
+ current = next
28
+
29
+ # braid = fraction of dead ends removed, sampled uniformly at random.
30
+ # Snapshot the population first so the denominator can't shift as we add edges.
31
+ dead_ends = [coord for coord in gr.adjacency_list if len(gr.adjacency_list[coord]) == 1]
32
+
33
+ for coord in sample(dead_ends, round(braid * len(dead_ends))):
34
+ options = [neighbor for neighbor in gr.grid_neighbors(coord) if neighbor not in gr.adjacency_list[coord]]
35
+ if options:
36
+ gr.add_edge(coord, choice(options))
37
+
38
+ return gr.adjacency_list
39
+
40
+ def fully_connected(width, height):
41
+ gr = graph.GridGraph.empty(width, height)
42
+ for coord in gr.adjacency_list:
43
+ for neighbor in gr.grid_neighbors(coord):
44
+ gr.add_edge(coord, neighbor)
45
+ return gr.adjacency_list
@@ -0,0 +1,89 @@
1
+ from collections import deque
2
+ from heapq import heappop, heappush
3
+ from typing import NamedTuple
4
+ from collections.abc import Callable
5
+
6
+ Coord = tuple[int, int]
7
+ AdjacencyList = dict[Coord, set[Coord]]
8
+
9
+
10
+ class Step(NamedTuple):
11
+ current: Coord
12
+ discovered: list[Coord] # only this step's, freshly built
13
+ distances: dict[Coord, int] # the search's own dicts, shared by
14
+ predecessors: dict[Coord, Coord | None] # every step and mutated as it runs
15
+
16
+
17
+ def trace_path(predecessors: dict[Coord, Coord | None], end: Coord) -> list[Coord]:
18
+ if end not in predecessors:
19
+ return []
20
+
21
+ path = list[Coord]()
22
+ curr = end
23
+ while curr is not None:
24
+ path.append(curr)
25
+ curr = predecessors[curr]
26
+
27
+ path.reverse()
28
+ return path
29
+
30
+
31
+ def _search(start: Coord, end: Coord, adjacency_list: AdjacencyList, heuristic: Callable[[Coord], float]):
32
+
33
+ current = start
34
+ distances = dict[Coord, int]({start: 0})
35
+ queue = []
36
+ walked = set[Coord]()
37
+ predecessors = {start: None}
38
+
39
+ while True:
40
+
41
+ discovered = list[Coord]()
42
+
43
+ walked.add(current)
44
+ for neighbor in adjacency_list[current]:
45
+ if neighbor in walked:
46
+ continue
47
+ new_dist = distances[current] + 1
48
+ if neighbor not in distances or new_dist < distances[neighbor]:
49
+ discovered.append(neighbor)
50
+ distances[neighbor] = new_dist
51
+ # store g, prioritise by f = g + h
52
+ heappush(queue, (new_dist + heuristic(neighbor), -new_dist, neighbor))
53
+ predecessors[neighbor] = current
54
+
55
+ # yield before exiting so the goal expansion is the final frame
56
+ yield Step(current, discovered, distances, predecessors)
57
+
58
+ if current == end or not queue:
59
+ return
60
+
61
+ _weight, _tiebreak, current = heappop(queue)
62
+
63
+
64
+ def _manhattan(end: Coord):
65
+ return lambda node: abs(end[0] - node[0]) + abs(end[1] - node[1])
66
+
67
+
68
+ def _drain(steps, end: Coord) -> list[Coord]:
69
+ # maxlen=1 consumes at C speed, keeping only the finished search's state
70
+ last = deque(steps, maxlen=1)
71
+ return trace_path(last[0].predecessors, end) if last else []
72
+
73
+
74
+ # *_steps yields a Step per expansion; the bare name just returns the path
75
+
76
+ def dijkstra_steps(start: Coord, end: Coord, adjacency_list: AdjacencyList):
77
+ yield from _search(start, end, adjacency_list, lambda _: 0)
78
+
79
+
80
+ def astar_steps(start: Coord, end: Coord, adjacency_list: AdjacencyList):
81
+ yield from _search(start, end, adjacency_list, _manhattan(end))
82
+
83
+
84
+ def dijkstra(start: Coord, end: Coord, adjacency_list: AdjacencyList) -> list[Coord]:
85
+ return _drain(dijkstra_steps(start, end, adjacency_list), end)
86
+
87
+
88
+ def astar(start: Coord, end: Coord, adjacency_list: AdjacencyList) -> list[Coord]:
89
+ return _drain(astar_steps(start, end, adjacency_list), end)
@@ -0,0 +1,80 @@
1
+ import os as _os
2
+
3
+ # pygame greets stdout on import unless this exists; it checks for the name, not a value
4
+ _os.environ.setdefault("PYGAME_HIDE_SUPPORT_PROMPT", "1")
5
+
6
+ import pygame as _pygame
7
+
8
+ Coord = tuple[int, int]
9
+ RGBValue = tuple[int,int,int]
10
+
11
+ class Visualizer:
12
+ bg = "White"
13
+ wall = "Black"
14
+ font_color = "Black"
15
+
16
+ def __init__(self, adjacency_list, display_size=800, fps=60):
17
+ self.adjacency_list = adjacency_list
18
+ self.width = max(x for x, _ in adjacency_list) + 1
19
+ self.height = max(y for _, y in adjacency_list) + 1
20
+ self.cell_size = round(display_size / max(self.width, self.height))
21
+
22
+ _pygame.init()
23
+ self.sc = _pygame.display.set_mode(
24
+ (self.width * self.cell_size, self.height * self.cell_size))
25
+ self.fps = fps
26
+ self._clock = _pygame.time.Clock()
27
+ self._font = _pygame.font.SysFont('Comic Sans MS', max(1, self.cell_size // 4))
28
+ # walls live on their own transparent layer so fills can't cover them
29
+ self._wall_surface = _pygame.Surface(self.sc.get_size(), _pygame.SRCALPHA)
30
+
31
+ # Drawing only writes to the surface; call present() to show a finished
32
+ # frame, so any number of draw calls can make up one frame.
33
+ def present(self):
34
+ for event in _pygame.event.get():
35
+ if event.type == _pygame.QUIT:
36
+ _pygame.quit()
37
+ raise SystemExit
38
+ self.sc.blit(self._wall_surface, (0, 0))
39
+ _pygame.display.flip()
40
+ self._clock.tick(self.fps)
41
+
42
+ def _get_cell(self, coords):
43
+ x,y = coords
44
+ left, top = x * self.cell_size, y * self.cell_size #a coordinate points to top left of the cell
45
+ right, bottom = left + self.cell_size, top + self.cell_size
46
+ return top, left, right, bottom
47
+
48
+ def draw_maze(self):
49
+ self.sc.fill(self.bg)
50
+ for coord in self.adjacency_list:
51
+ top, left, right, bottom = self._get_cell(coord)
52
+ neighbors = self.adjacency_list[coord]
53
+
54
+ # an unconnected neighbor means a wall on that side
55
+ x, y = coord
56
+ walls = (
57
+ ((x, y - 1), (left, top), (right, top)), #Move up
58
+ ((x - 1, y), (left, top), (left, bottom)), #Move left
59
+ ((x + 1, y), (right, top), (right, bottom)), #Move right
60
+ ((x, y + 1), (left, bottom), (right, bottom)), #Move down
61
+ )
62
+
63
+ for neighbor, start, end in walls:
64
+ if neighbor not in neighbors:
65
+ _pygame.draw.line(self._wall_surface, self.wall, start, end)
66
+
67
+ def fill_cell(self, coord: Coord, color: str | RGBValue):
68
+ top, left, _, _ = self._get_cell(coord)
69
+ _pygame.draw.rect(self.sc, color, (left, top, self.cell_size, self.cell_size))
70
+
71
+ def draw_number(self, coord: Coord, number):
72
+ top, left, _, _ = self._get_cell(coord)
73
+ text_surface = self._font.render(str(number), False, _pygame.Color(self.font_color))
74
+ self.sc.blit(text_surface, (left+self.cell_size/2, top+self.cell_size/2))
75
+
76
+ def sleep(self, ms):
77
+ # holds the last presented frame, staying responsive to window events
78
+ start = _pygame.time.get_ticks()
79
+ while _pygame.time.get_ticks() - start < ms:
80
+ self.present()