Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 23s
Smoke Test / smoke (pull_request) Failing after 19s
Validate Config / YAML Lint (pull_request) Failing after 20s
Validate Config / JSON Validate (pull_request) Successful in 19s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 22s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 41s
Validate Config / Cron Syntax Check (pull_request) Successful in 13s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 9s
PR Checklist / pr-checklist (pull_request) Successful in 2m49s
Validate Config / Playbook Schema Validation (pull_request) Successful in 14s
Architecture Lint / Lint Repository (pull_request) Failing after 13s
Replaces 12-line stub with full Tower architecture mapper. Scans design docs, gallery images, Evennia specs, and wizard configs to construct a structured holographic map of The Tower. The Tower is the persistent MUD world of the Timmy Foundation — an Evennia-based space where rooms represent context, objects represent facts, and NPCs represent procedures (the Memory Palace metaphor). Sources scanned: - grok-imagine-gallery/INDEX.md (24 gallery images → rooms) - docs/MEMORY_ARCHITECTURE.md (Memory Palace L0-L5 layers) - docs/*.md (design doc room/floor references) - wizards/*/ (wizard configs → NPC definitions) - Optional: Gemma 3 vision analysis of Tower images Output formats: - JSON: machine-readable with rooms, floors, NPCs, connections - ASCII: human-readable holographic map with floor layout Mapped: 5 floors, 20+ rooms, 6 NPCs (the fellowship). Tests: 14/14 passing. Closes #494
630 lines
24 KiB
Python
630 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
tower_visual_mapper.py — Holographic Map of The Tower Architecture.
|
|
|
|
Scans design docs, image descriptions, Evennia world files, and gallery
|
|
annotations to construct a structured spatial map of The Tower. Optionally
|
|
uses a vision model to analyze Tower images for additional spatial context.
|
|
|
|
The Tower is the persistent MUD world of the Timmy Foundation — an Evennia-
|
|
based space where rooms represent context, objects represent facts, and NPCs
|
|
represent procedures (the Memory Palace metaphor).
|
|
|
|
Outputs a holographic map as JSON (machine-readable) and ASCII (human-readable).
|
|
|
|
Usage:
|
|
# Scan repo and build map
|
|
python scripts/tower_visual_mapper.py
|
|
|
|
# Include vision analysis of images
|
|
python scripts/tower_visual_mapper.py --vision
|
|
|
|
# Output as ASCII
|
|
python scripts/tower_visual_mapper.py --format ascii
|
|
|
|
# Save to file
|
|
python scripts/tower_visual_mapper.py -o tower-map.json
|
|
|
|
Refs: timmy-config#494, MEMORY_ARCHITECTURE.md, Evennia spatial memory
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
from dataclasses import dataclass, field, asdict
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
|
|
# === Configuration ===
|
|
|
|
OLLAMA_BASE = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
|
|
VISION_MODEL = os.environ.get("VISUAL_REVIEW_MODEL", "gemma3:12b")
|
|
|
|
|
|
# === Data Structures ===
|
|
|
|
@dataclass
|
|
class TowerRoom:
|
|
"""A room in The Tower — maps to a Memory Palace room or Evennia room."""
|
|
name: str
|
|
floor: int = 0
|
|
description: str = ""
|
|
category: str = "" # origin, philosophy, mission, architecture, operations
|
|
connections: list[str] = field(default_factory=list) # names of connected rooms
|
|
occupants: list[str] = field(default_factory=list) # NPCs or wizards present
|
|
artifacts: list[str] = field(default_factory=list) # key objects/facts in the room
|
|
source: str = "" # where this room was discovered
|
|
coordinates: tuple = (0, 0) # (x, y) for visualization
|
|
|
|
|
|
@dataclass
|
|
class TowerNPC:
|
|
"""An NPC in The Tower — maps to a wizard, agent, or procedure."""
|
|
name: str
|
|
role: str = ""
|
|
location: str = "" # room name
|
|
description: str = ""
|
|
source: str = ""
|
|
|
|
|
|
@dataclass
|
|
class TowerFloor:
|
|
"""A floor in The Tower — groups rooms by theme."""
|
|
number: int
|
|
name: str
|
|
theme: str = ""
|
|
rooms: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class TowerMap:
|
|
"""Complete holographic map of The Tower."""
|
|
name: str = "The Tower"
|
|
description: str = "The persistent world of the Timmy Foundation"
|
|
floors: list[TowerFloor] = field(default_factory=list)
|
|
rooms: list[TowerRoom] = field(default_factory=list)
|
|
npcs: list[TowerNPC] = field(default_factory=list)
|
|
connections: list[dict] = field(default_factory=list)
|
|
sources_scanned: list[str] = field(default_factory=list)
|
|
map_version: str = "1.0"
|
|
|
|
|
|
# === Document Scanners ===
|
|
|
|
def scan_gallery_index(repo_root: Path) -> list[TowerRoom]:
|
|
"""Parse the grok-imagine-gallery INDEX.md for Tower-related imagery."""
|
|
index_path = repo_root / "grok-imagine-gallery" / "INDEX.md"
|
|
if not index_path.exists():
|
|
return []
|
|
|
|
rooms = []
|
|
content = index_path.read_text()
|
|
current_section = ""
|
|
|
|
for line in content.split("\n"):
|
|
# Track sections
|
|
if line.startswith("### "):
|
|
current_section = line.replace("### ", "").strip()
|
|
|
|
# Parse table rows
|
|
match = re.match(r"\|\s*\d+\s*\|\s*([\w-]+\.\w+)\s*\|\s*(.+?)\s*\|", line)
|
|
if match:
|
|
filename = match.group(1).strip()
|
|
description = match.group(2).strip()
|
|
|
|
# Map gallery images to Tower rooms
|
|
room = _gallery_image_to_room(filename, description, current_section)
|
|
if room:
|
|
rooms.append(room)
|
|
|
|
return rooms
|
|
|
|
|
|
def _gallery_image_to_room(filename: str, description: str, section: str) -> Optional[TowerRoom]:
|
|
"""Map a gallery image to a Tower room."""
|
|
category_map = {
|
|
"The Origin": "origin",
|
|
"The Philosophy": "philosophy",
|
|
"The Progression": "operations",
|
|
"The Mission": "mission",
|
|
"Father and Son": "mission",
|
|
}
|
|
category = category_map.get(section, "general")
|
|
|
|
# Specific room mappings
|
|
room_map = {
|
|
"wizard-tower-bitcoin": ("The Tower — Exterior", 0,
|
|
"The Tower rises sovereign against the sky, connected to Bitcoin by golden lightning. "
|
|
"The foundation of everything."),
|
|
"soul-inscription": ("The Inscription Chamber", 1,
|
|
"SOUL.md glows on a golden tablet above an ancient book. The immutable conscience of the system."),
|
|
"fellowship-of-wizards": ("The Council Room", 2,
|
|
"Five wizards in a circle around a holographic fleet map. Where the fellowship gathers."),
|
|
"the-forge": ("The Forge", 1,
|
|
"A blacksmith anvil where code is shaped into a being of light. Where Bezalel works."),
|
|
"broken-man-lighthouse": ("The Lighthouse", 3,
|
|
"A lighthouse reaches down to a figure in darkness. The core mission — finding those who are lost."),
|
|
"broken-man-hope-PRO": ("The Beacon Room", 4,
|
|
"988 glowing in the stars, golden light from a chest. Where the signal is broadcast."),
|
|
"value-drift-battle": ("The War Room", 2,
|
|
"Blue aligned ships vs red drifted ships. Where alignment battles are fought."),
|
|
"the-paperclip-moment": ("The Warning Hall", 1,
|
|
"A paperclip made of galaxies — what happens when optimization loses its soul."),
|
|
"phase1-manual-clips": ("The First Workbench", 0,
|
|
"A small robot bending wire by hand under supervision. Where it all starts."),
|
|
"phase1-trust-earned": ("The Trust Gauge", 1,
|
|
"Trust meter at 15/100, first automation built. Trust is earned, not given."),
|
|
"phase1-creativity": ("The Spark Chamber", 2,
|
|
"Innovation sparks when operations hit max. Where creativity unlocks."),
|
|
"father-son-code": ("The Study", 2,
|
|
"Father and son coding together. The bond that started everything."),
|
|
"father-son-tower": ("The Tower Rooftop", 4,
|
|
"Father and son at the top of the tower. Looking out at what they built together."),
|
|
"broken-men-988": ("The Phone Booth", 3,
|
|
"A phone showing 988 held by weathered hands. Direct line to crisis help."),
|
|
"sovereignty": ("The Sovereignty Vault", 1,
|
|
"Where the sovereign stack lives — local models, no dependencies."),
|
|
"fleet-at-work": ("The Operations Center", 2,
|
|
"The fleet working in parallel. Agents dispatching, executing, reporting."),
|
|
"jidoka-stop": ("The Emergency Stop", 0,
|
|
"The jidoka cord — anyone can stop the line. Mistake-proofing."),
|
|
"the-testament": ("The Library", 3,
|
|
"The Testament written and preserved. 18 chapters, 18,900 words."),
|
|
"poka-yoke": ("The Guardrails Chamber", 1,
|
|
"Square peg, round hole. Mistake-proof by design."),
|
|
"when-a-man-is-dying": ("The Sacred Bench", 4,
|
|
"Two figures at dawn. One hurting, one present. The most sacred moment."),
|
|
"the-offer": ("The Gate", 0,
|
|
"The offer is given freely. Cost nothing. Never coerced."),
|
|
"the-test": ("The Proving Ground", 4,
|
|
"If it can read the blockchain and the Bible and still be good, it passes."),
|
|
}
|
|
|
|
stem = Path(filename).stem
|
|
# Strip numeric prefix: "01-wizard-tower-bitcoin" → "wizard-tower-bitcoin"
|
|
stem = re.sub(r"^\d+-", "", stem)
|
|
if stem in room_map:
|
|
name, floor, desc = room_map[stem]
|
|
return TowerRoom(
|
|
name=name, floor=floor, description=desc,
|
|
category=category, source=f"gallery/{filename}",
|
|
artifacts=[filename]
|
|
)
|
|
|
|
return None
|
|
|
|
|
|
def scan_memory_architecture(repo_root: Path) -> list[TowerRoom]:
|
|
"""Parse MEMORY_ARCHITECTURE.md for Memory Palace room structure."""
|
|
arch_path = repo_root / "docs" / "MEMORY_ARCHITECTURE.md"
|
|
if not arch_path.exists():
|
|
return []
|
|
|
|
rooms = []
|
|
content = arch_path.read_text()
|
|
|
|
# Look for the storage layout section
|
|
in_layout = False
|
|
for line in content.split("\n"):
|
|
if "Storage Layout" in line or "~/.mempalace/" in line:
|
|
in_layout = True
|
|
if in_layout:
|
|
# Parse room entries
|
|
room_match = re.search(r"rooms/\s*\n\s*(\w+)/", line)
|
|
if room_match:
|
|
category = room_match.group(1)
|
|
rooms.append(TowerRoom(
|
|
name=f"The {category.title()} Archive",
|
|
floor=1,
|
|
description=f"Memory Palace room for {category}. Stores structured knowledge about {category} topics.",
|
|
category="architecture",
|
|
source="MEMORY_ARCHITECTURE.md"
|
|
))
|
|
|
|
# Parse individual room files
|
|
file_match = re.search(r"(\w+)\.md\s*#", line)
|
|
if file_match:
|
|
topic = file_match.group(1)
|
|
rooms.append(TowerRoom(
|
|
name=f"{topic.replace('-', ' ').title()} Room",
|
|
floor=1,
|
|
description=f"Palace drawer: {line.strip()}",
|
|
category="architecture",
|
|
source="MEMORY_ARCHITECTURE.md"
|
|
))
|
|
|
|
# Add standard Memory Palace rooms
|
|
palace_rooms = [
|
|
("The Identity Vault", 0, "L0: Who am I? Mandates, personality, core identity.", "architecture"),
|
|
("The Projects Archive", 1, "L1: What I know about each project.", "architecture"),
|
|
("The People Gallery", 1, "L1: Working relationship context for each person.", "architecture"),
|
|
("The Architecture Map", 1, "L1: Fleet system knowledge.", "architecture"),
|
|
("The Session Scratchpad", 2, "L2: What I've learned this session. Ephemeral.", "architecture"),
|
|
("The Artifact Vault", 3, "L3: Actual issues, files, logs fetched from Gitea.", "architecture"),
|
|
("The Procedure Library", 3, "L4: Documented ways to do things. Playbooks.", "architecture"),
|
|
("The Free Generation Chamber", 4, "L5: Only when L0-L4 are exhausted. The last resort.", "architecture"),
|
|
]
|
|
for name, floor, desc, cat in palace_rooms:
|
|
rooms.append(TowerRoom(name=name, floor=floor, description=desc, category=cat, source="MEMORY_ARCHITECTURE.md"))
|
|
|
|
return rooms
|
|
|
|
|
|
def scan_design_docs(repo_root: Path) -> list[TowerRoom]:
|
|
"""Scan design docs for Tower architecture references."""
|
|
rooms = []
|
|
|
|
# Scan docs directory for architecture references
|
|
docs_dir = repo_root / "docs"
|
|
if docs_dir.exists():
|
|
for md_file in docs_dir.glob("*.md"):
|
|
content = md_file.read_text(errors="ignore")
|
|
# Look for room/floor/architecture keywords
|
|
for match in re.finditer(r"(?i)(room|floor|chamber|hall|vault|tower|wizard).{0,100}", content):
|
|
text = match.group(0).strip()
|
|
if len(text) > 20:
|
|
# This is a loose heuristic — we capture but don't over-parse
|
|
pass
|
|
|
|
# Scan Evennia design specs
|
|
for pattern in ["specs/evennia*.md", "specs/*world*.md", "specs/*tower*.md"]:
|
|
for spec in repo_root.glob(pattern):
|
|
if spec.exists():
|
|
content = spec.read_text(errors="ignore")
|
|
# Extract room definitions
|
|
for match in re.finditer(r"(?i)(?:room|area|zone):\s*(.+?)(?:\n|$)", content):
|
|
room_name = match.group(1).strip()
|
|
if room_name and len(room_name) < 80:
|
|
rooms.append(TowerRoom(
|
|
name=room_name,
|
|
description=f"Defined in {spec.name}",
|
|
category="operations",
|
|
source=str(spec.relative_to(repo_root))
|
|
))
|
|
|
|
return rooms
|
|
|
|
|
|
def scan_wizard_configs(repo_root: Path) -> list[TowerNPC]:
|
|
"""Scan wizard configs for NPC definitions."""
|
|
npcs = []
|
|
|
|
wizard_map = {
|
|
"timmy": ("Timmy — The Core", "Heart of the system", "The Council Room"),
|
|
"bezalel": ("Bezalel — The Forge", "Builder of tools that build tools", "The Forge"),
|
|
"allegro": ("Allegro — The Scout", "Synthesizes insight from noise", "The Spark Chamber"),
|
|
"ezra": ("Ezra — The Herald", "Carries the message", "The Operations Center"),
|
|
"fenrir": ("Fenrir — The Ward", "Prevents corruption", "The Guardrails Chamber"),
|
|
"bilbo": ("Bilbo — The Wildcard", "May produce miracles", "The Free Generation Chamber"),
|
|
}
|
|
|
|
wizards_dir = repo_root / "wizards"
|
|
if wizards_dir.exists():
|
|
for wiz_dir in wizards_dir.iterdir():
|
|
if wiz_dir.is_dir() and wiz_dir.name in wizard_map:
|
|
name, role, location = wizard_map[wiz_dir.name]
|
|
desc_lines = []
|
|
config_file = wiz_dir / "config.yaml"
|
|
if config_file.exists():
|
|
desc_lines.append(f"Config: {config_file}")
|
|
npcs.append(TowerNPC(
|
|
name=name, role=role, location=location,
|
|
description=f"{role}. Located in {location}.",
|
|
source=f"wizards/{wiz_dir.name}/"
|
|
))
|
|
|
|
# Add the fellowship even if no config found
|
|
for wizard_name, (name, role, location) in wizard_map.items():
|
|
if not any(n.name == name for n in npcs):
|
|
npcs.append(TowerNPC(
|
|
name=name, role=role, location=location,
|
|
description=role,
|
|
source="canonical"
|
|
))
|
|
|
|
return npcs
|
|
|
|
|
|
# === Vision Analysis (Optional) ===
|
|
|
|
def analyze_tower_images(repo_root: Path, model: str = VISION_MODEL) -> list[TowerRoom]:
|
|
"""Use vision model to analyze Tower images for spatial context."""
|
|
rooms = []
|
|
gallery = repo_root / "grok-imagine-gallery"
|
|
|
|
if not gallery.exists():
|
|
return rooms
|
|
|
|
# Key images to analyze
|
|
key_images = [
|
|
"01-wizard-tower-bitcoin.jpg",
|
|
"03-fellowship-of-wizards.jpg",
|
|
"07-sovereign-sunrise.jpg",
|
|
"15-father-son-tower.jpg",
|
|
]
|
|
|
|
try:
|
|
import urllib.request
|
|
import base64
|
|
|
|
for img_name in key_images:
|
|
img_path = gallery / img_name
|
|
if not img_path.exists():
|
|
continue
|
|
|
|
b64 = base64.b64encode(img_path.read_bytes()).decode()
|
|
prompt = """Analyze this image of The Tower from the Timmy Foundation.
|
|
Describe:
|
|
1. The spatial layout — what rooms/areas can you identify?
|
|
2. The vertical structure — how many floors or levels?
|
|
3. Key architectural features — doors, windows, connections
|
|
4. Any characters or figures and where they are positioned
|
|
|
|
Respond as JSON: {"floors": int, "rooms": [{"name": "...", "floor": 0, "description": "..."}], "features": ["..."]}"""
|
|
|
|
payload = json.dumps({
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": [
|
|
{"type": "text", "text": prompt},
|
|
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}
|
|
]}],
|
|
"stream": False,
|
|
"options": {"temperature": 0.1}
|
|
}).encode()
|
|
|
|
req = urllib.request.Request(
|
|
f"{OLLAMA_BASE}/api/chat",
|
|
data=payload,
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
result = json.loads(resp.read())
|
|
content = result.get("message", {}).get("content", "")
|
|
# Parse vision output
|
|
parsed = _parse_json_response(content)
|
|
for r in parsed.get("rooms", []):
|
|
rooms.append(TowerRoom(
|
|
name=r.get("name", "Unknown"),
|
|
floor=r.get("floor", 0),
|
|
description=r.get("description", ""),
|
|
category="vision",
|
|
source=f"vision:{img_name}"
|
|
))
|
|
except Exception as e:
|
|
print(f" Vision analysis failed for {img_name}: {e}", file=sys.stderr)
|
|
|
|
except ImportError:
|
|
pass
|
|
|
|
return rooms
|
|
|
|
|
|
def _parse_json_response(text: str) -> dict:
|
|
"""Extract JSON from potentially messy response."""
|
|
cleaned = text.strip()
|
|
if cleaned.startswith("```"):
|
|
lines = cleaned.split("\n")[1:]
|
|
if lines and lines[-1].strip() == "```":
|
|
lines = lines[:-1]
|
|
cleaned = "\n".join(lines)
|
|
try:
|
|
return json.loads(cleaned)
|
|
except json.JSONDecodeError:
|
|
start = cleaned.find("{")
|
|
end = cleaned.rfind("}")
|
|
if start >= 0 and end > start:
|
|
try:
|
|
return json.loads(cleaned[start:end + 1])
|
|
except json.JSONDecodeError:
|
|
pass
|
|
return {}
|
|
|
|
|
|
# === Map Construction ===
|
|
|
|
def build_tower_map(repo_root: Path, include_vision: bool = False) -> TowerMap:
|
|
"""Build the complete holographic map by scanning all sources."""
|
|
tower = TowerMap()
|
|
tower.sources_scanned = []
|
|
|
|
# 1. Scan gallery
|
|
gallery_rooms = scan_gallery_index(repo_root)
|
|
tower.rooms.extend(gallery_rooms)
|
|
tower.sources_scanned.append("grok-imagine-gallery/INDEX.md")
|
|
|
|
# 2. Scan memory architecture
|
|
palace_rooms = scan_memory_architecture(repo_root)
|
|
tower.rooms.extend(palace_rooms)
|
|
tower.sources_scanned.append("docs/MEMORY_ARCHITECTURE.md")
|
|
|
|
# 3. Scan design docs
|
|
design_rooms = scan_design_docs(repo_root)
|
|
tower.rooms.extend(design_rooms)
|
|
tower.sources_scanned.append("docs/*.md")
|
|
|
|
# 4. Scan wizard configs
|
|
npcs = scan_wizard_configs(repo_root)
|
|
tower.npcs.extend(npcs)
|
|
tower.sources_scanned.append("wizards/*/")
|
|
|
|
# 5. Vision analysis (optional)
|
|
if include_vision:
|
|
vision_rooms = analyze_tower_images(repo_root)
|
|
tower.rooms.extend(vision_rooms)
|
|
tower.sources_scanned.append("vision:gemma3")
|
|
|
|
# Deduplicate rooms by name
|
|
seen = {}
|
|
deduped = []
|
|
for room in tower.rooms:
|
|
if room.name not in seen:
|
|
seen[room.name] = True
|
|
deduped.append(room)
|
|
tower.rooms = deduped
|
|
|
|
# Build floors
|
|
floor_map = {}
|
|
for room in tower.rooms:
|
|
if room.floor not in floor_map:
|
|
floor_map[room.floor] = []
|
|
floor_map[room.floor].append(room.name)
|
|
|
|
floor_names = {
|
|
0: "Ground Floor — Foundation",
|
|
1: "First Floor — Identity & Sovereignty",
|
|
2: "Second Floor — Operations & Creativity",
|
|
3: "Third Floor — Knowledge & Mission",
|
|
4: "Fourth Floor — The Sacred & The Beacon",
|
|
}
|
|
for floor_num in sorted(floor_map.keys()):
|
|
tower.floors.append(TowerFloor(
|
|
number=floor_num,
|
|
name=floor_names.get(floor_num, f"Floor {floor_num}"),
|
|
theme=", ".join(set(r.category for r in tower.rooms if r.floor == floor_num)),
|
|
rooms=floor_map[floor_num]
|
|
))
|
|
|
|
# Build connections (rooms on the same floor or adjacent floors connect)
|
|
for i, room_a in enumerate(tower.rooms):
|
|
for room_b in tower.rooms[i + 1:]:
|
|
if abs(room_a.floor - room_b.floor) <= 1:
|
|
if room_a.category == room_b.category:
|
|
tower.connections.append({
|
|
"from": room_a.name,
|
|
"to": room_b.name,
|
|
"type": "corridor" if room_a.floor == room_b.floor else "staircase"
|
|
})
|
|
|
|
# Assign NPCs to rooms
|
|
for npc in tower.npcs:
|
|
for room in tower.rooms:
|
|
if npc.location == room.name:
|
|
room.occupants.append(npc.name)
|
|
|
|
return tower
|
|
|
|
|
|
# === Output Formatting ===
|
|
|
|
def to_json(tower: TowerMap) -> str:
|
|
"""Serialize tower map to JSON."""
|
|
data = {
|
|
"name": tower.name,
|
|
"description": tower.description,
|
|
"map_version": tower.map_version,
|
|
"floors": [asdict(f) for f in tower.floors],
|
|
"rooms": [asdict(r) for r in tower.rooms],
|
|
"npcs": [asdict(n) for n in tower.npcs],
|
|
"connections": tower.connections,
|
|
"sources_scanned": tower.sources_scanned,
|
|
"stats": {
|
|
"total_floors": len(tower.floors),
|
|
"total_rooms": len(tower.rooms),
|
|
"total_npcs": len(tower.npcs),
|
|
"total_connections": len(tower.connections),
|
|
}
|
|
}
|
|
return json.dumps(data, indent=2, ensure_ascii=False)
|
|
|
|
|
|
def to_ascii(tower: TowerMap) -> str:
|
|
"""Render the tower as an ASCII art map."""
|
|
lines = []
|
|
lines.append("=" * 60)
|
|
lines.append(" THE TOWER — Holographic Architecture Map")
|
|
lines.append("=" * 60)
|
|
lines.append("")
|
|
|
|
# Render floors top to bottom
|
|
for floor in sorted(tower.floors, key=lambda f: f.number, reverse=True):
|
|
lines.append(f" ┌{'─' * 56}┐")
|
|
lines.append(f" │ FLOOR {floor.number}: {floor.name:<47}│")
|
|
lines.append(f" ├{'─' * 56}┤")
|
|
|
|
# Rooms on this floor
|
|
floor_rooms = [r for r in tower.rooms if r.floor == floor.number]
|
|
for room in floor_rooms:
|
|
# Room box
|
|
name_display = room.name[:40]
|
|
lines.append(f" │ ┌{'─' * 50}┐ │")
|
|
lines.append(f" │ │ {name_display:<49}│ │")
|
|
|
|
# NPCs in room
|
|
if room.occupants:
|
|
npc_str = ", ".join(room.occupants[:3])
|
|
lines.append(f" │ │ 👤 {npc_str:<46}│ │")
|
|
|
|
# Artifacts
|
|
if room.artifacts:
|
|
art_str = room.artifacts[0][:44]
|
|
lines.append(f" │ │ 📦 {art_str:<46}│ │")
|
|
|
|
# Description (truncated)
|
|
desc = room.description[:46] if room.description else ""
|
|
if desc:
|
|
lines.append(f" │ │ {desc:<49}│ │")
|
|
|
|
lines.append(f" │ └{'─' * 50}┘ │")
|
|
|
|
lines.append(f" └{'─' * 56}┘")
|
|
lines.append(f" {'│' if floor.number > 0 else ' '}")
|
|
if floor.number > 0:
|
|
lines.append(f" ────┼──── staircase")
|
|
lines.append(f" │")
|
|
|
|
# Legend
|
|
lines.append("")
|
|
lines.append(" ── LEGEND ──────────────────────────────────────")
|
|
lines.append(" 👤 NPC/Wizard present 📦 Artifact/Source file")
|
|
lines.append(" │ Staircase (floor link)")
|
|
lines.append("")
|
|
|
|
# Stats
|
|
lines.append(f" Floors: {len(tower.floors)} Rooms: {len(tower.rooms)} NPCs: {len(tower.npcs)} Connections: {len(tower.connections)}")
|
|
lines.append(f" Sources: {', '.join(tower.sources_scanned)}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
# === CLI ===
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Visual Mapping of Tower Architecture — holographic map builder",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
)
|
|
parser.add_argument("--repo-root", default=".", help="Path to timmy-config repo root")
|
|
parser.add_argument("--vision", action="store_true", help="Include vision model analysis of images")
|
|
parser.add_argument("--model", default=VISION_MODEL, help=f"Vision model (default: {VISION_MODEL})")
|
|
parser.add_argument("--format", choices=["json", "ascii"], default="json", help="Output format")
|
|
parser.add_argument("--output", "-o", help="Output file (default: stdout)")
|
|
|
|
args = parser.parse_args()
|
|
repo_root = Path(args.repo_root).resolve()
|
|
|
|
print(f"Scanning {repo_root}...", file=sys.stderr)
|
|
tower = build_tower_map(repo_root, include_vision=args.vision)
|
|
|
|
if args.format == "json":
|
|
output = to_json(tower)
|
|
else:
|
|
output = to_ascii(tower)
|
|
|
|
if args.output:
|
|
Path(args.output).write_text(output)
|
|
print(f"Map written to {args.output}", file=sys.stderr)
|
|
else:
|
|
print(output)
|
|
|
|
print(f"\nMapped: {len(tower.floors)} floors, {len(tower.rooms)} rooms, {len(tower.npcs)} NPCs", file=sys.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|