#!/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()