pathviz 0.1.0__py3-none-any.whl
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.
- pathviz/__init__.py +11 -0
- pathviz/graph.py +25 -0
- pathviz/maze.py +45 -0
- pathviz/search.py +89 -0
- pathviz/visualizer.py +80 -0
- pathviz-0.1.0.dist-info/METADATA +133 -0
- pathviz-0.1.0.dist-info/RECORD +9 -0
- pathviz-0.1.0.dist-info/WHEEL +4 -0
- pathviz-0.1.0.dist-info/licenses/LICENSE +21 -0
pathviz/__init__.py
ADDED
|
@@ -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"
|
pathviz/graph.py
ADDED
|
@@ -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]
|
pathviz/maze.py
ADDED
|
@@ -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
|
pathviz/search.py
ADDED
|
@@ -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)
|
pathviz/visualizer.py
ADDED
|
@@ -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()
|
|
@@ -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,9 @@
|
|
|
1
|
+
pathviz/__init__.py,sha256=EYw90BZTo8BFAcupAY3fNBRiP4ynH16Ut1RVDAP-Yr4,323
|
|
2
|
+
pathviz/graph.py,sha256=nF_Z9warEgd1BeM2WmTaW_LcEQKFucVtLuCSDvK_vjk,788
|
|
3
|
+
pathviz/maze.py,sha256=G6EBRqZC1UihTlFgrCscVqKKmUGz6pntQHxAtxvUpHs,1395
|
|
4
|
+
pathviz/search.py,sha256=EuKdC1jAigru99uWkD-rgxaD-kVv99UTs265Q8C_wuE,2631
|
|
5
|
+
pathviz/visualizer.py,sha256=yFKPLyOKxQC0EEu4kSHfQ3rK2Xcpr4pc2lxAJQF6tbg,2884
|
|
6
|
+
pathviz-0.1.0.dist-info/METADATA,sha256=lQuB_f3cbhEduEpAsSqcKwf5BkNXyUG1W84hwuGTQAE,4440
|
|
7
|
+
pathviz-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
8
|
+
pathviz-0.1.0.dist-info/licenses/LICENSE,sha256=BV9IbV-hJBcE8e4tYCeOSfskahHkIcG8UVUuBakJHKM,1072
|
|
9
|
+
pathviz-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|