TET-CRAFT / app.py
six1free's picture
v5.3 wip
1928ab8
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"<Host>: {msg['data']}")
except (socket.timeout, BlockingIOError): continue
except: self.running = False; break
def get_latest_world_state(self):
try: return self.world_state_queue.get_nowait()
except queue.Empty: return None
def send_cam_update(self):
try: send_msg(self.sock, {'type': 'cam_update', 'data': self.cam.get_state()})
except: pass
def send_chat(self, text):
try: send_msg(self.sock, {'type': 'chat', 'data': f"<{PLAYER_NAME}>: {text}"})
except: pass
def send_label(self, tet_id, label):
try: send_msg(self.sock, {'type': 'set_label', 'id': tet_id, 'label': label})
except: pass
def stop(self):
self.running = False
try: self.sock.shutdown(socket.SHUT_RDWR); self.sock.close()
except: pass
def stop_all_networking():
global host_instance, guest_instance
if host_instance: host_instance.stop(); host_instance = None
if guest_instance: guest_instance.stop(); guest_instance = None
atexit.register(stop_all_networking)
def shutdown_handler(signum, frame):
global GAME_RUNNING
GAME_RUNNING = False
stop_all_networking()
sys.exit(0)
signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)
def safe_world_update(world, new_state, last_hash=""):
try:
import hashlib
state_str = json.dumps(new_state, sort_keys=True, cls=NumpyEncoder)
current_hash = hashlib.md5(state_str.encode()).hexdigest()
if current_hash != last_hash: world.set_state(new_state)
return current_hash
except: return last_hash
def gradio_interface_loop():
def get_frame():
if GRADIO_FRAME_BUFFER is not None: return GRADIO_FRAME_BUFFER
return np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)
with gr.Interface(fn=get_frame, inputs=None, outputs=gr.Image(label="Hit the 'Generate' button!"), live=True, title="TET~CRAFT: The Fourth Temple v5.3D", description="Live simulation of DigitizingHumanity.com's Gamified, Decentralized, Salted, 5D Communication Manifold and Physics/Chemistry Simulator <br /> (A simulated player is adding one new TET/fact each hour to be misunderstood, warping time for the 15 min before that, has a thought and new perspective plain each minute, while each second it orbits)<br />Hit the 'Generate' button bellow for a fresh screenshot") as demo:
demo.launch(server_name="0.0.0.0", server_port=7860)
def main(threaded=False):
print("\n\nIf needed create and fill with 1 IP per line blacklist.cfg\n\nCLI Options:\n -connect <ip>:<port> (Initiate guest mode)\n -listen <port> (Initiate host mode port)\n -file <filename> (Load saved instant [json])\n -m (Mute sound)\n -t <scale> -z <zoom> -o <x,y,z>\n\nTET~CRAFT v5.3D Initializing...\n\n")
cli_connect_addr, cli_listen_port, cli_load_file = None, None, None
cli_time_scale, cli_zoom_factor, cli_cam_pan = None, None, None
cli_mute = False
args = sys.argv[1:]; i = 0
while i < len(args):
if args[i] == '-connect' and i + 1 < len(args): cli_connect_addr = args[i+1]; i += 1
elif args[i] == '-listen':
cli_listen_port = DEFAULT_PORT
if i + 1 < len(args) and args[i+1].isdigit(): cli_listen_port = int(args[i+1]); i += 1
elif args[i] == '-file' and i + 1 < len(args):
cli_load_file = args[i+1]; i += 1
cli_time_scale = .001
elif args[i] == '-t' and i + 1 < len(args): cli_time_scale = float(args[i+1]); i += 1
elif args[i] == '-z' and i + 1 < len(args): cli_zoom_factor = float(args[i+1]); i += 1
elif args[i] == '-o' and i + 1 < len(args): cli_cam_pan = [float(c) for c in args[i+1].split(',')]; i += 1
elif args[i] == '-m': cli_mute = True
i += 1
global WIDTH, HEIGHT, clock, game_mode, host_instance, guest_instance, net_avatars, net_messages, AUDIO_ENABLED, GRADIO_FRAME_BUFFER, GAME_RUNNING
pygame.init()
if not cli_mute:
try: pygame.mixer.init(44100, -16, 2, 512); AUDIO_ENABLED = True
except: print("Sound Init Failed. Running Silent."); AUDIO_ENABLED = False
else:
AUDIO_ENABLED = False
print("Audio Muted by CLI.")
# Initialize sounds AFTER mixer init
ping_sound = generate_ping_sound()
boing_sound = generate_boing_sound()
if ON_HUGGINGFACE:
WIDTH, HEIGHT = 1200, 600; screen = pygame.display.set_mode((WIDTH, HEIGHT))
# Try to find the specific file
if os.path.exists("nothing.json"):
cli_load_file = "nothing.json"
print("### HF Mode: Found nothing.json - Auto-loading... ###")
else:
# If not found, do nothing (cli_load_file stays None, standard Void logic triggers)
print("### HF Mode: nothing.json not found - Starting fresh in Void. ###")
else: screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.RESIZABLE)
pygame.display.set_caption("TET~CRAFT v5.3D The Fourth Temple")
clock = pygame.time.Clock(); font_l = pygame.font.SysFont('Georgia', 32); font_s = pygame.font.SysFont(None, 24)
world = World(boing_sound); cam = Camera()
show_intro(screen, cam)
show_name_input_screen(screen)
flags = {'t0': False, 't1': False, 't2': False, 'j1': False, 't3': False}; msgs = []
dragging, rotating, last_mouse = None, False, (0,0); time_scale = 0.0001
locked_sticky_target = None; frame_count = 0
past_projection = PastProjection4Sphere()
disk_surf = None
last_bot_move = time.time(); last_bot_spawn = time.time() - 3590; last_bot_thought = time.time()
# Bot State Machine
bot_state = 'IDLE' # IDLE, WAITING_FOR_ANSWER
bot_reply_timer = 0.0
last_timescale_surge = time.time() - 2000
if cli_time_scale is not None: time_scale = cli_time_scale
if cli_zoom_factor is not None: cam.dist = DEFAULT_CAM_DIST / max(0.01, cli_zoom_factor)
if cli_cam_pan is not None: cam.pan = (np.array(cli_cam_pan) - 0.5) * 2.0 * FIELD_SCALE
def stop_guest_mode():
global guest_instance, game_mode
if guest_instance: guest_instance.stop(); guest_instance = None; game_mode = 'single_player'; net_avatars.clear(); add_timed_message("Disconnected.", duration=3); return True
return False
def add_timed_message(text, y_offset=0, duration=4): msgs.append([text, y_offset, pygame.time.get_ticks() + duration * 1000])
def add_network_message(text): net_messages.append([text, time.time() + 8])
def reset_simulation(show_message=True):
nonlocal flags, time_scale, dragging, rotating, locked_sticky_target
global game_mode, host_instance, guest_instance, net_avatars, net_messages
if host_instance: host_instance.stop(); host_instance = None
if guest_instance: stop_guest_mode()
world.tets.clear(); world.joints.clear(); world.sticky_pairs.clear(); past_projection.points.clear()
net_avatars.clear(); net_messages.clear(); flags = {k: False for k in flags}
cam.__init__(); time_scale = 1.0; game_mode = 'single_player'; dragging, rotating = None, False
world.tech_tree = TechTree()
def save_world_to_file():
if not world.tets: add_timed_message("Cannot save empty.", duration=3); return
try:
with open(SAVE_FILENAME, 'w') as f: json.dump(world.get_state(), f, cls=NumpyEncoder, indent=2)
add_timed_message(f"Saved to {SAVE_FILENAME}", duration=3)
except IOError as e: add_timed_message(f"Error saving: {e}", duration=4)
def connect_as_guest(host_ip, port):
global guest_instance, game_mode
try:
save_world_to_file(); guest_instance, game_mode = Guest(host_ip, int(port), world, cam, add_network_message, ping_sound), 'guest'
add_timed_message(f"Connected {host_ip}:{port}", duration=3); world.tets.clear(); world.joints.clear(); world.sticky_pairs.clear(); return True
except Exception as e: add_timed_message(f"Failed connect: {e}", duration=4); return False
def discover_and_join():
global guest_instance, game_mode
if game_mode != 'single_player': return
host_addr = None
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1); sock.settimeout(1.0)
sock.sendto(b"DISCOVER_TETCRAFT_HOST", ('<broadcast>', DISCOVERY_PORT))
data, addr = sock.recvfrom(1024)
if data.startswith(b"TETCRAFT_HOST_HERE:"): host_addr = (addr[0], int(data.split(b':')[1]))
except socket.timeout: pass
if host_addr: connect_as_guest(host_addr[0], host_addr[1])
else:
user_input = get_user_input(screen, "Enter IP:Port:")
if user_input: parts = user_input.split(':'); connect_as_guest(parts[0], int(parts[1]) if len(parts) > 1 else DEFAULT_PORT)
def initiate_host_mode(port=None):
global host_instance, game_mode
if game_mode == 'single_player': host_instance, game_mode = Host(world, add_network_message, ping_sound, port), 'host'; add_timed_message(f"Hosting port {host_instance.port}", duration=3)
elif game_mode == 'host' and host_instance: host_instance.stop(); host_instance = None; game_mode = 'single_player'; add_timed_message("Host stopped", duration=3)
loaded_from_save = False
if cli_connect_addr: connect_as_guest(*(cli_connect_addr.split(':') if ':' in cli_connect_addr else (cli_connect_addr, DEFAULT_PORT)))
elif cli_listen_port: initiate_host_mode(cli_listen_port)
if cli_load_file:
if os.path.exists(cli_load_file):
try:
with open(cli_load_file, 'r', encoding='utf-8-sig') as f: world.set_state(json.load(f)); loaded_from_save = True
if world.tets:
cam.dist = DEFAULT_CAM_DIST
cam.pan = world.center_of_mass.copy()
except Exception as e: print(f"Load Error: {e}")
if not (loaded_from_save or cli_connect_addr or cli_listen_port): show_void_screen(screen, world)
if ON_HUGGINGFACE: host_instance = Host(world, lambda x: net_messages.append([x, time.time()+8]), ping_sound, DEFAULT_PORT); game_mode = 'host'
camera_follow_mode = False
last_step_size = 0.0
while GAME_RUNNING:
unscaled_dt = min(0.1, clock.tick(FPS) / 1000.0)
now = time.time()
# === FIX: Physics Speed Limit ===
# Never allow the physics to calculate more than 0.2 seconds of movement in one frame.
# This prevents "Teleportation Explosions" at high time scales.
raw_scaled_dt = unscaled_dt * time_scale
scaled_dt = min(raw_scaled_dt, 0.2)
# ================================
frame_count, fps = frame_count + 1, clock.get_fps()
if 0 < fps < 45 and time_scale > 1.0: time_scale = max(1.0, time_scale * 0.99)
is_interactive = (game_mode in ['single_player', 'host']) and not ON_HUGGINGFACE
hovered_vertex = None
mx, my = pygame.mouse.get_pos()
mouse_arr = np.array([mx, my], dtype=np.float64)
if world.tets:
curr_verts_world = np.array([t.local + t.pos for t in world.tets]).reshape(-1, 3)
curr_verts_screen = cam.project_many(curr_verts_world)
if is_interactive and not rotating:
dist_sq = np.sum((curr_verts_screen - mouse_arr)**2, axis=1)
if dist_sq.size > 0 and np.min(dist_sq) < SELECTION_RADIUS**2:
min_idx = np.argmin(dist_sq)
if curr_verts_screen[min_idx][0] > -9000:
hovered_vertex = (world.tets[min_idx // 4], min_idx % 4)
pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_HAND)
else:
hovered_vertex = None
# === NEW: Cursor Reset ===
if not dragging:
pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_ARROW)
else: curr_verts_screen = np.empty((0,2))
if is_interactive and hovered_vertex and not dragging:
h_tet = hovered_vertex[0]; h_tet.pos_prev[:] = h_tet.pos[:]; h_tet.local_prev[:] = h_tet.local[:]
h_tet.battery = min(1.0, h_tet.battery + 0.01)
# 3. Thoughts
if now - last_bot_thought > 61:
if len(world.tets) > 1:
# --- Helper: Find physical neighbors ---
def get_neighbors(target_tet, exclude_id=None):
nb = []
# 1. Check Joints (Strongest link)
for j in world.joints:
if j.A.id == target_tet.id and j.B.id != exclude_id: nb.append(j.B)
elif j.B.id == target_tet.id and j.A.id != exclude_id: nb.append(j.A)
# 2. Check Sticky Pairs (Desire)
if not nb:
for p in world.sticky_pairs:
if p[0].id == target_tet.id and p[2].id != exclude_id: nb.append(p[2])
elif p[2].id == target_tet.id and p[0].id != exclude_id: nb.append(p[0])
# 3. Check Proximity (Loose association)
if not nb:
for t in world.tets:
if t.id == target_tet.id or t.id == exclude_id: continue
if np.linalg.norm(t.pos - target_tet.pos) < EDGE_LEN * 4:
nb.append(t)
return nb
# 1. Pick Subject (T1)
t1 = random.choice(world.tets)
label1 = t1.label if t1.label else "Unknown"
# 2. Pick Object (T2) - Neighbor of T1
n1 = get_neighbors(t1)
# If no neighbors, pick random, but not T1
t2 = random.choice(n1) if n1 else random.choice([t for t in world.tets if t.id != t1.id])
label2 = t2.label if t2.label else "Void"
# 3. Pick Context (T3) - Neighbor of T2
# Try to avoid going back to T1 immediately to avoid A=B=A loops unless necessary
n2 = get_neighbors(t2, exclude_id=t1.id)
if n2:
t3 = random.choice(n2)
else:
# If T2 has no forward neighbors, check if there are other TETS available
others = [t for t in world.tets if t.id != t1.id and t.id != t2.id]
if others:
t3 = random.choice(others) # Jump to a new disconnected idea
else:
t3 = t1 # Forced loop (A -> B -> A)
label3 = t3.label if t3.label else "Mystery"
# 4. Get Symbols
sym1 = get_thought_symbol(t1, t2, world)
sym2 = get_thought_symbol(t2, t3, world)
# 5. Formulate Trinary Thought
thought = f"{label1} {sym1} {label2} {sym2} {label3} ?"
# Add to chat log
net_messages.append([f"[Thought]: {thought}", time.time() + 15])
print(f"[Bot Thought]: {thought}")
#Stage 5 Quantum thoughts
def get_neighbors(target_tet, exclude_id=None):
nb = []
for j in world.joints:
if j.A.id == target_tet.id and j.B.id != exclude_id: nb.append(j.B)
elif j.B.id == target_tet.id and j.A.id != exclude_id: nb.append(j.A)
if not nb:
for p in world.sticky_pairs:
if p[0].id == target_tet.id and p[2].id != exclude_id: nb.append(p[2])
elif p[2].id == target_tet.id and p[0].id != exclude_id: nb.append(p[0])
if not nb:
for t in world.tets:
if t.id == target_tet.id or t.id == exclude_id: continue
if np.linalg.norm(t.pos - target_tet.pos) < EDGE_LEN * 4: nb.append(t)
return nb
t1 = random.choice(world.tets)
label1 = t1.label if t1.label else "Unknown"
n1 = get_neighbors(t1)
t2 = random.choice(n1) if n1 else random.choice([t for t in world.tets if t.id != t1.id])
label2 = t2.label if t2.label else "Void"
n2 = get_neighbors(t2, exclude_id=t1.id)
t3 = random.choice(n2) if n2 else t1
label3 = t3.label if t3.label else "Mystery"
sym1 = get_thought_symbol(t1, t2, world)
sym2 = get_thought_symbol(t2, t3, world)
# PHASE 5: Inject Quantum Ontology
quantum_terms = ["ERD∇", "Ψ-coherence", "OBA⊗", "QuantumFold", "EntropicBridge"]
if t1.quantum_state != "ground" or t1.erd_coherence > 0.8:
sym1 = random.choice(quantum_terms)
thought = f"{label1} {sym1} {label2} {sym2} {label3} !"
net_messages.append([f"[Quantum Reply]: {thought}", time.time() + 15])
print(f"[Quantum Reply]: {thought}")
last_bot_thought = now
pygame.event.pump()
if not ON_HUGGINGFACE:
last_world_hash = ""
for e in pygame.event.get():
if e.type == pygame.QUIT: GAME_RUNNING = False
if e.type == pygame.VIDEORESIZE: WIDTH, HEIGHT = e.w, e.h; screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.RESIZABLE); disk_surf = None
keys = pygame.key.get_pressed(); mods = pygame.key.get_mods(); ctrl_held = (mods & pygame.KMOD_CTRL); alt_held = (mods & pygame.KMOD_ALT)
if e.type == pygame.KEYDOWN:
if e.key == pygame.K_v and is_interactive: save_world_to_file()
if e.key == pygame.K_BACKQUOTE: world.explode()
if is_interactive and e.key == pygame.K_SPACE: (world.spawn() if len(world.tets) < 2 else (world.spawn_polar_pair() if flags['j1'] else None))
if e.key == pygame.K_x:
cam.dist = DEFAULT_CAM_DIST
cam.pan = world.center_of_mass.copy()
camera_follow_mode = not camera_follow_mode
for t in world.tets:
t.pos_prev = t.pos.copy() # Velocity = 0
if e.key == pygame.K_c: time_scale = min(10.0, time_scale + 0.5)
if e.key == pygame.K_z: time_scale = max(0.1, time_scale - 0.5)
if e.key == pygame.K_TAB: (discover_and_join() if game_mode == 'single_player' else stop_guest_mode())
if e.key == pygame.K_h and is_interactive: initiate_host_mode()
if game_mode == 'guest':
if guest_instance:
world_state = guest_instance.get_latest_world_state()
if world_state: last_world_hash = safe_world_update(world, world_state, last_world_hash)
net_avatars = guest_instance.latest_avatars.copy()
if frame_count % 30 == 0: guest_instance.send_cam_update()
if e.type == pygame.MOUSEBUTTONDOWN and e.button == 3:
# GUEST CLICKING HOST AVATAR
clicked_avatar = None
for av_id, av_data in list(net_avatars.items()):
# Skip own avatar if in list
if av_id == f"guest_{guest_instance.sock.getsockname()[0]}:{guest_instance.sock.getsockname()[1]}": continue
av_pos = np.array(av_data['pos'])
av_screen = cam.project(av_pos)
if av_screen[0] > -10000:
d = math.sqrt((av_screen[0]-mx)**2 + (av_screen[1]-my)**2)
if d < 30: clicked_avatar = (av_id, av_data); break
if clicked_avatar and not alt_held:
msg = get_user_input(screen, f"Msg to {clicked_avatar[1].get('name','Host')}:")
if msg: guest_instance.send_chat(msg) # Guest sends normal chat, Host sees it
else:
rotating, last_mouse = True, e.pos
if e.type == pygame.MOUSEBUTTONUP and e.button == 3: rotating = False
if e.type == pygame.MOUSEWHEEL:
if alt_held: cam.pan += np.array([math.cos(cam.yaw), 0, -math.sin(cam.yaw)]) * (e.y * 20.0 * unscaled_dt)
else: cam.zoom(ZOOM_SPEED if e.y < 0 else 1/ZOOM_SPEED)
elif is_interactive:
if e.type == pygame.MOUSEBUTTONDOWN:
if e.button == 1 and hovered_vertex:
start_tet, start_idx = hovered_vertex
world.sticky_pairs = [p for p in world.sticky_pairs if not ((p[0].id == start_tet.id and p[1] == start_idx) or (p[2].id == start_tet.id and p[3] == start_idx))]
dragging = (hovered_vertex[0], hovered_vertex[1], cam.get_transformed_z(hovered_vertex[0].verts()[hovered_vertex[1]]))
locked_sticky_target = None
if e.button == 3:
if alt_held:
cam.dist = DEFAULT_CAM_DIST
cam.pan = world.center_of_mass.copy()
elif hovered_vertex:
t = hovered_vertex[0]; new_l = get_user_input(screen, f"Rename '{t.label}':", t.label)
if new_l: t.label = new_l
else:
# HOST CLICKING GUEST AVATARS
clicked_avatar = None
if host_instance:
for av_id, av_data in list(net_avatars.items()):
if av_id == 'host': continue # Don't click self
av_pos = np.array(av_data['pos'])
av_screen = cam.project(av_pos)
if av_screen[0] > -10000:
d = math.sqrt((av_screen[0]-mx)**2 + (av_screen[1]-my)**2)
if d < 30: clicked_avatar = (av_id, av_data); break
if clicked_avatar:
msg = get_user_input(screen, f"Msg to {clicked_avatar[1].get('name','Guest')}:")
if msg:
# Find socket
target_sock = None
for s, d in host_instance.clients.items():
if d['id'] == clicked_avatar[0]: target_sock = s; break
if target_sock:
send_msg(target_sock, {'type': 'chat', 'data': f"<{PLAYER_NAME}>: {msg}"})
add_network_message(f"<To {clicked_avatar[1].get('name')}: {msg}")
else:
rotating, last_mouse = True, e.pos
if e.type == pygame.MOUSEBUTTONUP:
if e.button == 1 and dragging and locked_sticky_target:
t1, i1 = dragging[0], dragging[1]; t2, i2 = locked_sticky_target[0], locked_sticky_target[1]
cnt = sum(1 for j in world.joints if (j.A.id == t1.id and j.ia == i1) or (j.B.id == t1.id and j.ib == i1))
if cnt < 6:
world.sticky_pairs.append((t1, i1, t2, i2))
print(f"User: {t1.label} desires {t2.label}")
if e.button == 1: dragging, locked_sticky_target = None, None
if e.button == 3: rotating = False
if e.type == pygame.MOUSEWHEEL:
if ctrl_held: time_scale = np.clip(time_scale * (1.1 if e.y > 0 else 0.9), 0.1, 10.0)
elif alt_held:
cam.pan += np.array([math.cos(cam.yaw), 0, -math.sin(cam.yaw)]) * (e.y * 20.0 * unscaled_dt)
else: cam.zoom(ZOOM_SPEED if e.y < 0 else 1/ZOOM_SPEED)
keys = pygame.key.get_pressed()
if keys[pygame.K_c]: time_scale = min(10.0, time_scale + 2.0 * unscaled_dt)
if keys[pygame.K_z]: time_scale = max(0.1, time_scale - 2.0 * unscaled_dt)
cam.pitch += ORBIT_SPEED * unscaled_dt * (int(keys[pygame.K_w]) - int(keys[pygame.K_s]))
cam.yaw += ORBIT_SPEED * unscaled_dt * (int(keys[pygame.K_d]) - int(keys[pygame.K_a]))
if keys[pygame.K_r]: cam.zoom(1/ZOOM_SPEED)
if keys[pygame.K_f]: cam.zoom(ZOOM_SPEED)
cam.pan += np.array([math.cos(cam.yaw), 0, -math.sin(cam.yaw)]) * (int(keys[pygame.K_q]) - int(keys[pygame.K_e])) * PAN_SPEED * unscaled_dt * (cam.dist / DEFAULT_CAM_DIST)
if rotating: mx, my = pygame.mouse.get_pos(); cam.yaw += (mx - last_mouse[0]) * 0.005; cam.pitch = np.clip(cam.pitch - (my - last_mouse[1]) * 0.005, -1.57, 1.57); last_mouse = (mx, my)
if dragging:
t_drag, i_drag, dd = dragging; m3d = cam.unproject(pygame.mouse.get_pos(), dd); delta = m3d - t_drag.verts()[i_drag]
t_drag.local[i_drag] += delta * MOUSE_PULL_STRENGTH * (DEFAULT_CAM_DIST / cam.dist); t_drag.pos += delta * BODY_PULL_STRENGTH * (DEFAULT_CAM_DIST / cam.dist)
best_dist_sq = SELECTION_RADIUS**2; current_hover_target = None
for tidx, tt in enumerate(world.tets):
if tt.id == t_drag.id: continue
for vidx in range(4):
idx_flat = tidx * 4 + vidx; pt = curr_verts_screen[idx_flat]; d_sq = np.sum((pt - mouse_arr)**2)
if d_sq < best_dist_sq: best_dist_sq = d_sq; current_hover_target = (tt, vidx)
locked_sticky_target = current_hover_target
else:
now = time.time()
# If you speed up time with unbound tets, bad news!
# if now - last_bot_spawn > 2700: # 2700 seconds = 45 minutes
# time_scale = 10.0
# elif now - last_bot_spawn < 2000:
# time_scale = 0.00001
# if now - last_bot_spawn > 2700 and now - last_bot_spawn > 3300:
# time_scale = 0.001
if world.tets:
cam.pan = world.center_of_mass.copy()
if now - last_bot_move > 59:
cam.pitch = max(-1, min(1, cam.pitch + random.uniform(-1, 1)))
cam.dist = DEFAULT_CAM_DIST
last_bot_move = now
else:
cam.yaw += 0.05 * unscaled_dt
zoom_scalar = 4.0 * 3.0 ** (math.sin(now * 0.05) * 2.0)
target_dist = DEFAULT_CAM_DIST * zoom_scalar
cam.dist += (target_dist - cam.dist) * 0.05
if now - last_bot_spawn > 3600:
world.spawn_polar_pair()
if len(world.tets) >= 2: world.tets[-1].label = random.choice(MYSTIC_WORDS); world.tets[-2].label = random.choice(MYSTIC_WORDS)
last_bot_spawn = now
print(f"TET pair created: {world.tets[-2].label,world.tets[-1].label}")
if game_mode == 'host' and host_instance:
while not host_instance.message_queue.empty():
try:
msg = host_instance.message_queue.get_nowait()
if msg[0] == 'set_label':
for t in world.tets:
if t.id == msg[1]: t.label = msg[2]; break
elif msg[0] == 'cleanup':
with host_instance.lock:
if msg[1] in host_instance.clients:
host_instance.clients.pop(msg[1], None)
try: msg[1].close()
except: pass
except: break
if frame_count % (2 + len(host_instance.clients)) == 0: host_instance.broadcast_state()
if game_mode != 'guest':
zf = DEFAULT_CAM_DIST / cam.dist; spin = np.clip(1/np.log(zf + 1) + 1, 0.1, 2.0)
# 1. Prepare Arrays (ONCE per frame)
world.rebuild_optimization_cache()
# 2. Run Logic (ONCE per frame)
world.update_logic(scaled_dt, lambda t, duration=2: msgs.append([t, 0, pygame.time.get_ticks() + duration * 1000]),cam=cam)
# 3. Sub-Stepped Physics (MANY times per frame)
# This keeps the physics stable even at high time scales
remaining_dt = scaled_dt
SAFE_DT = 0.03 # Max 30ms per step
num_steps = int(remaining_dt / SAFE_DT) + 1
if num_steps > 20:
num_steps = 20
remaining_dt = 20 * SAFE_DT # Cap max speed to prevent freeze
step_size = remaining_dt / num_steps
# === FIX: VERLET MOMENTUM RESCALING ===
# If time slows down, we must shrink the gap between pos and pos_prev
# otherwise the old momentum looks like 1000x speed in the new timeframe.
if last_step_size > 0 and abs(step_size - last_step_size) > 1e-9:
time_correction = step_size / last_step_size
# Clamp correction to prevent explosions if time pauses/unpauses
time_correction = max(0.0, min(time_correction, 5.0))
if abs(time_correction - 1.0) > 0.01:
# Apply scaling to the optimization cache directly for speed
# Vel = (Pos - Prev) -> NewVel = Vel * Ratio -> NewPrev = Pos - NewVel
# Update Cached Arrays
velocity_vectors = world.cached_pos - world.cached_prev
world.cached_prev = world.cached_pos - (velocity_vectors * time_correction)
# Update Objects
for i, t in enumerate(world.tets):
t.pos_prev = t.pos - (t.pos - t.pos_prev) * time_correction
# print(f"Time Warp: Momentum Rescaled by {time_correction:.4f}")
last_step_size = step_size
for _ in range(num_steps):
# Run pure physics
world.update_physics_only(step_size, time_scale, spin)
if len(world.tets) == 1 and not flags['t0']: flags['t0'] = True
if len(world.tets) >= 2 and not flags['t2']:
flags['t2'] = True
msgs.append(["Let time have somewhere to return to!", -50, pygame.time.get_ticks() + 6000])
world.sticky_pairs.extend([(world.tets[0], v, world.tets[1], v) for v in range(4)])
if len(world.tets) >= 2 and world.joints and not flags['j1']:
flags['j1'] = True
msgs.append(["God said: Let there be LIGHT!", -50, pygame.time.get_ticks() + 6000])
if len(world.tets) >= 3 and flags['j1'] and not flags['t3']:
flags['t3'] = True
msgs.append(["And God divided the light from the darkness...", 50, pygame.time.get_ticks() + 6000])
elif guest_instance:
if frame_count % 10 == 0: guest_instance.send_cam_update()
s = guest_instance.get_latest_world_state();
if s: world.set_state(s)
if len(world.tets) >= 2 and not flags['t2']: flags['t2'] = True
if len(world.tets) >= 2 and world.joints and not flags['j1']: flags['j1'] = True
if len(world.tets) >= 3 and flags['j1'] and not flags['t3']: flags['t3'] = True
mouse_busy = pygame.mouse.get_pressed()[0] or pygame.mouse.get_pressed()[2] or rotating or dragging
if (ON_HUGGINGFACE or camera_follow_mode) and world.tets and not mouse_busy:
target_pan = world.center_of_mass.copy()
if np.all(np.isfinite(target_pan)):
# Smoothly interpolate for a nicer feel (Optional, or just use =)
# cam.pan = target_pan
cam.pan += (target_pan - cam.pan) * 0.1 # Soft follow (10% per frame)
# Rendering
tl = np.clip((time_scale - 0.1) / 9.9, 0, 1)
bg = tuple(np.array([30,0,0]) * (1-tl) + np.array([5,5,10]) * tl if flags['t3'] else ((255,255,255) if flags['j1'] else (10,10,20)))
screen.fill(bg)
if flags['t3']:
past_projection.update_and_draw(screen, cam, world.center_of_mass, len(world.tets), time_scale, WIDTH, HEIGHT)
if disk_surf is None: disk_surf = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
disk_surf.fill((0,0,0,0))
draw_standard_black_hole_jit(disk_surf, cam, flags, tl, world.center_of_mass, world)
screen.blit(disk_surf, (0,0))
if flags['j1']:
ac = [(255,0,0), (255,255,255) if flags['t3'] else (0,0,0), (0,255,255)] if flags['t3'] else [(255,0,0), (0,0,0)]
for i, c in enumerate(ac):
pv, nv = np.zeros(3), np.zeros(3); pv[i], nv[i] = AXIS_LEN, -AXIS_LEN
pygame.draw.line(screen, c, cam.project(nv + world.center_of_mass), cam.project(pv + world.center_of_mass), 2)
elif len(world.tets) > 0: pygame.draw.circle(screen, (255,255,255), cam.project(world.center_of_mass), 3)
if world.tets:
# 1. ANALYZE MOLECULES (Group them before drawing)
# This lets us know which individual labels to HIDE because they are part of a group
all_groups = find_molecules(world)
bonded_ids = set()
for grp in all_groups:
name = get_molecule_name(grp, world)
if name and len(grp) > 1:
for t in grp: bonded_ids.add(t.id)
# 2. PREPARE GEOMETRY
awv = np.array([t.local + t.pos for t in world.tets]).reshape(-1, 3)
asv = cam.project_many(awv).reshape(len(world.tets), 4, 2)
id_idx = {t.id: i for i, t in enumerate(world.tets)}
# 3. DRAW BONDS (Joints & Sticky)
if is_interactive and dragging and locked_sticky_target:
pygame.draw.line(screen, (255,140,0), asv[id_idx[dragging[0].id], dragging[1]], asv[id_idx[locked_sticky_target[0].id], locked_sticky_target[1]], 2)
for t1, i1, t2, i2 in world.sticky_pairs:
if t1.id in id_idx and t2.id in id_idx: pygame.draw.line(screen, (255, 140, 0), asv[id_idx[t1.id], i1], asv[id_idx[t2.id], i2], 1)
for j in world.joints:
if j.A.id in id_idx and j.B.id in id_idx: pygame.draw.line(screen, (255, 255, 255), asv[id_idx[j.A.id], j.ia], asv[id_idx[j.B.id], j.ib], 1)
# 4. DRAW TETS (Sorted by depth)
z_depths = cam.get_transformed_z_many(np.array([t.pos for t in world.tets]))
sorted_indices = np.argsort(z_depths)
for idx in sorted_indices:
t = world.tets[idx];
if not np.all(np.isfinite(t.pos)): continue
screen_pts = asv[idx]; world_verts = awv[idx*4:(idx+1)*4]
# Colors & Magnetism
cc = list(t.colors) if t.colors else list(Tetrahedron.FACE_COLORS)
if t.is_magnetized: cc = [(0,0,0)]*4; cc[2 if t.magnetism==1 else 3] = Tetrahedron.FACE_COLORS[2 if t.magnetism==1 else 3]
# Transparency calculation
dist_from_cam = np.linalg.norm(t.pos - (cam.pan + cam.forward * cam.dist))
dist_from_origin = np.linalg.norm(t.pos - world.center_of_mass)
combined_alpha = min(np.clip(1.0 - (dist_from_cam / 500.0), 0.2, 1.0), np.clip(1.0 - (dist_from_origin / 300.0), 0.3, 1.0)) * t.battery
if math.isnan(combined_alpha): combined_alpha = 0.1
# Auras
if t.aura_color and t.magnetic_strength > 0:
center_screen = cam.project(t.pos)
if center_screen[0] > -10000:
aura_col = get_molecule_color(t.label if t.label else t.molecule_type, time.time())
if not aura_col: aura_col = t.aura_color
aura_radius = int(EDGE_LEN * 8 * t.magnetic_strength)
aura_alpha = int(80 * t.magnetic_strength * combined_alpha)
aura_surf = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
for ring in range(3): pygame.draw.circle(aura_surf, (*aura_col, aura_alpha // (ring + 1)), center_screen, aura_radius - ring * 3, 2)
screen.blit(aura_surf, (0, 0))
# Draw Faces
face_z = np.mean(cam.get_transformed_z_many(world_verts[Tetrahedron.FACES_NP]), axis=1)
for fidx in np.argsort(face_z)[::-1]:
points = screen_pts[Tetrahedron.FACES_NP[fidx]]
if not np.any(points < -100):
pygame.draw.polygon(screen, tuple(int(c * combined_alpha) for c in cc[fidx]), points)
# Draw Edges
for i, j in t.EDGES_NP: pygame.draw.line(screen, (0,0,0), screen_pts[i], screen_pts[j], 1)
# Draw Valency Dots
for i, p in enumerate(screen_pts):
state = t.corner_states[i]
if state == 1:
pulse = int(155 + 100 * math.sin(time.time() * 10))
pygame.draw.circle(screen, (pulse, pulse, pulse), p, 3)
pygame.draw.circle(screen, (255, 0, 0), p, 1)
elif state == 2: pygame.draw.circle(screen, (50, 200, 50), p, 2)
elif state == 3: pygame.draw.circle(screen, (0, 100, 255), p, 3, 1)
draw_bot_thought_bubble(screen, cam, t, font_s)
# --- 1. USER CUSTOM LABEL (Always Visible) ---
# e.g. "My Fact", "Truth"
if t.label and t.label not in MOLECULE_SYMBOLS:
surf = font_s.render(t.label, True, (255, 255, 0))
# Draw slightly above the TET
screen.blit(surf, surf.get_rect(center=cam.project(t.pos + [0, 1.5, 0])))
# --- 2. ATOMIC LABEL (Hidden if Bonded) ---
# e.g. "H", "C", "Fe"
# Only show if this atom stands alone. If bonded, the Group Label takes over.
is_face_locked = False
if t.id in bonded_ids:
# Deep check: Is it actually part of a 3-joint bond?
connections = 0
for j in world.joints:
if (j.A.id == t.id and j.B.id in bonded_ids) or (j.B.id == t.id and j.A.id in bonded_ids):
connections += 1
if connections >= 3: is_face_locked = True
if not is_face_locked:
if t.molecule_type:
chem_name = MOLECULE_DATABASE.get(t.molecule_type, (None, t.molecule_type))[1]
s2 = font_s.render(str(chem_name), True, (200, 200, 200))
screen.blit(s2, s2.get_rect(center=cam.project(t.pos + [0, -1.5, 0])))
# --- 3. MOLECULE GROUP LABEL (Floating Center) ---
for grp in all_groups:
#print(f"Group: {len(grp),grp}")
if len(grp) > 1:
name = get_molecule_name(grp, world)
if name:
avg_pos = np.mean([atom.pos for atom in grp], axis=0)
screen_pos = cam.project(avg_pos)
if screen_pos[0] > -10000:
# Render Text (Plain White)
s = font_s.render(name, True, (255, 255, 255))
# Simple Drop Shadow (Black)
s_shadow = font_s.render(name, True, (0, 0, 0))
dest = s.get_rect(center=screen_pos)
# Draw shadow then text
screen.blit(s_shadow, (dest.x + 2, dest.y + 2))
screen.blit(s, dest)
#print(f"{s}\n")
if hasattr(world, 'reaction_particles'):
world.spawn_reaction_particles(screen, cam, world._last_synth_reactions, WIDTH, HEIGHT)
world._last_synth_reactions = []
if hasattr(world, 'quantum_particles'):
world.spawn_quantum_visuals(screen, cam, world._last_quantum_events, WIDTH, HEIGHT)
world._last_quantum_events = []
for avatar_id, avatar_data in list(net_avatars.items()):
draw_player_avatar(screen, cam, np.array(avatar_data['pos']), avatar_data['color'], avatar_id, name=avatar_data.get('name'))
now_ticks = pygame.time.get_ticks(); text_color = (0,0,0) if (flags['j1'] and not flags['t3']) else (200,200,200)
msgs = [m for m in msgs if now_ticks < m[2]]
for ts, yo, et in msgs:
s = font_l.render(ts, True, text_color); s.set_alpha(max(0, min(255, (et - now_ticks) * 255 / 1000)))
screen.blit(s, s.get_rect(center=(WIDTH//2, HEIGHT//2 + yo)))
for i, (txt, et) in enumerate([m for m in net_messages if time.time() < m[1]]):
s = font_s.render(txt, True, (255,200,100)); s.set_alpha(max(0, min(255, (et - time.time()) * 100))); screen.blit(s, (10, 40+i*25))
stage_surf = font_s.render(f"Era: {world.tech_tree.current_stage}", True, (200, 100, 255))
screen.blit(stage_surf, (WIDTH - stage_surf.get_width() - 10, HEIGHT - 60))
# Genesis Field Display
field_txt = f"Ψ: {world.cached_psi:.2f} | Φ: {world.cached_phi:.2f} | Ω: {world.cached_omega:.2f}"
field_surf = font_s.render(field_txt, True, (100, 255, 150))
screen.blit(field_surf, (10, HEIGHT - 60))
zf = DEFAULT_CAM_DIST / cam.dist; zoom_text = f"{zf:.1f}x" if zf > 10 else f"{zf:.2f}x"
status_text = f"Mode: {game_mode.replace('_',' ').title()} | TETs: {len(world.tets)} | Unions: {len(world.joints)} | Desires: {len(world.sticky_pairs)} | Zoom: {zoom_text} | Time: {time_scale:.1f}x"
top_leg = font_s.render(status_text, True, (0,255,255))
screen.blit(top_leg, top_leg.get_rect(center=(WIDTH//2, 20)))
uptime_surf = font_s.render(f"v5.3D Up: {str(datetime.timedelta(seconds=int(time.time() - START_TIME)))} FPS: {int(fps)}", True, (100, 255, 150))
screen.blit(uptime_surf, (WIDTH - uptime_surf.get_width() - 10, 30))
bot_leg1 = font_s.render("RMB Label | LMB Pull/Join TETs | WASD/RMB Orbit | X/Alt+RMB Center | V Save Instant", True, (0,255,255))
bot_leg2 = font_s.render("H Host Mode | TAB Client Mode | R/F/Scroll Zoom | Q/E/Alt+Scroll Pan | Z/C/Ctrl+Scroll Timescale", True, (0,255,255))
screen.blit(bot_leg1, bot_leg1.get_rect(center=(WIDTH//2, HEIGHT-35)))
screen.blit(bot_leg2, bot_leg2.get_rect(center=(WIDTH//2, HEIGHT-15)))
# draw_molecular_labels(screen, cam, world, font_l)
if hovered_vertex:
t_hover = hovered_vertex[0]
# Get the label, fallback to molecule type, or "Unknown"
display_text = t_hover.label if t_hover.label else (t_hover.molecule_type if t_hover.molecule_type else "")
if display_text:
# Add Chem Info if available (e.g. "Water [H2O]")
if t_hover.molecule_type and t_hover.molecule_type not in display_text:
display_text = f"{display_text} [{t_hover.molecule_type}]"
# Render Text
text_surf = font_s.render(display_text, True, (0, 255, 255)) # Cyan text
# Calculate position (offset 20px from mouse)
mx, my = pygame.mouse.get_pos()
x_pos = min(mx + 20, WIDTH - text_surf.get_width() - 10) # Keep on screen
y_pos = min(my + 20, HEIGHT - text_surf.get_height() - 10)
# Draw Background Box (Semi-transparent black)
padding = 6
bg_rect = pygame.Rect(x_pos - padding, y_pos - padding,
text_surf.get_width() + padding*2,
text_surf.get_height() + padding*2)
s = pygame.Surface((bg_rect.width, bg_rect.height), pygame.SRCALPHA)
s.fill((0, 0, 0, 200)) # Dark background
pygame.draw.rect(s, (0, 255, 255), s.get_rect(), 1) # Cyan border
screen.blit(s, bg_rect.topleft)
screen.blit(text_surf, (x_pos, y_pos))
# Render
pygame.display.flip()
if ON_HUGGINGFACE and GRADIO_AVAILABLE:
try: GRADIO_FRAME_BUFFER = np.transpose(pygame.surfarray.array3d(screen), (1, 0, 2))
except: pass
stop_all_networking(); pygame.quit()
if __name__ == "__main__":
if ON_HUGGINGFACE:
t = threading.Thread(target=main, kwargs={'threaded': True}, daemon=True); t.start()
if GRADIO_AVAILABLE:
gradio_interface_loop()
while True: time.sleep(1)
else:
print("Gradio not installed. Headless loop.");
while True: time.sleep(1)
else: main()