lab_retro/retro/graph.py

163 lines
4.9 KiB
Python

from retro.errors import GraphError
class Graph:
def __init__(self, vertices=None, edges=None):
self.vertices = vertices or []
self.edges = edges or []
def __str__(self):
return '\n'.join(str(e) for e in self.edges)
def get_or_create_vertex(self, x, y):
for v in self.vertices:
if x == v.x and y == v.y:
return v
for e in self.edges:
if e.crosses(x, y):
return self.split_edge(e, x, y)
v = Vertex(x, y)
self.vertices.append(v)
return v
def get_or_create_edge(self, x0, y0, x1, y1):
v0 = self.get_or_create_vertex(x0, y0)
v1 = self.get_or_create_vertex(x1, y1)
new_edge = Edge(v0, v1)
for e in self.edges:
if e == new_edge:
new_edge.remove()
return e
return new_edge
def split_edge(self, edge, x, y):
"""
Splits an edge by inserting a new vertex along the edge.
"""
if not edge.crosses(x, y):
raise GraphError(f"Can't split edge {edge} at ({x}, {y})")
self.remove_edge(edge)
v = Vertex(x, y)
self.vertices.append(v)
self.edges.append(Edge(edge.begin, v))
self.edges.append(Edge(v, edge.end))
def remove_edge(self, edge):
if edge not in self.edges:
raise GraphError(f"Edge {edge} is not in the graph")
self.edges.remove(edge)
edge.begin.edges.remove(edge)
edge.end.edges.remove(edge)
def render(self, terminal):
for v in self.vertices:
v.render(terminal)
for e in self.edges:
e.render(terminal)
class Vertex:
CHARACTERS = {
"0000": " ",
"0001": "",
"0010": "",
"0011": "",
"0100": "",
"0101": "",
"0110": "",
"0111": "",
"1000": "",
"1001": "",
"1010": "",
"1011": "",
"1100": "",
"1101": "",
"1110": "",
"1111": "",
}
def __init__(self, x, y):
self.x = x
self.y = y
self.edges = []
def __str__(self):
return f"({self.x}, {self.y})"
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def neighbors(self):
vertices = []
for edge in self.edges:
if self == edge.begin:
vertices.append(edge.end)
else:
vertices.append(edge.begin)
return vertices
def render(self, terminal):
print(terminal.move_xy(self.x, self.y) + self.get_character())
def get_character(self):
u = self.has_up_edge()
r = self.has_right_edge()
d = self.has_down_edge()
l = self.has_left_edge()
code = ''.join([str(int(direction)) for direction in [u, r, d, l]])
return self.CHARACTERS[code]
def has_up_edge(self):
return any([v.x == self.x and v.y < self.y for v in self.neighbors()])
def has_right_edge(self):
return any([v.y == self.y and self.x < v.x for v in self.neighbors()])
def has_down_edge(self):
return any([v.x == self.x and self.y < v.y for v in self.neighbors()])
def has_left_edge(self):
return any([v.y == self.y and v.x < self.x for v in self.neighbors()])
class Edge:
def __init__(self, begin, end):
if not isinstance(begin, Vertex) or not isinstance(end, Vertex):
raise ValueError("Tried to initialize an Edge with a non-vertex")
if begin.x < end.x or begin.y < end.y:
self.begin = begin
self.end = end
else:
self.begin = end
self.end = begin
if not (self.is_horizontal() or self.is_vertical()):
raise ValueError("Edges must be horizontal or vertical.")
if self.is_horizontal() and self.is_vertical():
raise ValueError("Self-edges are not allowed.")
self.begin.edges.append(self)
self.end.edges.append(self)
def __str__(self):
return f"{self.begin} -> {self.end}"
def render(self, terminal):
if self.is_horizontal():
with terminal.location(self.begin.x + 1, self.begin.y):
line = "" * (self.end.x - self.begin.x - 1)
print(line)
else:
for y in range(self.begin.y + 1, self.end.y):
print(terminal.move_xy(self.begin.x, y) + "")
def is_horizontal(self):
return self.begin.y == self.end.y
def is_vertical(self):
return self.begin.x == self.end.x
def crosses(self, x, y):
if self.is_horizontal():
return self.begin.y == y and self.begin.x < x and x < self.end.x
else:
return self.begin.x == x and self.begin.y < y and y < self.end.y
def remove(self):
self.begin.edges.remove(self)
self.end.edges.remove(self)