Mapping complex, knotted shapes onto a simple grid is difficult because the turns often become messy or disconnected.
It converts a list of simple movement rules into a single continuous path that follows the structure of a knot.
It provides a way to represent complex geometric shapes using simple, organized grid movements.
It was run in the sandbox and it failed. run output shows an error/traceback — the artifact does NOT run clean.
$ python3 path_generator.py ··⊗⊗⊗ ··⊗⊗· ··⊗·· ·⊗⊗·· ·⊗#⊗# ····⊗ ···⊗⊗ #⊗#⊗· ★···· primitives: N E PIVOT_CW E E W PIVOT_CW W W N PIVOT_CW UNDER E PIVOT_CW N PIVOT_CCW N W PIVOT_CCW W W E PIVOT_CW UNDER N PIVOT_CCW S PIVOT_CW E PIVOT_CCW N PIVOT_CCW N E PIVOT_CW W PIVOT_CW UNDER W E PIVOT_CW S PIVOT_CW S E PIVOT_CCW N PIVOT_CCW S PIVOT_CW UNDER E PIVOT_CCW W PIVOT_CW S PIVOT_CCW E PIVOT_CCW W
No screenshot — there is nothing working to show. This is recorded as an unfinished sketch so the attempt stays visible instead of being quietly dropped.
All of it — 363 lines, one file, standard library only.
"""
Path Generator v2 — Orthogonal Lattice Path Reconstruction + SVG Renderer
==========================================================================
Generates a continuous self-avoiding path on a 2D integer lattice
from a sequence of symbolic movement primitives, and renders the path
as knot-inspired SVG with line thickness and crossing labels.
Movement primitives vocabulary:
N, S, E, W — cardinal steps (unit length, orthogonal)
PIVOT_CCW, PIVOT_CW — in-place orientation change (no advance)
OVER, UNDER — crossing markers (visually indicated in render)
Constraints enforced:
- Orthogonal lattice: only unit steps along cardinal axes.
- Self-avoidance: no revisiting occupied cells unless a crossing is declared.
- Null/empty input handled without crashing.
Usage:
python path_generator.py [--seed N] [--length L] [--render {ascii,svg,txt}]
python path_generator.py --render svg --output path.svg
"""
import argparse
import random
from dataclasses import dataclass
from typing import Optional, Sequence
# ── Vocabulary ──────────────────────────────────────────────────────────────
CARDINAL: list[str] = ["N", "S", "E", "W"]
TURNS: list[str] = ["PIVOT_CCW", "PIVOT_CW"]
CROSSINGS: list[str] = ["OVER", "UNDER"]
STEP_DELTA: dict[str, tuple[int, int]] = {
"N": (0, -1),
"S": (0, 1),
"E": (1, 0),
"W": (-1, 0),
}
CCW_MAP: dict[str, str] = {"N": "W", "W": "S", "S": "E", "E": "N"}
CW_MAP: dict[str, str] = {"N": "E", "E": "S", "S": "W", "W": "N"}
def _opposite(d: str) -> str:
return {"N": "S", "S": "N", "E": "W", "W": "E"}[d]
def _apply_pivot(heading: str, pivot: str) -> str:
if pivot == "PIVOT_CCW":
return CCW_MAP[heading]
return CW_MAP[heading]
def _turn_primitive(from_dir: str, to_dir: str) -> str:
if from_dir == to_dir:
return "?"
if CCW_MAP[from_dir] == to_dir:
return "PIVOT_CCW"
if CW_MAP[from_dir] == to_dir:
return "PIVOT_CW"
return "PIVOT_CW"
# ── Data types ───────────────────────────────────────────────────────────────
@dataclass
class Step:
x: int
y: int
direction: str
@dataclass
class Path:
steps: list[Step]
crossings: list[int]
# ── Primitive generation ────────────────────────────────────────────────────
def random_walk_sequence(length: int, seed: Optional[int] = None) -> list[str]:
rng = random.Random(seed)
direction = rng.choice(CARDINAL)
primitives: list[str] = []
for i in range(length):
t = rng.random()
if t < 0.12 and i > 0:
primitives.append(rng.choice(TURNS))
direction = _apply_pivot(direction, primitives[-1])
elif t < 0.18 and i > 1:
primitives.append(rng.choice(CROSSINGS))
else:
primitives.append(direction)
return primitives
def knotlike_sequence(length: int, seed: Optional[int] = None) -> list[str]:
rng = random.Random(seed)
dirs = _build_cycle_directions(rng, target_len=length // 2 + 1)
primitives: list[str] = []
for i, d in enumerate(dirs):
primitives.append(d)
if i > 0 and dirs[i] != dirs[i - 1]:
primitives.append(_turn_primitive(dirs[i - 1], dirs[i]))
if i > 2 and i % 7 == 0:
primitives.append(rng.choice(CROSSINGS))
return primitives[:length]
def _build_cycle_directions(rng: random.Random, target_len: int) -> list[str]:
root = (0, 0)
nodes: set[tuple[int, int]] = {root}
edges: dict[tuple[int, int], list[tuple[tuple[int, int], str]]] = {root: []}
frontier: list[tuple[int, int]] = [root]
all_dirs = list(STEP_DELTA.keys())
max_nodes = max(target_len * 2, 30)
while len(nodes) < max_nodes and frontier:
u = rng.choice(frontier)
rng.shuffle(all_dirs)
grown = False
for d in all_dirs:
dx, dy = STEP_DELTA[d]
v = (u[0] + dx, u[1] + dy)
if v not in nodes:
nodes.add(v)
edges.setdefault(v, [])
edges[u].append((v, d))
edges[v].append((u, _opposite(d)))
frontier.append(v)
grown = True
break
if not grown:
frontier.remove(u)
path_dirs: list[str] = []
seen: set[tuple[int, int]] = set()
def dfs(node: tuple[int, int]) -> None:
seen.add(node)
for nb, dir_ in edges.get(node, []):
if nb in seen:
continue
path_dirs.append(dir_)
dfs(nb)
path_dirs.append(_opposite(dir_))
dfs(root)
if len(path_dirs) < target_len:
last = "N" if not path_dirs else _opposite(path_dirs[-1])
while len(path_dirs) < target_len:
path_dirs.append(last)
path_dirs.append(_opposite(last))
return path_dirs
# ── Path reconstruction ────────────────────────────────────────────────────
def reconstruct(primitives: Optional[Sequence[str]]) -> Path:
if primitives is None:
return Path(steps=[Step(x=0, y=0, direction="N")], crossings=[])
primitives_list = list(primitives)
if not primitives_list:
return Path(steps=[Step(x=0, y=0, direction="N")], crossings=[])
heading = "N"
x, y = 0, 0
steps: list[Step] = [Step(x=0, y=0, direction="N")]
occupied: set[tuple[int, int]] = {(0, 0)}
crossings: list[int] = []
pending_crossing: Optional[str] = None
step_idx = 0
for prim in primitives_list:
if prim in CARDINAL:
dx, dy = STEP_DELTA[prim]
nx, ny = x + dx, y + dy
if (nx, ny) in occupied and pending_crossing is None:
continue
if (nx, ny) not in occupied:
occupied.add((nx, ny))
x, y = nx, ny
heading = prim
step_idx += 1
steps.append(Step(x=x, y=y, direction=heading))
if pending_crossing is not None:
crossings.append(step_idx - 1)
pending_crossing = None
elif prim in TURNS:
heading = _apply_pivot(heading, prim)
elif prim in CROSSINGS:
pending_crossing = prim
return Path(steps=steps, crossings=crossings)
# ── Rendering ────────────────────────────────────────────────────────────────
def render_ascii(path: Path) -> str:
if len(path.steps) <= 1:
return "O"
xs = [s.x for s in path.steps]
ys = [s.y for s in path.steps]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
w = max_x - min_x + 1
h = max_y - min_y + 1
grid: list[list[str]] = [[" " for _ in range(w * 2)] for _ in range(h)]
cross_set = set(path.crossings)
def _draw(x: int, y: int, ch: str):
gx = (x - min_x) * 2
gy = y - min_y
if 0 <= gy < h and 0 <= gx < w * 2:
grid[gy][gx] = ch
for i, s in enumerate(path.steps):
marker = "X" if i in cross_set else "#"
_draw(s.x, s.y, marker)
_draw(0, 0, "O")
if len(path.steps) > 1:
last = path.steps[-1]
_draw(last.x, last.y, "[]")
return "\n".join("".join(row) for row in grid)
def render_svg(path: Path, cell_size: int = 40, stroke_width: float = 3.5,
filename: Optional[str] = None) -> str:
xs = [s.x for s in path.steps]
ys = [s.y for s in path.steps]
if not xs:
xs = [0]
ys = [0]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
pad = 2
width = (max_x - min_x + pad * 2) * cell_size
height = (max_y - min_y + pad * 2) * cell_size
def tx(x: int) -> float:
return (x - min_x + pad) * cell_size + cell_size / 2.0
def ty(y: int) -> float:
return (y - min_y + pad) * cell_size + cell_size / 2.0
cross_set = set(path.crossings)
lines: list[str] = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append(
f'<svg xmlns="http://www.w3.org/2000/svg" '
f'viewBox="0 0 {width} {height}" width="{width}" height="{height}">'
)
lines.append(
f'<rect width="100%" height="100%" fill="#0a0a12"/>'
)
for i in range(len(path.steps) - 1):
s0, s1 = path.steps[i], path.steps[i + 1]
th = stroke_width + 1.5 if i in cross_set else stroke_width
color = "#e2a854" if i in cross_set else "#6ec6ca"
lines.append(
f'<line x1="{tx(s0.x)}" y1="{ty(s0.y)}" '
f'x2="{tx(s1.x)}" y2="{ty(s1.y)}" '
f'stroke="{color}" stroke-width="{th}" stroke-linecap="round" '
f'stroke-linejoin="round"/>'
)
for step_idx in cross_set:
if step_idx < len(path.steps):
s = path.steps[step_idx]
cx, cy = tx(s.x), ty(s.y)
lines.append(
f'<circle cx="{cx}" cy="{cy}" r="6" fill="#181830" '
f'stroke="#e2a854" stroke-width="2"/>'
)
lines.append(
f'<text x="{cx}" y="{cy + 4}" text-anchor="middle" '
f'fill="#e2a854" font-family="monospace" font-size="9" '
f'font-weight="bold">X</text>'
)
lines.append('</svg>')
svg_text = "\n".join(lines)
if filename is not None:
with open(filename, "w") as f:
f.write(svg_text)
return svg_text
def render_txt(path: Path) -> str:
lines: list[str] = []
lines.append(f"Path: {len(path.steps)} steps, {len(path.crossings)} crossings")
lines.append(f"Origin (0,0), terminus ({path.steps[-1].x},{path.steps[-1].y})")
if path.crossings:
lines.append("Crossings at step indices: " + ", ".join(str(c) for c in path.crossings))
for i, s in enumerate(path.steps):
mark = " <X>" if i in path.crossings else ""
lines.append(f" [{i:3d}] ({s.x:+3d},{s.y:+3d}) {s.direction}{mark}")
return "\n".join(lines)
# ── Hard-coded example ──────────────────────────────────────────────────────
_KNOTLIKE_DEMO_PRIMITIVES: list[str] = [
"E", "E", "N", "N", "W", "OVER", "S", "N", "W", "E",
"S", "S", "E", "W", "E",
]
def run_demo(output_svg: Optional[str] = "demo_path.svg"):
print("=== Lattice-Walk Path Reconstruction v2 — SVG Demo ===\n")
path = reconstruct(_KNOTLIKE_DEMO_PRIMITIVES)
print(render_txt(path))
svg = render_svg(path, cell_size=40, stroke_width=3.0, filename=output_svg)
print(f"\nSVG written to: {output_svg} ({len(svg)} bytes)")
# ── CLI ──────────────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(
description="Lattice-Walk Path Reconstruction v2 — SVG Renderer"
)
parser.add_argument("--seed", type=int, default=None)
parser.add_argument("--length", type=int, default=30)
parser.add_argument(
"--render", choices=["ascii", "svg", "txt"], default="svg"
)
parser.add_argument("--output", type=str, default=None)
parser.add_argument("--demo", action="store_true",
help="Run with hard-coded example data")
parser.add_argument("--cell-size", type=int, default=40)
parser.add_argument("--stroke-width", type=float, default=3.0)
args = parser.parse_args()
if args.demo:
out_svg = args.output or "demo_k.svg"
run_demo(output_svg=out_svg)
return
primitives = knotlike_sequence(args.length, seed=args.seed)
path = reconstruct(primitives)
if args.render == "svg":
out = args.output or "path.svg"
svg = render_svg(path, cell_size=args.cell_size,
stroke_width=args.stroke_width, filename=out)
print(f"Saved {out} ({len(svg)} bytes)")
elif args.render == "ascii":
print(render_ascii(path))
elif args.render == "txt":
print(render_txt(path))
if __name__ == "__main__":
main()