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 # ============================ @njit(fastmath=True, cache=True) 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) @njit(fastmath=True, cache=True) 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 @njit(fastmath=True, cache=True) 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 @njit(fastmath=True, cache=True) 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 @njit(fastmath=True, cache=True) 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) @njit(fastmath=True, cache=True) 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 --- @njit(fastmath=True, cache=True) 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 --- @njit(fastmath=True, cache=True) 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 @njit(fastmath=True, cache=True) 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 @njit(fastmath=True, cache=True) 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 @njit(fastmath=True, cache=True) 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 @njit(fastmath=True, cache=True) 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] @njit(cache=True) 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) @njit(cache=True) 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 @property def current_stage(self): return self.STAGES[self.stage_idx][0] @property 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": {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
(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)
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 : (Initiate guest mode)\n -listen (Initiate host mode port)\n -file (Load saved instant [json])\n -m (Mute sound)\n -t -z -o \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", ('', 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" 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()