Skip to content

Commit 317ab4d

Browse files
Merge pull request steam-bell-92#1329 from KunwarSidhu47/main
feat: Add Traveling Salesperson (TSP) Visualizer
2 parents 008511f + a4ebb4b commit 317ab4d

62 files changed

Lines changed: 730 additions & 53 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

projects_registry.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,5 +651,22 @@
651651
"search"
652652
],
653653
"path": "utilities/Pathfinding-Visualizer/Pathfinding-Visualizer.py"
654+
},
655+
{
656+
"name": "TSP Visualizer",
657+
"emoji": "🗺️",
658+
"category": "utilities",
659+
"difficulty": "advanced",
660+
"description": "Visualize the Traveling Salesperson Problem (TSP) using Nearest Neighbor and Brute Force algorithms.",
661+
"keywords": [
662+
"tsp",
663+
"visualizer",
664+
"algorithm",
665+
"salesperson",
666+
"path",
667+
"shortest",
668+
"search"
669+
],
670+
"path": "utilities/TSP-Visualizer/TSP-Visualizer.py"
654671
}
655672
]

tests/test_smoke.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def test_smoke():
2+
"""A minimal smoke test to ensure the test suite runs."""
3+
assert True
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import pygame
2+
import math
3+
import itertools
4+
import random
5+
import sys
6+
7+
pygame.init()
8+
WIDTH, HEIGHT = 800, 600
9+
WIN = pygame.display.set_mode((WIDTH, HEIGHT))
10+
pygame.display.set_caption("TSP Visualizer")
11+
12+
# Colors
13+
WHITE = (255, 255, 255)
14+
BLACK = (10, 10, 10)
15+
GRAY = (50, 50, 50)
16+
RED = (255, 100, 100)
17+
GREEN = (100, 255, 100)
18+
BLUE = (100, 100, 255)
19+
YELLOW = (255, 255, 100)
20+
21+
font = pygame.font.SysFont("comicsansms", 20)
22+
small_font = pygame.font.SysFont("comicsansms", 15)
23+
24+
def get_distance(p1, p2):
25+
return math.hypot(p1[0] - p2[0], p1[1] - p2[1])
26+
27+
def path_distance(nodes, order):
28+
dist = 0
29+
for i in range(len(order) - 1):
30+
dist += get_distance(nodes[order[i]], nodes[order[i+1]])
31+
if len(order) > 0:
32+
dist += get_distance(nodes[order[-1]], nodes[order[0]])
33+
return dist
34+
35+
def draw_text(win, text, x, y, color=WHITE):
36+
text_surface = font.render(text, True, color)
37+
win.blit(text_surface, (x, y))
38+
39+
def draw_nodes(win, nodes, order=[], current_edge=None, best_dist=0):
40+
win.fill(BLACK)
41+
42+
# Draw instructions
43+
draw_text(win, "Click to add nodes | [R] Random | [C] Clear | [1] Nearest Neighbor | [2] Brute Force", 10, 10, GRAY)
44+
draw_text(win, f"Nodes: {len(nodes)}", 10, 40, WHITE)
45+
if best_dist > 0:
46+
draw_text(win, f"Distance: {best_dist:.2f}", 10, 70, YELLOW)
47+
48+
# Draw the path
49+
if len(order) > 1:
50+
for i in range(len(order) - 1):
51+
pygame.draw.line(win, GREEN, nodes[order[i]], nodes[order[i+1]], 2)
52+
pygame.draw.line(win, GREEN, nodes[order[-1]], nodes[order[0]], 2)
53+
54+
if current_edge:
55+
pygame.draw.line(win, RED, current_edge[0], current_edge[1], 2)
56+
57+
# Draw nodes
58+
for i, node in enumerate(nodes):
59+
color = BLUE if i == 0 else WHITE
60+
pygame.draw.circle(win, color, node, 6)
61+
62+
pygame.display.update()
63+
64+
def solve_nearest_neighbor(nodes):
65+
if len(nodes) < 2: return
66+
67+
unvisited = list(range(1, len(nodes)))
68+
current = 0
69+
order = [0]
70+
71+
while unvisited:
72+
# visualization step
73+
for event in pygame.event.get():
74+
if event.type == pygame.QUIT:
75+
pygame.quit()
76+
sys.exit()
77+
78+
nearest = min(unvisited, key=lambda x: get_distance(nodes[current], nodes[x]))
79+
80+
# Animate the connection being tested
81+
draw_nodes(WIN, nodes, order, current_edge=(nodes[current], nodes[nearest]), best_dist=path_distance(nodes, order))
82+
pygame.time.delay(100)
83+
84+
order.append(nearest)
85+
unvisited.remove(nearest)
86+
current = nearest
87+
88+
draw_nodes(WIN, nodes, order, best_dist=path_distance(nodes, order))
89+
90+
def solve_brute_force(nodes):
91+
if len(nodes) < 2: return
92+
if len(nodes) > 10:
93+
print("Too many nodes for brute force! (Max 10 recommended)")
94+
return
95+
96+
min_dist = float('inf')
97+
best_order = []
98+
99+
# fix the start node to 0 to reduce permutations by N
100+
nodes_idx = list(range(1, len(nodes)))
101+
count = 0
102+
total = math.factorial(len(nodes_idx))
103+
104+
for perm in itertools.permutations(nodes_idx):
105+
for event in pygame.event.get():
106+
if event.type == pygame.QUIT:
107+
pygame.quit()
108+
sys.exit()
109+
110+
current_order = [0] + list(perm)
111+
dist = path_distance(nodes, current_order)
112+
if dist < min_dist:
113+
min_dist = dist
114+
best_order = current_order
115+
116+
count += 1
117+
if count % max(1, total // 100) == 0: # animate periodically
118+
draw_nodes(WIN, nodes, best_order, current_edge=(nodes[current_order[-1]], nodes[current_order[0]]), best_dist=min_dist)
119+
120+
draw_nodes(WIN, nodes, best_order, best_dist=min_dist)
121+
122+
def main():
123+
nodes = []
124+
order = []
125+
best_dist = 0
126+
run = True
127+
128+
while run:
129+
draw_nodes(WIN, nodes, order, best_dist=best_dist)
130+
131+
for event in pygame.event.get():
132+
if event.type == pygame.QUIT:
133+
run = False
134+
135+
if event.type == pygame.MOUSEBUTTONDOWN:
136+
x, y = pygame.mouse.get_pos()
137+
nodes.append((x, y))
138+
order = []
139+
best_dist = 0
140+
141+
if event.type == pygame.KEYDOWN:
142+
if event.key == pygame.K_c:
143+
nodes = []
144+
order = []
145+
best_dist = 0
146+
elif event.key == pygame.K_r:
147+
nodes = []
148+
for _ in range(10):
149+
x = random.randint(50, WIDTH - 50)
150+
y = random.randint(100, HEIGHT - 50)
151+
nodes.append((x, y))
152+
order = []
153+
best_dist = 0
154+
elif event.key == pygame.K_1:
155+
if len(nodes) > 1:
156+
solve_nearest_neighbor(nodes)
157+
elif event.key == pygame.K_2:
158+
if len(nodes) > 1:
159+
if len(nodes) > 10:
160+
draw_text(WIN, "Too many nodes for Brute Force! Use <= 10.", 10, 100, RED)
161+
pygame.display.update()
162+
pygame.time.delay(2000)
163+
else:
164+
solve_brute_force(nodes)
165+
166+
pygame.quit()
167+
168+
if __name__ == "__main__":
169+
main()
-3.04 KB
-3.48 KB
-2.46 KB
-2.7 KB
-2.19 KB
-2.54 KB
-2.21 KB

0 commit comments

Comments
 (0)