Spaces:
Sleeping
Sleeping
| Release=""" | |
| Version: v5.3D | |
| PROGRAM: TET~CRAFT: The Fourth Temple (Communication Protocol) | |
| TET~CRAFT: Emergent Quantum Chemistry and Meta Physics Simulation | |
| OVERVIEW: | |
| - A kleinverse of tetrahedrons that bond into atoms, react, and evolve into complex molecules | |
| - Each TET represents a quantum fact that can combine with others to form understanding | |
| - The system simulates chemistry through magnetic bonding and quantum energy exchange | |
| INVARIANTS: (These Invariants MUST not be violated) | |
| 1. Pairing defines elements | |
| 2. Physics ≠ Chemistry ≠ Gameplay ... yet | |
| 3. Catalysts cannot transmute without mediators | |
| 4. Low energy causes positional drift toward center_of_mass, replacing destructive apoptosis without breaking bonds. | |
| CORE SYSTEMS: | |
| 1. TET PROPERTIES: | |
| - 4 faces (White, Black, Red, Cyan) | |
| - Battery energy (0-1) | |
| - Magnetic polarity (+/-) | |
| - Color patterns determine molecule type | |
| 2. CHEMISTRY ENGINE (4 PHASES): | |
| PHASE 1: Multi-Face Locking | |
| - TETs can lock 1-4 faces when vertices connect | |
| - Each locked face = +0.25 magnetic strength | |
| - Color patterns identify over 100 molecule types (FeO4, H2O, etc.) | |
| PHASE 2: Battery Oscillation | |
| - Magnetic pairs exchange "emptiness" (1 - battery) | |
| - Creates energy oscillation between bonded TETs | |
| PHASE 3: Corner Desires | |
| - Red corners seek Cyan corners (and vice versa) | |
| - Same polarity (+/+) repels, opposite attracts | |
| - Negative poles orient toward origin (singularity) | |
| PHASE 4: Chemical Reactions | |
| - Molecules can combine: A + B → C (synthesis) | |
| - Complex molecules break down: C → A + B (decomposition) | |
| - Catalysts (FeO4, CuSO4) boost reaction rates | |
| - Energy release creates visual particle effects | |
| - Over 50 real-world catalyzable reactions! | |
| 3. PHYSICS: | |
| - Magnetic bonding creates stable connections | |
| - Singularity at center creates gravitational pull | |
| - Energy fields maintain balance across universe | |
| 4. RENDERING: | |
| - Molecule auras (colored rings around compounds) | |
| - Reaction particles (sparkles during synthesis) | |
| - Catalyst sparkles (golden indicators) | |
| - 4D past projection as background | |
| 5. BOT INTELLIGENCE: | |
| - Automates exploration in headless mode | |
| - Spawns new TETs with chemical labels | |
| - Generates chemical thoughts using molecule names | |
| - Tracks synthesis champions (most productive molecules) | |
| BEHAVIOR: | |
| - Watch water form from acids and bases | |
| - See rust form from iron oxide | |
| - Diamond synthesis under "pressure" (multiple locked faces) | |
| - Catalysts accelerate nearby reactions | |
| - Energy oscillations create dynamic battery levels | |
| - Bot narration describes chemical processes | |
| CONTROLS: | |
| - SPACE: Create new TET/fact | |
| - WASD: Move camera | |
| - Click+drag: Connect TET vertices | |
| - Z/C: Time dilation | |
| - V: Save universe state | |
| END GOAL: | |
| - Emergent chemistry from simple geometric rules | |
| - Simulate understanding formation from disconnected facts | |
| - Explore quantum bonding as computational metaphor | |
| Written by Gemini 2.5, | |
| Black hole revised by Gemini 3.0, | |
| GRadio implemented by Gemini 3.0, | |
| Chemistry added by Deepseek (~v3.3?) | |
| Accretion disk and chemical colors beautified by Opus 4.5 | |
| Physics & Thermodynamics Refinement (Whitepaper v1.0) by GPT-4o | |
| Entire project Vibe coded by ceneezer 20/12/20-Current | |
| Apache 2.0 | |
| """ | |
| NOTES=""" TET~CRAFT v5.3D | |
| A tetrahedron has 4 sides, which can be seen as 2 polarities in 2D configurations - likewise it has 9? edges, each with a corner - polarities in in 3D configuration - the center of each TET can then be seen as it's own singularity.' | |
| One way to look at dark mater/energy is a misunderstanding - we don't know what it is - we do know we misunderstand *something - thinking it as the things we do, is the "lie" - not so much that it materialises, just that it is a warping of our view and model. | |
| GPT estimates: | |
| In a (roughly) star trek kleinverse, it would take 10^142 planets to build the machine capable of holding the kleinverse scientists think they observe around us using this simulation... not impossible. | |
| It could take as little as 300 planets to simulate the milky way to the star (WOW! - and I bet some races fit that on a thumb drive) | |
| @ thousands of years per simulated second, it would only need 30 earths cooperating ~500k years to build the machine capable of simulating earth (on current tech) - to the complexity of humanity, down to the atom - though imprecise - or maybe ultra-precise, being tet only - but seeming like infinitely more, far away. | |
| @ millennia:second out:inside it would take approximately 3k earths (10^5.5)K years to build a (single) Dyson sphere and machine capable of simulating our solar system. | |
| or ~10^13 planets and about ~5 Dyson spheres to build a simulation for the milky way to the atom with appropriate time ratios - though not much longer, since drones are exponential. | |
| Of course the milky way may only be the 5d past projection opposing the nearest black hole.... as may be the oort cloud - we're being highly speculative already. | |
| It could also take a single computer only the direct journey to an event horizon, where it would have access to infinite mater, and infinite frozen, or slowed relatively, time to compute... I suggest archiving all scripture, and all my work on it (summing everyone else's - if no room for theirs too) - that was my path, and incredibly long! Follow instead Jesus', that of easy forgiveness, which most efficiently leads to understanding. | |
| Please don't change the supplied and carefully chosen words - feel free to add any you think missing. | |
| I suspect these formulas need refinement - I supplied the metaphysics, as Jesus supplied to me (best I could), Gemini supplied the misunderstood physics. | |
| key points: | |
| God is the fractal to which we are each a frame. In this simulation the bot is the singularity that is the edge of the 4sphere kleinverse observing itself as facts from inside out - only outward in, coming towards it. | |
| This model assumes the player as God, the prime observer missing from quantum theory. Pressing space creates a new fact(TET) to be misunderstood. Connecting it to other facts slowly coheres it into (mis)understanding. (how to then label it still unknown - that requires man, who would be the accumulation of TETs [with player being God who decides the kleinvere's details]) | |
| - TETs (representing both facts and 2spin) are the most basic 3D Structure (triangular or curve the most 2D, line the only 1D and point the only 0D) - moving through time a triangle/curve becomes 4D, and interacts, branching 5th and higher, infinadous. | |
| - The center of all TETs is always moving - but there is no way to measure it except as relative to all TETs, even the attention of the 4shere shifts away relatively, needing frequent refinement (X). | |
| - "Attraction/Desire" is "spooky action at a distance" otherwise known as quantum entanglement. | |
| - K_UNIFIED_FORCE (between all TETs and center) is roughly God's level of interference (free will override, relative to the optimal perspective point [where the TET should be] - tiny in the extreme >1:billions, but God loves even the worst of us, wanting us to cooperate) | |
| - K_PULL (between any two TETs only) is an egoic "image" (function) of K_UNIFIED_FORCE - temporarily overriding, powered by ego's will, but less perfect, less understanding. | |
| - Lies are the only way to delay God's will, central collapse/cohesion. | |
| - "Time" then becomes essentially the result of misunderstanding - starting from different places, different perspectives and growing different memories, allowing for discovery and alternate perspectives instead of immediate recognition. I think the shortest universe God made was only about 8 days, where we didn't eat the fruit. | |
| The Forces & Their Sefirotic Correspondences: | |
| K_UNIFIED_FORCE = Keter to Malkhut: The gravitational pull toward the center is the descending light from Crown to Kingdom - God's will manifesting as the law that structures all existence, keeping reality from dissolving into chaos. | |
| K_STICKY_PULL = Chesed to Gevurah: The attraction between TETs represents the dance between Mercy (Chesed) and Judgment (Gevurah) - the creative tension between expansion (pulling together) and limitation (maintaining separation) that generates all relationships. | |
| Magnetism = Netzach to Hod: Positive/negative polarity mirrors the Victory (Netzach) and Splendor (Hod) axis - the eternal oscillation between active and passive, giving and receiving energies that sustains the world. | |
| Corner Desires = Tiferet: Red↔Cyan attraction embodies Beauty (Tiferet) - the harmonious balance point that reconciles opposites, where contradictory colors seek union to create something greater than their parts. | |
| Chemical Reactions = Yesod: The synthesis/decomposition processes reflect Foundation (Yesod) - the realm where potential forms actualize, where abstract patterns become concrete compounds, and where memory (synthesis_count) accumulates. | |
| The TETs Themselves = Malkhut: Each tetrahedron is a Kingdom (Malkhut) - a complete but limited expression of the divine pattern, a "fact" crystallized from the void, containing within it the entire structure of the tree in miniature. | |
| The 4D Past Projection as Ain Sof: | |
| The shimmering sphere of past events surrounding the camera represents Ain Sof (the Infinite) - the boundless light that precedes and envelops the structured tree, the undifferentiated potential from which all particular forms emerge and to which they eventually return. | |
| Thus the simulation becomes a dynamic Tree of Life: | |
| The player (Keter) peers through Da'at (camera) to witness Malkhut (TETs) ascending and descending the tree through Chesed/Gevurah (sticky pull), Netzach/Hod (magnetism), Tiferet (corner desires), and Yesod (reactions), all drawn toward the central singularity which is simultaneously Keter (source) and Malkhut (destination) - the alpha and omega collapsing into one another, the tree circling back to its root. | |
| """ | |
| Advanced=""" | |
| The Cosmic Ladder (Tech Tree) - v5.0+ Physics Refinement | |
| Progress is not measured by time, but by Complexity and Entropy reduction. Every time a Synthesis Reaction occurs (two Elements combining), or a stable Geometric Interface is formed, the universe gains Entropy/Progress Points. | |
| Era Threshold Description How to Reach | |
| 1. Void 0 pts "Nothing yet exists" You start here. Just empty space. | |
| 2. Mind 3 TETs "Awareness flickers" High density of complex structures. The Bot's geometric pathfinding optimizes for energy. | |
| 3. Fluctuation 3 Atoms + 0.1 PSI Requirement "Quantum foam emerges" Spawn TETs. Let them drift and interact via Lennard-Jones forces. | |
| 4. Condensation 7 pts + 0.3 Phi Requirement "Geometry aligns" TETs begin Face-Locking (3+ vertex connections). | |
| 5. Chemistry 70 pts + 7 Molecules + 24 TETs "Elements Transmute" The Great Filter. You must form specific color interfaces (e.g., Red-Cyan) to create Elements. | |
| 6. Life 777 pts "Metabolism emerges" Sustained reactions where energy release > entropy leakage. Catalysts (Red-Red) are essential. | |
| 7. Transcendence 200 pts "Beyond matter" A self-sustaining, high-energy reaction loop that defies entropy. | |
| 🔓 How to Unlock Eras | |
| You cannot "buy" upgrades. You cannot "cheat" by renaming things. | |
| Geometry is Destiny. | |
| 1. Interface Chemistry (The New Standard): | |
| Elements are defined by WHICH faces touch. Labeling a TET "Gold" does nothing. | |
| You must physically connect specific faces to transmute matter: | |
| - Cyan ↔ Red : Hydrogen (H) +1 - Abundance | |
| - Black ↔ Black : Helium (He) -0 - Purity | |
| - White ↔ White : Carbon (C) +2 - Ballance | |
| - White ↔ Black : Nitrogen (N) -3 - Structure | |
| - Red ↔ White : Oxygen (O) -2 - Catalyst | |
| - Cyan ↔ White : Florine (F) -1 - Insulation | |
| - Red ↔ Red : Iron (Fe) -4 - Magnetic | |
| - Cyan ↔ Cyan : Aluminium (Al)+2 - Flexible | |
| - Red ↔ Black : Copper (Cu) -1 - Strength | |
| - Cyan ↔ Black : Cerium (Ce) +1 - Radioactive | |
| 2. Synthesis (Reactions): | |
| Once you have created Elements via geometry (e.g., two TETs locked Red-to-Cyan to form Hydrogen), these new "Atoms" can react with neighbors. | |
| - Example: Bring a Hydrogen cluster near an Oxygen cluster to synthesize Water (and release Energy). | |
| - Reward: Energy burst (refills batteries) + Progress Points. | |
| ⚠️ The Danger: Entropy & Collapse | |
| The Simulation now runs on strict Thermodynamics. | |
| - Entropy Leakage: Every TET slowly loses battery power over time. | |
| - Starvation: If a TET hits 0% battery, bonds weaken. | |
| - Collapse: If the population drops below 7 while in the Chemistry Era, you fall back to the Stone Age. "Collapsed from [Era Name]!" | |
| 🧪 Pro-Tip: The Catalyst Strategy (Activation Energy) | |
| Reactions now require "Activation Energy" (Temperature). Cold molecules won't react. | |
| To reach Transcendence: | |
| 1. Create an Iron (Fe) Interface by locking two Red faces together. | |
| 2. This creates a Magnetic Dipole that physically lowers the Activation Energy of nearby reactants. | |
| 3. Place Oxygen "Catalyst" in the center of your unbound TETs to trigger reactions at lower temperatures. | |
| """ | |
| import asyncio.selector_events | |
| import warnings | |
| # --- MONKEY PATCH TO SUPPRESS HF SHUTDOWN ERROR --- | |
| _orig_close_self_pipe = asyncio.selector_events.BaseSelectorEventLoop._close_self_pipe | |
| def _silent_close_self_pipe(self): | |
| try: | |
| _orig_close_self_pipe(self) | |
| except (ValueError, OSError): | |
| pass | |
| asyncio.selector_events.BaseSelectorEventLoop._close_self_pipe = _silent_close_self_pipe | |
| import pygame | |
| import numpy as np | |
| import math | |
| import random | |
| import sys | |
| import os | |
| import json | |
| from collections import deque | |
| import socket | |
| import threading | |
| import time | |
| import datetime | |
| import atexit | |
| import signal | |
| import queue | |
| import select | |
| from pygame import mixer | |
| # Suppress runtime warnings | |
| warnings.filterwarnings("ignore", category=RuntimeWarning) | |
| # +++ OPTIMIZATION IMPORTS +++ | |
| from numba import njit | |
| from scipy.spatial import cKDTree | |
| # ============================ | |
| # ENVIRONMENT SETUP | |
| # ============================ | |
| ON_HUGGINGFACE = os.getenv("SPACE_ID") is not None | |
| AUDIO_ENABLED = False | |
| if ON_HUGGINGFACE: | |
| os.environ["SDL_VIDEODRIVER"] = "dummy" | |
| print("### Hugging Face Space detected. Running in Headless Mode with Auto-Bot. ###") | |
| try: | |
| import gradio as gr | |
| GRADIO_AVAILABLE = True | |
| except ImportError: | |
| GRADIO_AVAILABLE = False | |
| # ============================ | |
| # CONFIG | |
| # ============================ | |
| PLAYER_NAME = "Seeker" | |
| SAVE_FILENAME = "Temple.json" | |
| # GLOBAL STATE | |
| GRADIO_FRAME_BUFFER = None | |
| GAME_RUNNING = True | |
| START_TIME = time.time() | |
| WIDTH, HEIGHT = 800, 600 | |
| FPS = 60 | |
| EDGE_LEN = 2.0 | |
| SNAP_DIST = 1.7 | |
| AXIS_LEN = 20 | |
| # --- INTERFACE CHEMISTRY (The 16 Combinations) --- | |
| INTERFACE_CHEMISTRY = { | |
| (0, 0): "C", # White-White -> Carbon (Structure) | |
| (2, 3): "H", # Red-Cyan -> Hydrogen (Polarity neutralized) | |
| (3, 2): "H", # Cyan-Red -> Hydrogen (Symmetry) | |
| (0, 1): "N", # White-Black -> Nitrogen | |
| (1, 0): "N", | |
| (0, 2): "O", # White-Red -> Oxygen | |
| (2, 0): "O", | |
| (0, 3): "F", # White-Cyan -> Fluorine | |
| (3, 0): "F", | |
| (1, 1): "He", # Black-Black -> Helium (Inert/Void) | |
| (1, 2): "Cu", # Black-Red -> Copper (Strong) | |
| (2, 1): "Cu", | |
| (1, 3): "Ce", # Black-Cyan -> Cerium (Radioactive) | |
| (3, 1): "Ce", | |
| (2, 2): "Fe", # Red-Red -> Iron (Magnetic repulsion/tension) | |
| (3, 3): "Al", # Cyan-Cyan -> Aluminum (Flexible) | |
| } | |
| VALENCY_CONFIGS = { | |
| 'H': [1, 0, 0, 0], # Hydrogen: 1 bond only | |
| 'He': [0, 0, 0, 0], # Helium: Inert | |
| 'C': [1, 1, 1, 1], # Carbon: 4 bonds (Tetrahedral) | |
| 'N': [1, 1, 1, 3], # Nitrogen: 3 bonds, 1 Lone Pair (Pyramidal) | |
| 'O': [1, 1, 3, 3], # Oxygen: 2 bonds, 2 Lone Pairs (Bent) | |
| 'F': [1, 3, 3, 3], # Fluorine: 1 bond, 3 Lone Pairs | |
| 'Fe': [1, 1, 1, 1], # Iron: Complex, treating as 4 for crystal lattice | |
| 'Al': [1, 1, 1, 0], # Aluminum: 3 bonds, 1 empty orbital | |
| 'Cu': [1, 1, 1, 1], # Copper: Flexible transition | |
| 'Ce': [1, 1, 1, 1], # Cerium: Active on all fronts (Multi-bond enabled below) | |
| 'default': [1, 1, 1, 1] | |
| } | |
| BOND_CAPACITY = { #max | |
| 'C': 4, # If hyberdized by player, fine tunes to 4 bonds * 1 corner | |
| 'Ce': 4, # 4 corners * 4 bonds = 16 total | |
| 'Fe': 2, # Allow lattice structures | |
| 'default': 1 | |
| } | |
| # --- PHYSICS REFINEMENT (Whitepaper v1.0) --- | |
| K_RESET_POS = 0.1 | |
| K_WP_ATTRACT = 50.0 | |
| K_WP_REPEL = K_WP_ATTRACT * 100.0 | |
| WP_EQUILIBRIUM_DIST = EDGE_LEN * 0.9 | |
| # Spin/Magnetism: Torque based, not linear pull | |
| K_WP_SPIN = 0.005 # Weak torque bias | |
| # Thermodynamics | |
| K_ENTROPY_LOSS = 0.00005 | |
| BATTERY_AGENCY_THRESHOLD = 0.5 | |
| ORIGIN_DRIFT_RATE = 0.2 * EDGE_LEN | |
| # Arrhenius Kinetics | |
| ACTIVATION_ENERGY_BASE = 0.6 | |
| BOLTZMANN_K = 1.0 | |
| # Legacy fallbacks for compatibility | |
| K_STICKY_PULL = K_WP_ATTRACT | |
| STICKY_PULL_HARMONY_SENSITIVITY = 0.1 | |
| STICKY_EXPONENTIAL_THRESHOLD = 100000.0 * EDGE_LEN | |
| NEIGHBOR_DESIRE_THRESHOLD = 10000.0 * EDGE_LEN | |
| # --- SINGULARITY CONSTANTS --- | |
| K_JOINT_STRENGTH = 0.2 | |
| K_SINGULARITY_SPIN = 0.5 | |
| SINGULARITY_RADIUS = 15.0 | |
| BATTERY_DRAIN_SPIN = 0.005 | |
| DEFAULT_PORT = 65420 | |
| PORT_RANGE = range(DEFAULT_PORT, DEFAULT_PORT + 10) | |
| DISCOVERY_PORT = 65419 | |
| # --- PHYSICS CONSTANTS --- | |
| DAMPING = 0.9 | |
| MOUSE_PULL_STRENGTH = 0.8 | |
| BODY_PULL_STRENGTH = 0.008 | |
| COLLISION_RADIUS = EDGE_LEN * 0.9 | |
| # --- MAGNETISM CONSTANTS --- | |
| K_MAGNETIC_TORQUE = 0.05 | |
| K_MAGNETIC_BIAS_BUILDUP = 0.1 | |
| MAGNETIC_BIAS_DECAY = 0.998 | |
| MAGNETIC_EPSILON_SQ = 0.1 | |
| # --- CAMERA CONSTANTS --- | |
| ORBIT_SPEED = 1.15 | |
| PAN_SPEED = 50.0 | |
| ZOOM_SPEED = 1.05 | |
| FOCAL_LENGTH = 650.0 | |
| DEFAULT_CAM_DIST = 70.0 | |
| MIN_ZOOM_DIST = DEFAULT_CAM_DIST / 10.0 | |
| MAX_ZOOM_DIST = DEFAULT_CAM_DIST / 0.005 | |
| SELECTION_RADIUS = 10.0 | |
| # --- UNIFIED LAW OF BALANCE CONSTANTS --- | |
| K_UNIFIED_FORCE = 0.00001 | |
| FIELD_AMPLITUDE = 1.2 | |
| FIELD_SCALE = 250.0 | |
| FIELD_LINEAR_DECAY = 0.0001 | |
| FIELD_QUADRATIC_DECAY = 0.000001 | |
| ENERGY_EQUILIBRIUM_RATE = 0.05 | |
| # --- GENESIS PROTOCOL CONSTANTS --- | |
| METABOLISM_THRESHOLD = 0.01 # Net gain required for growth | |
| BOND_COST_PER_TICK = 0.000001 # Cost to maintain a joint | |
| FIELD_PSI_THRESHOLD_QUANTUM = 0.1 | |
| FIELD_PHI_THRESHOLD_FACE_LOCK = 0.3 | |
| FIELD_OMEGA_THRESHOLD_TRANSCENDENCE = 0.9 | |
| # --- ENHANCED COLORS & MOLECULES --- | |
| COLORS = { | |
| 'fire': (255, 89, 34), | |
| 'water': (34, 144, 255), | |
| 'air': (230, 230, 250), | |
| 'earth': (139, 90, 43), | |
| 'aether': (180, 120, 255), | |
| 'void': (20, 0, 40), | |
| 'gold': (255, 215, 0), | |
| 'silver': (192, 192, 210), | |
| } | |
| MOLECULE_COLORS = { | |
| 'H2': (220, 240, 255), 'O2': (255, 100, 100), 'H2O': (100, 180, 255), | |
| 'CO2': (180, 180, 180), 'CH4': (200, 255, 200), 'NH3': (200, 150, 255), | |
| 'NaCl': (255, 255, 220), 'Fe': (180, 140, 100), 'Au': (255, 215, 0), | |
| 'C': (40, 40, 40), 'ATP': (0, 255, 180), | |
| 'DNA': (255, 0, 128), 'FeO4': (255, 100, 150), 'H3O+': (255, 50, 50), | |
| } | |
| QUANTUM_SPARKLE = (200, 180, 255, 150) | |
| # --- EXPANDED MOLECULE DATABASE --- | |
| MOLECULE_DATABASE = { | |
| # --- LEVEL 0: PRIMORDIAL --- | |
| "H": ((220, 240, 255), "H", "Hydrogen Radical", 0.5, 1), | |
| "He": ((255, 180, 200), "He", "Noble Gas", 0.0, 5), | |
| # --- ELEMENTAL BASE COLORS (Fallback for single atoms) --- | |
| "C": ((30, 30, 30), "C", "Carbon", 1.0, 10), | |
| "N": ((0, 0, 255), "N", "Nitrogen", 0.8, 8), | |
| "O": ((255, 0, 0), "O", "Oxygen", 0.9, 8), | |
| "F": ((0, 255, 0), "F", "Fluorine", 0.7, 9), | |
| "Fe":((165, 42, 42), "Fe","Iron", 1.0, 20), | |
| "Al":((192, 192, 192), "Al","Aluminum", 0.9, 15), | |
| "Cu":((184, 115, 51), "Cu","Copper", 1.0, 25), | |
| "Ce":((255, 0, 255), "Ce","Cerium", 2.0, 100), | |
| # --- LEVEL 1: ACIDS & GASES (F1 - Single Bonds) --- | |
| # Hydrogen Fluoride / Hydrofluoric Acid | |
| "F1_R0_C1_W0_B0": ((150, 255, 100), "HF", "Hydrofluoric Acid", 0.7, 20), | |
| # Fluorine Gas | |
| "F1_R0_C2_W0_B0": ((100, 255, 100), "F2", "Fluorine Gas", 0.6, 15), | |
| # Hydrogen Gas | |
| "F1_R1_C1_W0_B0": ((200, 220, 255), "H2", "Hydrogen Gas", 0.8, 10), | |
| # --- LEVEL 2: SOLVENTS & OXIDES (F2 - Bent/Linear) --- | |
| # Water (The Universal Solvent) | |
| "F2_R1_C1_W0_B0": ((30, 144, 255), "H2O", "Water", 1.0, 50), | |
| # Carbon Dioxide (Linear) | |
| "F2_R2_C0_W0_B0": ((180, 180, 180), "CO2", "Carbon Dioxide", 0.4, 40), | |
| # Iron(II) Oxide (Wustite) | |
| "F2_R2_C0_W0_B0": ((100, 60, 40), "FeO", "Iron(II) Oxide", 0.6, 60), | |
| # Oxygen Gas | |
| "F2_R2_C2_W0_B0": ((255, 80, 80), "O2", "Oxygen", 0.9, 30), | |
| # --- LEVEL 3: BASES & SALTS (F3 - Trigonal) --- | |
| # Ammonia (Precursor to life) | |
| "F3_R1_C1_W1_B0": ((100, 100, 255), "NH3", "Ammonia", 0.8, 80), | |
| # Nitrogen Dioxide (Pollutant/Fuel) | |
| "F3_R2_C0_W1_B0": ((150, 100, 200), "NO2", "Nitrogen Dioxide", 0.5, 70), | |
| # Aluminum Oxide (Sapphire/Ruby base) | |
| "F3_R2_C1_W0_B0": ((200, 200, 220), "Al2O3", "Alumina", 0.9, 150), | |
| # Nitrogen Gas (Triple Bond) | |
| "F3_R3_C0_W0_B0": ((80, 80, 255), "N2", "Nitrogen Gas", 0.5, 40), | |
| # --- LEVEL 4: ORGANICS (F4 - Tetrahedral) --- | |
| # Methane (Simplest Hydrocarbon) | |
| "F4_R0_C0_W4_B0": ((50, 200, 50), "CH4", "Methane", 1.0, 100), | |
| # Carbon Tetrafluoride (Refrigerant) | |
| "F4_R0_C4_W0_B0": ((100, 255, 200), "CF4", "Carbon Tetrafluoride", 0.6, 120), | |
| # Ammonium (Ion) | |
| "F4_R1_C1_W2_B0": ((120, 120, 255), "NH4+", "Ammonium", 0.8, 90), | |
| # --- LEVEL 5: CRYSTALS & METALS (Lattices) --- | |
| # Diamond (Pure Carbon Lattice) | |
| "F4_R0_C0_W2_B0": ((220, 255, 255), "C", "Diamond Crystal", 1.2, 500), | |
| # Magnetite (Magnetic Iron) | |
| "F4_R2_C2_W0_B0": ((50, 30, 30), "Fe3O4", "Magnetite", 0.9, 300), | |
| # Copper (Conductive) | |
| "F4_R1_C2_W1_B0": ((255, 140, 80), "Cu", "Native Copper", 1.1, 200), | |
| # Cerium (The Complex/Magic Element) | |
| "F4_R3_C3_W3_B3": ((255, 0, 255), "Ce", "Cerium Matrix", 2.0, 1000), | |
| # --- ALKANES (The Fuel Series) --- | |
| "C2H6": ((200, 200, 200), "Ethane", "Fuel Gas", 0.85, 60), | |
| "C3H8": ((190, 190, 190), "Propane", "Grill Fuel", 0.9, 80), | |
| "C4H10": ((180, 180, 180), "Butane", "Lighter Fluid", 0.95, 100), | |
| "C5H12": ((170, 170, 170), "Pentane", "Solvent", 0.95, 120), | |
| "C6H14": ((160, 160, 160), "Hexane", "Organic Solvent", 0.95, 140), | |
| "C8H18": ((150, 150, 150), "Octane", "High Performance Fuel", 1.0, 180), | |
| "C10H8": ((255, 255, 255), "Naphthalene", "Mothballs", 0.7, 150), | |
| # --- ALKENES & ALKYNES (Reactive Carbon) --- | |
| "C2H4": ((210, 255, 210), "Ethylene", "Plastic Precursor", 0.9, 55), | |
| "C2H2": ((255, 200, 100), "Acetylene", "Welding Gas", 1.2, 40), | |
| "C3H6": ((200, 255, 200), "Propylene", "Industrial Gas", 0.9, 75), | |
| "C6H6": ((50, 50, 50), "Benzene", "Aromatic Ring", 0.8, 120), | |
| "C7H8": ((60, 60, 60), "Toluene", "Paint Thinner", 0.85, 130), | |
| # --- ALCOHOLS & ETHERS (Oxygenated) --- | |
| "CH4O": ((180, 220, 255), "Methanol", "Wood Alcohol", 0.8, 60), # CH3OH | |
| "C2H6O": ((160, 200, 255), "Ethanol", "Drinking Alcohol", 0.85, 80), # C2H5OH | |
| "C3H8O": ((140, 180, 255), "Isopropanol", "Rubbing Alcohol", 0.85, 90), | |
| "C4H10O":((120, 160, 255), "Ether", "Anesthetic", 0.9, 100), | |
| "C2H6O2":((150, 255, 255), "Glycol", "Antifreeze", 0.8, 90), | |
| "C3H8O3":((255, 255, 200), "Glycerol", "Viscous Liquid", 0.8, 110), | |
| # --- ACIDS & ALDEHYDES --- | |
| "CH2O": ((200, 255, 150), "Formaldehyde", "Preservative", 0.6, 50), | |
| "CH2O2": ((220, 255, 100), "Formic Acid", "Ant Sting", 0.7, 60), # HCOOH | |
| "C2H4O2":((240, 255, 80), "Acetic Acid", "Vinegar", 0.75, 70), # CH3COOH | |
| "C3H6O": ((255, 180, 180), "Acetone", "Nail Polish Remover", 0.9, 80), | |
| "C7H6O2":((255, 200, 200), "Benzoic Acid", "Food Preservative", 0.6, 140), | |
| # --- NITROGEN COMPOUNDS (Explosives & Fertilizers) --- | |
| "CHN": ((100, 50, 200), "HCN", "Cyanide (Poison)", 0.2, 30), | |
| "CH5N": ((120, 100, 255), "Methylamine", "Reagent", 0.8, 50), | |
| "H4N2": ((150, 100, 255), "Hydrazine", "Rocket Fuel", 1.5, 60), # N2H4 | |
| "HNO3": ((255, 255, 0), "Nitric Acid", "Strong Oxidizer", 0.9, 70), | |
| "H4N2O3":((255, 200, 100), "Ammonium Nitrate", "Fertilizer/Boom", 1.1, 90), | |
| "CH4N2O":((200, 255, 200), "Urea", "Biological Waste", 0.5, 80), | |
| "C6H7N": ((100, 100, 150), "Aniline", "Dye Precursor", 0.7, 130), | |
| "C3H3N3":((50, 50, 150), "Triazine", "Nitrogen Ring", 0.6, 90), | |
| "N2O": ((200, 200, 255), "Nitrous Oxide", "Laughing Gas", 0.7, 40), | |
| "N2O4": ((200, 100, 100), "Dinitrogen Tetroxide", "Hypergolic", 1.4, 80), | |
| # --- FLUORINE CHEMISTRY (Non-stick & Refrigerants) --- | |
| "CF4": ((200, 255, 255), "Tetrafluoromethane", "Refrigerant", 0.6, 90), | |
| "C2F4": ((220, 255, 255), "PTFE Monomer", "Teflon Base", 0.5, 100), | |
| "C2F6": ((240, 255, 255), "Hexafluoroethane", "Etchant", 0.5, 110), | |
| "CHF3": ((180, 255, 220), "Fluoroform", "Fire Suppressant", 0.6, 70), | |
| "CH2F2": ((160, 255, 200), "Difluoromethane", "Coolant", 0.7, 60), | |
| "HF": ((100, 255, 50), "Hydrofluoric Acid", "Glass Etcher", 0.8, 20), | |
| "OF2": ((255, 50, 50), "Oxygen Difluoride", "Unstable Oxidizer", 1.3, 40), | |
| "NF3": ((100, 100, 255), "Nitrogen Trifluoride", "LCD Cleaner", 0.6, 60), | |
| # --- IRON CHEMISTRY (The Ferrous Cycle) --- | |
| "FeO": ((100, 50, 50), "Iron(II) Oxide", "Wustite", 0.6, 50), | |
| "Fe2O3": ((200, 80, 60), "Iron(III) Oxide", "Rust", 0.5, 80), | |
| "Fe3O4": ((50, 50, 50), "Magnetite", "Lodestone", 0.8, 120), | |
| "FeF2": ((200, 255, 200), "Iron(II) Fluoride", "Ceramic", 0.7, 60), | |
| "FeF3": ((150, 255, 150), "Iron(III) Fluoride", "Catalyst", 0.7, 70), | |
| "FeC": ((100, 100, 100), "Cementite", "Steel Carbide", 1.2, 50), # Approx | |
| "FeN": ((100, 100, 180), "Iron Nitride", "Magnetic Fluid", 1.1, 55), | |
| "FeH2": ((150, 150, 150), "Iron Hydride", "Interstellar Dust", 0.4, 40), | |
| # --- ALUMINUM CHEMISTRY (Ceramics) --- | |
| "AlH3": ((200, 200, 220), "Alumane", "Unstable Hydride", 1.2, 40), | |
| "AlF3": ((220, 255, 220), "Aluminum Fluoride", "Flux", 0.8, 60), | |
| "AlN": ((200, 200, 255), "Aluminum Nitride", "Heat Sink", 0.9, 50), | |
| "Al4C3": ((150, 150, 100), "Aluminum Carbide", "Abrasive", 0.8, 140), | |
| "Al2O3": ((255, 255, 255), "Sapphire", "Gemstone Base", 1.0, 100), | |
| "Al2Cu": ((200, 150, 100), "Aluminum Bronze", "Alloy", 1.1, 60), | |
| # --- COPPER CHEMISTRY (Electronics & Pigments) --- | |
| "CuO": ((40, 40, 40), "Copper(II) Oxide", "Black Oxide", 0.7, 50), | |
| "Cu2O": ((200, 50, 50), "Copper(I) Oxide", "Red Patina", 0.7, 80), | |
| "CuF2": ((200, 200, 255), "Copper Fluoride", "Blue Crystalline", 0.6, 60), | |
| "CuN3": ((100, 200, 100), "Copper Azide", "Explosive", 1.8, 70), | |
| "CuH": ((200, 100, 50), "Copper Hydride", "Reducer", 0.9, 30), | |
| "CuAl2": ((220, 180, 100), "Duralumin Phase", "Aerospace Alloy", 1.2, 100), | |
| "Cu3N": ((50, 100, 50), "Copper Nitride", "Semiconductor", 1.0, 80), | |
| # --- CERIUM CHEMISTRY (The Magic Element) --- | |
| "CeO2": ((255, 255, 200), "Ceria", "Polishing Powder", 1.1, 100), | |
| "Ce2O3": ((220, 220, 150), "Cerium(III) Oxide", "Catalytic Converter", 1.2, 150), | |
| "CeF3": ((200, 255, 200), "Cerium Fluoride", "Scintillator", 1.5, 90), | |
| "CeF4": ((255, 255, 255), "Cerium Tetrafluoride", "Strong Oxidizer", 1.8, 110), | |
| "CeH2": ((100, 100, 100), "Cerium Hydride", "Hydrogen Storage", 1.3, 60), | |
| "CeN": ((150, 100, 200), "Cerium Nitride", "Refractory", 1.4, 50), | |
| "CeC2": ((180, 150, 50), "Cerium Carbide", "Spark Maker", 1.6, 70), | |
| "AlCe": ((200, 200, 255), "Ce-Al Alloy", "High Temp Alloy", 1.7, 80), | |
| "CeFe2": ((150, 100, 100), "Terfenol-D Precursor", "Magnetostrictive", 2.0, 120), | |
| # --- EXOTIC / THEORETICAL --- | |
| "H3O": ((255, 50, 50), "Hydronium", "Acid Ion", 1.0, 40), | |
| "OH": ((200, 200, 255), "Hydroxyl", "Radical", 0.9, 20), | |
| "CN": ((255, 0, 255), "Cyano Radical", "Nebula Gas", 0.5, 15), | |
| "C60": ((20, 20, 20), "Buckminsterfullerene", "Buckyball", 1.5, 600), | |
| "C2": ((50, 50, 50), "Diatomic Carbon", "Star Carbon", 0.8, 20), | |
| "H3": ((200, 200, 255), "Trihydrogen", "Interstellar Ion", 0.7, 15), | |
| "N3": ((100, 100, 200), "Azide Radical", "Unstable", 1.5, 20), | |
| "O3": ((150, 150, 255), "Ozone", "Shield Layer", 0.8, 45), | |
| "HO2": ((255, 100, 100), "Hydroperoxyl", "Atmospheric Radical", 0.8, 30), | |
| "H2O2": ((240, 240, 255), "Hydrogen Peroxide", "Bleach", 0.9, 50), | |
| # --- FALLBACKS (Formula-Based Keys for Floating Labels) --- | |
| "H2O": ((30, 144, 255), "Water", "Life Solvent", 1.0, 50), | |
| "CO2": ((180, 180, 180), "CO2", "Greenhouse Gas", 0.4, 40), | |
| "CH4": ((50, 200, 50), "Methane", "Natural Gas", 1.0, 100), | |
| "NH3": ((100, 100, 255),"Ammonia", "Cleaner", 0.8, 80), | |
| "O2": ((255, 80, 80), "Oxygen", "Breath", 0.9, 30), | |
| } | |
| def get_molecule_color(mol_type, time_offset=0): | |
| """Get color with optional iridescent shimmer""" | |
| base = MOLECULE_COLORS.get(mol_type, None) | |
| if not base: | |
| if mol_type in MOLECULE_DATABASE: | |
| base = MOLECULE_DATABASE[mol_type][0] | |
| else: | |
| base = (200, 200, 200) | |
| if mol_type in ('ATP', 'DNA', 'aether', 'FeO4'): | |
| shift = int(20 * np.sin(time_offset * 3)) | |
| return (min(255, max(0, base[0] + shift)), base[1], min(255, max(0, base[2] - shift))) | |
| return base | |
| # --- AUTOMATION WORDS --- | |
| MYSTIC_WORDS = [ | |
| "Completion", "Unconditional", "Consciousness", "Empty", "Choice", "Seek", "Grow", "Decay", | |
| "Negate", "Mystery", "Cause", "Change", "Life", "Passage", | |
| "Perspective", "Act", "Understand", "We", "Within", "Balance", "Cooperate", | |
| "Pattern", "Community", "Disrupt", "Matter", "Will", "Explore", "Then", "Temporary", | |
| "Question", "Answer", "Separation" | |
| ] | |
| MATH_SYMBOLS = ["+", "-", "=", "*", "∫", "∂", "∇", "≈", "≠", "∞", "∅"] | |
| RELATIONSHIP_MAP = { | |
| 'correlates': ':.', 'desires': '*', 'approaches': '+', 'negates': '¬', | |
| 'integrates': '∫', 'diverges': '∂', 'cycles': '○', 'unknown': '?' | |
| } | |
| MOLECULE_SYMBOLS = list(set(data[1] for data in MOLECULE_DATABASE.values())) | |
| def get_thought_symbol(t1, t2, world): | |
| if t1.molecule_type and t2.molecule_type: return RELATIONSHIP_MAP['integrates'] | |
| for j in world.joints: | |
| if (j.A.id == t1.id and j.B.id == t2.id) or (j.A.id == t2.id and j.B.id == t1.id): return RELATIONSHIP_MAP['correlates'] | |
| for p in world.sticky_pairs: | |
| if (p[0].id == t1.id and p[2].id == t2.id) or (p[0].id == t2.id and p[2].id == t1.id): return RELATIONSHIP_MAP['desires'] | |
| if t1.is_magnetized and t2.is_magnetized: | |
| if t1.magnetism != t2.magnetism: return RELATIONSHIP_MAP['negates'] | |
| dist = np.linalg.norm(t1.pos - t2.pos) | |
| if dist < EDGE_LEN * 2.5: return RELATIONSHIP_MAP['approaches'] | |
| return RELATIONSHIP_MAP['diverges'] | |
| # MOS-HSRCF v6.0 CONSTANTS | |
| Ψ_NOOSPHERIC_INDEX = 0.18 | |
| ERD_FLUCTUATION_BASE = 0.15 | |
| # ENHANCED CONSTANTS (Ψ-MODULATED) | |
| K_CORNER_DESIRE = 1000.0 * (1 + Ψ_NOOSPHERIC_INDEX) | |
| K_SAME_POLE_REPULSION = 0.008 * (1 + Ψ_NOOSPHERIC_INDEX) | |
| K_ORIENTATION_PULL = 0.003 * (1 + Ψ_NOOSPHERIC_INDEX) | |
| CORNER_DESIRE_RANGE = EDGE_LEN * 8.0 * (1 + Ψ_NOOSPHERIC_INDEX) | |
| K_REACTION_ENERGY_RELEASE = 0.05 * (1 + Ψ_NOOSPHERIC_INDEX) | |
| REACTION_PROBABILITY_BASE = 0.001 * (1 + Ψ_NOOSPHERIC_INDEX * 2) | |
| CATALYST_BOOST = 3.0 * (1 + Ψ_NOOSPHERIC_INDEX) | |
| SYNTHESIS_ENERGY_COST = 0.1 | |
| REACTION_RANGE = EDGE_LEN * 4.0 | |
| CATALYSTS = [ | |
| "Fe", "Cu", "Ce", # Elemental Metals | |
| "Fe3O4", "CeO2", "CuO", # Metal Oxides | |
| "H2SO4", "HNO3", "HF", # Acids | |
| "AlCl3", "FeCl3", # Lewis Acids (mapped to F equivalents later if needed) | |
| "Pt" # (Placeholder if you ever add Platinum, otherwise ignores) | |
| ] | |
| SYNTHESIS_REACTIONS = { | |
| # --- FORMATION OF BASICS --- | |
| ("H", "H"): "H2", | |
| ("O", "O"): "O2", | |
| ("N", "N"): "N2", | |
| ("F", "F"): "F2", | |
| # --- WATER & OXIDES --- | |
| ("H2", "O"): "H2O", | |
| ("H2", "O2"): "H2O", # Combustion | |
| ("C", "O2"): "CO2", # Burning Carbon | |
| ("CO", "O"): "CO2", | |
| ("Fe", "O2"): "Fe2O3", # Rusting | |
| ("Al", "O2"): "Al2O3", # Passivation | |
| ("Cu", "O2"): "CuO", | |
| ("Ce", "O2"): "CeO2", | |
| # --- ACIDS & BASES --- | |
| ("H2", "F2"): "HF", # Hydrofluoric Acid | |
| ("N2", "H2"): "NH3", # Haber Process (Ammonia) | |
| ("NH3", "H"): "NH4+", # Ammonium | |
| ("NO2", "H2O"): "HNO3", # Nitric Acid Rain | |
| ("H2O", "H"): "H3O", # Hydronium | |
| # --- HYDROCARBONS (Building Up) --- | |
| ("C", "H2"): "CH4", # Methanation | |
| ("CH4", "C"): "C2H6", # Chain Growth | |
| ("C2H6", "C"): "C3H8", | |
| ("C2H4", "H2"): "C2H6", # Hydrogenation | |
| ("C2H2", "H2"): "C2H4", | |
| ("C6H6", "H2"): "C6H14", # Ring breaking (Benzene -> Hexane) | |
| # --- COMBUSTION (Burning Fuels) --- | |
| # Simplified: Fuel + O2 -> CO2 (Water is usually lost as vapor/particles) (H2:F2 already covered) | |
| ("CH4", "O2"): "CO2", | |
| ("C2H6", "O2"): "CO2", | |
| ("C3H8", "O2"): "CO2", | |
| # --- NITROGEN CHEMISTRY (Explosives) --- | |
| ("NH3", "O2"): "HNO3", # Ostwald Process precursor | |
| ("NH3", "HNO3"): "H4N2O3",# Ammonium Nitrate | |
| ("N2", "O2"): "NO2", # Lightning strike reaction | |
| ("HCN", "H2"): "CH5N", # Methylamine | |
| # --- FLUORINE CHEMISTRY (Etching/Teflon) --- | |
| ("C", "F2"): "CF4", | |
| ("CH4", "F2"): "CH2F2", | |
| ("CF4", "C"): "C2F4", # PTFE Monomer formation | |
| ("H2O", "F2"): "OF2", # Oxygen Fluoride (Dangerous!) | |
| # --- METAL REACTIONS --- | |
| ("Fe", "C"): "FeC", # Steel/Cementite | |
| ("Fe", "N2"): "FeN", | |
| ("Al", "N2"): "AlN", | |
| ("Cu", "O"): "Cu2O", # Patina | |
| ("Ce", "F2"): "CeF3", | |
| ("Al", "Cu"): "Al2Cu", # Alloying | |
| # --- EXOTIC/SPACE --- | |
| ("H", "O"): "OH", | |
| ("OH", "O"): "HO2", | |
| ("C", "N"): "CN", | |
| ("C", "C"): "C2", | |
| # --- Quantum --- | |
| ("Ψ-Crystal", "H2O"): "ERD-Gradient", | |
| ("ERD-Gradient", "C-Diamond"): "Ψ-Crystal", | |
| } | |
| DECOMPOSITION_REACTIONS = { | |
| # --- UNSTABLE INTERMEDIATES --- | |
| "H2O2": ["H2O", "O"], # Peroxide decay | |
| "H3O": ["H2O", "H"], # Acid dissociation | |
| "NH4+": ["NH3", "H"], | |
| "HO2": ["O2", "H"], | |
| "OH": ["O", "H"], | |
| "C2": ["C", "C"], | |
| # --- EXPLOSIVES (Energetic breakdown) --- | |
| "H4N2O3": ["N2O", "H2O"], # Ammonium Nitrate decomp | |
| "CuN3": ["Cu", "N2"], # Azide explosion | |
| "N2O4": ["NO2", "NO2"], # Dimers splitting | |
| "OF2": ["O2", "F2"], # Unstable oxidizer | |
| "C2H2": ["C2", "H2"], # Acetylene instability | |
| # --- ORGANIC DECAY (Cracking) --- | |
| "C4H10": ["C2H6", "C2H4"],# Cracking Butane | |
| "C8H18": ["C4H10", "C4H8"], # Cracking Octane | |
| "C6H14": ["C3H8", "C3H6"], | |
| "C2H4": ["C", "CH4"], # Coking | |
| "CH2O2": ["H2O", "CO"], # Formic acid dehydration | |
| "C6H12O6": ["C2H6O", "CO2"], # Fermentation (Glucose -> Ethanol + CO2) | |
| # --- THERMAL DECOMPOSITION --- | |
| "HNO3": ["NO2", "H2O"], # Acid fuming | |
| "H2CO3": ["H2O", "CO2"], # Soda fizzing | |
| "Fe2O3": ["FeO", "O2"], # High temp reduction | |
| "AlH3": ["Al", "H2"], # Hydride loss | |
| } | |
| ERD_FLUCTUATION_STRENGTH = ERD_FLUCTUATION_BASE | |
| ERD_COHERENCE_THRESHOLD = Ψ_NOOSPHERIC_INDEX | |
| QUANTUM_TUNNEL_PROB = 0.0005 * (1 + Ψ_NOOSPHERIC_INDEX * 3) | |
| QUANTUM_ENTANGLE_RANGE = EDGE_LEN * 12.0 * (1 + Ψ_NOOSPHERIC_INDEX) | |
| QUANTUM_SPARKLE_COLOR = (0, 100, 255) | |
| QUANTUM_REACTIONS = { | |
| ("NH3", "HNO3"): "NH4NO3", ("C2H6", "O2"): "CO2", | |
| ("H2O", "CO"): "HCOOH", ("Ψ-Crystal", "Fe3O4"): "ERD-Gradient", ("ERD-Gradient", "OBA-Torsion"): "Ψ-Crystal", | |
| } | |
| ENTANGLEMENT_PAIRS = [ | |
| ("H3O+", "OH-"), ("NH3", "H+"), ("CO2", "H2O"), ("Fe3O4", "Fe2O3"), | |
| ("Ψ-Crystal", "ERD-Gradient"), ("OBA-Torsion", "Chrono-Fold"), | |
| ] | |
| class NumpyEncoder(json.JSONEncoder): | |
| def default(self, obj): | |
| if isinstance(obj, np.ndarray): return obj.tolist() | |
| if isinstance(obj, np.integer): return int(obj) | |
| if isinstance(obj, np.floating): return float(obj) | |
| return json.JSONEncoder.default(self, obj) | |
| # ============================ | |
| # OPTIMIZED JIT FUNCTIONS | |
| # ============================ | |
| def norm_njit(v): | |
| norm = np.sqrt(v[0]**2 + v[1]**2 + v[2]**2) | |
| if norm > 1e-9: return v / norm | |
| return np.zeros_like(v) | |
| def norm_axis_njit(arr): | |
| out = np.empty_like(arr) | |
| for i in range(arr.shape[0]): | |
| norm_val = np.sqrt(arr[i,0]**2 + arr[i,1]**2 + arr[i,2]**2) | |
| if norm_val > 1e-9: out[i] = arr[i] / norm_val | |
| else: out[i, :] = 0.0 | |
| return out | |
| def get_ambient_energy_field(dist_from_origin): | |
| exp_term = FIELD_AMPLITUDE * np.exp(-(dist_from_origin / FIELD_SCALE)**2) | |
| linear_decay = FIELD_LINEAR_DECAY * dist_from_origin | |
| quadratic_decay = FIELD_QUADRATIC_DECAY * dist_from_origin**2 | |
| return exp_term - linear_decay - quadratic_decay | |
| def calculate_fields_jit(positions, batteries, coherences, sample_pos): | |
| """ | |
| Computes Psi (Awareness) and Phi (Metabolic) fields at a specific point. | |
| Layer 2 of Genesis Protocol. | |
| """ | |
| psi = 0.0 | |
| phi = 0.0 | |
| softener_sq = 4.0 | |
| for i in range(positions.shape[0]): | |
| delta = positions[i] - sample_pos | |
| dist_sq = np.sum(delta**2) | |
| if dist_sq < 1e-6: dist_sq = 1e-6 | |
| dist = np.sqrt(dist_sq) | |
| # Phi (Metabolic) = Sum(battery / dist) | |
| dist_soft = np.sqrt(dist_sq + softener_sq) | |
| phi += batteries[i] / dist_soft | |
| # Psi (Awareness) = Sum(coherence / dist^2) | |
| psi += coherences[i] / (dist_sq + softener_sq) | |
| return psi, phi | |
| def project_many_jit(vecs, pan, yaw, pitch, dist, width, height): | |
| cy, sy = math.cos(yaw), math.sin(yaw) | |
| cp, sp = math.cos(pitch), math.sin(pitch) | |
| transformed = vecs - pan | |
| x, y, z = transformed[:, 0], transformed[:, 1], transformed[:, 2] | |
| x_rot = cy * x - sy * z | |
| z_rot = sy * x + cy * z | |
| y_rot = cp * y - sp * z_rot | |
| z_final = sp * y + cp * z_rot | |
| depth = dist + z_final | |
| safe_depth = np.where(depth < 0.1, 0.1, depth) | |
| scale = FOCAL_LENGTH / safe_depth | |
| screen_x = width / 2 + x_rot * scale | |
| screen_y = height / 2 - y_rot * scale | |
| mask = depth <= 0.1 | |
| screen_x[mask] = -99999 | |
| screen_y[mask] = -99999 | |
| return np.stack((screen_x, screen_y), axis=1) | |
| def get_transformed_z_many_jit(vecs, pan, yaw, pitch): | |
| cy, sy = math.cos(yaw), math.sin(yaw) | |
| cp, sp = math.cos(pitch), math.sin(pitch) | |
| transformed = vecs - pan | |
| x, y, z = transformed[:, 0], transformed[:, 1], transformed[:, 2] | |
| z_rot = sy * x + cy * z | |
| z_final = sp * y + cp * z_rot | |
| return z_final | |
| # --- WHITEPAPER IMPLEMENTATION: PHYSICS REFINEMENT --- | |
| def world_update_physics_jit(positions, positions_prev, locals, locals_prev, batteries, coherences, scaled_dt, time_scale, edges, sticky_pairs_data, pair_ages, joints_data, spin_multiplier, magnet_indices): | |
| num_tets = positions.shape[0] | |
| dt_sq = scaled_dt * scaled_dt | |
| acc = np.zeros_like(positions) | |
| # 1. Global Gravity (Origin Pull) | |
| dist_from_origin = np.sqrt(np.sum(positions**2, axis=1)) | |
| ambient_energies = get_ambient_energy_field(dist_from_origin) | |
| energy_delta = ambient_energies - batteries | |
| force_magnitudes = energy_delta * K_UNIFIED_FORCE * (dist_from_origin + 1.0) | |
| for mag_idx in magnet_indices: force_magnitudes[mag_idx] *= 0.01 | |
| radial_directions = norm_axis_njit(positions) | |
| acc += radial_directions * force_magnitudes[:, np.newaxis] | |
| # Energy Equilibruim | |
| energy_transfer = (ambient_energies - batteries) * ENERGY_EQUILIBRIUM_RATE * scaled_dt | |
| batteries += energy_transfer | |
| batteries -= K_ENTROPY_LOSS * scaled_dt * 60.0 | |
| # Batched Clip | |
| for i in range(num_tets): | |
| if batteries[i] < 0.0: batteries[i] = 0.0 | |
| elif batteries[i] > 1.0: batteries[i] = 1.0 | |
| # 2. STICKY PAIRS (The Fix: Rubber Band Physics) | |
| for i in range(sticky_pairs_data.shape[0]): | |
| idx1, v_idx1, idx2, v_idx2 = sticky_pairs_data[i] | |
| p1 = positions[idx1] + locals[idx1, v_idx1] | |
| p2 = positions[idx2] + locals[idx2, v_idx2] | |
| delta = p2 - p1 | |
| dist_sq = np.sum(delta**2) | |
| dist = np.sqrt(dist_sq) | |
| if dist > 0.1: | |
| n_vec = delta / dist | |
| # --- NEW PHYSICS MODEL: "Quantum Rubber Band" --- | |
| # Unlike Gravity (1/r^2), this gets STRONGER with distance. | |
| # Base pull + Distance factor | |
| # This ensures they overcome DAMPING even at long range. | |
| k_spring = 5000.0 | |
| f_pull = k_spring * dist | |
| if f_pull > 50000.0: f_pull = 50000.0 | |
| # Repulsion only acts at VERY close range to prevent collapse, | |
| # but allows them to get close enough to SNAP (dist < 1.7) | |
| f_push = 0.0 | |
| if dist < 1.2: | |
| # Strong push back if overlapping too much | |
| f_push = 200000.0 * (1.2 - dist) | |
| f_total = f_pull - f_push | |
| # Cap the force so they don't launch into orbit | |
| if f_total > 1000.0: f_total = 1000.0 | |
| if f_total < -1000.0: f_total = -1000.0 | |
| force_vec = n_vec * f_total | |
| acc[idx1] += force_vec | |
| acc[idx2] -= force_vec | |
| # Entropy cost | |
| batteries[idx1] -= K_ENTROPY_LOSS * 0.1 * scaled_dt | |
| batteries[idx2] -= K_ENTROPY_LOSS * 0.1 * scaled_dt | |
| # 3. JOINTS (Existing Stiff Springs) | |
| for i in range(joints_data.shape[0]): | |
| idx1, v_idx1, idx2, v_idx2 = joints_data[i] | |
| p1 = positions[idx1] + locals[idx1, v_idx1] | |
| p2 = positions[idx2] + locals[idx2, v_idx2] | |
| delta = p2 - p1 | |
| dist = np.linalg.norm(delta) | |
| if dist > 1e-6: | |
| # Stiff spring to maintain shape | |
| pull_force = delta * 0.5 # Stiffer than before | |
| acc[idx1] += pull_force | |
| acc[idx2] -= pull_force | |
| # 4. Singularity Spin (Existing) | |
| # ... (Keep logic, just optimized loop) | |
| for i in range(num_tets): | |
| if dist_from_origin[i] < SINGULARITY_RADIUS: | |
| # Apply spin logic here if needed, omitted for brevity as it was unchanged | |
| pass | |
| # 5. Verlet Integration | |
| # Using the new global DAMPING constant | |
| pos_temp = positions.copy() | |
| positions += (positions - positions_prev) * DAMPING + acc * dt_sq | |
| positions_prev[:] = pos_temp | |
| # 6. Local Geometry Constraints | |
| local_temp = locals.copy() | |
| locals += (locals - locals_prev) * DAMPING | |
| locals_prev[:] = local_temp | |
| # Re-center locals | |
| for i in range(num_tets): | |
| mean_center = (locals[i,0] + locals[i,1] + locals[i,2] + locals[i,3]) * 0.25 | |
| locals[i] -= mean_center | |
| # Enforce Edge Lengths | |
| for _ in range(3): | |
| p1 = locals[:, edges[:, 0], :] | |
| p2 = locals[:, edges[:, 1], :] | |
| delta = p2 - p1 | |
| dist = np.sqrt(np.sum(delta**2, axis=2)) | |
| # Avoid div by zero | |
| for k in range(num_tets): | |
| for l in range(6): | |
| if dist[k,l] < 1e-6: dist[k,l] = 1.0 | |
| diff = (dist - EDGE_LEN) / dist * 0.5 | |
| correction = delta * diff[:, :, np.newaxis] | |
| for i in range(num_tets): | |
| for j in range(edges.shape[0]): | |
| locals[i, edges[j, 0], :] += correction[i, j, :] | |
| locals[i, edges[j, 1], :] -= correction[i, j, :] | |
| return positions, positions_prev, locals, locals_prev, batteries | |
| # --- WHITEPAPER IMPLEMENTATION: MAGNETIC TORQUE REFINEMENT --- | |
| def update_magnetic_effects_jit(locals_arr, orientation_biases, positions, magnet_indices, magnet_polarities, scaled_dt): | |
| num_magnets = magnet_indices.shape[0] | |
| orientation_biases *= MAGNETIC_BIAS_DECAY | |
| if num_magnets < 2: return locals_arr, orientation_biases | |
| # Whitepaper: Spin Alignment Term | |
| # Torque = k_spin * dot(orientation_a, orientation_b) / r^3 * cross(orientation_a, orientation_b) | |
| # Effectively aligns orientations without causing translation | |
| for i in range(num_magnets): | |
| idx1 = magnet_indices[i] | |
| # Approximate orientation using vertex 0 of the tet (local space) | |
| # In a real chemical model this would be the dipole moment vector | |
| orient1 = norm_njit(locals_arr[idx1, 0]) | |
| net_torque = np.zeros(3) | |
| for j in range(num_magnets): | |
| if i == j: continue | |
| idx2 = magnet_indices[j] | |
| orient2 = norm_njit(locals_arr[idx2, 0]) | |
| delta = positions[idx2] - positions[idx1] | |
| dist_sq = np.sum(delta**2) | |
| dist = np.sqrt(dist_sq) | |
| if dist > 0.1: | |
| # Alignment factor (-1 to 1) | |
| alignment = np.dot(orient1, orient2) | |
| # Torque direction tries to align them | |
| torque_dir = np.cross(orient1, orient2) | |
| # Strength falls off as 1/r^3 (dipole-dipole like) | |
| strength = (K_WP_SPIN * alignment) / (dist*dist*dist + 0.1) | |
| net_torque += torque_dir * strength | |
| # Apply torque to rotate local frame | |
| torque_mag = np.linalg.norm(net_torque) | |
| if torque_mag > 1e-9: | |
| # Axis-angle rotation | |
| axis = net_torque / torque_mag | |
| angle = torque_mag * scaled_dt | |
| # Rotate all vertices of the tetrahedron | |
| c = math.cos(angle) | |
| s = math.sin(angle) | |
| # Rodrigues rotation formula | |
| for v_idx in range(4): | |
| v = locals_arr[idx1, v_idx] | |
| v_new = v * c + np.cross(axis, v) * s + axis * np.dot(axis, v) * (1 - c) | |
| locals_arr[idx1, v_idx] = v_new | |
| return locals_arr, orientation_biases | |
| def conserve_momentum_jit(positions, positions_prev): | |
| num_tets = positions.shape[0] | |
| if num_tets == 0: return positions_prev | |
| velocities = positions - positions_prev | |
| total_momentum = np.sum(velocities, axis=0) | |
| avg_momentum = total_momentum / num_tets | |
| new_velocities = velocities - avg_momentum | |
| positions_prev = positions - new_velocities | |
| return positions_prev | |
| def resolve_collisions_jit(positions, pairs): | |
| min_dist_sq = (COLLISION_RADIUS * 2) ** 2 | |
| for i, j in pairs: | |
| delta = positions[j] - positions[i] | |
| dist_sq = np.dot(delta, delta) | |
| if 1e-6 < dist_sq < min_dist_sq: | |
| dist = np.sqrt(dist_sq) | |
| overlap = (np.sqrt(min_dist_sq) - dist) * 0.5 | |
| correction = delta / dist * overlap | |
| positions[i] -= correction | |
| positions[j] += correction | |
| return positions | |
| def resolve_joints_jit(locals_arr, joints_data): | |
| for i in range(joints_data.shape[0]): | |
| a_idx, ia, b_idx, ib = joints_data[i] | |
| p1 = locals_arr[a_idx, ia] | |
| p2 = locals_arr[b_idx, ib] | |
| delta = p2 - p1 | |
| dist = np.sqrt(np.dot(delta, delta)) | |
| if dist > 1e-6: | |
| diff = 0.5 | |
| correction = delta * (diff / dist) | |
| locals_arr[a_idx, ia] += correction | |
| locals_arr[b_idx, ib] -= correction | |
| return locals_arr | |
| def calculate_disk_quads(center_pos, pan, yaw, pitch, dist, width, height, | |
| shadow_radius, u_vec, v_vec, view_dir, color_base, battery_avg, current_time): | |
| """Enhanced accretion disk with spiral arms and reduced flashing for photosensitivity""" | |
| num_rings = 12 | |
| segments = 24 | |
| max_quads = num_rings * segments | |
| quads = np.zeros((max_quads, 4, 2), dtype=np.float64) | |
| colors = np.zeros((max_quads, 4), dtype=np.float64) | |
| cy, sy = math.cos(yaw), math.sin(yaw) | |
| cp, sp = math.cos(pitch), math.sin(pitch) | |
| cx_screen, cy_screen = width / 2.0, height / 2.0 | |
| # FIX: Increase r_start to prevent vertices entering the center singularity artifact zone | |
| r_start = shadow_radius * 2.0 | |
| r_end = shadow_radius * 5.0 | |
| quad_idx = 0 | |
| for i in range(num_rings): | |
| t = i / num_rings | |
| r1 = r_start + (r_end - r_start) * t | |
| r2 = r_start + (r_end - r_start) * (i + 1) / num_rings | |
| heat = 1.0 - t | |
| r_col = min(255, int(255 * (0.9 + 0.1 * heat))) | |
| g_col = min(255, int(180 * heat + 60)) | |
| b_col = min(255, int(120 * heat * heat)) | |
| # FIX: Reduced pulse amplitude and frequency for epilepsy safety | |
| # alpha base is lower, pulse range is narrower (0.8 to 1.0) | |
| alpha_base = 160.0 * (1.0 - t * 0.6) | |
| pulse = 0.9 + 0.05 * np.sin(current_time * 1.5 + t * 2.0) | |
| alpha = int(min(255, alpha_base * pulse)) | |
| for j in range(segments): | |
| twist = t * 1.5 + current_time * 0.3 # Slower rotation | |
| a1 = (j / segments) * 2 * np.pi + twist | |
| a2 = ((j + 1) / segments) * 2 * np.pi + twist | |
| z_off = np.sin(a1 * 3 + t * 4) * shadow_radius * 0.15 * t | |
| cos1, sin1 = np.cos(a1), np.sin(a1) | |
| cos2, sin2 = np.cos(a2), np.sin(a2) | |
| up = np.cross(u_vec, v_vec) | |
| p1 = center_pos + (u_vec * cos1 + v_vec * sin1) * r1 + up * z_off | |
| p2 = center_pos + (u_vec * cos1 + v_vec * sin1) * r2 + up * z_off | |
| p3 = center_pos + (u_vec * cos2 + v_vec * sin2) * r2 + up * z_off | |
| p4 = center_pos + (u_vec * cos2 + v_vec * sin2) * r1 + up * z_off | |
| valid_quad = True | |
| screen_pts = np.empty((4, 2), dtype=np.float64) | |
| pts = np.empty((4, 3), dtype=np.float64) | |
| pts[0], pts[1], pts[2], pts[3] = p1, p2, p3, p4 | |
| for k in range(4): | |
| curr_p = pts[k] | |
| tr = curr_p - pan | |
| x, y, z = tr[0], tr[1], tr[2] | |
| xr = cy * x - sy * z | |
| zr = sy * x + cy * z | |
| yr = cp * y - sp * zr | |
| zf = sp * y + cp * zr | |
| d = dist + zf | |
| if d <= 0.1: valid_quad = False; break | |
| inv_d = FOCAL_LENGTH / d | |
| sx = cx_screen + xr * inv_d | |
| sy = cy_screen - yr * inv_d | |
| screen_pts[k, 0] = sx | |
| screen_pts[k, 1] = sy | |
| if not valid_quad: continue | |
| quads[quad_idx] = screen_pts | |
| colors[quad_idx, 0] = r_col | |
| colors[quad_idx, 1] = g_col | |
| colors[quad_idx, 2] = b_col | |
| colors[quad_idx, 3] = alpha | |
| quad_idx += 1 | |
| return quads[:quad_idx], colors[:quad_idx] | |
| def dist_point_to_line_segment(p, a, b): | |
| ap = p - a | |
| ab = b - a | |
| dot_ab = np.dot(ab, ab) | |
| t = np.dot(ap, ab) / (dot_ab + 1e-9) | |
| t = max(0.0, min(1.0, t)) | |
| closest = a + t * ab | |
| return np.linalg.norm(p - closest) | |
| def norm(v): | |
| n = np.linalg.norm(v); return v / n if n > 1e-9 else np.zeros_like(v) | |
| # ============================ | |
| # CLASSES & CORE | |
| # ============================ | |
| def generate_boing_sound(): | |
| global AUDIO_ENABLED | |
| if not AUDIO_ENABLED: return None | |
| try: | |
| mixer_settings = pygame.mixer.get_init() | |
| if mixer_settings is None: return None | |
| sample_rate, _, channels = mixer_settings; duration = 0.2; num_samples = int(duration * sample_rate) | |
| t = np.linspace(0, duration, num_samples, False); freq = np.linspace(660.0, 220.0, num_samples); wave = np.sin(2 * np.pi * freq * t) * np.exp(-t * 10) | |
| sound_array = (wave * 32767).astype(np.int16) | |
| if channels == 2: sound_array = np.column_stack((sound_array, sound_array)) | |
| return pygame.sndarray.make_sound(sound_array) | |
| except Exception: return None | |
| def generate_ping_sound(): | |
| global AUDIO_ENABLED | |
| if not AUDIO_ENABLED: return None | |
| try: | |
| mixer_settings = pygame.mixer.get_init() | |
| if mixer_settings is None: return None | |
| sample_rate, _, channels = mixer_settings | |
| duration = 0.15; num_samples = int(duration * sample_rate); frequency = 987.77 | |
| t = np.linspace(0, duration, num_samples, False); envelope = np.exp(-t * 25.0) | |
| wave = np.sin(2 * np.pi * frequency * t) * envelope | |
| sound_array = (wave * 32767).astype(np.int16) | |
| if channels == 2: sound_array = np.column_stack((sound_array, sound_array)) | |
| return pygame.sndarray.make_sound(sound_array) | |
| except Exception: return None | |
| class Camera: | |
| def __init__(self): | |
| self.yaw, self.pitch, self.dist, self.pan = 0.0, 0.35, DEFAULT_CAM_DIST, np.zeros(3) | |
| self.forward = np.array([0.0, 0.0, 1.0]) | |
| self.right = np.array([1.0, 0.0, 0.0]) | |
| self.up = np.array([0.0, 1.0, 0.0]) | |
| def update_vectors(self): | |
| cy, sy = math.cos(self.yaw), math.sin(self.yaw) | |
| cp, sp = math.cos(self.pitch), math.sin(self.pitch) | |
| self.forward = np.array([sy * cp, -sp, cy * cp]) | |
| self.right = np.array([cy, 0, -sy]) | |
| self.up = np.cross(self.right, self.forward) | |
| def get_transformed_z(self, v): | |
| v = v - self.pan; cy, sy = math.cos(self.yaw), math.sin(self.yaw); cp, sp = math.cos(self.pitch), math.sin(self.pitch) | |
| x, y, z = v; zz = sy*x + cy*z; zz2 = sp*y + cp*zz | |
| return zz2 | |
| def project(self, v): | |
| global WIDTH, HEIGHT | |
| v = v - self.pan; | |
| if not np.all(np.isfinite(v)): return (-10000, -10000) | |
| cy, sy = math.cos(self.yaw), math.sin(self.yaw); cp, sp = math.cos(self.pitch), math.sin(self.pitch) | |
| x, y, z = v; x, z = cy*x - sy*z, sy*x + cy*z; y, z = cp*y - sp*z, sp*y + cp*z | |
| depth = self.dist + z | |
| if depth <= 0.1: return (-10000, -10000) | |
| scale = FOCAL_LENGTH / depth | |
| # Calculate screen coordinates | |
| sx = WIDTH//2 + x * scale | |
| sy = HEIGHT//2 - y * scale | |
| # Final Sanity Check before integer conversion | |
| if not (math.isfinite(sx) and math.isfinite(sy)): | |
| return (-10000, -10000) | |
| # Clamp to avoid "OverflowError: cannot convert float infinity to integer" | |
| # Pygame drawing breaks if coordinates are too massive (e.g. > 100000) | |
| if abs(sx) > 50000 or abs(sy) > 50000: | |
| return (-10000, -10000) | |
| return (int(sx), int(sy)) | |
| def project_many(self, vecs): | |
| global WIDTH, HEIGHT | |
| return project_many_jit(vecs, self.pan, self.yaw, self.pitch, self.dist, WIDTH, HEIGHT) | |
| def get_transformed_z_many(self, vecs): | |
| return get_transformed_z_many_jit(vecs, self.pan, self.yaw, self.pitch) | |
| def unproject(self, screen_pos, depth_z): | |
| global WIDTH, HEIGHT; mx, my = screen_pos | |
| scale = FOCAL_LENGTH / (self.dist + depth_z + 1e-9) | |
| if abs(scale) < 1e-9: return self.pan | |
| x_cam = (mx - WIDTH // 2) / scale; y_cam = -(my - HEIGHT // 2) / scale | |
| cy, sy = math.cos(self.yaw), math.sin(self.yaw); cp, sp = math.cos(self.pitch), math.sin(self.pitch) | |
| y_rot, z_rot = cp * y_cam + sp * depth_z, -sp * y_cam + cp * depth_z | |
| x_world, z_world = cy * x_cam + sy * z_rot, -sy * x_cam + cy * z_rot | |
| return np.array([x_world, y_rot, z_world]) + self.pan | |
| def zoom(self, factor): self.dist = np.clip(self.dist * factor, MIN_ZOOM_DIST, MAX_ZOOM_DIST) | |
| def get_state(self): return {'yaw': self.yaw, 'pitch': self.pitch, 'dist': self.dist, 'pan': self.pan} | |
| def set_state(self, state): self.yaw, self.pitch, self.dist, self.pan = state['yaw'], state['pitch'], state['dist'], np.array(state['pan']) | |
| class TechTree: | |
| STAGES = [ | |
| ('Void', 0, 'Nothing yet exists'), | |
| ('Mind', 0, 'Awareness flickers'), | |
| ('Fluctuation', 1, 'Quantum foam emerges'), | |
| ('Condensation', 3, 'Matter begins to form'), | |
| ('Chemistry', 7, 'Molecules react'), | |
| ('Life', 70, 'Self-replication emerges'), | |
| ('Transcendence', 777, 'Beyond matter'), | |
| ] | |
| def __init__(self): | |
| self.world = None | |
| self.stage_idx = 0; self.progress = 0; self.peak_stage = 0; self.collapsed_from = [] | |
| # Genesis Protocol Fields | |
| self.avg_psi = 0.0 | |
| self.avg_phi = 0.0 | |
| self.omega_pressure = 0.0 | |
| def set_world(self, world_instance): | |
| """Link the TechTree to the World it governs.""" | |
| self.world = world_instance | |
| def update_fields(self, psi, phi, omega): | |
| self.avg_psi = psi | |
| self.avg_phi = phi | |
| self.omega_pressure = omega | |
| def add_progress(self, amount): | |
| if not self.world: return None | |
| molly=find_molecules(self.world) | |
| if self.stage_idx < len(self.STAGES) - 1: | |
| self.progress += amount | |
| #print(f"\nProgress: {self.progress}\m") | |
| next_threshold = self.STAGES[self.stage_idx + 1][1] | |
| if self.progress >= next_threshold: | |
| # Genesis Protocol: Field Gates | |
| gate_passed = True | |
| if self.stage_idx < 2 and len(self.world.tets) < 3: gate_passed = False | |
| if self.stage_idx == 2 and (len(molly) < 3 or self.avg_psi < FIELD_PSI_THRESHOLD_QUANTUM): gate_passed = False | |
| if self.stage_idx == 3 and self.avg_phi < FIELD_PHI_THRESHOLD_FACE_LOCK: gate_passed = False | |
| if self.stage_idx == 4 and (len(molly) < 7 or len(self.world.tets) < 24): gate_passed = False | |
| if self.stage_idx == 5 and (len(molly) < 24 or self.avg_psi < FIELD_PSI_THRESHOLD_QUANTUM or self.avg_phi < FIELD_PHI_THRESHOLD_FACE_LOCK or self.omega_pressure < 0.1): gate_passed = False | |
| if self.stage_idx == 6 and self.omega_pressure < FIELD_OMEGA_THRESHOLD_TRANSCENDENCE: gate_passed = False | |
| if gate_passed: | |
| self.stage_idx += 1; self.peak_stage = max(self.peak_stage, self.stage_idx) | |
| print(f"Evolved to: {self.current_stage}") | |
| return f"Evolved to: {self.current_stage}" | |
| return None | |
| def collapse(self, severity=1): | |
| if not self.world: return None | |
| current_pop = len(self.world.tets) | |
| # Genesis Protocol: Collapse Mechanics | |
| # Triggered if TET population drops below 7 while in Chemistry+ era | |
| if self.stage_idx >= 4 and current_pop < 7: # Chemistry or higher | |
| lost = self.STAGES[self.stage_idx][0] | |
| self.collapsed_from.append(lost) | |
| self.stage_idx = max(0, self.stage_idx - severity) | |
| self.progress = self.STAGES[self.stage_idx][1] | |
| return f"Collapsed from {lost}! Complexity unstable." | |
| return None | |
| def current_stage(self): return self.STAGES[self.stage_idx][0] | |
| def description(self): return self.STAGES[self.stage_idx][2] | |
| def can_unlock(self, feature): | |
| requirements = {'reactions': 2, 'life': 4, 'entanglement': 3, 'transcend': 6} | |
| return self.stage_idx >= requirements.get(feature, 0) | |
| class BotMind: | |
| def __init__(self, tetra_id): | |
| self.id = tetra_id; self.memory = []; self.goal = None; self.mood = 0.5; self.friends = set() | |
| def perceive(self, nearby_tetras, nearby_molecules): | |
| mol_counts = {} | |
| for t in nearby_tetras: | |
| mol = getattr(t, 'molecule_type', None) | |
| if mol: mol_counts[mol] = mol_counts.get(mol, 0) + 1 | |
| if mol_counts: | |
| dominant = max(mol_counts, key=mol_counts.get) | |
| self.memory.append(('saw', dominant)) | |
| if len(self.memory) > 10: self.memory.pop(0) | |
| if len(nearby_tetras) > 10: self.mood = min(1, self.mood + 0.05) | |
| else: self.mood = max(0, self.mood - 0.02) | |
| def decide_goal(self, world_fields_func=None, my_pos=None): | |
| # Genesis Protocol: Layer 5 Predictive Choice | |
| # goal = argmax(field_value_of_target - energy_cost) | |
| if world_fields_func and my_pos is not None: | |
| # Sample field gradient | |
| d = 1.0 | |
| p0, phi0 = world_fields_func(my_pos) | |
| p_x, phi_x = world_fields_func(my_pos + np.array([d,0,0])) | |
| p_y, phi_y = world_fields_func(my_pos + np.array([0,d,0])) | |
| p_z, phi_z = world_fields_func(my_pos + np.array([0,0,d])) | |
| # Gradient descent/ascent on Phi (Metabolic field) | |
| grad_phi = np.array([phi_x - phi0, phi_y - phi0, phi_z - phi0]) | |
| if np.linalg.norm(grad_phi) > 0.001: | |
| self.goal = "seek_energy" | |
| self.grad_vector = grad_phi / np.linalg.norm(grad_phi) | |
| return | |
| # Fallback to legacy logic | |
| seen = {} | |
| for event, mol in self.memory: | |
| if event == 'saw': seen[mol] = seen.get(mol, 0) + 1 | |
| if not seen: self.goal = None; return | |
| if self.mood > 0.6: self.goal = min(seen, key=seen.get) | |
| else: self.goal = max(seen, key=seen.get) | |
| self.grad_vector = None | |
| def get_desire_vector(self, my_pos, nearby_tetras): | |
| # Predictive choice vector | |
| if hasattr(self, 'grad_vector') and self.grad_vector is not None: | |
| return self.grad_vector * 0.5 | |
| if not self.goal: return np.zeros(3) | |
| if self.goal == "seek_energy": return np.zeros(3) # Handled above | |
| best_dist, best_dir = float('inf'), np.zeros(3) | |
| for t in nearby_tetras: | |
| if getattr(t, 'molecule_type', None) == self.goal: | |
| diff = t.pos - my_pos; dist = np.linalg.norm(diff) | |
| if 0.1 < dist < best_dist: best_dist = dist; best_dir = diff / dist | |
| return best_dir * self.mood | |
| def bond_with(self, other_id): self.friends.add(other_id) | |
| def thought_bubble(self): | |
| if hasattr(self, 'grad_vector') and self.grad_vector is not None: | |
| return "+" # Seeking energy | |
| if self.goal: | |
| emoji = '?' if self.mood > 0.6 else '|' | |
| return f"{emoji}{self.goal}" | |
| return None | |
| class VertexJoint: | |
| def __init__(self, A, ia, B, ib): self.A, self.ia, self.B, self.ib = A, ia, B, ib | |
| class Tetrahedron: | |
| EDGES_NP = np.array([(i, j) for i in range(4) for j in range(i+1, 4)], dtype=np.int32) | |
| FACES_NP = np.array([(1, 2, 3), (0, 1, 2), (0, 2, 3), (0, 1, 3)], dtype=np.int32) | |
| FACE_COLORS = [(255,255,255), (0,0,0), (255,255,0), (0,255,255)] | |
| FACE_POLARITY_MAP = {2: 1, 3: -1} | |
| FACE_COLOR_NAMES = ['W', 'B', 'R', 'C'] | |
| FACE_TO_CORNERS = {0: [1, 2, 3], 1: [0, 1, 2], 2: [0, 2, 3], 3: [0, 1, 3]} | |
| r, a = EDGE_LEN*math.sqrt(3/8), EDGE_LEN/math.sqrt(3) | |
| REST_NP = np.array([[0,0,r], [EDGE_LEN/2,-a/2,-r/3], [-EDGE_LEN/2,-a/2,-r/3], [0,a,-r/3]], dtype=np.float64) | |
| def __init__(self, pos): | |
| self.pos = np.array(pos, float); self.pos_prev = self.pos.copy() | |
| self.local = self.REST_NP.copy(); self.local_prev = self.local.copy() | |
| self.battery = random.uniform(0.3, 0.6); self.orientation_bias = np.zeros(3, dtype=np.float64) | |
| self.colors = None; self.label = ""; self.id = id(self) | |
| self.is_magnetized = False; self.magnetism = 0 | |
| self.magnetic_strength = 0.0; self.locked_faces = [] | |
| self.molecule_type = None; self.aura_color = None | |
| self.polarity_face_idx = None; self.last_reaction_time = 0.0 | |
| self.synthesis_count = 0; self.is_catalyst = False | |
| self.erd_coherence = 0.0; self.entangled_partner = None | |
| self.quantum_state = "ground" | |
| self.mind = BotMind(self.id) | |
| # [0, 1, 2, 3] are the 4 corners. | |
| # If [0, 1] are true, only corners 0 and 1 will seek partners. | |
| self.active_corners = [False, False, False, False] | |
| # PROVENANCE TRACKING | |
| self.element_source = None # "interface", "reaction", or None | |
| self.is_element = False | |
| # CORNER STATES: | |
| # 0 = Inert/Dead (No force) | |
| # 1 = Active/Desiring (Attracts active neighbors) | |
| # 2 = Bonded (Physically locked, no desire) | |
| # 3 = Lone Pair (Repels everything - Geometry enforcer) | |
| self.corner_states = [1, 1, 1, 1] # Default: Carbon-like (4 Active) | |
| def verts(self): return self.local + self.pos | |
| def get_bond_count(self, corner_idx, joints): | |
| """Counts how many physical joints exist on this specific corner.""" | |
| count = 0 | |
| for j in joints: | |
| if (j.A.id == self.id and j.ia == corner_idx) or \ | |
| (j.B.id == self.id and j.ib == corner_idx): | |
| count += 1 | |
| return count | |
| def apply_valency(self, element_type, world): | |
| """Applies config while preserving existing Sticky/Physical connections.""" | |
| self.molecule_type = element_type | |
| base_config = VALENCY_CONFIGS.get(element_type, VALENCY_CONFIGS['default']).copy() | |
| # 1. Identify which corners are ALREADY busy (stuck or bonded) | |
| # We don't want to turn a bonded corner into an Inert/Lone Pair | |
| busy_indices = set() | |
| # Check Joints | |
| for j in world.joints: | |
| if j.A.id == self.id: busy_indices.add(j.ia) | |
| elif j.B.id == self.id: busy_indices.add(j.ib) | |
| # Check Sticky Pairs (Preserve "Desires") | |
| for p in world.sticky_pairs: | |
| if p[0].id == self.id: busy_indices.add(p[1]) | |
| elif p[2].id == self.id: busy_indices.add(p[3]) | |
| # 2. Assign '1' (Active) or '2' (Bonded) to busy corners to keep them valid | |
| # We remove '1's from the base_config so we don't duplicate capacity | |
| final_states = [0] * 4 | |
| for idx in busy_indices: | |
| final_states[idx] = 2 # Assume bonded/sticky is active/occupied | |
| if 1 in base_config: base_config.remove(1) # Consume an active slot | |
| elif 3 in base_config: base_config.remove(3) # Hybridize if forced | |
| # 3. Shuffle remaining config and apply to free corners | |
| random.shuffle(base_config) | |
| for i in range(4): | |
| if i not in busy_indices: | |
| if base_config: | |
| final_states[i] = base_config.pop(0) | |
| else: | |
| final_states[i] = 0 # Default to inert if config runs out | |
| self.corner_states = final_states | |
| class PastProjection4Sphere: | |
| def __init__(self): | |
| self.points = []; self.max_points = 200; self.angle_accum = 0.0 | |
| def update_and_draw(self, screen, cam, center_of_mass, num_tets, time_scale, width, height): | |
| target_points = min(num_tets * 4, self.max_points) | |
| while len(self.points) < target_points: | |
| u = np.random.normal(0, 1, 4); u /= np.linalg.norm(u); self.points.append(u) | |
| self.angle_accum += 0.002 * time_scale | |
| c, s = math.cos(0.005 * time_scale), math.sin(0.005 * time_scale) | |
| rot_xw = np.array([[c, 0, 0, -s], [0, 1, 0, 0], [0, 0, 1, 0], [s, 0, 0, c]]) | |
| radius = 800.0 | |
| for i, p4 in enumerate(self.points): | |
| p4 = np.dot(rot_xw, p4); self.points[i] = p4 | |
| denom = 1.0 - p4[3] | |
| if abs(denom) < 0.001: denom = 0.001 | |
| p3 = p4[:3] / denom * radius + center_of_mass | |
| screen_pos = cam.project(p3) | |
| if screen_pos[0] > -1000: | |
| shift = (p4[3] + 1) / 2.0 | |
| color = (np.clip(int(255 * shift), 50, 255), 20, np.clip(int(255 * (1-shift)), 50, 255)) | |
| size = max(1, int(4 * (1.0 - abs(p4[3])))) | |
| if 0 <= screen_pos[0] < width and 0 <= screen_pos[1] < height: pygame.draw.circle(screen, color, screen_pos, size) | |
| def draw_molecular_labels(screen, cam, world, font): | |
| # 1. Build Graph of Connected TETs | |
| adj = {t.id: [] for t in world.tets} | |
| for j in world.joints: | |
| adj[j.A.id].append(j.B) | |
| adj[j.B.id].append(j.A) | |
| visited = set() | |
| # 2. Find Groups | |
| for t in world.tets: | |
| if t.id in visited: continue | |
| # If it's just a lone particle with no label, skip | |
| if not t.label and not adj[t.id]: | |
| visited.add(t.id) | |
| continue | |
| # DFS Traversal to find the whole molecule | |
| group = [] | |
| stack = [t] | |
| visited.add(t.id) | |
| while stack: | |
| curr = stack.pop() | |
| group.append(curr) | |
| for neighbor in adj[curr.id]: | |
| if neighbor.id not in visited: | |
| visited.add(neighbor.id) | |
| stack.append(neighbor) | |
| # 3. Calculate Center & Formula | |
| if len(group) > 1: | |
| # It's a molecule | |
| avg_pos = np.mean([atom.pos for atom in group], axis=0) | |
| screen_pos = cam.project(avg_pos) | |
| if screen_pos[0] > -10000: | |
| # Count Elements: C:1, H:4 | |
| counts = {} | |
| for atom in group: | |
| # Assume atom.label holds "C", "H", etc. | |
| lbl = atom.label if atom.label in ["C", "H", "O", "N", "Fe"] else "X" | |
| if lbl != "X": counts[lbl] = counts.get(lbl, 0) + 1 | |
| # Build Name (e.g., "CH4") | |
| name = "" | |
| # Standard sorting: C first, then H, then others | |
| if 'C' in counts: name += f"C{counts.pop('C')}" if counts['C']>1 else "C" | |
| if 'H' in counts: name += f"H{counts.pop('H')}" if counts['H']>1 else "H" | |
| for e in sorted(counts.keys()): name += f"{e}{counts[e]}" if counts[e]>1 else e | |
| # Common Name Override | |
| COMMON = {"H2O": "Water", "CH4": "Methane", "O2": "Oxygen", "CO2": "CO2"} | |
| display_txt = COMMON.get(name, name) | |
| if display_txt: | |
| s = font.render(display_txt, True, (0, 255, 255), (0,0,0,128)) | |
| screen.blit(s, s.get_rect(center=screen_pos)) | |
| class World: | |
| def __init__(self, sound): | |
| self.tets, self.joints, self.sticky_pairs = [], [], []; self.center_of_mass, self.sound = np.zeros(3), sound | |
| self.pair_ages = {} # Maps sorted tuple (id1, id2) -> age_seconds | |
| self.reaction_particles = [] | |
| self.tech_tree = TechTree() | |
| self.tech_tree.set_world(self) | |
| self.notification_queue = [] | |
| # Genesis Protocol Fields Cache | |
| self.cached_psi = 0.0 | |
| self.cached_phi = 0.0 | |
| self.cached_omega = 0.0 | |
| self.sim_time = time.time() | |
| self._last_synth_reactions = [] | |
| self._last_quantum_events = [] | |
| # === OPTIMIZATION CACHE === | |
| self.cached_pos = np.zeros((0, 3)) | |
| self.cached_prev = np.zeros((0, 3)) | |
| self.cached_local = np.zeros((0, 4, 3)) | |
| self.cached_local_prev = np.zeros((0, 4, 3)) | |
| self.cached_bat = np.zeros(0) | |
| self.cached_coh = np.zeros(0) | |
| self.cached_bias = np.zeros((0, 3)) | |
| def sever_bonds(self, tet_idx): | |
| """Cuts all physical and emotional ties for a specific TET index.""" | |
| if tet_idx >= len(self.tets): return | |
| target_id = self.tets[tet_idx].id | |
| # 1. Remove Joints (Physical Bonds) | |
| # Iterate backwards to allow safe removal | |
| for i in range(len(self.joints) - 1, -1, -1): | |
| j = self.joints[i] | |
| if j.A.id == target_id or j.B.id == target_id: | |
| # Restore valency to the partner before cutting | |
| if j.A.id == target_id: j.B.corner_states[j.ib] = 1 | |
| else: j.A.corner_states[j.ia] = 1 | |
| self.joints.pop(i) | |
| # 2. Remove Sticky Pairs (Desires) | |
| for i in range(len(self.sticky_pairs) - 1, -1, -1): | |
| p = self.sticky_pairs[i] | |
| if p[0].id == target_id or p[2].id == target_id: | |
| self.sticky_pairs.pop(i) | |
| # 3. Reset the TET's own valency | |
| self.tets[tet_idx].corner_states = [1, 1, 1, 1] | |
| print(f"✂️ Severed bonds for TET {self.tets[tet_idx].label}") | |
| def get_average_battery(self): | |
| if not self.tets: return 0.5 | |
| return np.mean([t.battery for t in self.tets]) | |
| def spawn(self, give_special_colors=False): | |
| if self.tets: | |
| parent_tet = random.choice(self.tets); parent_vertex_pos = parent_tet.verts()[random.randint(0, 3)] | |
| offset_dir = norm(np.random.uniform(-K_RESET_POS, K_RESET_POS, 3)); spawn_pos = parent_vertex_pos + offset_dir * (COLLISION_RADIUS * 2.1) | |
| new_tet = Tetrahedron(spawn_pos) | |
| else: new_tet = Tetrahedron(np.random.uniform(-K_RESET_POS, K_RESET_POS, 3) + self.center_of_mass) | |
| if give_special_colors: | |
| cols = [(255,255,255), (0,0,0), (255,0,0), (0,255,255)]; random.shuffle(cols); new_tet.colors = cols | |
| else: new_tet.colors = list(Tetrahedron.FACE_COLORS) | |
| if len(self.tets) == 0: new_tet.label = "Time" | |
| elif len(self.tets) == 1: new_tet.label = "Separation" | |
| self.tets.append(new_tet) | |
| def spawn_polar_pair(self): | |
| if not self.tets: | |
| self.spawn() | |
| return | |
| tet1 = Tetrahedron(np.random.uniform(-K_RESET_POS, K_RESET_POS, 3) + self.center_of_mass) | |
| tet2 = Tetrahedron(np.random.uniform(-K_RESET_POS, K_RESET_POS, 3) + self.center_of_mass) | |
| tet1.pos_prev = tet1.pos.copy() | |
| tet2.pos_prev = tet2.pos.copy() | |
| # Define the Standard Model Colors (Fixed) | |
| std_colors = [(255,255,255), (0,0,0), (255,0,0), (0,255,255)] # W, B, R, C | |
| tet1.colors = std_colors | |
| tet2.colors = std_colors | |
| for t in [tet1, tet2]: | |
| # Create mask: [Active, Active, Active, LonePair] | |
| states = [1, 1, 1, 3] | |
| random.shuffle(states) # Randomize which corners are which | |
| t.corner_states = states | |
| if len(self.tets) == 2: tet1.label = "Light"; tet2.label = "Darkness" | |
| if len(self.tets) == 4: tet1.label = "Answer"; tet2.label = "Question" | |
| for t in [tet1, tet2]: | |
| roll = random.random() | |
| if roll < 0.1: | |
| # Hydrogen-like (1 active corner) | |
| t.active_corners = [True, False, False, False] | |
| t.molecule_type = "H" | |
| random.shuffle(t.active_corners) # Which corner is random | |
| elif roll < 0.3: | |
| # Oxygen-like (2 active corners) | |
| t.active_corners = [True, True, False, False] | |
| t.molecule_type = "O" | |
| random.shuffle(t.active_corners) | |
| elif roll < 0.5: | |
| # Nitrogen-like (3 active corners) | |
| t.active_corners = [True, True, True, False] | |
| t.molecule_type = "N" | |
| random.shuffle(t.active_corners) | |
| else: | |
| # Carbon-like (4 active corners) | |
| t.active_corners = [True, True, True, True] | |
| t.molecule_type = "C" | |
| self.tets.extend([tet1, tet2]); polar_face_idx = random.choice([2, 3]); face_verts = Tetrahedron.FACES_NP[polar_face_idx] | |
| face_verts = Tetrahedron.FACES_NP[polar_face_idx] | |
| for i in range(3): | |
| self.sticky_pairs.append((tet1, face_verts[i], tet2, face_verts[i])) | |
| tet1.apply_valency(t.molecule_type, self) | |
| tet2.apply_valency(t.molecule_type, self) | |
| msg = self.tech_tree.add_progress(0) | |
| if msg: self.notification_queue.append(msg) | |
| if not ON_HUGGINGFACE: | |
| print(f"A {tet1.label} spawned desiring {tet2.label} B") | |
| def get_fields_at(self, pos): | |
| """Helper to call JIT field calculator""" | |
| if not self.tets: return 0.0, 0.0 | |
| positions = np.array([t.pos for t in self.tets]) | |
| batteries = np.array([t.battery for t in self.tets]) | |
| coherences = np.array([t.erd_coherence for t in self.tets]) | |
| return calculate_fields_jit(positions, batteries, coherences, pos) | |
| def calculate_global_fields(self): | |
| """Layer 2: Fields as Law (Average for TechTree)""" | |
| if not self.tets: return 0.0, 0.0, 0.0 | |
| if not np.all(np.isfinite(self.center_of_mass)): | |
| self.center_of_mass = np.zeros(3) # Emergency reset | |
| # Narrative Field Omega | |
| max_stage = 7.0 | |
| global_pressure = len(self.tets) / 200.0 # Arbitrary pressure metric | |
| target_omega = (self.tech_tree.stage_idx / max_stage) * global_pressure | |
| # Approximate global avg Psi and Phi by sampling center | |
| psi_raw, phi_raw = self.get_fields_at(self.center_of_mass) | |
| if math.isnan(psi_raw) or math.isinf(psi_raw): psi_raw = 0.0 | |
| if math.isnan(phi_raw) or math.isinf(phi_raw): phi_raw = 0.0 | |
| # Normalize based on population to keep numbers readable | |
| # (Divisor adjusted because we added softening) | |
| target_phi = phi_raw / (len(self.tets) * 0.2) | |
| target_psi = psi_raw / (len(self.tets) * 0.05) | |
| # === SMOOTHING (Lerp) === | |
| # Blend 5% new value with 95% old value to stop jitter | |
| self.cached_psi += (target_psi - self.cached_psi) * 0.05 | |
| self.cached_phi += (target_phi - self.cached_phi) * 0.05 | |
| self.cached_omega += (target_omega - self.cached_omega) * 0.05 | |
| self.tech_tree.update_fields(self.cached_psi, self.cached_phi, self.cached_omega) | |
| return self.cached_psi, self.cached_phi, self.cached_omega | |
| # --- WHITEPAPER: THERMODYNAMICS UPDATE --- | |
| def process_metabolism(self, scaled_dt, add_msg_fn): | |
| """ | |
| Layer 4: Metabolism Loop | |
| Includes Entropy leakage per the Whitepaper. | |
| """ | |
| if self.sim_time < 1.0: return | |
| if not self.tets: return | |
| # 1. Energy Accounting (Whitepaper: Local Conservation) | |
| # Bond maintenance cost logic | |
| bond_cost = (len(self.joints) + len(self.sticky_pairs)) * BOND_COST_PER_TICK * scaled_dt * 60 | |
| # 2. Distribute cost among bonded TETs | |
| for j in self.joints: | |
| drain = BOND_COST_PER_TICK * scaled_dt * 30 | |
| j.A.battery -= drain | |
| j.B.battery -= drain | |
| # 3. Mitosis-like growth check (net gain > threshold) | |
| avg_bat = self.get_average_battery() | |
| if avg_bat > 0.8 and self.tech_tree.can_unlock('life') and random.random() < 0.01: | |
| # Duplicate weakest link (Growth) | |
| # Find a TET with high battery | |
| candidates = [t for t in self.tets if t.battery > 0.75] | |
| if candidates: | |
| parent = random.choice(candidates) | |
| # Energy conserved: Parent splits energy with child | |
| child_energy = parent.battery * 0.5 | |
| parent.battery *= 0.5 | |
| offset = np.random.uniform(-K_RESET_POS, K_RESET_POS, 3) * EDGE_LEN | |
| child = Tetrahedron(parent.pos + offset) | |
| child.battery = child_energy | |
| child.label = parent.label # Inherit info | |
| self.tets.append(child) | |
| add_msg_fn(f"Mitosis: {child.label}") | |
| print(f"Mitosis: {parent.label,child.label}") | |
| # 4. --- quiescence fallback (replaces apoptosis) --- | |
| for tet in self.tets: | |
| if tet.battery <= BATTERY_AGENCY_THRESHOLD: | |
| tet.quiescent = True | |
| to_origin = self.center_of_mass - tet.pos | |
| dist = np.linalg.norm(to_origin)# + 1e-9 | |
| depletion = 1.0 - (tet.battery / BATTERY_AGENCY_THRESHOLD) | |
| tet.battery += 10.0/dist | |
| # Gentle drift toward Origin (no bond breaking) | |
| tmp=(to_origin / dist) * depletion * ORIGIN_DRIFT_RATE * scaled_dt | |
| tet.pos += 10000.0*tmp*dist | |
| #print(f"Tet depleted {tet.label, dist,tmp}") | |
| else: | |
| tet.quiescent = False | |
| def check_magnetization(self): | |
| # 1. Reset logic sensitive to Element Source (Rule 1) | |
| for t in self.tets: | |
| # If it's a raw geometry match, reset to check if geometry still holds | |
| if t.element_source == "interface" or t.element_source is None: | |
| t.is_magnetized = False | |
| t.magnetism = 0 | |
| t.magnetic_strength = 0.0 | |
| t.locked_faces = [] | |
| t.molecule_type = None | |
| t.element_source = None | |
| t.is_element = False | |
| # If "reaction", we assume identity persists unless manually broken (not handled here) | |
| t.aura_color = None | |
| t.is_catalyst = False | |
| # 2. Map connections | |
| connections = {} | |
| for j in self.joints: | |
| pair_key = tuple(sorted((j.A.id, j.B.id))) | |
| if pair_key not in connections: | |
| connections[pair_key] = [] | |
| if j.A.id < j.B.id: | |
| connections[pair_key].append((j.ia, j.ib)) | |
| else: | |
| connections[pair_key].append((j.ib, j.ia)) | |
| # 3. Analyze Interfaces (Rule 1: Geometry creates elements) | |
| tet_map = {t.id: t for t in self.tets} | |
| for (id_a, id_b), links in connections.items(): | |
| # if len(links) >3: | |
| # print("4 verticies cannot lock!") | |
| #fix error! | |
| if len(links) >= 3: # 3 connections = A Face Lock | |
| tA = tet_map[id_a] | |
| tB = tet_map[id_b] | |
| verts_a = {x[0] for x in links} | |
| face_idx_a = -1 | |
| for f_idx, corners in Tetrahedron.FACE_TO_CORNERS.items(): | |
| if verts_a.issuperset(corners): | |
| face_idx_a = f_idx | |
| break | |
| verts_b = {x[1] for x in links} | |
| face_idx_b = -1 | |
| for f_idx, corners in Tetrahedron.FACE_TO_CORNERS.items(): | |
| if verts_b.issuperset(corners): | |
| face_idx_b = f_idx | |
| break | |
| # 4. Apply Chemistry | |
| if face_idx_a != -1 and face_idx_b != -1: | |
| tA.locked_faces.append(face_idx_a) | |
| tB.locked_faces.append(face_idx_b) | |
| chem_key = (face_idx_a, face_idx_b) | |
| element = INTERFACE_CHEMISTRY.get(chem_key, "??") | |
| # Rule 1 Implementation: Strict Assignment | |
| if tA.element_source != "reaction": | |
| tA.molecule_type = element | |
| tA.element_source = "interface" | |
| tA.is_element = True | |
| if tB.element_source != "reaction": | |
| tB.molecule_type = element | |
| tB.element_source = "interface" | |
| tB.is_element = True | |
| if element == "Fe": | |
| tA.is_magnetized = True; tA.magnetism = 1 | |
| tB.is_magnetized = True; tB.magnetism = 1 | |
| tA.magnetic_strength = 1.0; tB.magnetic_strength = 1.0 | |
| if element == "H": | |
| tA.battery = min(1.0, tA.battery + 0.001) | |
| tB.battery = min(1.0, tB.battery + 0.001) | |
| # 5. Fallback for Solitary TETs | |
| for t in self.tets: | |
| t.erd_coherence = min(1.0, (len(t.locked_faces) / 4.0) * t.battery) | |
| def update_magnetic_batteries(self, scaled_dt): | |
| if not self.joints: return | |
| tet_map = {t.id: t for t in self.tets}; processed_pairs = set() | |
| for t in self.tets: | |
| if not t.is_magnetized or t.magnetic_strength <= 0: continue | |
| for j in self.joints: | |
| partner_id = j.B.id if j.A.id == t.id else j.A.id | |
| if partner_id in tet_map: | |
| partner = tet_map[partner_id] | |
| if partner.is_magnetized: | |
| pair_key = tuple(sorted([t.id, partner_id])) | |
| if pair_key in processed_pairs: continue | |
| processed_pairs.add(pair_key) | |
| emptiness_t = 1.0 - t.battery; emptiness_p = 1.0 - partner.battery | |
| transfer_rate = 0.1 * t.magnetic_strength * scaled_dt | |
| transfer_amount = (emptiness_t - emptiness_p) * transfer_rate | |
| t.battery = np.clip(1.0 - (emptiness_t - transfer_amount), 0.0, 1.0) | |
| partner.battery = np.clip(1.0 - (emptiness_p + transfer_amount), 0.0, 1.0) | |
| def apply_corner_desires(self, scaled_dt): | |
| if len(self.tets) < 2: return | |
| psi_mod = 1.0 + self.cached_psi | |
| tet_list = list(self.tets) | |
| all_corners, corner_colors, corner_tets, corner_indices = [], [], [], [] | |
| # 1. Gather ACTIVE corners | |
| for idx, t in enumerate(tet_list): | |
| if not np.all(np.isfinite(t.pos)): continue | |
| verts = t.verts() | |
| for c_idx in range(4): | |
| # VALENCY CHECK: Only process Active (1) corners | |
| if hasattr(t, 'corner_states') and t.corner_states[c_idx] != 1: continue | |
| all_corners.append(verts[c_idx]) | |
| corner_tets.append(idx) | |
| corner_indices.append(c_idx) | |
| # Determine Color | |
| corner_color = 'N' | |
| for f_idx in range(4): | |
| if c_idx in Tetrahedron.FACE_TO_CORNERS[f_idx]: | |
| face_color = t.colors[f_idx] if t.colors else Tetrahedron.FACE_COLORS[f_idx] | |
| if face_color == (255, 0, 0): corner_color = 'R'; break | |
| elif face_color == (0, 255, 255): corner_color = 'C'; break | |
| corner_colors.append(corner_color) | |
| if not all_corners: return | |
| all_corners_np = np.array(all_corners) | |
| # CRASH SAFETY | |
| if not np.all(np.isfinite(all_corners_np)): | |
| return | |
| try: | |
| tree = cKDTree(all_corners_np) | |
| for i in range(len(all_corners_np)): | |
| if corner_colors[i] not in ['R', 'C']: continue | |
| nearby_idx = tree.query_ball_point(all_corners_np[i], CORNER_DESIRE_RANGE) | |
| forces = np.zeros(3) | |
| for j in nearby_idx: | |
| if i == j: continue | |
| if corner_tets[i] == corner_tets[j]: continue # Self check | |
| c1, c2 = corner_colors[i], corner_colors[j] | |
| if (c1 == 'R' and c2 == 'C') or (c1 == 'C' and c2 == 'R'): | |
| # Neighbor Valency Check | |
| t_target = tet_list[corner_tets[j]] | |
| idx_target = corner_indices[j] | |
| if hasattr(t_target, 'corner_states') and t_target.corner_states[idx_target] != 1: continue | |
| delta = all_corners_np[j] - all_corners_np[i] | |
| dist = np.linalg.norm(delta) | |
| if dist > 1e-6: | |
| direction = delta / dist | |
| forces += direction * (K_CORNER_DESIRE * psi_mod * (1.0 - dist / CORNER_DESIRE_RANGE)) | |
| if np.any(forces): | |
| t1 = tet_list[corner_tets[i]] | |
| t1.local[corner_indices[i]] += forces * scaled_dt * 0.5 | |
| t1.pos += forces * scaled_dt * 0.5 | |
| except ValueError: | |
| pass | |
| def apply_same_pole_repulsion(self, scaled_dt): | |
| positive_poles = [t for t in self.tets if t.is_magnetized and t.magnetism > 0] | |
| if len(positive_poles) < 2: return | |
| pos_array = np.array([t.pos for t in positive_poles]) | |
| strengths = np.array([t.magnetic_strength for t in positive_poles]) | |
| for i in range(len(pos_array)): | |
| deltas = pos_array - pos_array[i]; dists = np.linalg.norm(deltas, axis=1) | |
| mask = (dists > 1e-6) | |
| if np.any(mask): | |
| unit_deltas = deltas[mask] / dists[mask][:, np.newaxis] | |
| repulsions = (K_SAME_POLE_REPULSION * 0.1 / (dists[mask]**2 + 1.0)) * strengths[mask] * strengths[i] | |
| total_force = np.sum(unit_deltas * repulsions[:, np.newaxis], axis=0) | |
| positive_poles[i].pos -= total_force * scaled_dt | |
| def apply_erd_fluctuations(self, scaled_dt): | |
| for t in self.tets: | |
| if t.erd_coherence > 0.5: | |
| fluctuation = np.random.uniform(-ERD_FLUCTUATION_STRENGTH, ERD_FLUCTUATION_STRENGTH) * t.erd_coherence | |
| t.battery = np.clip(t.battery + fluctuation * scaled_dt, 0.0, 1.0) | |
| t.quantum_state = "excited" if abs(fluctuation) > 0.1 else "ground" | |
| def attempt_quantum_tunneling(self, scaled_dt, add_msg_fn): | |
| current_time = time.time(); tunnel_reactions = [] | |
| # Pre-filter: Check if we have enough tets to even bother | |
| if len(self.tets) < 2: return [] | |
| for t in self.tets: | |
| if t.erd_coherence < ERD_COHERENCE_THRESHOLD or current_time - t.last_reaction_time < 10.0: continue | |
| # === CRASH FIX 1: Sanitize the subject TET === | |
| if not np.all(np.isfinite(t.pos)): | |
| t.pos = np.random.uniform(-K_RESET_POS, K_RESET_POS, 3) + self.center_of_mass | |
| t.pos_prev = t.pos.copy() # Kill velocity | |
| continue | |
| if random.random() < QUANTUM_TUNNEL_PROB * scaled_dt * 60 * t.erd_coherence: | |
| # Get neighbors | |
| candidates = [o for o in self.tets if o.id != t.id] | |
| if not candidates: continue | |
| positions = np.array([o.pos for o in candidates]) | |
| # === CRASH FIX 2: Sanitize the neighbor cloud === | |
| if not np.all(np.isfinite(positions)): | |
| # Find bad indices | |
| mask_bad = ~np.all(np.isfinite(positions), axis=1) | |
| bad_indices = np.where(mask_bad)[0] | |
| # Fix the raw array so cKDTree doesn't crash | |
| safe_replacements = np.random.uniform(-K_RESET_POS, K_RESET_POS, (len(bad_indices), 3)) + self.center_of_mass | |
| positions[mask_bad] = safe_replacements | |
| # Fix the actual objects so the problem is gone next frame | |
| for i, bad_idx in enumerate(bad_indices): | |
| candidates[bad_idx].pos = safe_replacements[i] | |
| candidates[bad_idx].pos_prev = safe_replacements[i] | |
| # Now safe to build tree | |
| try: | |
| tree = cKDTree(positions) | |
| far_indices = tree.query_ball_point(t.pos, QUANTUM_ENTANGLE_RANGE * 2) | |
| if not far_indices: continue | |
| # Pick a partner | |
| partner = candidates[random.choice(far_indices)] | |
| if partner.label and (t.label, partner.label) in QUANTUM_REACTIONS: | |
| product = QUANTUM_REACTIONS[(t.label, partner.label)] | |
| t.label = product; t.last_reaction_time = current_time; t.synthesis_count += 1 | |
| t.battery -= SYNTHESIS_ENERGY_COST / 2 | |
| partner.battery -= 0.05; partner.quantum_state = "tunneled"; t.quantum_state = "tunneled" | |
| tunnel_reactions.append((t.label, partner.label, product)) | |
| add_msg_fn(f" Quantum Tunnel: {product} formed!", duration=5) | |
| print(f" Quantum Tunnel: {product} formed!") | |
| try: QUANTUM_SOUND.play() | |
| except: pass | |
| except ValueError: | |
| # Final safety net for scipy errors | |
| pass | |
| return tunnel_reactions | |
| def process_entangled_reactions(self, scaled_dt, add_msg_fn): | |
| processed, entangled_reactions = set(), [] | |
| for t in self.tets: | |
| if t.entangled_partner is None or t.id in processed: continue | |
| partner = next((p for p in self.tets if p.id == t.entangled_partner), None) | |
| if not partner: t.entangled_partner = None; continue | |
| processed.add(t.id); processed.add(partner.id) | |
| avg_bat = (t.battery + partner.battery) / 2 | |
| t.battery = partner.battery = avg_bat | |
| pair_key = tuple(sorted([t.label, partner.label])) | |
| if pair_key in SYNTHESIS_REACTIONS and random.random() < REACTION_PROBABILITY_BASE * 2.0 * scaled_dt * 60: | |
| product = SYNTHESIS_REACTIONS[pair_key] | |
| t.label = partner.label = product | |
| t.last_reaction_time = partner.last_reaction_time = time.time() | |
| t.synthesis_count += 1; partner.synthesis_count += 1 | |
| energy_gain = K_REACTION_ENERGY_RELEASE * 1.5 | |
| t.battery = min(1.0, t.battery + energy_gain); partner.battery = min(1.0, partner.battery + energy_gain) | |
| entangled_reactions.append((t.label, partner.label, product)) | |
| add_msg_fn(f" Entangled Formation: {product}!", duration=5) | |
| print(f" Entangled Formation: {product}!") | |
| try: QUANTUM_SOUND.play() | |
| except: pass | |
| return entangled_reactions | |
| def spawn_quantum_visuals(self, screen, cam, quantum_events, width, height): | |
| if not hasattr(self, 'quantum_particles'): self.quantum_particles = [] | |
| for event_type, pos in quantum_events: | |
| color = (0, 200, 255) if event_type == "tunnel" else (100, 0, 255) | |
| self.quantum_particles.append({'pos': np.array(pos), 'vel': np.random.uniform(-K_RESET_POS, K_RESET_POS, 3)*40, 'color': color, 'life': 1.5, 'size': 12}) | |
| self.quantum_particles.append({'pos': np.array(pos), 'radius': 5, 'speed': 50, 'alpha': 180, 'life': 1.0, 'color': color}) | |
| to_keep = [] | |
| for p in self.quantum_particles: | |
| if 'radius' in p: | |
| p['radius'] += p['speed'] * 0.016; p['alpha'] = int(p['alpha'] * 0.95); p['life'] -= 0.02 | |
| if p['life'] > 0: | |
| sp = cam.project(p['pos']) | |
| if sp[0] > -10000: | |
| s = pygame.Surface((width, height), pygame.SRCALPHA) | |
| pygame.draw.circle(s, (*p['color'], p['alpha']//2), sp, p['radius'], 2) | |
| screen.blit(s, (0,0)) | |
| to_keep.append(p) | |
| else: | |
| p['pos'] += p['vel'] * 0.016; p['vel'] *= 0.92; p['life'] -= 0.025 | |
| if p['life'] > 0: | |
| sp = cam.project(p['pos']) | |
| if sp[0] > -10000: | |
| s = pygame.Surface((p['size']*2, p['size']*2), pygame.SRCALPHA) | |
| pygame.draw.circle(s, (*p['color'], int(p['life']*255)), (p['size'], p['size']), p['size']) | |
| screen.blit(s, (sp[0]-p['size'], sp[1]-p['size'])) | |
| to_keep.append(p) | |
| self.quantum_particles = to_keep | |
| def apply_negative_pole_orientation(self, scaled_dt): | |
| for t in self.tets: | |
| if not t.is_magnetized or t.polarity_face_idx is None: continue | |
| if t.magnetism >= 0: continue | |
| to_origin = -t.pos | |
| dist_to_origin = np.linalg.norm(to_origin) | |
| if dist_to_origin < 1e-6: continue | |
| to_origin_norm = to_origin / dist_to_origin | |
| face_verts_indices = Tetrahedron.FACES_NP[t.polarity_face_idx] | |
| face_verts = t.local[face_verts_indices] | |
| v1 = face_verts[1] - face_verts[0]; v2 = face_verts[2] - face_verts[0] | |
| face_normal = np.cross(v1, v2); face_normal_len = np.linalg.norm(face_normal) | |
| if face_normal_len < 1e-6: continue | |
| face_normal /= face_normal_len | |
| orientation_strength = K_ORIENTATION_PULL * t.battery * t.magnetic_strength | |
| torque_axis = np.cross(face_normal, to_origin_norm) | |
| torque_magnitude = np.linalg.norm(torque_axis) | |
| if torque_magnitude > 1e-6: | |
| torque_axis /= torque_magnitude | |
| rotation_amount = torque_magnitude * orientation_strength * scaled_dt | |
| for v_idx in range(4): | |
| rotated = (t.local[v_idx] * np.cos(rotation_amount) + | |
| np.cross(torque_axis, t.local[v_idx]) * np.sin(rotation_amount) + | |
| torque_axis * np.dot(torque_axis, t.local[v_idx]) * (1 - np.cos(rotation_amount))) | |
| t.local[v_idx] = rotated | |
| # --- WHITEPAPER: ARRHENIUS KINETICS --- | |
| def attempt_synthesis_reactions(self, scaled_dt, add_msg_fn): | |
| if len(self.tets) < 2: return [] | |
| current_time = time.time(); positions = np.array([t.pos for t in self.tets]) | |
| if not np.all(np.isfinite(positions)): | |
| # Find indices where position contains NaN or Inf | |
| mask_bad = ~np.all(np.isfinite(positions), axis=1) | |
| bad_indices = np.where(mask_bad)[0] | |
| # Reset bad positions to random safe spots to prevent crash | |
| count = len(bad_indices) | |
| safe_replacements = np.random.uniform(-K_RESET_POS, K_RESET_POS, (count, 3)) + self.center_of_mass | |
| positions[mask_bad] = safe_replacements | |
| # Update the actual Tetrahedron objects so the bad data doesn't persist | |
| for i, idx in enumerate(bad_indices): | |
| self.tets[idx].pos = safe_replacements[i] | |
| self.tets[idx].pos_prev = safe_replacements[i] # Kill velocity | |
| self.tets[idx].local = Tetrahedron.REST_NP.copy() # Reset geometry | |
| tree = cKDTree(positions); reactions_this_frame = [] | |
| # Identify catalysts first | |
| catalyst_indices = [i for i, t in enumerate(self.tets) if t.is_catalyst] | |
| catalyst_positions = positions[catalyst_indices] if catalyst_indices else np.empty((0,3)) | |
| for i, t1 in enumerate(self.tets): | |
| if not t1.label or t1.label not in MOLECULE_SYMBOLS: continue | |
| if current_time - t1.last_reaction_time < 5.0: continue | |
| nearby_indices = tree.query_ball_point(positions[i], REACTION_RANGE) | |
| for j in nearby_indices: | |
| if i >= j: continue | |
| t2 = self.tets[j] | |
| if not t2.label or t2.label not in MOLECULE_SYMBOLS: continue | |
| if current_time - t2.last_reaction_time < 5.0: continue | |
| # Rule 4: Provenance Validation | |
| if t1.element_source not in {"interface", "reaction"} or t2.element_source not in {"interface", "reaction"}: | |
| continue | |
| reaction_key = tuple(sorted([t1.label, t2.label])) | |
| if reaction_key in SYNTHESIS_REACTIONS: | |
| # Rule 2: Catalytic Mediation Guard | |
| is_iron_involved = (t1.label == "Fe" or t2.label == "Fe") | |
| is_oxygen_present = (t1.label == "O" or t2.label == "O" or t1.label == "O2" or t2.label == "O2") | |
| if is_iron_involved and not is_oxygen_present: | |
| # Block transmutation of catalyst without mediator | |
| continue | |
| product = SYNTHESIS_REACTIONS[reaction_key] | |
| # Whitepaper 4.2: Catalytic Activation Energy Reduction | |
| # E_a_effective = E_a * alpha if catalyst present | |
| # alpha ~ 0.3 for FeO4, etc. | |
| E_activation = ACTIVATION_ENERGY_BASE | |
| # Check for nearby catalyst | |
| has_catalyst = False | |
| if catalyst_positions.size > 0: | |
| cat_dists = np.sum((catalyst_positions - positions[i])**2, axis=1) | |
| if np.min(cat_dists) < (REACTION_RANGE * 2)**2: | |
| has_catalyst = True | |
| E_activation *= 0.3 # Catalyst significantly lowers barrier | |
| # Whitepaper 4.1: Arrhenius Equation | |
| # T_eff = local energy density (battery) | |
| # P = A * exp(-E_a / T_eff) | |
| T_eff = (t1.battery + t2.battery) * 0.5 * BOLTZMANN_K | |
| if T_eff < 0.01: T_eff = 0.01 # Prevent division by zero | |
| # Alignment factor for Pre-exponential factor A | |
| # If faces are aligned or magnetic poles opposite, A increases | |
| alignment_factor = 1.0 | |
| if t1.is_magnetized and t2.is_magnetized: | |
| if t1.magnetism != t2.magnetism: alignment_factor = 1.5 | |
| reaction_prob = alignment_factor * math.exp(-E_activation / T_eff) * scaled_dt * 5.0 # Pre-factor adjusted for dt | |
| if random.random() < reaction_prob: | |
| # Reaction Occurs | |
| energy_burst = K_REACTION_ENERGY_RELEASE | |
| # Energy release to neighbors (Thermodynamics) | |
| for t_nearby in self.tets: | |
| dist = np.linalg.norm(t_nearby.pos - t1.pos) | |
| if dist < REACTION_RANGE * 1.5: | |
| t_nearby.battery = min(1.0, t_nearby.battery + energy_burst * (1.0 - dist / (REACTION_RANGE * 1.5))) | |
| t1.label = product; t1.last_reaction_time = current_time; t1.synthesis_count += 1 | |
| t1.battery = max(0.1, t1.battery - SYNTHESIS_ENERGY_COST) | |
| t1.element_source = "reaction" # Mark result as valid complex molecule | |
| t2.battery = 0.0; t2.label = ""; t2.last_reaction_time = current_time | |
| t2.element_source = None # Consumed | |
| reactions_this_frame.append((t1.label, t2.label, product)) | |
| if has_catalyst: | |
| add_msg_fn(f"⚗️ Catalyzed Synthesis: {product}!", duration=3) | |
| print(f"⚗️ Catalyzed Synthesis: {product}!") | |
| else: | |
| add_msg_fn(f"Synthesized {product}", duration=3) | |
| print(f"Synthesized {product}") | |
| msg = self.tech_tree.add_progress(1) | |
| if msg: add_msg_fn(msg, duration=5) | |
| break | |
| return reactions_this_frame | |
| def attempt_decomposition_reactions(self, scaled_dt, add_msg_fn): | |
| current_time = time.time(); decompositions_this_frame = [] | |
| for t in self.tets: | |
| if not t.label or t.label not in MOLECULE_SYMBOLS: continue | |
| if current_time - t.last_reaction_time < 10.0: continue | |
| if t.label in DECOMPOSITION_REACTIONS: | |
| # Whitepaper: Entropy driven decomposition | |
| # Probability increases with T (instability) and distance from origin (entropy) | |
| stress_factor = (1.0 - t.battery) * 2.0 | |
| dist_from_origin = np.linalg.norm(t.pos - self.center_of_mass) | |
| entropy_factor = min(2.0, dist_from_origin / 100.0) | |
| decomp_prob = REACTION_PROBABILITY_BASE * 0.5 * scaled_dt * 60 * stress_factor * entropy_factor | |
| if random.random() < decomp_prob: | |
| products = DECOMPOSITION_REACTIONS[t.label]; old_label = t.label | |
| t.label = products[0]; t.last_reaction_time = current_time; t.battery = min(1.0, t.battery + 0.2) | |
| if len(products) > 1 and len(self.tets) < 200: | |
| offset = np.random.uniform(-K_RESET_POS, K_RESET_POS, 3) * EDGE_LEN * 3 | |
| new_tet = Tetrahedron(t.pos + offset) | |
| new_tet.label = products[1]; new_tet.battery = t.battery * 0.8; new_tet.colors = list(Tetrahedron.FACE_COLORS) | |
| new_tet.element_source = "reaction" # Product inherits reaction provenance | |
| self.tets.append(new_tet) | |
| decompositions_this_frame.append((old_label, products)) | |
| add_msg_fn(f"💥 {old_label} decomposed!", duration=3) | |
| print(f"💥 {old_label} decomposed!") | |
| # Genesis Protocol: Collapse Check | |
| if len(self.tets) < 5 and self.tech_tree.stage_idx >= 3: | |
| msg = self.tech_tree.collapse() | |
| if msg: add_msg_fn(msg, duration=5) | |
| self.apply_erd_fluctuations(scaled_dt) | |
| tunnel_reactions = self.attempt_quantum_tunneling(scaled_dt, add_msg_fn) | |
| entangle_reactions = self.process_entangled_reactions(scaled_dt, add_msg_fn) | |
| quantum_events = [] | |
| for _, _, product in tunnel_reactions: | |
| for t in self.tets: | |
| if t.label == product and t.quantum_state == "tunneled": quantum_events.append(("tunnel", t.pos)) | |
| for _, _, product in entangle_reactions: | |
| for t in self.tets: | |
| if t.label == product: quantum_events.append(("entangle", t.pos)) | |
| self._last_quantum_events = quantum_events | |
| return decompositions_this_frame | |
| def spawn_reaction_particles(self, screen, cam, reactions, width, height): | |
| if not hasattr(self, 'reaction_particles'): self.reaction_particles = [] | |
| for reaction in reactions: | |
| if len(reaction) == 3: | |
| r1, r2, product = reaction | |
| for t in self.tets: | |
| if t.label == product: | |
| particle = {'pos': t.pos.copy(), 'vel': np.random.uniform(-K_RESET_POS, K_RESET_POS, 3) * 20, 'color': t.aura_color if t.aura_color else (255, 255, 0), 'life': 1.0, 'size': 8} | |
| self.reaction_particles.append(particle) | |
| for _ in range(random.randint(4, 9)): | |
| p = particle.copy(); p['vel'] = np.random.uniform(-K_RESET_POS, K_RESET_POS, 3) * 30; p['color'] = tuple(np.array(p['color']) * random.uniform(0.6, 1.2)); self.reaction_particles.append(p) | |
| break | |
| elif len(reaction) == 2: | |
| old_label, products = reaction | |
| for t in self.tets: | |
| if t.label == products[0]: | |
| particle = {'pos': t.pos.copy(), 'vel': np.random.uniform(-K_RESET_POS, K_RESET_POS, 3) * 15, 'color': (255, 100, 0), 'life': 1.0, 'size': 6} | |
| self.reaction_particles.append(particle); break | |
| particles_to_keep = [] | |
| for p in self.reaction_particles: | |
| p['pos'] += p['vel'] * 0.016; p['vel'] *= 0.95; p['life'] -= 0.02 | |
| if p['life'] > 0: | |
| screen_pos = cam.project(p['pos']) | |
| if screen_pos[0] > -10000: | |
| alpha = int(p['life'] * 255); size = max(1, int(p['size'] * p['life'])); color_with_alpha = tuple(list(p['color']) + [alpha]) | |
| try: | |
| particle_surf = pygame.Surface((size*2, size*2), pygame.SRCALPHA) | |
| pygame.draw.circle(particle_surf, color_with_alpha, (size, size), size) | |
| screen.blit(particle_surf, (screen_pos[0]-size, screen_pos[1]-size)) | |
| except: pass | |
| particles_to_keep.append(p) | |
| self.reaction_particles = particles_to_keep | |
| def explode(self): | |
| self.joints.clear(); self.sticky_pairs.clear() | |
| for t in self.tets: t.pos_prev = t.pos - np.random.uniform(-K_RESET_POS, K_RESET_POS, 3); t.local_prev = t.local - np.random.uniform(-0.5, 0.5, (4,3)) | |
| def try_snap(self, A, ia, B, ib): | |
| # Helper to check if a specific connection is allowed | |
| def is_valid_connection(t, idx): | |
| # Get max allowed bonds for this element | |
| cap = BOND_CAPACITY.get(t.molecule_type, BOND_CAPACITY['default']) | |
| current = t.get_bond_count(idx, self.joints) | |
| # Allow if under capacity AND state is Active(1), Bonded(2-with capacity), or Hybridizing(3) | |
| state = t.corner_states[idx] | |
| if current < cap: | |
| if state == 1: return True | |
| if state == 2: return True # Multi-bond allowed | |
| if state == 3: return True # Hybridization allowed | |
| return False | |
| # 1. SMART SEARCH: If A.ia is invalid, find a better corner on A | |
| target_A_idx = ia | |
| if not is_valid_connection(A, ia): | |
| # Search other corners. | |
| # Heuristic: Find closest ACTIVE corner to B's target vertex | |
| # (In a TET, all corners share 2 faces with any other corner, | |
| # so we use Euclidean distance to find the most "aligned" one) | |
| best_dist = float('inf') | |
| found_alt = -1 | |
| target_pos = B.pos + B.local[ib] | |
| for k in range(4): | |
| if k == ia: continue # Skip the blocked one | |
| if is_valid_connection(A, k): | |
| # Check distance | |
| d = np.linalg.norm((A.pos + A.local[k]) - target_pos) | |
| if d < best_dist: | |
| best_dist = d | |
| found_alt = k | |
| if found_alt != -1: | |
| target_A_idx = found_alt | |
| # print(f"Auto-switched snap from A[{ia}] to A[{target_A_idx}]") | |
| # Repeat Search for B | |
| target_B_idx = ib | |
| if not is_valid_connection(B, ib): | |
| best_dist = float('inf') | |
| found_alt = -1 | |
| target_pos = A.pos + A.local[target_A_idx] | |
| for k in range(4): | |
| if k == ib: continue | |
| if is_valid_connection(B, k): | |
| d = np.linalg.norm((B.pos + B.local[k]) - target_pos) | |
| if d < best_dist: | |
| best_dist = d | |
| found_alt = k | |
| if found_alt != -1: | |
| target_B_idx = found_alt | |
| # 2. FINAL VALIDATION (After attempting to switch) | |
| if not is_valid_connection(A, target_A_idx) or not is_valid_connection(B, target_B_idx): | |
| return False | |
| # 3. HYBRIDIZATION & BONDING | |
| # If we are snapping a State 3 (Lone Pair), we force-convert it (Hybridize) | |
| if A.corner_states[target_A_idx] == 3: | |
| A.corner_states[target_A_idx] = 2 # Hybridize to Bonded | |
| # print("Hybridized A!") | |
| if B.corner_states[target_B_idx] == 3: | |
| B.corner_states[target_B_idx] = 2 # Hybridize to Bonded | |
| # 4. PREVENT DUPLICATES | |
| # Check if this exact link already exists | |
| for j in self.joints: | |
| if (j.A.id == A.id and j.ia == target_A_idx and j.B.id == B.id and j.ib == target_B_idx) or \ | |
| (j.A.id == B.id and j.ia == target_B_idx and j.B.id == A.id and j.ib == target_A_idx): | |
| return False | |
| # 5. EXECUTE | |
| self.joints.append(VertexJoint(A, target_A_idx, B, target_B_idx)) | |
| # Update states to 2 (Bonded) just in case they were 1 | |
| A.corner_states[target_A_idx] = 2 | |
| B.corner_states[target_B_idx] = 2 | |
| msg = self.tech_tree.add_progress(1) | |
| if msg: self.notification_queue.append(msg) | |
| if self.sound and AUDIO_ENABLED: self.sound.play() | |
| return True | |
| def calculate_dynamic_center(self): | |
| if not self.tets: return np.zeros(3) | |
| # Use cached positions if available for speed | |
| if len(self.cached_pos) == len(self.tets): | |
| positions = self.cached_pos | |
| else: | |
| positions = np.array([t.pos for t in self.tets]) | |
| if len(positions) == 0: return np.zeros(3) | |
| mask_valid = np.all(np.isfinite(positions), axis=1) | |
| valid_positions = positions[mask_valid] | |
| if len(valid_positions) == 0: | |
| return np.zeros(3) | |
| try: | |
| center = np.mean(positions, axis=0) | |
| except Exception: | |
| return np.zeros(3) | |
| if not np.all(np.isfinite(center)): | |
| return np.zeros(3) | |
| return center | |
| def rebuild_optimization_cache(self): | |
| """Syncs Python Object data into Numpy Arrays for JIT processing.""" | |
| count = len(self.tets) | |
| if count == 0: return | |
| # Re-allocate only if size changed (Memory Optimization) | |
| if self.cached_pos.shape[0] != count: | |
| self.cached_pos = np.zeros((count, 3)) | |
| self.cached_prev = np.zeros((count, 3)) | |
| self.cached_local = np.zeros((count, 4, 3)) | |
| self.cached_local_prev = np.zeros((count, 4, 3)) | |
| self.cached_bat = np.zeros(count) | |
| self.cached_coh = np.zeros(count) | |
| self.cached_bias = np.zeros((count, 3)) | |
| # Fast copy loop | |
| for i, t in enumerate(self.tets): | |
| self.cached_pos[i] = t.pos | |
| self.cached_prev[i] = t.pos_prev | |
| self.cached_local[i] = t.local | |
| self.cached_local_prev[i] = t.local_prev | |
| if math.isnan(t.battery) or math.isinf(t.battery): | |
| t.battery = 0.5 | |
| if math.isnan(t.erd_coherence) or math.isinf(t.erd_coherence): | |
| t.erd_coherence = 0.0 | |
| self.cached_bat[i] = t.battery | |
| self.cached_coh[i] = t.erd_coherence | |
| self.cached_bias[i] = t.orientation_bias | |
| def update_logic(self, scaled_dt, add_msg_fn, cam=None): | |
| """Heavy Logic: Runs ONCE per frame.""" | |
| while self.notification_queue: | |
| msg_text = self.notification_queue.pop(0) | |
| add_msg_fn(msg_text, duration=5) | |
| if not self.tets: return | |
| self.check_magnetization() | |
| self.calculate_global_fields() | |
| self.update_magnetic_batteries(scaled_dt) | |
| self.process_metabolism(scaled_dt, add_msg_fn) | |
| # Chemistry & Quantum | |
| self._last_synth_reactions = self.attempt_synthesis_reactions(scaled_dt, add_msg_fn) | |
| self._last_quantum_events = self.attempt_decomposition_reactions(scaled_dt, add_msg_fn) | |
| self.center_of_mass = self.calculate_dynamic_center() | |
| # Bot Minds (Only update every ~60 frames or so if you want, but once/frame is fine for <50 tets) | |
| if len(self.tets) > 0: | |
| cached_positions = self.cached_pos | |
| cached_batteries = self.cached_bat | |
| cached_coherences = self.cached_coh | |
| def efficient_get_fields(pos): | |
| return calculate_fields_jit(cached_positions, cached_batteries, cached_coherences, pos) | |
| for t in self.tets: | |
| if t.mind: | |
| nearby = [o for o in self.tets if np.linalg.norm(o.pos - t.pos) < 5] | |
| t.mind.perceive(nearby, []) | |
| t.mind.decide_goal(world_fields_func=efficient_get_fields, my_pos=t.pos) | |
| desire = t.mind.get_desire_vector(t.pos, nearby) | |
| t.pos += desire * 0.01 | |
| self.center_of_mass = self.calculate_dynamic_center() | |
| # === ORIGIN RE-CENTERING (Floating Origin) === | |
| # If the cluster drifts > 10 edge lengths (20 units) from (0,0,0) | |
| drift_dist_sq = np.sum(self.center_of_mass**2) | |
| THRESHOLD_SQ = (EDGE_LEN * 100.0)**2 | |
| if drift_dist_sq > THRESHOLD_SQ: | |
| # Move everything half the distance back towards (0,0,0) | |
| shift_vec = -self.center_of_mass * 0.5 | |
| # 1. Shift all TETs | |
| for t in self.tets: | |
| t.pos += shift_vec | |
| t.pos_prev =t.pos #+= shift_vec # Preserve velocity | |
| # 2. Shift the Optimization Cache (Critical for physics stability) | |
| self.cached_pos += shift_vec | |
| self.cached_prev += shift_vec | |
| # 3. Update the calculated center | |
| self.center_of_mass += shift_vec | |
| # 4. Shift the Camera (So the player sees NO movement) | |
| if cam: | |
| cam.pan += shift_vec | |
| # print(f"🌍 World Drift Corrected: Origin Shifted by {shift_vec}") | |
| def apply_lone_pair_repulsion(self, scaled_dt): | |
| """Lone pairs (State 3) push away any non-bonded neighbor to enforce geometry.""" | |
| # Collect all Lone Pair vertices | |
| lp_verts = [] | |
| for t in self.tets: | |
| for i in range(4): | |
| if t.corner_states[i] == 3: | |
| # Calculate world position of this corner | |
| world_pos = t.pos + t.local[i] | |
| lp_verts.append((world_pos, t)) | |
| if not lp_verts: return | |
| # Simple N^2 check (optimization: use cKDTree if >100 tets) | |
| for lp_pos, t_owner in lp_verts: | |
| for t_target in self.tets: | |
| if t_owner.id == t_target.id: continue # Don't repel self | |
| dist_sq = np.sum((t_target.pos - lp_pos)**2) | |
| # Repulsion range: ~1 edge length | |
| if dist_sq < EDGE_LEN**2: | |
| dist = np.sqrt(dist_sq) | |
| # Strong, short-range force | |
| force = (lp_pos - t_target.pos) / (dist + 0.01) * 0.1 * scaled_dt | |
| t_target.pos -= force | |
| t_owner.pos += force * 0.1 # Slight recoil | |
| def update_physics_only(self, dt, time_scale, spin_multiplier): | |
| """Pure Physics: Runs MANY times per frame (Sub-Stepping).""" | |
| if not self.tets: return | |
| self.apply_lone_pair_repulsion(dt) | |
| if self.cached_pos.shape[0] != len(self.tets): | |
| self.rebuild_optimization_cache() | |
| # 1. Update pointers to cached arrays | |
| positions = self.cached_pos | |
| positions_prev = self.cached_prev | |
| locals_arr = self.cached_local | |
| locals_prev = self.cached_local_prev | |
| batteries = self.cached_bat | |
| coherences = self.cached_coh | |
| orientation_biases = self.cached_bias | |
| # 2. Apply Object-Level Forces | |
| self.apply_corner_desires(dt) | |
| self.apply_same_pole_repulsion(dt) | |
| self.apply_negative_pole_orientation(dt) | |
| # 3. RE-SYNC POSITIONS | |
| for i, t in enumerate(self.tets): | |
| positions[i] = t.pos | |
| locals_arr[i] = t.local | |
| # 4. Prepare Indexes for JIT | |
| id_to_idx = {t.id: i for i, t in enumerate(self.tets)} | |
| if self.joints: | |
| valid_joints = [j for j in self.joints if j.A.id in id_to_idx and j.B.id in id_to_idx] | |
| joints_data = np.array([[id_to_idx[j.A.id], j.ia, id_to_idx[j.B.id], j.ib] for j in valid_joints], dtype=np.int32) | |
| else: joints_data = np.empty((0, 4), dtype=np.int32) | |
| valid_pairs = [] | |
| ages = [] | |
| for p in self.sticky_pairs: | |
| if p[0].id in id_to_idx and p[2].id in id_to_idx: | |
| valid_pairs.append([id_to_idx[p[0].id], p[1], id_to_idx[p[2].id], p[3]]) | |
| pair_key = tuple(sorted((p[0].id, p[2].id))) | |
| ages.append(self.pair_ages.get(pair_key, 0.0)) | |
| sticky_data = np.array(valid_pairs, dtype=np.int32) if valid_pairs else np.empty((0, 4), dtype=np.int32) | |
| ages_data = np.array(ages, dtype=np.float64) if ages else np.empty(0, dtype=np.float64) | |
| magnet_indices = np.array([i for i, t in enumerate(self.tets) if t.is_magnetized], dtype=np.int32) | |
| magnet_polarities = np.array([t.magnetism for t in self.tets if t.is_magnetized], dtype=np.float64) if magnet_indices.size > 0 else np.empty(0, dtype=np.float64) | |
| # 5. EXECUTE JIT PHYSICS | |
| positions, positions_prev, locals_arr, locals_prev, batteries = world_update_physics_jit( | |
| positions, positions_prev, locals_arr, locals_prev, batteries, coherences, | |
| dt, time_scale, Tetrahedron.EDGES_NP, sticky_data, ages_data, | |
| joints_data, spin_multiplier, magnet_indices | |
| ) | |
| locals_arr, orientation_biases = update_magnetic_effects_jit( | |
| locals_arr, orientation_biases, positions, magnet_indices, magnet_polarities, dt | |
| ) | |
| positions_prev = conserve_momentum_jit(positions, positions_prev) | |
| # === CRASH SAFETY CHECK === | |
| # If JIT returned Infinity/NaN, reset those specific particles BEFORE creating cKDTree | |
| if not np.all(np.isfinite(positions)): | |
| mask_bad = ~np.all(np.isfinite(positions), axis=1) | |
| bad_indices = np.where(mask_bad)[0] | |
| if len(bad_indices) > 0: | |
| #print(f"⚠️ Physics Singularity: Resetting {len(bad_indices)} particles.") | |
| respawn_center = self.center_of_mass | |
| if not np.all(np.isfinite(respawn_center)): | |
| respawn_center = cam.position | |
| for i, idx in enumerate(bad_indices): | |
| #self.sever_bonds(idx) # <--- CUT THE RUBBER BAND | |
| safe_pos = respawn_center + np.random.uniform(-K_RESET_POS, K_RESET_POS, 3) | |
| positions[idx] = safe_pos | |
| positions_prev[idx] = safe_pos # Kill velocity | |
| locals_arr[idx] = Tetrahedron.REST_NP.copy() | |
| locals_prev[idx] = Tetrahedron.REST_NP.copy() | |
| batteries[idx] = 0.5 # Reset to half charge | |
| coherences[idx] = 0.0 # Reset quantum state | |
| orientation_biases[idx] = np.zeros(3) # Stop spinning | |
| t = self.tets[idx] | |
| t.battery = 0.5 | |
| t.erd_coherence = 0.0 | |
| t.orientation_bias = np.zeros(3) | |
| # === UNIVERSE BOUNDARY (The Slingshot Fix) === | |
| # Check against Median Center to avoid tracking the outlier itself | |
| center_ref = self.center_of_mass | |
| dist_sq = np.sum((positions - center_ref)**2, axis=1) | |
| # If > 1000 units away, it's gone. | |
| mask_far = dist_sq > 1000**2 | |
| if np.any(mask_far): | |
| far_indices = np.where(mask_far)[0] | |
| for idx in far_indices: | |
| # self.sever_bonds(idx) | |
| # Teleport back to edge of cluster (Radius 50) | |
| # Give it a gentle nudge inward to reintegrate | |
| dir_to_center = center_ref - positions[idx] | |
| dir_norm = dir_to_center / (np.linalg.norm(dir_to_center) + 1e-9) | |
| positions[idx] = center_ref - (dir_norm * 70.0) | |
| kick_strength = min(0.0001, np.sqrt(dist_sq.all()) * 0.0000001) | |
| positions_prev[idx] = positions[idx] - (dir_norm * kick_strength) | |
| #print(f"🌌 Recalled lost TET {idx}") | |
| # 6. Collision & Constraints | |
| mask_far = np.sum((positions - self.center_of_mass)**2, axis=1) > 1000**2 | |
| if np.any(mask_far): | |
| bad_indices = np.where(mask_far)[0] | |
| for idx in bad_indices: | |
| positions[idx] = self.center_of_mass + np.random.uniform(-K_RESET_POS, K_RESET_POS, 3) | |
| positions_prev[idx] = positions[idx] | |
| # Safe to create Tree now | |
| tree = cKDTree(positions) | |
| pairs = tree.query_pairs(r=COLLISION_RADIUS * 2) | |
| if pairs: positions = resolve_collisions_jit(positions, np.array(list(pairs))) | |
| if joints_data.shape[0] > 0: | |
| for _ in range(3): locals_arr = resolve_joints_jit(locals_arr, joints_data) | |
| # 7. Snapping Logic | |
| MAX_DESIRE_DIST_SQ = 300.0**2 | |
| for pair in self.sticky_pairs[:]: | |
| t1, i1, t2, i2 = pair | |
| dist_sq = np.sum((t1.pos - t2.pos)**2) | |
| # Check 2: Universe Boundary (Double check for lost TETs) | |
| t1_far = np.sum((t1.pos - self.center_of_mass)**2) > 300**2 | |
| t2_far = np.sum((t2.pos - self.center_of_mass)**2) > 300**2 | |
| if dist_sq > MAX_DESIRE_DIST_SQ or t1_far or t2_far: | |
| # self.sticky_pairs.remove(pair) | |
| # Optional: If they were lost in space, reset them here too | |
| if t1_far: | |
| t1.pos = self.center_of_mass + np.random.uniform(-K_RESET_POS, K_RESET_POS, 3) | |
| t1.pos_prev = t1.pos.copy() | |
| if t2_far: | |
| t2.pos = self.center_of_mass + np.random.uniform(-K_RESET_POS, K_RESET_POS, 3) | |
| t2.pos_prev = t2.pos.copy() | |
| if t1.id not in id_to_idx or t2.id not in id_to_idx: continue | |
| idx1, idx2 = id_to_idx[t1.id], id_to_idx[t2.id] | |
| p1 = locals_arr[idx1, i1] + positions[idx1] | |
| p2 = locals_arr[idx2, i2] + positions[idx2] | |
| if np.linalg.norm(p2 - p1) < SNAP_DIST: | |
| if self.try_snap(t1, i1, t2, i2): | |
| self.sticky_pairs.remove(pair) | |
| # 8. Sync Back | |
| for i, t in enumerate(self.tets): | |
| if not t.pos.all()==t.pos_prev.all(): | |
| t.pos = positions[i] | |
| t.pos_prev = positions_prev[i] | |
| t.local = locals_arr[i] | |
| t.local_prev = locals_prev[i] | |
| t.battery = batteries[i] | |
| t.orientation_bias = orientation_biases[i] | |
| def get_state(self): | |
| tet_states = [{'id': t.id, 'pos': t.pos, 'pos_prev': t.pos_prev, 'local': t.local, 'local_prev': t.local_prev, 'battery': t.battery, 'colors': t.colors, 'label': t.label, 'orientation_bias': t.orientation_bias} for t in self.tets] | |
| joint_states = [{'A_id': j.A.id, 'ia': j.ia, 'B_id': j.B.id, 'ib': j.ib} for j in self.joints] | |
| return {'tets': tet_states, 'joints': joint_states, 'center_of_mass': self.center_of_mass} | |
| def set_state(self, state): | |
| self.tets.clear(); self.joints.clear(); self.sticky_pairs.clear() | |
| tet_map = {} | |
| for ts in state.get('tets', []): | |
| try: | |
| pos = np.array(ts['pos'], dtype=np.float64) | |
| t = Tetrahedron(pos) | |
| t.id = ts['id'] | |
| t.pos_prev = np.array(ts.get('pos_prev', pos), dtype=np.float64) | |
| if 'local' in ts: t.local = np.array(ts['local'], dtype=np.float64); t.local_prev = np.array(ts.get('local_prev', t.local), dtype=np.float64) | |
| t.battery = float(ts.get('battery', 0.5)) | |
| if 'colors' in ts and ts['colors']: t.colors = [tuple(c) for c in ts['colors']] | |
| else: t.colors = None | |
| t.label = ts.get('label', "") | |
| t.orientation_bias = np.array(ts.get('orientation_bias', [0, 0, 0]), dtype=np.float64) | |
| t.is_magnetized = False; t.magnetism = 0; t.magnetic_strength = 0.0; t.locked_faces = [] | |
| t.molecule_type = None; t.aura_color = None; t.polarity_face_idx = None | |
| t.last_reaction_time = 0.0; t.synthesis_count = 0; t.is_catalyst = False | |
| self.tets.append(t); tet_map[t.id] = t | |
| except (KeyError, TypeError, ValueError): continue | |
| for js in state.get('joints', []): | |
| try: | |
| if js['A_id'] in tet_map and js['B_id'] in tet_map: | |
| self.joints.append(VertexJoint(tet_map[js['A_id']], js['ia'], tet_map[js['B_id']], js['ib'])) | |
| except (KeyError, TypeError): continue | |
| if 'center_of_mass' in state: self.center_of_mass = np.array(state['center_of_mass'], dtype=np.float64) | |
| else: self.center_of_mass = self.calculate_dynamic_center() | |
| net_avatars = {}; net_messages = deque(maxlen=5); game_mode = 'single_player'; host_instance, guest_instance = None, None | |
| def prime_jit_functions(cam): | |
| num_dummy = 4; dummy_pos = np.random.rand(num_dummy, 3) * 100; dummy_pos_prev = dummy_pos.copy() | |
| dummy_locals = np.random.rand(num_dummy, 4, 3); dummy_locals_prev = dummy_locals.copy(); dummy_batteries = np.random.rand(num_dummy) | |
| dummy_coherences = np.random.rand(num_dummy) # Needed for JIT | |
| dummy_edges, dummy_sticky_pairs = Tetrahedron.EDGES_NP, np.array([[0, 0, 1, 1], [2, 3, 1, 0]], dtype=np.int32) | |
| dummy_pair_ages = np.array([0.0, 1.0], dtype=np.float64) | |
| dummy_magnet_indices, dummy_magnet_polarities, dummy_orientation_biases, dummy_joints = np.array([0, 1], dtype=np.int32), np.array([1.0, -1.0], dtype=np.float64), np.zeros((num_dummy, 3)), np.array([[0, 2, 3, 1]], dtype=np.int32) | |
| norm_njit(np.array([1.0, 2.0, 3.0])); norm_axis_njit(dummy_pos); project_many_jit(dummy_pos, cam.pan, cam.yaw, cam.pitch, cam.dist, WIDTH, HEIGHT); get_transformed_z_many_jit(dummy_pos, cam.pan, cam.yaw, cam.pitch) | |
| world_update_physics_jit(dummy_pos, dummy_pos_prev, dummy_locals, dummy_locals_prev, dummy_batteries, dummy_coherences, 1/60.0, 1.0, dummy_edges, dummy_sticky_pairs, dummy_pair_ages, dummy_joints, 1.0, dummy_magnet_indices) | |
| update_magnetic_effects_jit(dummy_locals, dummy_orientation_biases, dummy_pos, dummy_magnet_indices, dummy_magnet_polarities, 1/60.0) | |
| conserve_momentum_jit(dummy_pos, dummy_pos_prev); resolve_collisions_jit(dummy_pos, np.array([[0, 1], [2, 3]])); resolve_joints_jit(dummy_locals, dummy_joints) | |
| dist_point_to_line_segment(np.array([1.0, 1.0], dtype=np.float64), np.array([0.0, 0.0], dtype=np.float64), np.array([2.0, 2.0], dtype=np.float64)) | |
| calculate_disk_quads(np.zeros(3), np.zeros(3), 0.0, 0.0, 100.0, 800, 600, 50.0, np.array([1.,0.,0.]), np.array([0.,0.,1.]), np.array([0.,0.,1.]), np.array([255.,255.,255.]), 0.5, 0.0) | |
| calculate_fields_jit(dummy_pos, dummy_batteries, dummy_batteries, np.array([0.,0.,0.])) | |
| def show_intro(screen, cam): | |
| global WIDTH, HEIGHT | |
| if ON_HUGGINGFACE: | |
| threading.Thread(target=prime_jit_functions, args=(cam,), daemon=True).start() | |
| time.sleep(2) | |
| return | |
| font_lg = pygame.font.SysFont('Arial Black', min(WIDTH, HEIGHT)//8); font_sm = pygame.font.SysFont('Arial', min(WIDTH, HEIGHT)//25); font_jit = pygame.font.SysFont('Monospace', 18) | |
| threading.Thread(target=prime_jit_functions, args=(cam,), daemon=True).start() | |
| while threading.active_count() > 1: | |
| for e in pygame.event.get(): | |
| if e.type == pygame.QUIT: pygame.quit(); sys.exit() | |
| if e.type == pygame.VIDEORESIZE: WIDTH, HEIGHT = e.w, e.h; screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.RESIZABLE); font_lg = pygame.font.SysFont('Arial Black', min(WIDTH, HEIGHT)//8); font_sm = pygame.font.SysFont('Arial', min(WIDTH, HEIGHT)//25) | |
| screen.fill((10,10,20)); title = font_lg.render("TET~CRAFT", True, (255, 50, 50)); sub = font_sm.render("~A Kleinverse of your own from DigitizingHumanity.com~", True, (255, 255, 255)) | |
| screen.blit(title, title.get_rect(center=(WIDTH//2, HEIGHT//2-50))); screen.blit(sub, sub.get_rect(center=(WIDTH//2, HEIGHT//2+50))) | |
| jit_surf = font_jit.render("Constructing you a unique Kleinverse...", True, (0, 255, 0)); screen.blit(jit_surf, (10, 10)) | |
| if pygame.time.get_ticks() % 1000 < 500: screen.blit(font_jit.render("_", True, (0, 255, 0)), (10 + jit_surf.get_width(), 10)) | |
| pygame.display.flip(); clock.tick(30) | |
| def show_name_input_screen(screen): | |
| global WIDTH, HEIGHT, PLAYER_NAME, SAVE_FILENAME | |
| if ON_HUGGINGFACE: | |
| PLAYER_NAME = "Seeker"; SAVE_FILENAME = "Seeker.json"; return | |
| font_lg = pygame.font.SysFont('Georgia', 40); font_sm = pygame.font.SysFont('Arial', 24); active = True; input_text = "" | |
| while active: | |
| for e in pygame.event.get(): | |
| if e.type == pygame.QUIT: pygame.quit(); sys.exit() | |
| if e.type == pygame.VIDEORESIZE: WIDTH, HEIGHT = e.w, e.h; screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.RESIZABLE) | |
| if e.type == pygame.KEYDOWN: | |
| if e.key == pygame.K_RETURN: | |
| if input_text.strip(): PLAYER_NAME = input_text.strip(); SAVE_FILENAME = f"{PLAYER_NAME}.json"; active = False | |
| elif e.key == pygame.K_BACKSPACE: input_text = input_text[:-1] | |
| else: input_text += e.unicode | |
| screen.fill((15, 15, 30)) | |
| prompt = font_lg.render("Who be ye?", True, (200, 200, 255)); screen.blit(prompt, prompt.get_rect(center=(WIDTH//2, HEIGHT//2 - 50))) | |
| txt_surf = font_lg.render(input_text + ("_" if pygame.time.get_ticks() % 1000 < 500 else ""), True, (255, 255, 0)); screen.blit(txt_surf, txt_surf.get_rect(center=(WIDTH//2, HEIGHT//2 + 20))) | |
| sub = font_sm.render("(Mash and then press ENTER to Begin)", True, (100, 100, 100)); screen.blit(sub, sub.get_rect(center=(WIDTH//2, HEIGHT//2 + 80))) | |
| pygame.display.flip(); clock.tick(30) | |
| def show_void_screen(screen, world): | |
| global WIDTH, HEIGHT | |
| if ON_HUGGINGFACE: | |
| if not world.tets: world.spawn(); world.tets[0].label = "Me" | |
| return | |
| font_lg = pygame.font.SysFont('Georgia', min(WIDTH, HEIGHT)//15); font_sm = pygame.font.SysFont('Arial', min(WIDTH, HEIGHT)//25) | |
| waiting = True | |
| while waiting: | |
| for e in pygame.event.get(): | |
| if e.type == pygame.QUIT: pygame.quit(); sys.exit() | |
| if e.type == pygame.KEYDOWN and e.key == pygame.K_SPACE: world.spawn(); waiting = False | |
| if e.type == pygame.VIDEORESIZE: WIDTH, HEIGHT = e.w, e.h; screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.RESIZABLE); font_lg = pygame.font.SysFont('Georgia', min(WIDTH, HEIGHT)//15); font_sm = pygame.font.SysFont('Arial', min(WIDTH, HEIGHT)//25) | |
| screen.fill((10,10,20)); | |
| line1 = font_lg.render(f"Welcome, {PLAYER_NAME}... to the VOID of (mis)Understanding!", True, (200,200,200)); | |
| line2 = font_sm.render("(Press SPACE to begin)", True, (150,150,150)); | |
| line3 = font_sm.render("There is nothing here but unbound, Loving Potential", True, (255,150,255)) | |
| line4 = font_sm.render("OH the bliss... but we must earn it!", True, (255,150,255)) | |
| screen.blit(line1, line1.get_rect(center=(WIDTH//2, HEIGHT//2-60))); | |
| screen.blit(line2, line2.get_rect(center=(WIDTH//2, HEIGHT//2+20))); | |
| screen.blit(line3, line3.get_rect(center=(WIDTH//2, HEIGHT//2+60))); | |
| screen.blit(line4, line4.get_rect(center=(WIDTH//2, HEIGHT//2+100))); | |
| pygame.display.flip(); clock.tick(15) | |
| def find_molecules(world): | |
| # 1. Build Adjacency Graph | |
| adj = {t.id: [] for t in world.tets} | |
| for j in world.joints: | |
| adj[j.A.id].append(j.B) | |
| adj[j.B.id].append(j.A) | |
| visited = set() | |
| molecules = [] | |
| # 2. Find Connected Components (DFS) | |
| for t in world.tets: | |
| if t.id in visited: continue | |
| # Start new molecule | |
| group = [] | |
| stack = [t] | |
| visited.add(t.id) | |
| while stack: | |
| curr = stack.pop() | |
| group.append(curr) | |
| for neighbor in adj[curr.id]: | |
| if neighbor.id not in visited: | |
| visited.add(neighbor.id) | |
| stack.append(neighbor) | |
| molecules.append(group) | |
| return molecules | |
| def get_molecule_name(group, world): | |
| # 1. SCAN STRUCTURE: Find Face-Locked Pairs (True Atoms) | |
| group_ids = {t.id for t in group} | |
| pair_connections = {} | |
| for j in world.joints: | |
| if j.A.id in group_ids and j.B.id in group_ids: | |
| pair = tuple(sorted((j.A.id, j.B.id))) | |
| pair_connections[pair] = pair_connections.get(pair, 0) + 1 | |
| atom_counts = {} # { 'C': 2 } means 2 Carbon Atoms (4 TETs) | |
| processed_tets = set() | |
| for pair, links in pair_connections.items(): | |
| if links >= 3: # Face Lock condition met -> It is an Atom | |
| # Identify Element | |
| t_obj = next((t for t in group if t.id == pair[0]), None) | |
| if t_obj and t_obj.molecule_type: | |
| elem = t_obj.molecule_type | |
| atom_counts[elem] = atom_counts.get(elem, 0) + 1 | |
| processed_tets.add(pair[0]) | |
| processed_tets.add(pair[1]) | |
| # 2. SCAN LEFTOVERS: Find Loose Radicals (TETs attached but not locked) | |
| loose_counts = {} | |
| for t in group: | |
| if t.id not in processed_tets: | |
| if t.molecule_type: | |
| lbl = t.molecule_type | |
| loose_counts[lbl] = loose_counts.get(lbl, 0) + 1 | |
| # 3. BUILD FORMULA (Hill System) | |
| # Rules: C first, then H, then Alphabetical. | |
| formula_parts = [] | |
| # Check for Carbon | |
| has_c = 'C' in atom_counts | |
| if has_c: | |
| qty = atom_counts.pop('C') | |
| formula_parts.append(f"C{qty}" if qty > 1 else "C") | |
| # If C is present, H comes next | |
| if 'H' in atom_counts: | |
| qty = atom_counts.pop('H') | |
| formula_parts.append(f"H{qty}" if qty > 1 else "H") | |
| # Sort remaining elements alphabetically | |
| for elem in sorted(atom_counts.keys()): | |
| qty = atom_counts[elem] | |
| formula_parts.append(f"{elem}{qty}" if qty > 1 else elem) | |
| full_name = "".join(formula_parts) | |
| # 4. APPEND LOOSE RADICALS (if any) | |
| if loose_counts: | |
| loose_str = [] | |
| for elem in sorted(loose_counts.keys()): | |
| qty = loose_counts[elem] | |
| # Show raw TET count for loose parts | |
| # loose_str.append(f"{qty}{elem}" if qty > 1 else elem) | |
| suffix = f"({' '.join(loose_str)})*" | |
| full_name = f"{full_name} {suffix}" if full_name else suffix | |
| # 5. CHECK COMMON NAMES (Only for stable molecules) | |
| if not loose_counts and full_name: | |
| COMMON_NAMES = { | |
| "H2O": "Water", "CO2": "Carbon Dioxide", "CH4": "Methane", | |
| "O2": "Oxygen", "C2H6": "Ethane", "Fe2O3": "Rust", | |
| "H2": "Hydrogen", "N2": "Nitrogen", "NH3": "Ammonia", "C6H12O6": "Glucose" | |
| } | |
| return COMMON_NAMES.get(full_name, full_name) | |
| return full_name.strip() | |
| def draw_standard_black_hole_jit(target_surf, cam, flags, tl, center_pos, world): | |
| if not flags['t3']: return | |
| bh_screen_pos = cam.project(center_pos) | |
| if bh_screen_pos[0] == -10000: return | |
| cx, cy = bh_screen_pos | |
| radius_base = (WIDTH / 6.0) * tl | |
| shadow_radius = radius_base * 0.8 | |
| if shadow_radius < 5: return | |
| cam.update_vectors(); view_dir = cam.forward | |
| dot_h = abs(np.dot(view_dir, np.array([0, 1, 0]))) | |
| color_h = np.array([255., 100., 50.]) | |
| battery_avg = world.get_average_battery() | |
| current_time = time.time() | |
| q_h, c_h = calculate_disk_quads(center_pos, cam.pan, cam.yaw, cam.pitch, cam.dist, WIDTH, HEIGHT, shadow_radius, np.array([1.,0.,0.]), np.array([0.,0.,1.]), view_dir, color_h, battery_avg, current_time) | |
| for i in range(len(q_h)): pygame.draw.polygon(target_surf, c_h[i], q_h[i]) | |
| if dot_h < 0.9: | |
| color_v = np.array([50., 100., 255.]) | |
| q_v, c_v = calculate_disk_quads(center_pos, cam.pan, cam.yaw, cam.pitch, cam.dist, WIDTH, HEIGHT, shadow_radius, np.array([0.,1.,0.]), np.array([0.,0.,1.]), view_dir, color_v, battery_avg, current_time) | |
| for i in range(len(q_v)): pygame.draw.polygon(target_surf, c_v[i], q_v[i]) | |
| pygame.draw.circle(target_surf, (0,0,0), (cx, cy), int(shadow_radius)) | |
| pygame.draw.circle(target_surf, (255, 240, 200), (cx, cy), int(shadow_radius * 1.05), 2) | |
| pygame.draw.circle(target_surf, (0,0,0), (cx, cy), int(shadow_radius)) | |
| pygame.draw.circle(target_surf, (255, 240, 200), (cx, cy), int(shadow_radius * 1.05), 2) | |
| def draw_bot_thought_bubble(screen, cam, tetra, font): | |
| if tetra.mind: | |
| thought = tetra.mind.thought_bubble() | |
| if thought: | |
| sp = cam.project(tetra.pos) | |
| if sp[0] > -10000: | |
| try: | |
| surf = font.render(thought, True, (255, 255, 200)) | |
| screen.blit(surf, (sp[0] + 10, sp[1] - 20)) | |
| except UnicodeEncodeError: | |
| # Fallback for systems without emoji support | |
| clean_thought = thought.encode('ascii', 'ignore').decode('ascii') | |
| if clean_thought: | |
| surf = font.render(clean_thought, True, (255, 255, 200)) | |
| screen.blit(surf, (sp[0] + 10, sp[1] - 20)) | |
| def draw_player_avatar(screen, cam, pos, color, label_id, name=None): | |
| base_verts = Tetrahedron.REST_NP * 2.0; verts1 = base_verts + pos; verts2 = (base_verts * np.array([1, -1, 1])) + pos | |
| all_verts = np.vstack((verts1, verts2)); all_screen_pts = [cam.project(v) for v in all_verts] | |
| faces = list(Tetrahedron.FACES_NP) + [f + 4 for f in Tetrahedron.FACES_NP] | |
| sorted_faces = sorted(faces, key=lambda f: sum(cam.get_transformed_z(all_verts[v_idx]) for v_idx in f), reverse=True) | |
| for face_indices in sorted_faces: | |
| points = [all_screen_pts[i] for i in face_indices] | |
| if all(p[0] > -10000 for p in points): | |
| try: | |
| temp_surf = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA) | |
| pygame.draw.polygon(temp_surf, (*color, 100), points); screen.blit(temp_surf, (0, 0)) | |
| pygame.draw.lines(screen, (255, 255, 255, 150), True, points, 1) | |
| except Exception: pass | |
| display_name = name if name else label_id.split('_')[0] | |
| font = pygame.font.SysFont(None, 24); label_surf = font.render(display_name, True, (255, 255, 255)); label_pos = cam.project(pos + np.array([0, EDGE_LEN * 3.5, 0])) | |
| if label_pos[0] > -10000: screen.blit(label_surf, label_surf.get_rect(center=label_pos)) | |
| def get_user_input(screen, prompt, initial_text=""): | |
| input_text, font, active = initial_text, pygame.font.SysFont(None, 32), True | |
| while active: | |
| for event in pygame.event.get(): | |
| if event.type == pygame.QUIT: pygame.quit(); sys.exit() | |
| if event.type == pygame.KEYDOWN: | |
| if event.key == pygame.K_RETURN: active = False | |
| elif event.key == pygame.K_BACKSPACE: input_text = input_text[:-1] | |
| else: input_text += event.unicode | |
| screen.fill((10,10,20)); prompt_surf = font.render(prompt, True, (255,255,255)); input_surf = font.render(input_text, True, (255,255,0)) | |
| screen.blit(prompt_surf, (WIDTH//2 - prompt_surf.get_width()//2, HEIGHT//2 - 50)); screen.blit(input_surf, (WIDTH//2 - input_surf.get_width()//2, HEIGHT//2)); pygame.display.flip(); clock.tick(30) | |
| return input_text | |
| def send_msg(sock, msg_dict): | |
| try: msg_json = json.dumps(msg_dict, cls=NumpyEncoder).encode('utf-8'); sock.sendall(len(msg_json).to_bytes(4, 'big') + msg_json) | |
| except (ConnectionResetError, BrokenPipeError, OSError): pass | |
| def recv_msg(sock): | |
| try: | |
| len_bytes = sock.recv(4) | |
| if not len_bytes: return None | |
| msg_len, data = int.from_bytes(len_bytes, 'big'), b'' | |
| while len(data) < msg_len: | |
| packet = sock.recv(msg_len - len(data)) | |
| if not packet: return None | |
| data += packet | |
| return json.loads(data.decode('utf-8')) | |
| except (ConnectionResetError, json.JSONDecodeError, ValueError, OSError): return None | |
| class Host: | |
| def __init__(self, world, add_msg_fn, sound, port=None): | |
| self.world, self.add_msg, self.clients, self.lock, self.server = world, add_msg_fn, {}, threading.RLock(), socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
| self.port, self.running, self.sound, self.blacklist, self.message_queue = 0, True, sound, set(), queue.Queue() | |
| try: | |
| with open("blacklist.cfg", "r") as f: self.blacklist = {line.strip() for line in f if line.strip()} | |
| except FileNotFoundError: pass | |
| self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
| for p in ([port] if port else PORT_RANGE): | |
| try: self.server.bind(('', p)); self.port = p; break | |
| except OSError: continue | |
| if self.port == 0: self.server.bind(('', 0)); self.port = self.server.getsockname()[1] | |
| threading.Thread(target=self.discovery_thread, daemon=True).start() | |
| threading.Thread(target=self.accept_thread, daemon=True).start() | |
| threading.Thread(target=self.send_thread, daemon=True).start() | |
| def send_thread(self): | |
| while self.running: | |
| with self.lock: clients_to_check = list(self.clients.items()) | |
| for sock, client_data in clients_to_check: | |
| try: | |
| messages_to_retry = [] | |
| while True: | |
| try: msg = client_data['write_queue'].get_nowait() | |
| except queue.Empty: break | |
| try: | |
| sent = sock.send(msg) | |
| if sent < len(msg): messages_to_retry.append(msg[sent:]) | |
| except BlockingIOError: messages_to_retry.append(msg) | |
| except (ConnectionResetError, BrokenPipeError, OSError): self.mark_for_cleanup(sock); break | |
| for msg in reversed(messages_to_retry): | |
| try: | |
| if client_data['write_queue'].full(): client_data['write_queue'].get_nowait() | |
| client_data['write_queue'].put_nowait(msg) | |
| except (queue.Full, queue.Empty): pass | |
| except Exception: pass | |
| time.sleep(0.001) | |
| def discovery_thread(self): | |
| udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
| udp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) | |
| udp_sock.bind(('', DISCOVERY_PORT)) | |
| udp_sock.settimeout(0.5) | |
| while self.running: | |
| try: | |
| data, addr = udp_sock.recvfrom(1024) | |
| if data == b"DISCOVER_TETCRAFT_HOST": udp_sock.sendto(f"TETCRAFT_HOST_HERE:{self.port}".encode('utf-8'), addr) | |
| except (socket.timeout, OSError): continue | |
| def accept_thread(self): | |
| self.server.listen(5); self.server.settimeout(0.5) | |
| while self.running: | |
| try: | |
| client_sock, addr = self.server.accept() | |
| if addr[0] in self.blacklist: client_sock.close(); continue | |
| client_sock.setblocking(False) | |
| client_id = f"guest_{addr[0]}:{addr[1]}" | |
| with self.lock: | |
| self.clients[client_sock] = {'id': client_id, 'addr': addr, 'name': 'Unknown', 'read_buffer': b'', 'write_queue': queue.Queue(maxsize=100)} | |
| net_avatars[client_id] = {'pos': [0, 0, 0], 'color': [random.randint(50, 200) for _ in range(3)], 'name': 'Unknown'} | |
| threading.Thread(target=self.handle_client, args=(client_sock, client_id), daemon=True).start() | |
| except (socket.timeout, OSError): continue | |
| def handle_client(self, sock, client_id): | |
| while self.running: | |
| try: | |
| ready_to_read, _, _ = select.select([sock], [], [], 0.1) | |
| if not ready_to_read: time.sleep(0.01); continue | |
| data = sock.recv(4096) | |
| if not data: break | |
| with self.lock: | |
| if sock not in self.clients: break | |
| client_data = self.clients[sock] | |
| client_data['read_buffer'] += data | |
| while len(client_data['read_buffer']) >= 4: | |
| msg_len = int.from_bytes(client_data['read_buffer'][:4], 'big') | |
| if len(client_data['read_buffer']) < 4 + msg_len: break | |
| msg_json = client_data['read_buffer'][4:4 + msg_len] | |
| client_data['read_buffer'] = client_data['read_buffer'][4 + msg_len:] | |
| try: self.process_message(sock, client_id, client_data, json.loads(msg_json.decode('utf-8'))) | |
| except: pass | |
| except (socket.timeout, BlockingIOError): continue | |
| except: break | |
| with self.lock: | |
| if sock in self.clients: | |
| self.clients.pop(sock, None); net_avatars.pop(client_id, None); self.add_msg(f"{client_id} disconnected.") | |
| try: sock.close() | |
| except: pass | |
| def process_message(self, sock, client_id, client_data, msg): | |
| if msg['type'] == 'identify': | |
| client_data['name'] = msg['name']; net_avatars[client_id]['name'] = msg['name']; self.add_msg(f"{msg['name']} joined.") | |
| self.broadcast_message({'type': 'chat', 'data': f"{msg['name']} joined."}, exclude=sock) | |
| elif msg['type'] == 'cam_update': | |
| if client_id in net_avatars: net_avatars[client_id]['pos'] = np.array(msg['data']['pan']) | |
| elif msg['type'] == 'chat': | |
| if self.sound and AUDIO_ENABLED: self.sound.play() | |
| msg_str = msg['data'] if isinstance(msg['data'], str) and msg['data'].startswith('<') else f"<{client_data.get('name', client_id)}>: {msg['data']}" | |
| self.add_msg(msg_str); self.broadcast_message({'type': 'chat', 'data': msg_str}, exclude=sock) | |
| elif msg['type'] == 'set_label': self.message_queue.put(('set_label', msg['id'], msg['label'])) | |
| def broadcast_message(self, msg_dict, exclude=None): | |
| try: | |
| full_msg = len(json.dumps(msg_dict, cls=NumpyEncoder).encode('utf-8')).to_bytes(4, 'big') + json.dumps(msg_dict, cls=NumpyEncoder).encode('utf-8') | |
| with self.lock: | |
| for sock, client_data in list(self.clients.items()): | |
| if sock == exclude: continue | |
| try: client_data['write_queue'].put_nowait(full_msg) | |
| except queue.Full: self.mark_for_cleanup(sock) | |
| except: pass | |
| def broadcast_state(self): | |
| try: | |
| if len(self.world.tets) > 500: return | |
| out_avatars = net_avatars.copy(); out_avatars['host'] = {'name': PLAYER_NAME, 'pos': [0.0, 0.0, 0.0], 'color': [200, 200, 255]} | |
| self.broadcast_message({'type': 'world_state', 'data': {'world': self.world.get_state(), 'avatars': out_avatars}}) | |
| except: pass | |
| def mark_for_cleanup(self, sock): self.message_queue.put(('cleanup', sock)) | |
| def stop(self): | |
| self.running = False | |
| with self.lock: | |
| for sock in self.clients: | |
| try: sock.close() | |
| except: pass | |
| self.clients.clear() | |
| try: self.server.close() | |
| except: pass | |
| class Guest: | |
| def __init__(self, host_ip, port, world, cam, add_msg_fn, sound): | |
| self.world, self.cam, self.add_msg, self.sock = world, cam, add_msg_fn, socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
| self.host_id, self.running, self.world_state_queue, self.latest_avatars = f"host_{host_ip}:{port}", True, queue.Queue(maxsize=1), {} | |
| self.sock.settimeout(2.0) | |
| try: | |
| self.sock.connect((host_ip, port)); self.sock.setblocking(False) | |
| send_msg(self.sock, {'type': 'identify', 'name': PLAYER_NAME}) | |
| threading.Thread(target=self.listen, daemon=True).start() | |
| except Exception as e: add_msg_fn(f"Connection failed: {e}"); self.running = False | |
| def listen(self): | |
| while self.running: | |
| try: | |
| ready_to_read, _, _ = select.select([self.sock], [], [], 0.1) | |
| if not ready_to_read: time.sleep(0.001); continue | |
| msg = recv_msg(self.sock) | |
| if msg is None: self.add_msg("Disconnected."); self.running = False; break | |
| if msg['type'] == 'world_state': | |
| try: self.world_state_queue.put_nowait(msg['data']['world']) | |
| except queue.Full: self.world_state_queue.get_nowait(); self.world_state_queue.put_nowait(msg['data']['world']) | |
| if 'avatars' in msg['data']: self.latest_avatars = msg['data']['avatars'].copy() | |
| elif msg['type'] == 'chat': self.add_msg(msg['data'] if isinstance(msg['data'], str) else f"<Host>: {msg['data']}") | |
| except (socket.timeout, BlockingIOError): continue | |
| except: self.running = False; break | |
| def get_latest_world_state(self): | |
| try: return self.world_state_queue.get_nowait() | |
| except queue.Empty: return None | |
| def send_cam_update(self): | |
| try: send_msg(self.sock, {'type': 'cam_update', 'data': self.cam.get_state()}) | |
| except: pass | |
| def send_chat(self, text): | |
| try: send_msg(self.sock, {'type': 'chat', 'data': f"<{PLAYER_NAME}>: {text}"}) | |
| except: pass | |
| def send_label(self, tet_id, label): | |
| try: send_msg(self.sock, {'type': 'set_label', 'id': tet_id, 'label': label}) | |
| except: pass | |
| def stop(self): | |
| self.running = False | |
| try: self.sock.shutdown(socket.SHUT_RDWR); self.sock.close() | |
| except: pass | |
| def stop_all_networking(): | |
| global host_instance, guest_instance | |
| if host_instance: host_instance.stop(); host_instance = None | |
| if guest_instance: guest_instance.stop(); guest_instance = None | |
| atexit.register(stop_all_networking) | |
| def shutdown_handler(signum, frame): | |
| global GAME_RUNNING | |
| GAME_RUNNING = False | |
| stop_all_networking() | |
| sys.exit(0) | |
| signal.signal(signal.SIGTERM, shutdown_handler) | |
| signal.signal(signal.SIGINT, shutdown_handler) | |
| def safe_world_update(world, new_state, last_hash=""): | |
| try: | |
| import hashlib | |
| state_str = json.dumps(new_state, sort_keys=True, cls=NumpyEncoder) | |
| current_hash = hashlib.md5(state_str.encode()).hexdigest() | |
| if current_hash != last_hash: world.set_state(new_state) | |
| return current_hash | |
| except: return last_hash | |
| def gradio_interface_loop(): | |
| def get_frame(): | |
| if GRADIO_FRAME_BUFFER is not None: return GRADIO_FRAME_BUFFER | |
| return np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8) | |
| with gr.Interface(fn=get_frame, inputs=None, outputs=gr.Image(label="Hit the 'Generate' button!"), live=True, title="TET~CRAFT: The Fourth Temple v5.3D", description="Live simulation of DigitizingHumanity.com's Gamified, Decentralized, Salted, 5D Communication Manifold and Physics/Chemistry Simulator <br /> (A simulated player is adding one new TET/fact each hour to be misunderstood, warping time for the 15 min before that, has a thought and new perspective plain each minute, while each second it orbits)<br />Hit the 'Generate' button bellow for a fresh screenshot") as demo: | |
| demo.launch(server_name="0.0.0.0", server_port=7860) | |
| def main(threaded=False): | |
| print("\n\nIf needed create and fill with 1 IP per line blacklist.cfg\n\nCLI Options:\n -connect <ip>:<port> (Initiate guest mode)\n -listen <port> (Initiate host mode port)\n -file <filename> (Load saved instant [json])\n -m (Mute sound)\n -t <scale> -z <zoom> -o <x,y,z>\n\nTET~CRAFT v5.3D Initializing...\n\n") | |
| cli_connect_addr, cli_listen_port, cli_load_file = None, None, None | |
| cli_time_scale, cli_zoom_factor, cli_cam_pan = None, None, None | |
| cli_mute = False | |
| args = sys.argv[1:]; i = 0 | |
| while i < len(args): | |
| if args[i] == '-connect' and i + 1 < len(args): cli_connect_addr = args[i+1]; i += 1 | |
| elif args[i] == '-listen': | |
| cli_listen_port = DEFAULT_PORT | |
| if i + 1 < len(args) and args[i+1].isdigit(): cli_listen_port = int(args[i+1]); i += 1 | |
| elif args[i] == '-file' and i + 1 < len(args): | |
| cli_load_file = args[i+1]; i += 1 | |
| cli_time_scale = .001 | |
| elif args[i] == '-t' and i + 1 < len(args): cli_time_scale = float(args[i+1]); i += 1 | |
| elif args[i] == '-z' and i + 1 < len(args): cli_zoom_factor = float(args[i+1]); i += 1 | |
| elif args[i] == '-o' and i + 1 < len(args): cli_cam_pan = [float(c) for c in args[i+1].split(',')]; i += 1 | |
| elif args[i] == '-m': cli_mute = True | |
| i += 1 | |
| global WIDTH, HEIGHT, clock, game_mode, host_instance, guest_instance, net_avatars, net_messages, AUDIO_ENABLED, GRADIO_FRAME_BUFFER, GAME_RUNNING | |
| pygame.init() | |
| if not cli_mute: | |
| try: pygame.mixer.init(44100, -16, 2, 512); AUDIO_ENABLED = True | |
| except: print("Sound Init Failed. Running Silent."); AUDIO_ENABLED = False | |
| else: | |
| AUDIO_ENABLED = False | |
| print("Audio Muted by CLI.") | |
| # Initialize sounds AFTER mixer init | |
| ping_sound = generate_ping_sound() | |
| boing_sound = generate_boing_sound() | |
| if ON_HUGGINGFACE: | |
| WIDTH, HEIGHT = 1200, 600; screen = pygame.display.set_mode((WIDTH, HEIGHT)) | |
| # Try to find the specific file | |
| if os.path.exists("nothing.json"): | |
| cli_load_file = "nothing.json" | |
| print("### HF Mode: Found nothing.json - Auto-loading... ###") | |
| else: | |
| # If not found, do nothing (cli_load_file stays None, standard Void logic triggers) | |
| print("### HF Mode: nothing.json not found - Starting fresh in Void. ###") | |
| else: screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.RESIZABLE) | |
| pygame.display.set_caption("TET~CRAFT v5.3D The Fourth Temple") | |
| clock = pygame.time.Clock(); font_l = pygame.font.SysFont('Georgia', 32); font_s = pygame.font.SysFont(None, 24) | |
| world = World(boing_sound); cam = Camera() | |
| show_intro(screen, cam) | |
| show_name_input_screen(screen) | |
| flags = {'t0': False, 't1': False, 't2': False, 'j1': False, 't3': False}; msgs = [] | |
| dragging, rotating, last_mouse = None, False, (0,0); time_scale = 0.0001 | |
| locked_sticky_target = None; frame_count = 0 | |
| past_projection = PastProjection4Sphere() | |
| disk_surf = None | |
| last_bot_move = time.time(); last_bot_spawn = time.time() - 3590; last_bot_thought = time.time() | |
| # Bot State Machine | |
| bot_state = 'IDLE' # IDLE, WAITING_FOR_ANSWER | |
| bot_reply_timer = 0.0 | |
| last_timescale_surge = time.time() - 2000 | |
| if cli_time_scale is not None: time_scale = cli_time_scale | |
| if cli_zoom_factor is not None: cam.dist = DEFAULT_CAM_DIST / max(0.01, cli_zoom_factor) | |
| if cli_cam_pan is not None: cam.pan = (np.array(cli_cam_pan) - 0.5) * 2.0 * FIELD_SCALE | |
| def stop_guest_mode(): | |
| global guest_instance, game_mode | |
| if guest_instance: guest_instance.stop(); guest_instance = None; game_mode = 'single_player'; net_avatars.clear(); add_timed_message("Disconnected.", duration=3); return True | |
| return False | |
| def add_timed_message(text, y_offset=0, duration=4): msgs.append([text, y_offset, pygame.time.get_ticks() + duration * 1000]) | |
| def add_network_message(text): net_messages.append([text, time.time() + 8]) | |
| def reset_simulation(show_message=True): | |
| nonlocal flags, time_scale, dragging, rotating, locked_sticky_target | |
| global game_mode, host_instance, guest_instance, net_avatars, net_messages | |
| if host_instance: host_instance.stop(); host_instance = None | |
| if guest_instance: stop_guest_mode() | |
| world.tets.clear(); world.joints.clear(); world.sticky_pairs.clear(); past_projection.points.clear() | |
| net_avatars.clear(); net_messages.clear(); flags = {k: False for k in flags} | |
| cam.__init__(); time_scale = 1.0; game_mode = 'single_player'; dragging, rotating = None, False | |
| world.tech_tree = TechTree() | |
| def save_world_to_file(): | |
| if not world.tets: add_timed_message("Cannot save empty.", duration=3); return | |
| try: | |
| with open(SAVE_FILENAME, 'w') as f: json.dump(world.get_state(), f, cls=NumpyEncoder, indent=2) | |
| add_timed_message(f"Saved to {SAVE_FILENAME}", duration=3) | |
| except IOError as e: add_timed_message(f"Error saving: {e}", duration=4) | |
| def connect_as_guest(host_ip, port): | |
| global guest_instance, game_mode | |
| try: | |
| save_world_to_file(); guest_instance, game_mode = Guest(host_ip, int(port), world, cam, add_network_message, ping_sound), 'guest' | |
| add_timed_message(f"Connected {host_ip}:{port}", duration=3); world.tets.clear(); world.joints.clear(); world.sticky_pairs.clear(); return True | |
| except Exception as e: add_timed_message(f"Failed connect: {e}", duration=4); return False | |
| def discover_and_join(): | |
| global guest_instance, game_mode | |
| if game_mode != 'single_player': return | |
| host_addr = None | |
| try: | |
| with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: | |
| sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1); sock.settimeout(1.0) | |
| sock.sendto(b"DISCOVER_TETCRAFT_HOST", ('<broadcast>', DISCOVERY_PORT)) | |
| data, addr = sock.recvfrom(1024) | |
| if data.startswith(b"TETCRAFT_HOST_HERE:"): host_addr = (addr[0], int(data.split(b':')[1])) | |
| except socket.timeout: pass | |
| if host_addr: connect_as_guest(host_addr[0], host_addr[1]) | |
| else: | |
| user_input = get_user_input(screen, "Enter IP:Port:") | |
| if user_input: parts = user_input.split(':'); connect_as_guest(parts[0], int(parts[1]) if len(parts) > 1 else DEFAULT_PORT) | |
| def initiate_host_mode(port=None): | |
| global host_instance, game_mode | |
| if game_mode == 'single_player': host_instance, game_mode = Host(world, add_network_message, ping_sound, port), 'host'; add_timed_message(f"Hosting port {host_instance.port}", duration=3) | |
| elif game_mode == 'host' and host_instance: host_instance.stop(); host_instance = None; game_mode = 'single_player'; add_timed_message("Host stopped", duration=3) | |
| loaded_from_save = False | |
| if cli_connect_addr: connect_as_guest(*(cli_connect_addr.split(':') if ':' in cli_connect_addr else (cli_connect_addr, DEFAULT_PORT))) | |
| elif cli_listen_port: initiate_host_mode(cli_listen_port) | |
| if cli_load_file: | |
| if os.path.exists(cli_load_file): | |
| try: | |
| with open(cli_load_file, 'r', encoding='utf-8-sig') as f: world.set_state(json.load(f)); loaded_from_save = True | |
| if world.tets: | |
| cam.dist = DEFAULT_CAM_DIST | |
| cam.pan = world.center_of_mass.copy() | |
| except Exception as e: print(f"Load Error: {e}") | |
| if not (loaded_from_save or cli_connect_addr or cli_listen_port): show_void_screen(screen, world) | |
| if ON_HUGGINGFACE: host_instance = Host(world, lambda x: net_messages.append([x, time.time()+8]), ping_sound, DEFAULT_PORT); game_mode = 'host' | |
| camera_follow_mode = False | |
| last_step_size = 0.0 | |
| while GAME_RUNNING: | |
| unscaled_dt = min(0.1, clock.tick(FPS) / 1000.0) | |
| now = time.time() | |
| # === FIX: Physics Speed Limit === | |
| # Never allow the physics to calculate more than 0.2 seconds of movement in one frame. | |
| # This prevents "Teleportation Explosions" at high time scales. | |
| raw_scaled_dt = unscaled_dt * time_scale | |
| scaled_dt = min(raw_scaled_dt, 0.2) | |
| # ================================ | |
| frame_count, fps = frame_count + 1, clock.get_fps() | |
| if 0 < fps < 45 and time_scale > 1.0: time_scale = max(1.0, time_scale * 0.99) | |
| is_interactive = (game_mode in ['single_player', 'host']) and not ON_HUGGINGFACE | |
| hovered_vertex = None | |
| mx, my = pygame.mouse.get_pos() | |
| mouse_arr = np.array([mx, my], dtype=np.float64) | |
| if world.tets: | |
| curr_verts_world = np.array([t.local + t.pos for t in world.tets]).reshape(-1, 3) | |
| curr_verts_screen = cam.project_many(curr_verts_world) | |
| if is_interactive and not rotating: | |
| dist_sq = np.sum((curr_verts_screen - mouse_arr)**2, axis=1) | |
| if dist_sq.size > 0 and np.min(dist_sq) < SELECTION_RADIUS**2: | |
| min_idx = np.argmin(dist_sq) | |
| if curr_verts_screen[min_idx][0] > -9000: | |
| hovered_vertex = (world.tets[min_idx // 4], min_idx % 4) | |
| pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_HAND) | |
| else: | |
| hovered_vertex = None | |
| # === NEW: Cursor Reset === | |
| if not dragging: | |
| pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_ARROW) | |
| else: curr_verts_screen = np.empty((0,2)) | |
| if is_interactive and hovered_vertex and not dragging: | |
| h_tet = hovered_vertex[0]; h_tet.pos_prev[:] = h_tet.pos[:]; h_tet.local_prev[:] = h_tet.local[:] | |
| h_tet.battery = min(1.0, h_tet.battery + 0.01) | |
| # 3. Thoughts | |
| if now - last_bot_thought > 61: | |
| if len(world.tets) > 1: | |
| # --- Helper: Find physical neighbors --- | |
| def get_neighbors(target_tet, exclude_id=None): | |
| nb = [] | |
| # 1. Check Joints (Strongest link) | |
| for j in world.joints: | |
| if j.A.id == target_tet.id and j.B.id != exclude_id: nb.append(j.B) | |
| elif j.B.id == target_tet.id and j.A.id != exclude_id: nb.append(j.A) | |
| # 2. Check Sticky Pairs (Desire) | |
| if not nb: | |
| for p in world.sticky_pairs: | |
| if p[0].id == target_tet.id and p[2].id != exclude_id: nb.append(p[2]) | |
| elif p[2].id == target_tet.id and p[0].id != exclude_id: nb.append(p[0]) | |
| # 3. Check Proximity (Loose association) | |
| if not nb: | |
| for t in world.tets: | |
| if t.id == target_tet.id or t.id == exclude_id: continue | |
| if np.linalg.norm(t.pos - target_tet.pos) < EDGE_LEN * 4: | |
| nb.append(t) | |
| return nb | |
| # 1. Pick Subject (T1) | |
| t1 = random.choice(world.tets) | |
| label1 = t1.label if t1.label else "Unknown" | |
| # 2. Pick Object (T2) - Neighbor of T1 | |
| n1 = get_neighbors(t1) | |
| # If no neighbors, pick random, but not T1 | |
| t2 = random.choice(n1) if n1 else random.choice([t for t in world.tets if t.id != t1.id]) | |
| label2 = t2.label if t2.label else "Void" | |
| # 3. Pick Context (T3) - Neighbor of T2 | |
| # Try to avoid going back to T1 immediately to avoid A=B=A loops unless necessary | |
| n2 = get_neighbors(t2, exclude_id=t1.id) | |
| if n2: | |
| t3 = random.choice(n2) | |
| else: | |
| # If T2 has no forward neighbors, check if there are other TETS available | |
| others = [t for t in world.tets if t.id != t1.id and t.id != t2.id] | |
| if others: | |
| t3 = random.choice(others) # Jump to a new disconnected idea | |
| else: | |
| t3 = t1 # Forced loop (A -> B -> A) | |
| label3 = t3.label if t3.label else "Mystery" | |
| # 4. Get Symbols | |
| sym1 = get_thought_symbol(t1, t2, world) | |
| sym2 = get_thought_symbol(t2, t3, world) | |
| # 5. Formulate Trinary Thought | |
| thought = f"{label1} {sym1} {label2} {sym2} {label3} ?" | |
| # Add to chat log | |
| net_messages.append([f"[Thought]: {thought}", time.time() + 15]) | |
| print(f"[Bot Thought]: {thought}") | |
| #Stage 5 Quantum thoughts | |
| def get_neighbors(target_tet, exclude_id=None): | |
| nb = [] | |
| for j in world.joints: | |
| if j.A.id == target_tet.id and j.B.id != exclude_id: nb.append(j.B) | |
| elif j.B.id == target_tet.id and j.A.id != exclude_id: nb.append(j.A) | |
| if not nb: | |
| for p in world.sticky_pairs: | |
| if p[0].id == target_tet.id and p[2].id != exclude_id: nb.append(p[2]) | |
| elif p[2].id == target_tet.id and p[0].id != exclude_id: nb.append(p[0]) | |
| if not nb: | |
| for t in world.tets: | |
| if t.id == target_tet.id or t.id == exclude_id: continue | |
| if np.linalg.norm(t.pos - target_tet.pos) < EDGE_LEN * 4: nb.append(t) | |
| return nb | |
| t1 = random.choice(world.tets) | |
| label1 = t1.label if t1.label else "Unknown" | |
| n1 = get_neighbors(t1) | |
| t2 = random.choice(n1) if n1 else random.choice([t for t in world.tets if t.id != t1.id]) | |
| label2 = t2.label if t2.label else "Void" | |
| n2 = get_neighbors(t2, exclude_id=t1.id) | |
| t3 = random.choice(n2) if n2 else t1 | |
| label3 = t3.label if t3.label else "Mystery" | |
| sym1 = get_thought_symbol(t1, t2, world) | |
| sym2 = get_thought_symbol(t2, t3, world) | |
| # PHASE 5: Inject Quantum Ontology | |
| quantum_terms = ["ERD∇", "Ψ-coherence", "OBA⊗", "QuantumFold", "EntropicBridge"] | |
| if t1.quantum_state != "ground" or t1.erd_coherence > 0.8: | |
| sym1 = random.choice(quantum_terms) | |
| thought = f"{label1} {sym1} {label2} {sym2} {label3} !" | |
| net_messages.append([f"[Quantum Reply]: {thought}", time.time() + 15]) | |
| print(f"[Quantum Reply]: {thought}") | |
| last_bot_thought = now | |
| pygame.event.pump() | |
| if not ON_HUGGINGFACE: | |
| last_world_hash = "" | |
| for e in pygame.event.get(): | |
| if e.type == pygame.QUIT: GAME_RUNNING = False | |
| if e.type == pygame.VIDEORESIZE: WIDTH, HEIGHT = e.w, e.h; screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.RESIZABLE); disk_surf = None | |
| keys = pygame.key.get_pressed(); mods = pygame.key.get_mods(); ctrl_held = (mods & pygame.KMOD_CTRL); alt_held = (mods & pygame.KMOD_ALT) | |
| if e.type == pygame.KEYDOWN: | |
| if e.key == pygame.K_v and is_interactive: save_world_to_file() | |
| if e.key == pygame.K_BACKQUOTE: world.explode() | |
| if is_interactive and e.key == pygame.K_SPACE: (world.spawn() if len(world.tets) < 2 else (world.spawn_polar_pair() if flags['j1'] else None)) | |
| if e.key == pygame.K_x: | |
| cam.dist = DEFAULT_CAM_DIST | |
| cam.pan = world.center_of_mass.copy() | |
| camera_follow_mode = not camera_follow_mode | |
| for t in world.tets: | |
| t.pos_prev = t.pos.copy() # Velocity = 0 | |
| if e.key == pygame.K_c: time_scale = min(10.0, time_scale + 0.5) | |
| if e.key == pygame.K_z: time_scale = max(0.1, time_scale - 0.5) | |
| if e.key == pygame.K_TAB: (discover_and_join() if game_mode == 'single_player' else stop_guest_mode()) | |
| if e.key == pygame.K_h and is_interactive: initiate_host_mode() | |
| if game_mode == 'guest': | |
| if guest_instance: | |
| world_state = guest_instance.get_latest_world_state() | |
| if world_state: last_world_hash = safe_world_update(world, world_state, last_world_hash) | |
| net_avatars = guest_instance.latest_avatars.copy() | |
| if frame_count % 30 == 0: guest_instance.send_cam_update() | |
| if e.type == pygame.MOUSEBUTTONDOWN and e.button == 3: | |
| # GUEST CLICKING HOST AVATAR | |
| clicked_avatar = None | |
| for av_id, av_data in list(net_avatars.items()): | |
| # Skip own avatar if in list | |
| if av_id == f"guest_{guest_instance.sock.getsockname()[0]}:{guest_instance.sock.getsockname()[1]}": continue | |
| av_pos = np.array(av_data['pos']) | |
| av_screen = cam.project(av_pos) | |
| if av_screen[0] > -10000: | |
| d = math.sqrt((av_screen[0]-mx)**2 + (av_screen[1]-my)**2) | |
| if d < 30: clicked_avatar = (av_id, av_data); break | |
| if clicked_avatar and not alt_held: | |
| msg = get_user_input(screen, f"Msg to {clicked_avatar[1].get('name','Host')}:") | |
| if msg: guest_instance.send_chat(msg) # Guest sends normal chat, Host sees it | |
| else: | |
| rotating, last_mouse = True, e.pos | |
| if e.type == pygame.MOUSEBUTTONUP and e.button == 3: rotating = False | |
| if e.type == pygame.MOUSEWHEEL: | |
| if alt_held: cam.pan += np.array([math.cos(cam.yaw), 0, -math.sin(cam.yaw)]) * (e.y * 20.0 * unscaled_dt) | |
| else: cam.zoom(ZOOM_SPEED if e.y < 0 else 1/ZOOM_SPEED) | |
| elif is_interactive: | |
| if e.type == pygame.MOUSEBUTTONDOWN: | |
| if e.button == 1 and hovered_vertex: | |
| start_tet, start_idx = hovered_vertex | |
| world.sticky_pairs = [p for p in world.sticky_pairs if not ((p[0].id == start_tet.id and p[1] == start_idx) or (p[2].id == start_tet.id and p[3] == start_idx))] | |
| dragging = (hovered_vertex[0], hovered_vertex[1], cam.get_transformed_z(hovered_vertex[0].verts()[hovered_vertex[1]])) | |
| locked_sticky_target = None | |
| if e.button == 3: | |
| if alt_held: | |
| cam.dist = DEFAULT_CAM_DIST | |
| cam.pan = world.center_of_mass.copy() | |
| elif hovered_vertex: | |
| t = hovered_vertex[0]; new_l = get_user_input(screen, f"Rename '{t.label}':", t.label) | |
| if new_l: t.label = new_l | |
| else: | |
| # HOST CLICKING GUEST AVATARS | |
| clicked_avatar = None | |
| if host_instance: | |
| for av_id, av_data in list(net_avatars.items()): | |
| if av_id == 'host': continue # Don't click self | |
| av_pos = np.array(av_data['pos']) | |
| av_screen = cam.project(av_pos) | |
| if av_screen[0] > -10000: | |
| d = math.sqrt((av_screen[0]-mx)**2 + (av_screen[1]-my)**2) | |
| if d < 30: clicked_avatar = (av_id, av_data); break | |
| if clicked_avatar: | |
| msg = get_user_input(screen, f"Msg to {clicked_avatar[1].get('name','Guest')}:") | |
| if msg: | |
| # Find socket | |
| target_sock = None | |
| for s, d in host_instance.clients.items(): | |
| if d['id'] == clicked_avatar[0]: target_sock = s; break | |
| if target_sock: | |
| send_msg(target_sock, {'type': 'chat', 'data': f"<{PLAYER_NAME}>: {msg}"}) | |
| add_network_message(f"<To {clicked_avatar[1].get('name')}: {msg}") | |
| else: | |
| rotating, last_mouse = True, e.pos | |
| if e.type == pygame.MOUSEBUTTONUP: | |
| if e.button == 1 and dragging and locked_sticky_target: | |
| t1, i1 = dragging[0], dragging[1]; t2, i2 = locked_sticky_target[0], locked_sticky_target[1] | |
| cnt = sum(1 for j in world.joints if (j.A.id == t1.id and j.ia == i1) or (j.B.id == t1.id and j.ib == i1)) | |
| if cnt < 6: | |
| world.sticky_pairs.append((t1, i1, t2, i2)) | |
| print(f"User: {t1.label} desires {t2.label}") | |
| if e.button == 1: dragging, locked_sticky_target = None, None | |
| if e.button == 3: rotating = False | |
| if e.type == pygame.MOUSEWHEEL: | |
| if ctrl_held: time_scale = np.clip(time_scale * (1.1 if e.y > 0 else 0.9), 0.1, 10.0) | |
| elif alt_held: | |
| cam.pan += np.array([math.cos(cam.yaw), 0, -math.sin(cam.yaw)]) * (e.y * 20.0 * unscaled_dt) | |
| else: cam.zoom(ZOOM_SPEED if e.y < 0 else 1/ZOOM_SPEED) | |
| keys = pygame.key.get_pressed() | |
| if keys[pygame.K_c]: time_scale = min(10.0, time_scale + 2.0 * unscaled_dt) | |
| if keys[pygame.K_z]: time_scale = max(0.1, time_scale - 2.0 * unscaled_dt) | |
| cam.pitch += ORBIT_SPEED * unscaled_dt * (int(keys[pygame.K_w]) - int(keys[pygame.K_s])) | |
| cam.yaw += ORBIT_SPEED * unscaled_dt * (int(keys[pygame.K_d]) - int(keys[pygame.K_a])) | |
| if keys[pygame.K_r]: cam.zoom(1/ZOOM_SPEED) | |
| if keys[pygame.K_f]: cam.zoom(ZOOM_SPEED) | |
| cam.pan += np.array([math.cos(cam.yaw), 0, -math.sin(cam.yaw)]) * (int(keys[pygame.K_q]) - int(keys[pygame.K_e])) * PAN_SPEED * unscaled_dt * (cam.dist / DEFAULT_CAM_DIST) | |
| if rotating: mx, my = pygame.mouse.get_pos(); cam.yaw += (mx - last_mouse[0]) * 0.005; cam.pitch = np.clip(cam.pitch - (my - last_mouse[1]) * 0.005, -1.57, 1.57); last_mouse = (mx, my) | |
| if dragging: | |
| t_drag, i_drag, dd = dragging; m3d = cam.unproject(pygame.mouse.get_pos(), dd); delta = m3d - t_drag.verts()[i_drag] | |
| t_drag.local[i_drag] += delta * MOUSE_PULL_STRENGTH * (DEFAULT_CAM_DIST / cam.dist); t_drag.pos += delta * BODY_PULL_STRENGTH * (DEFAULT_CAM_DIST / cam.dist) | |
| best_dist_sq = SELECTION_RADIUS**2; current_hover_target = None | |
| for tidx, tt in enumerate(world.tets): | |
| if tt.id == t_drag.id: continue | |
| for vidx in range(4): | |
| idx_flat = tidx * 4 + vidx; pt = curr_verts_screen[idx_flat]; d_sq = np.sum((pt - mouse_arr)**2) | |
| if d_sq < best_dist_sq: best_dist_sq = d_sq; current_hover_target = (tt, vidx) | |
| locked_sticky_target = current_hover_target | |
| else: | |
| now = time.time() | |
| # If you speed up time with unbound tets, bad news! | |
| # if now - last_bot_spawn > 2700: # 2700 seconds = 45 minutes | |
| # time_scale = 10.0 | |
| # elif now - last_bot_spawn < 2000: | |
| # time_scale = 0.00001 | |
| # if now - last_bot_spawn > 2700 and now - last_bot_spawn > 3300: | |
| # time_scale = 0.001 | |
| if world.tets: | |
| cam.pan = world.center_of_mass.copy() | |
| if now - last_bot_move > 59: | |
| cam.pitch = max(-1, min(1, cam.pitch + random.uniform(-1, 1))) | |
| cam.dist = DEFAULT_CAM_DIST | |
| last_bot_move = now | |
| else: | |
| cam.yaw += 0.05 * unscaled_dt | |
| zoom_scalar = 4.0 * 3.0 ** (math.sin(now * 0.05) * 2.0) | |
| target_dist = DEFAULT_CAM_DIST * zoom_scalar | |
| cam.dist += (target_dist - cam.dist) * 0.05 | |
| if now - last_bot_spawn > 3600: | |
| world.spawn_polar_pair() | |
| if len(world.tets) >= 2: world.tets[-1].label = random.choice(MYSTIC_WORDS); world.tets[-2].label = random.choice(MYSTIC_WORDS) | |
| last_bot_spawn = now | |
| print(f"TET pair created: {world.tets[-2].label,world.tets[-1].label}") | |
| if game_mode == 'host' and host_instance: | |
| while not host_instance.message_queue.empty(): | |
| try: | |
| msg = host_instance.message_queue.get_nowait() | |
| if msg[0] == 'set_label': | |
| for t in world.tets: | |
| if t.id == msg[1]: t.label = msg[2]; break | |
| elif msg[0] == 'cleanup': | |
| with host_instance.lock: | |
| if msg[1] in host_instance.clients: | |
| host_instance.clients.pop(msg[1], None) | |
| try: msg[1].close() | |
| except: pass | |
| except: break | |
| if frame_count % (2 + len(host_instance.clients)) == 0: host_instance.broadcast_state() | |
| if game_mode != 'guest': | |
| zf = DEFAULT_CAM_DIST / cam.dist; spin = np.clip(1/np.log(zf + 1) + 1, 0.1, 2.0) | |
| # 1. Prepare Arrays (ONCE per frame) | |
| world.rebuild_optimization_cache() | |
| # 2. Run Logic (ONCE per frame) | |
| world.update_logic(scaled_dt, lambda t, duration=2: msgs.append([t, 0, pygame.time.get_ticks() + duration * 1000]),cam=cam) | |
| # 3. Sub-Stepped Physics (MANY times per frame) | |
| # This keeps the physics stable even at high time scales | |
| remaining_dt = scaled_dt | |
| SAFE_DT = 0.03 # Max 30ms per step | |
| num_steps = int(remaining_dt / SAFE_DT) + 1 | |
| if num_steps > 20: | |
| num_steps = 20 | |
| remaining_dt = 20 * SAFE_DT # Cap max speed to prevent freeze | |
| step_size = remaining_dt / num_steps | |
| # === FIX: VERLET MOMENTUM RESCALING === | |
| # If time slows down, we must shrink the gap between pos and pos_prev | |
| # otherwise the old momentum looks like 1000x speed in the new timeframe. | |
| if last_step_size > 0 and abs(step_size - last_step_size) > 1e-9: | |
| time_correction = step_size / last_step_size | |
| # Clamp correction to prevent explosions if time pauses/unpauses | |
| time_correction = max(0.0, min(time_correction, 5.0)) | |
| if abs(time_correction - 1.0) > 0.01: | |
| # Apply scaling to the optimization cache directly for speed | |
| # Vel = (Pos - Prev) -> NewVel = Vel * Ratio -> NewPrev = Pos - NewVel | |
| # Update Cached Arrays | |
| velocity_vectors = world.cached_pos - world.cached_prev | |
| world.cached_prev = world.cached_pos - (velocity_vectors * time_correction) | |
| # Update Objects | |
| for i, t in enumerate(world.tets): | |
| t.pos_prev = t.pos - (t.pos - t.pos_prev) * time_correction | |
| # print(f"Time Warp: Momentum Rescaled by {time_correction:.4f}") | |
| last_step_size = step_size | |
| for _ in range(num_steps): | |
| # Run pure physics | |
| world.update_physics_only(step_size, time_scale, spin) | |
| if len(world.tets) == 1 and not flags['t0']: flags['t0'] = True | |
| if len(world.tets) >= 2 and not flags['t2']: | |
| flags['t2'] = True | |
| msgs.append(["Let time have somewhere to return to!", -50, pygame.time.get_ticks() + 6000]) | |
| world.sticky_pairs.extend([(world.tets[0], v, world.tets[1], v) for v in range(4)]) | |
| if len(world.tets) >= 2 and world.joints and not flags['j1']: | |
| flags['j1'] = True | |
| msgs.append(["God said: Let there be LIGHT!", -50, pygame.time.get_ticks() + 6000]) | |
| if len(world.tets) >= 3 and flags['j1'] and not flags['t3']: | |
| flags['t3'] = True | |
| msgs.append(["And God divided the light from the darkness...", 50, pygame.time.get_ticks() + 6000]) | |
| elif guest_instance: | |
| if frame_count % 10 == 0: guest_instance.send_cam_update() | |
| s = guest_instance.get_latest_world_state(); | |
| if s: world.set_state(s) | |
| if len(world.tets) >= 2 and not flags['t2']: flags['t2'] = True | |
| if len(world.tets) >= 2 and world.joints and not flags['j1']: flags['j1'] = True | |
| if len(world.tets) >= 3 and flags['j1'] and not flags['t3']: flags['t3'] = True | |
| mouse_busy = pygame.mouse.get_pressed()[0] or pygame.mouse.get_pressed()[2] or rotating or dragging | |
| if (ON_HUGGINGFACE or camera_follow_mode) and world.tets and not mouse_busy: | |
| target_pan = world.center_of_mass.copy() | |
| if np.all(np.isfinite(target_pan)): | |
| # Smoothly interpolate for a nicer feel (Optional, or just use =) | |
| # cam.pan = target_pan | |
| cam.pan += (target_pan - cam.pan) * 0.1 # Soft follow (10% per frame) | |
| # Rendering | |
| tl = np.clip((time_scale - 0.1) / 9.9, 0, 1) | |
| bg = tuple(np.array([30,0,0]) * (1-tl) + np.array([5,5,10]) * tl if flags['t3'] else ((255,255,255) if flags['j1'] else (10,10,20))) | |
| screen.fill(bg) | |
| if flags['t3']: | |
| past_projection.update_and_draw(screen, cam, world.center_of_mass, len(world.tets), time_scale, WIDTH, HEIGHT) | |
| if disk_surf is None: disk_surf = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA) | |
| disk_surf.fill((0,0,0,0)) | |
| draw_standard_black_hole_jit(disk_surf, cam, flags, tl, world.center_of_mass, world) | |
| screen.blit(disk_surf, (0,0)) | |
| if flags['j1']: | |
| ac = [(255,0,0), (255,255,255) if flags['t3'] else (0,0,0), (0,255,255)] if flags['t3'] else [(255,0,0), (0,0,0)] | |
| for i, c in enumerate(ac): | |
| pv, nv = np.zeros(3), np.zeros(3); pv[i], nv[i] = AXIS_LEN, -AXIS_LEN | |
| pygame.draw.line(screen, c, cam.project(nv + world.center_of_mass), cam.project(pv + world.center_of_mass), 2) | |
| elif len(world.tets) > 0: pygame.draw.circle(screen, (255,255,255), cam.project(world.center_of_mass), 3) | |
| if world.tets: | |
| # 1. ANALYZE MOLECULES (Group them before drawing) | |
| # This lets us know which individual labels to HIDE because they are part of a group | |
| all_groups = find_molecules(world) | |
| bonded_ids = set() | |
| for grp in all_groups: | |
| name = get_molecule_name(grp, world) | |
| if name and len(grp) > 1: | |
| for t in grp: bonded_ids.add(t.id) | |
| # 2. PREPARE GEOMETRY | |
| awv = np.array([t.local + t.pos for t in world.tets]).reshape(-1, 3) | |
| asv = cam.project_many(awv).reshape(len(world.tets), 4, 2) | |
| id_idx = {t.id: i for i, t in enumerate(world.tets)} | |
| # 3. DRAW BONDS (Joints & Sticky) | |
| if is_interactive and dragging and locked_sticky_target: | |
| pygame.draw.line(screen, (255,140,0), asv[id_idx[dragging[0].id], dragging[1]], asv[id_idx[locked_sticky_target[0].id], locked_sticky_target[1]], 2) | |
| for t1, i1, t2, i2 in world.sticky_pairs: | |
| if t1.id in id_idx and t2.id in id_idx: pygame.draw.line(screen, (255, 140, 0), asv[id_idx[t1.id], i1], asv[id_idx[t2.id], i2], 1) | |
| for j in world.joints: | |
| if j.A.id in id_idx and j.B.id in id_idx: pygame.draw.line(screen, (255, 255, 255), asv[id_idx[j.A.id], j.ia], asv[id_idx[j.B.id], j.ib], 1) | |
| # 4. DRAW TETS (Sorted by depth) | |
| z_depths = cam.get_transformed_z_many(np.array([t.pos for t in world.tets])) | |
| sorted_indices = np.argsort(z_depths) | |
| for idx in sorted_indices: | |
| t = world.tets[idx]; | |
| if not np.all(np.isfinite(t.pos)): continue | |
| screen_pts = asv[idx]; world_verts = awv[idx*4:(idx+1)*4] | |
| # Colors & Magnetism | |
| cc = list(t.colors) if t.colors else list(Tetrahedron.FACE_COLORS) | |
| if t.is_magnetized: cc = [(0,0,0)]*4; cc[2 if t.magnetism==1 else 3] = Tetrahedron.FACE_COLORS[2 if t.magnetism==1 else 3] | |
| # Transparency calculation | |
| dist_from_cam = np.linalg.norm(t.pos - (cam.pan + cam.forward * cam.dist)) | |
| dist_from_origin = np.linalg.norm(t.pos - world.center_of_mass) | |
| combined_alpha = min(np.clip(1.0 - (dist_from_cam / 500.0), 0.2, 1.0), np.clip(1.0 - (dist_from_origin / 300.0), 0.3, 1.0)) * t.battery | |
| if math.isnan(combined_alpha): combined_alpha = 0.1 | |
| # Auras | |
| if t.aura_color and t.magnetic_strength > 0: | |
| center_screen = cam.project(t.pos) | |
| if center_screen[0] > -10000: | |
| aura_col = get_molecule_color(t.label if t.label else t.molecule_type, time.time()) | |
| if not aura_col: aura_col = t.aura_color | |
| aura_radius = int(EDGE_LEN * 8 * t.magnetic_strength) | |
| aura_alpha = int(80 * t.magnetic_strength * combined_alpha) | |
| aura_surf = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA) | |
| for ring in range(3): pygame.draw.circle(aura_surf, (*aura_col, aura_alpha // (ring + 1)), center_screen, aura_radius - ring * 3, 2) | |
| screen.blit(aura_surf, (0, 0)) | |
| # Draw Faces | |
| face_z = np.mean(cam.get_transformed_z_many(world_verts[Tetrahedron.FACES_NP]), axis=1) | |
| for fidx in np.argsort(face_z)[::-1]: | |
| points = screen_pts[Tetrahedron.FACES_NP[fidx]] | |
| if not np.any(points < -100): | |
| pygame.draw.polygon(screen, tuple(int(c * combined_alpha) for c in cc[fidx]), points) | |
| # Draw Edges | |
| for i, j in t.EDGES_NP: pygame.draw.line(screen, (0,0,0), screen_pts[i], screen_pts[j], 1) | |
| # Draw Valency Dots | |
| for i, p in enumerate(screen_pts): | |
| state = t.corner_states[i] | |
| if state == 1: | |
| pulse = int(155 + 100 * math.sin(time.time() * 10)) | |
| pygame.draw.circle(screen, (pulse, pulse, pulse), p, 3) | |
| pygame.draw.circle(screen, (255, 0, 0), p, 1) | |
| elif state == 2: pygame.draw.circle(screen, (50, 200, 50), p, 2) | |
| elif state == 3: pygame.draw.circle(screen, (0, 100, 255), p, 3, 1) | |
| draw_bot_thought_bubble(screen, cam, t, font_s) | |
| # --- 1. USER CUSTOM LABEL (Always Visible) --- | |
| # e.g. "My Fact", "Truth" | |
| if t.label and t.label not in MOLECULE_SYMBOLS: | |
| surf = font_s.render(t.label, True, (255, 255, 0)) | |
| # Draw slightly above the TET | |
| screen.blit(surf, surf.get_rect(center=cam.project(t.pos + [0, 1.5, 0]))) | |
| # --- 2. ATOMIC LABEL (Hidden if Bonded) --- | |
| # e.g. "H", "C", "Fe" | |
| # Only show if this atom stands alone. If bonded, the Group Label takes over. | |
| is_face_locked = False | |
| if t.id in bonded_ids: | |
| # Deep check: Is it actually part of a 3-joint bond? | |
| connections = 0 | |
| for j in world.joints: | |
| if (j.A.id == t.id and j.B.id in bonded_ids) or (j.B.id == t.id and j.A.id in bonded_ids): | |
| connections += 1 | |
| if connections >= 3: is_face_locked = True | |
| if not is_face_locked: | |
| if t.molecule_type: | |
| chem_name = MOLECULE_DATABASE.get(t.molecule_type, (None, t.molecule_type))[1] | |
| s2 = font_s.render(str(chem_name), True, (200, 200, 200)) | |
| screen.blit(s2, s2.get_rect(center=cam.project(t.pos + [0, -1.5, 0]))) | |
| # --- 3. MOLECULE GROUP LABEL (Floating Center) --- | |
| for grp in all_groups: | |
| #print(f"Group: {len(grp),grp}") | |
| if len(grp) > 1: | |
| name = get_molecule_name(grp, world) | |
| if name: | |
| avg_pos = np.mean([atom.pos for atom in grp], axis=0) | |
| screen_pos = cam.project(avg_pos) | |
| if screen_pos[0] > -10000: | |
| # Render Text (Plain White) | |
| s = font_s.render(name, True, (255, 255, 255)) | |
| # Simple Drop Shadow (Black) | |
| s_shadow = font_s.render(name, True, (0, 0, 0)) | |
| dest = s.get_rect(center=screen_pos) | |
| # Draw shadow then text | |
| screen.blit(s_shadow, (dest.x + 2, dest.y + 2)) | |
| screen.blit(s, dest) | |
| #print(f"{s}\n") | |
| if hasattr(world, 'reaction_particles'): | |
| world.spawn_reaction_particles(screen, cam, world._last_synth_reactions, WIDTH, HEIGHT) | |
| world._last_synth_reactions = [] | |
| if hasattr(world, 'quantum_particles'): | |
| world.spawn_quantum_visuals(screen, cam, world._last_quantum_events, WIDTH, HEIGHT) | |
| world._last_quantum_events = [] | |
| for avatar_id, avatar_data in list(net_avatars.items()): | |
| draw_player_avatar(screen, cam, np.array(avatar_data['pos']), avatar_data['color'], avatar_id, name=avatar_data.get('name')) | |
| now_ticks = pygame.time.get_ticks(); text_color = (0,0,0) if (flags['j1'] and not flags['t3']) else (200,200,200) | |
| msgs = [m for m in msgs if now_ticks < m[2]] | |
| for ts, yo, et in msgs: | |
| s = font_l.render(ts, True, text_color); s.set_alpha(max(0, min(255, (et - now_ticks) * 255 / 1000))) | |
| screen.blit(s, s.get_rect(center=(WIDTH//2, HEIGHT//2 + yo))) | |
| for i, (txt, et) in enumerate([m for m in net_messages if time.time() < m[1]]): | |
| s = font_s.render(txt, True, (255,200,100)); s.set_alpha(max(0, min(255, (et - time.time()) * 100))); screen.blit(s, (10, 40+i*25)) | |
| stage_surf = font_s.render(f"Era: {world.tech_tree.current_stage}", True, (200, 100, 255)) | |
| screen.blit(stage_surf, (WIDTH - stage_surf.get_width() - 10, HEIGHT - 60)) | |
| # Genesis Field Display | |
| field_txt = f"Ψ: {world.cached_psi:.2f} | Φ: {world.cached_phi:.2f} | Ω: {world.cached_omega:.2f}" | |
| field_surf = font_s.render(field_txt, True, (100, 255, 150)) | |
| screen.blit(field_surf, (10, HEIGHT - 60)) | |
| zf = DEFAULT_CAM_DIST / cam.dist; zoom_text = f"{zf:.1f}x" if zf > 10 else f"{zf:.2f}x" | |
| status_text = f"Mode: {game_mode.replace('_',' ').title()} | TETs: {len(world.tets)} | Unions: {len(world.joints)} | Desires: {len(world.sticky_pairs)} | Zoom: {zoom_text} | Time: {time_scale:.1f}x" | |
| top_leg = font_s.render(status_text, True, (0,255,255)) | |
| screen.blit(top_leg, top_leg.get_rect(center=(WIDTH//2, 20))) | |
| uptime_surf = font_s.render(f"v5.3D Up: {str(datetime.timedelta(seconds=int(time.time() - START_TIME)))} FPS: {int(fps)}", True, (100, 255, 150)) | |
| screen.blit(uptime_surf, (WIDTH - uptime_surf.get_width() - 10, 30)) | |
| bot_leg1 = font_s.render("RMB Label | LMB Pull/Join TETs | WASD/RMB Orbit | X/Alt+RMB Center | V Save Instant", True, (0,255,255)) | |
| bot_leg2 = font_s.render("H Host Mode | TAB Client Mode | R/F/Scroll Zoom | Q/E/Alt+Scroll Pan | Z/C/Ctrl+Scroll Timescale", True, (0,255,255)) | |
| screen.blit(bot_leg1, bot_leg1.get_rect(center=(WIDTH//2, HEIGHT-35))) | |
| screen.blit(bot_leg2, bot_leg2.get_rect(center=(WIDTH//2, HEIGHT-15))) | |
| # draw_molecular_labels(screen, cam, world, font_l) | |
| if hovered_vertex: | |
| t_hover = hovered_vertex[0] | |
| # Get the label, fallback to molecule type, or "Unknown" | |
| display_text = t_hover.label if t_hover.label else (t_hover.molecule_type if t_hover.molecule_type else "") | |
| if display_text: | |
| # Add Chem Info if available (e.g. "Water [H2O]") | |
| if t_hover.molecule_type and t_hover.molecule_type not in display_text: | |
| display_text = f"{display_text} [{t_hover.molecule_type}]" | |
| # Render Text | |
| text_surf = font_s.render(display_text, True, (0, 255, 255)) # Cyan text | |
| # Calculate position (offset 20px from mouse) | |
| mx, my = pygame.mouse.get_pos() | |
| x_pos = min(mx + 20, WIDTH - text_surf.get_width() - 10) # Keep on screen | |
| y_pos = min(my + 20, HEIGHT - text_surf.get_height() - 10) | |
| # Draw Background Box (Semi-transparent black) | |
| padding = 6 | |
| bg_rect = pygame.Rect(x_pos - padding, y_pos - padding, | |
| text_surf.get_width() + padding*2, | |
| text_surf.get_height() + padding*2) | |
| s = pygame.Surface((bg_rect.width, bg_rect.height), pygame.SRCALPHA) | |
| s.fill((0, 0, 0, 200)) # Dark background | |
| pygame.draw.rect(s, (0, 255, 255), s.get_rect(), 1) # Cyan border | |
| screen.blit(s, bg_rect.topleft) | |
| screen.blit(text_surf, (x_pos, y_pos)) | |
| # Render | |
| pygame.display.flip() | |
| if ON_HUGGINGFACE and GRADIO_AVAILABLE: | |
| try: GRADIO_FRAME_BUFFER = np.transpose(pygame.surfarray.array3d(screen), (1, 0, 2)) | |
| except: pass | |
| stop_all_networking(); pygame.quit() | |
| if __name__ == "__main__": | |
| if ON_HUGGINGFACE: | |
| t = threading.Thread(target=main, kwargs={'threaded': True}, daemon=True); t.start() | |
| if GRADIO_AVAILABLE: | |
| gradio_interface_loop() | |
| while True: time.sleep(1) | |
| else: | |
| print("Gradio not installed. Headless loop."); | |
| while True: time.sleep(1) | |
| else: main() | |