#!/usr/bin/env python3 """ Generate Conway's Game of Life grid layout JSON by querying running Quine instance. Prerequisites: Quine must be running with Conway's generic recipe loaded. Usage: python3 generate-conways-layout.py conways-small.json python3 generate-conways-layout.py conways-large.json --spacing 15 --host localhost:8080 python3 generate-conways-layout.py config.json --output custom-layout.json """ import json import argparse import sys import urllib.request import urllib.parse from pathlib import Path def query_quine(host, query): """Execute Cypher query against running Quine instance.""" url = f"http://{host}/api/v1/query/cypher" payload = {"text": query} try: data = json.dumps(payload).encode('utf-8') req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) with urllib.request.urlopen(req, timeout=10) as response: return json.loads(response.read().decode('utf-8')) except urllib.error.URLError as e: print(f"Error: Could not connect to Quine at {host}") print("Make sure Quine is running with the Conway's generic recipe loaded") print(f"Details: {e}") sys.exit(1) except Exception as e: print(f"Error: Request to Quine failed: {e}") sys.exit(1) def generate_layout(config_file, spacing=100, output_file=None, quine_host="localhost:8080"): """Generate grid layout JSON by querying Quine for actual node UUIDs.""" # Read the configuration file try: with open(config_file, 'r') as f: config = json.load(f) except FileNotFoundError: print(f"Error: Configuration file '{config_file}' not found") sys.exit(1) except json.JSONDecodeError as e: print(f"Error: Invalid JSON in '{config_file}': {e}") sys.exit(1) # Extract grid dimensions grid_width = config.get('gridWidth') grid_height = config.get('gridHeight') if not grid_width or not grid_height: print(f"Error: Config file must contain 'gridWidth' and 'gridHeight' properties") sys.exit(1) total_cells = grid_width * grid_height print(f"šŸ” Querying Quine at {quine_host} for Cell nodes...") # Query Quine for all Cell nodes with their coordinates and UUIDs cell_query = "MATCH (c:Cell) RETURN strId(c), c.x, c.y ORDER BY c.y, c.x" result = query_quine(quine_host, cell_query) if not result: print("Error: No data returned from Quine query") sys.exit(1) # Parse query results cells = [] if "results" in result: for row in result["results"]: if len(row) >= 3: uuid, x, y = row[0], row[1], row[2] cells.append({"uuid": uuid, "x": int(x), "y": int(y)}) if len(cells) != total_cells: print(f"Warning: Expected {total_cells} cells, but found {len(cells)} in Quine") print("Make sure the generic recipe has been run with the correct config file") if not cells: print("Error: No Cell nodes found in Quine") print("Make sure you've loaded the Conway's generic recipe with your config file") sys.exit(1) print(f"āœ… Found {len(cells)} Cell nodes in Quine") # Create layout positions dictionary positions = {} for cell in cells: x, y = cell["x"], cell["y"] uuid = cell["uuid"] # Calculate visual position based on grid coordinates and spacing pos_x = (x - grid_width // 2) * spacing # Center the grid pos_y = (y - grid_height // 2) * spacing positions[uuid] = { "x": pos_x, "y": pos_y, "fixed": True } # Create layout object in Quine's expected format layout = { "past": [ { "type": "Layout", "positions": positions } ], "future": [] } # Determine output filename if not output_file: config_path = Path(config_file) output_file = config_path.stem + "-layout.json" # Write layout file with exact formatting to match working layout files print(f"šŸ“ Writing layout to: {output_file}") try: with open(output_file, 'w') as f: f.write('{\n') f.write(' "past": [\n') f.write(' {\n') f.write(' "type": "Layout",\n') f.write(' "positions": {\n') # Write positions with compact formatting position_lines = [] for uuid, pos in positions.items(): line = f' "{uuid}": {{ "x": {pos["x"]}, "y": {pos["y"]}, "fixed": true }}' position_lines.append(line) f.write(',\n'.join(position_lines)) f.write('\n') f.write(' }\n') f.write(' }\n') f.write(' ],\n') f.write(' "future": []\n') f.write('}\n') print(f"āœ… Generated layout: {output_file}") print(f" Grid: {grid_width}Ɨ{grid_height} ({len(cells)} nodes)") print(f" Spacing: {spacing} pixels") print(f" UUID-based positioning for Quine compatibility") print(f"\nšŸ“‹ Next steps:") print(f" 1. Load layout in Quine UI or via API") print(f" 2. Start Conway's Game using quick queries or sample queries") except IOError as e: print(f"Error: Could not write layout file '{output_file}': {e}") sys.exit(1) def main(): parser = argparse.ArgumentParser( description="Generate Conway's Game of Life grid layout by querying running Quine instance", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python3 generate-conways-layout.py conways-small.json python3 generate-conways-layout.py conways-large.json --spacing 15 python3 generate-conways-layout.py config.json --host localhost:8080 --output my-layout.json Prerequisites: - Quine must be running (default: localhost:8080) - Conway's generic recipe must be loaded with your config file - Cell nodes must exist in the graph """ ) parser.add_argument('config_file', help='Conway\'s configuration JSON file (for grid dimensions)') parser.add_argument('--spacing', '-s', type=int, default=100, help='Spacing between grid cells in pixels (default: 100)') parser.add_argument('--output', '-o', help='Output layout filename (default: -layout.json)') parser.add_argument('--host', default='localhost:8080', help='Quine host:port (default: localhost:8080)') args = parser.parse_args() # Validate input file exists if not Path(args.config_file).exists(): print(f"Error: Configuration file '{args.config_file}' does not exist") sys.exit(1) generate_layout(args.config_file, args.spacing, args.output, args.host) if __name__ == "__main__": main()