Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import pygame
- import numpy as np
- import random
- from collections import defaultdict
- pygame.init()
- # Updated Constants
- WORLD_SCALE = 2
- W, H = 1290 * WORLD_SCALE, 600 * WORLD_SCALE
- SCREEN_W, SCREEN_H = 1920, 1080
- DT, FPS = 0.1, 60
- ATTACH_DIST, DIGESTER_RANGE = 15, 15
- LINE_LEN_RANGE = (10, 30)
- MIN_DOTS, MIN_LINES = int(80 * WORLD_SCALE), int(80 * WORLD_SCALE)
- INITIAL_DOTS = int(160 * WORLD_SCALE)
- INITIAL_LINES = int(160 * WORLD_SCALE)
- MIN_MOVEDOTS = int(10 * WORLD_SCALE)
- COLORS = {
- 'black': (0, 0, 0), 'move': (100, 100, 100), 'digester': (255, 0, 255),
- 'storage': (255, 255, 0), 'copy': (0, 255, 0), 'line': (255, 255, 255),
- 'oscillator': (0, 255, 255), 'egg': (200, 200, 200), 'food': (0, 255, 0),
- 'asshole': (139, 69, 19), 'shit': (92, 64, 51), 'mycelium': (200, 200, 200),
- 'filter': (0, 0, 255),
- 'soil': (194, 178, 128), 'parasite': (255, 0, 0),
- 'parasite_line': (255, 165, 0), 'parasite_growth': (255, 255, 0),
- 'seed': (0, 128, 0), # Dark green for seeds
- 'tree': (139, 69, 19), # Brown for tree lines
- 'sun': (255, 255, 0), # Bright yellow for sun
- 'cutter': (255, 255, 0), # Red for cutter lines
- 'fungus_dot': (150, 75, 0), # Brownish for FungusDot
- 'fungus_line': (200, 150, 100), # Light brown for FungusLine
- }
- BUFFER = 100
- GRID_CELL_SIZE = 25
- SPAWN_INTERVAL = 300
- MAX_SPAWN = int(5 * WORLD_SCALE)
- SUB_STEPS = 4
- ZOOM_MIN, ZOOM_MAX, ZOOM_SPEED = 0.1, 10.0, 0.1
- BASE_DOT_RADIUS = 5
- LIFE_INITIAL = 300
- SHIT_RATE = 26
- ASSHOLE_LIFE_EXTENSION = 8
- SHIT_DOT_LIFE_EXTENSION = -0.7
- SHIT_DOT_DECAY = 100
- ASSHOLE_ENERGY_COST = -3.2
- VIRUS_LIFE_DAMAGE = 2.8
- VIRUS_REPRODUCTION_RATE = 11
- FILTER_RANGE = 20
- MYCELIUM_SEARCH_ENERGY_COST = 6
- MYCELIUM_SEARCH_RADIUS_START = 100
- MYCELIUM_SEARCH_RADIUS_STEP = 100
- MYCELIUM_SEARCH_RADIUS_MAX = 100
- MYCELIUM_ENERGY_INITIAL = 100
- SHIT_INITIAL_ENERGY = 200
- FOOD_ENERGY_INITIAL = 200
- MYCELIUM_ENERGY_MIN = 1
- MYCELIUM_WAVE_AMPLITUDE = 5
- MYCELIUM_WAVE_FREQUENCY = 0.1
- NETWORK_BATCH_SIZE = 1
- MYCELIUM_SEARCH_INTERVAL = 0.2
- NETWORK_UPDATE_INTERVAL = 0.2
- SHIT_FOR_FILTER = 200
- FOOD_FOR_FILTER = 200
- INDIVIDUAL_DOT_BATCH_SIZE = 1
- MAX_TREE_LINES = 3 # Reduced from 15
- TREE_FOOD_ENERGY = 25 # Energy granted when digesting a FoodDot from a tree
- TREE_WAVE_FREQUENCY = 0.15
- TREE_WAVE_AMPLITUDE = 8
- SUN_RADIUS = 20
- SUN_INFLUENCE_RADIUS = 200
- SUN_SPEED = 100 # Pixels per second
- SUN_HEIGHT_STEP = H / 4 # Vertical step after each pass
- SEED_ENERGY_GAIN = 100 # Energy per second in sun's influence
- SEED_ENERGY_PER_TREE = 10 # Energy needed to sprout a tree line
- CUTTER_RANGE = 15 # Range for cutter line to cut tree lines
- FUNGUS_ENERGY_GAIN = 1 # Energy gained per second in sun's influence
- FUNGUS_ENERGY_PER_LINE = 3 # Energy needed to grow a FungusLine
- MAX_FUNGUS_LINES = 3 # Maximum FungusLines per FungusDot
- PROPS = {
- 'MoveDot': {'defaults': {'vel': lambda: np.array([random.uniform(-5, 5), random.uniform(-5, 5)]), 'color': COLORS['move']}},
- 'EnergyStorageDot': {'defaults': {'energy': 3, 'color': COLORS['storage']}, 'inheritable': {'energy': True}},
- 'DigesterDot': {'defaults': {'color': COLORS['digester']}},
- 'CopyDot': {'defaults': {'egg_timer': 0, 'color': COLORS['copy']}},
- 'FoodDot': {'defaults': {'color': COLORS['food'], 'energy': MYCELIUM_ENERGY_INITIAL}},
- 'AssholeDot': {'defaults': {'color': COLORS['asshole'], 'shit_timer': 0}},
- 'ShitDot': {'defaults': {'color': COLORS['shit'], 'energy': MYCELIUM_ENERGY_INITIAL}},
- 'Line': {'defaults': {'color': COLORS['line'], 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)}, 'inheritable': {'base_length': True}},
- 'OscillatorLine': {
- 'defaults': {
- 'color': COLORS['oscillator'], 'phase': lambda: random.uniform(0, 2 * np.pi),
- 'base_length': lambda: random.uniform(*LINE_LEN_RANGE), 'freq_factor': lambda: random.uniform(0.045, 1.5)
- },
- 'inheritable': {'base_length': True, 'phase': True, 'freq_factor': True}
- },
- 'VirusLine': {'defaults': {'color': (255, 0, 0), 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)}, 'inheritable': {'base_length': True}},
- 'MyceliumLine': {'defaults': {'color': COLORS['mycelium'], 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)}},
- 'FilterLine': {'defaults': {'color': COLORS['filter'], 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)}, 'inheritable': {'base_length': True}},
- 'ParasiteLine': {
- 'defaults': {
- 'color': COLORS['parasite_line'],
- 'base_length': lambda: random.uniform(20, 40),
- 'phase': lambda: random.uniform(0, 2 * np.pi),
- 'freq_factor': lambda: random.uniform(1.0, 2.0)
- },
- 'inheritable': {'base_length': True, 'phase': True, 'freq_factor': True}
- },
- 'ParasiteGrowthLine': {
- 'defaults': {'color': COLORS['parasite_growth'], 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)},
- 'inheritable': {'base_length': True},
- 'SeedDot': {'defaults': {'color': COLORS['seed'], 'energy': 0}},
- 'TreeLine': {'defaults': {'color': COLORS['tree'], 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)}, 'inheritable': {'base_length': True}},
- 'CutterLine': {'defaults': {'color': COLORS['cutter'], 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)}, 'inheritable': {'base_length': True}},
- 'FungusDot': {'defaults': {'color': COLORS['fungus_dot'], 'energy': 0}},
- 'FungusLine': {'defaults': {'color': COLORS['fungus_line'],'base_length': lambda: random.uniform(*LINE_LEN_RANGE)},'inheritable': {'base_length': True}}
- }
- }
- # Precompute sin/cos lookup table (unchanged)
- PHASE_RESOLUTION = 1000
- PHASE_TABLE = np.linspace(0, 2 * np.pi, PHASE_RESOLUTION, endpoint=False)
- SIN_TABLE = np.sin(PHASE_TABLE)
- COS_TABLE = np.cos(PHASE_TABLE)
- def get_trig(phase):
- idx = int((phase % (2 * np.pi)) / (2 * np.pi) * PHASE_RESOLUTION) % PHASE_RESOLUTION
- return SIN_TABLE[idx], COS_TABLE[idx]
- # Camera class (unchanged)
- class Camera:
- def __init__(self, width, height):
- self.pos = np.array([W / 2, H / 2], dtype=np.float32)
- self.zoom = 1.0
- self.width, self.height = width, height
- self.dragging = False
- self.drag_start = None
- def world_to_screen(self, pos):
- rel_pos = (pos - self.pos) * self.zoom
- screen_pos = rel_pos + np.array([self.width / 2, self.height / 2])
- return screen_pos
- def screen_to_world(self, pos):
- rel_pos = pos - np.array([self.width / 2, self.height / 2])
- world_pos = rel_pos / self.zoom + self.pos
- return world_pos
- def apply_zoom(self, delta, mouse_pos):
- old_zoom = self.zoom
- self.zoom = max(ZOOM_MIN, min(ZOOM_MAX, self.zoom * (1 + delta * ZOOM_SPEED)))
- if old_zoom != self.zoom:
- mouse_world_before = self.screen_to_world(mouse_pos)
- mouse_world_after = mouse_world_before
- self.pos = mouse_world_after - (mouse_pos - np.array([self.width / 2, self.height / 2])) / self.zoom
- def handle_event(self, event, grid, shapes=None):
- if event.type == pygame.MOUSEBUTTONDOWN:
- if event.button == 1:
- self.dragging = True
- self.drag_start = np.array(event.pos, dtype=np.float32)
- elif event.button == 4:
- self.apply_zoom(1, event.pos)
- if shapes:
- for shape in shapes.values():
- update_shape_geometry(shape, self)
- elif event.button == 5:
- self.apply_zoom(-1, event.pos)
- if shapes:
- for shape in shapes.values():
- update_shape_geometry(shape, self)
- elif event.type == pygame.MOUSEBUTTONUP:
- if event.button == 1:
- self.dragging = False
- elif event.type == pygame.MOUSEMOTION and self.dragging:
- current_pos = np.array(event.pos, dtype=np.float32)
- delta = (current_pos - self.drag_start) / self.zoom
- self.pos -= delta
- self.drag_start = current_pos
- if shapes:
- for shape in shapes.values():
- update_shape_geometry(shape, self)
- elif event.type == pygame.KEYDOWN:
- if event.key == pygame.K_PLUS or event.key == pygame.K_EQUALS:
- self.apply_zoom(1, np.array([self.width / 2, self.height / 2]))
- if shapes:
- for shape in shapes.values():
- update_shape_geometry(shape, self)
- elif event.key == pygame.K_MINUS:
- self.apply_zoom(-1, np.array([self.width / 2, self.height / 2]))
- if shapes:
- for shape in shapes.values():
- update_shape_geometry(shape, self)
- def get_visible_area(self):
- top_left = self.screen_to_world(np.array([0, 0]))
- bottom_right = self.screen_to_world(np.array([self.width, self.height]))
- return top_left[0], top_left[1], bottom_right[0], bottom_right[1]
- # Spatial Grid (unchanged)
- class SpatialGrid:
- def __init__(self, w, h, size):
- self.size, self.size_inv = size, 1 / size
- self.grid = defaultdict(set)
- self.cols, self.rows = int((w + 2 * BUFFER) / size) + 1, int((h + 2 * BUFFER) / size) + 1
- def add(self, obj, pos):
- c, r = int((pos[0] + BUFFER) * self.size_inv), int((pos[1] + BUFFER) * self.size_inv)
- if 0 <= c < self.cols and 0 <= r < self.rows:
- self.grid[(c, r)].add(obj)
- def remove(self, obj, pos):
- c, r = int((pos[0] + BUFFER) * self.size_inv), int((pos[1] + BUFFER) * self.size_inv)
- self.grid[(c, r)].discard(obj)
- def get_nearby(self, pos, range_):
- c, r = int((pos[0] + BUFFER) * self.size_inv), int((pos[1] + BUFFER) * self.size_inv)
- cells = int(range_ / self.size) + 1
- return {obj for dc in range(-cells, cells + 1) for dr in range(-cells, cells + 1)
- for obj in self.grid.get((c + dc, r + dr), set()) if 0 <= c + dc < self.cols and 0 <= r + dr < self.rows}
- def get_in_area(self, min_x, min_y, max_x, max_y):
- min_c = int((min_x + BUFFER) * self.size_inv)
- min_r = int((min_y + BUFFER) * self.size_inv)
- max_c = int((max_x + BUFFER) * self.size_inv) + 1
- max_r = int((max_y + BUFFER) * self.size_inv) + 1
- objects = set()
- for c in range(min_c, max_c):
- for r in range(min_r, max_r):
- if 0 <= c < self.cols and 0 <= r < self.rows:
- objects.update(self.grid.get((c, r), set()))
- return objects
- # Base Classes (unchanged)
- class Dot:
- def __init__(self, x, y):
- self.pos = np.array([x, y], dtype=np.float32)
- self.lines, self.root, self.rank = set(), self, 0
- self.energy = 0
- # Removed: self.attached_to_tree = None
- # Removed: self.is_anchored = False
- class Line:
- def __init__(self, p1, p2):
- self.p1, self.p2 = np.array(p1, dtype=np.float32), np.array(p2, dtype=np.float32)
- self.dot1 = self.dot2 = None
- self.color, self.base_length = COLORS['line'], random.uniform(*LINE_LEN_RANGE)
- self.mid = (self.p1 + self.p2) * 0.5
- def ends(self):
- p1 = self.dot1.pos if self.dot1 else self.p1
- p2 = self.dot2.pos if self.dot2 else self.p2
- return (p1.tolist(), p2.tolist())
- def update_mid(self):
- e1, e2 = self.ends()
- self.mid = (np.array(e1) + np.array(e2)) * 0.5
- # Mycelium Network Classes (unchanged)
- class MyceliumNetwork:
- def __init__(self, initial_dot):
- self.dots = [initial_dot]
- self.lines = []
- self.searching_dot = initial_dot
- self.search_radius = MYCELIUM_SEARCH_RADIUS_START
- self.search_timer = 0
- initial_dot.network = self
- initial_dot.is_searching = False
- def add_dot(self, dot, line):
- self.dots.append(dot)
- self.lines.append(line)
- dot.network = self
- dot.is_searching = False
- def merge(self, other_network, line):
- self.dots.extend(other_network.dots)
- self.lines.extend(other_network.lines)
- self.lines.append(line)
- for dot in other_network.dots:
- dot.network = self
- dot.is_searching = False
- self.searching_dot = random.choice(self.dots)
- self.search_radius = MYCELIUM_SEARCH_RADIUS_START
- self.search_timer = 0
- def remove_dot(self, dot, grid, dots, mycelium_networks):
- if dot not in self.dots:
- return
- self.dots.remove(dot)
- lines_to_remove = [line for line in self.lines if line.dot1 == dot or line.dot2 == dot]
- for line in lines_to_remove:
- self.lines.remove(line)
- if line in mycelium_networks:
- mycelium_networks.remove(line)
- grid.remove(line, line.mid)
- dot.network = None
- if not self.dots:
- for line in self.lines[:]:
- if line in mycelium_networks:
- mycelium_networks.remove(line)
- grid.remove(line, line.mid)
- self.lines.clear()
- elif self.searching_dot == dot and self.dots:
- self.searching_dot = random.choice(self.dots)
- self.search_radius = MYCELIUM_SEARCH_RADIUS_START
- self.search_timer = 0
- def update(self, dt, grid, dots, mycelium_networks):
- for dot in self.dots[:]:
- if dot.energy < MYCELIUM_ENERGY_MIN:
- self.remove_dot(dot, grid, dots, mycelium_networks)
- if dot in dots:
- dots.remove(dot)
- grid.remove(dot, dot.pos)
- if not self.dots:
- for line in self.lines[:]:
- if line in mycelium_networks:
- mycelium_networks.remove(line)
- grid.remove(line, line.mid)
- self.lines.clear()
- return False
- if dt < NETWORK_UPDATE_INTERVAL:
- return True
- energy_cost = MYCELIUM_SEARCH_ENERGY_COST * NETWORK_UPDATE_INTERVAL
- per_dot_cost = energy_cost / len(self.dots) if self.dots else 0
- for dot in self.dots:
- dot.energy = max(0, dot.energy - per_dot_cost)
- total_energy = sum(d.energy for d in self.dots)
- if total_energy < MYCELIUM_ENERGY_MIN:
- for dot in self.dots[:]:
- if dot in dots:
- dots.remove(dot)
- grid.remove(dot, dot.pos)
- for line in self.lines[:]:
- if line in mycelium_networks:
- mycelium_networks.remove(line)
- grid.remove(line, line.mid)
- self.dots.clear()
- self.lines.clear()
- return False
- self.search_timer += NETWORK_UPDATE_INTERVAL
- if self.search_timer >= MYCELIUM_SEARCH_INTERVAL:
- self.search_timer = 0
- self.search_radius += MYCELIUM_SEARCH_RADIUS_STEP
- if self.search_radius > MYCELIUM_SEARCH_RADIUS_MAX:
- self.search_radius = MYCELIUM_SEARCH_RADIUS_START
- self.searching_dot = random.choice(self.dots) if self.dots else None
- if self.searching_dot and self.dots:
- nearby = grid.get_nearby(self.searching_dot.pos, self.search_radius)
- for obj in nearby:
- if isinstance(obj, (FoodDot, ShitDot)) and obj != self.searching_dot and obj not in self.dots:
- distance = np.linalg.norm(self.searching_dot.pos - obj.pos)
- if distance <= self.search_radius:
- line = MyceliumLine(self.searching_dot.pos, obj.pos)
- line.dot1, line.dot2 = self.searching_dot, obj
- line.cap_length() # Enforce length cap
- if obj.network and obj.network != self:
- self.merge(obj.network, line)
- elif not obj.network:
- self.add_dot(obj, line)
- mycelium_networks.append(line)
- grid.add(line, line.mid)
- break
- return True
- def try_spawn_filter_line(self, grid, lines, mycelium_networks, dots):
- shit_energy = sum(d.energy for d in self.dots if isinstance(d, ShitDot))
- food_energy = sum(d.energy for d in self.dots if isinstance(d, FoodDot))
- if shit_energy < SHIT_FOR_FILTER or food_energy < FOOD_FOR_FILTER:
- return False
- candidates = [d for d in self.dots if isinstance(d, (FoodDot, ShitDot))]
- if not candidates:
- return False
- base_dot = random.choice(candidates)
- shit_dots = [d for d in self.dots if isinstance(d, ShitDot) and d != base_dot]
- food_dots = [d for d in self.dots if isinstance(d, FoodDot) and d != base_dot]
- shit_energy_excl = sum(d.energy for d in shit_dots)
- food_energy_excl = sum(d.energy for d in food_dots)
- base_energy_cost = 0
- if isinstance(base_dot, ShitDot):
- remaining_shit_needed = SHIT_FOR_FILTER - min(shit_energy_excl, SHIT_FOR_FILTER)
- base_energy_cost = remaining_shit_needed
- else:
- remaining_food_needed = FOOD_FOR_FILTER - min(food_energy_excl, FOOD_FOR_FILTER)
- base_energy_cost = remaining_food_needed
- if base_dot.energy < base_energy_cost + MYCELIUM_ENERGY_MIN:
- return False
- dots_to_remove = []
- if shit_dots:
- shit_per_dot = min(SHIT_FOR_FILTER, shit_energy_excl) / len(shit_dots)
- for dot in shit_dots:
- dot.energy -= shit_per_dot
- if dot.energy < MYCELIUM_ENERGY_MIN:
- dots_to_remove.append(dot)
- if food_dots:
- food_per_dot = min(FOOD_FOR_FILTER, food_energy_excl) / len(food_dots)
- for dot in food_dots:
- dot.energy -= food_per_dot
- if dot.energy < MYCELIUM_ENERGY_MIN:
- dots_to_remove.append(dot)
- base_dot.energy -= base_energy_cost
- for dot in dots_to_remove:
- self.remove_dot(dot, grid, dots, mycelium_networks)
- if dot in dots:
- dots.remove(dot)
- grid.remove(dot, dot.pos)
- if base_dot.energy < MYCELIUM_ENERGY_MIN or base_dot not in dots:
- return False
- p1 = base_dot.pos.copy()
- angle = random.uniform(0, 2 * np.pi)
- length = random.uniform(*LINE_LEN_RANGE)
- p2 = p1 + length * np.array([np.cos(angle), np.sin(angle)])
- # 1/3 chance for FilterLine, CutterLine, or FungusDot
- r = random.random()
- if r < 0.333:
- new_line = FilterLine(p1, p2)
- new_line.update_mid()
- lines.append(new_line)
- grid.add(new_line, new_line.mid)
- elif r < 0.666:
- new_line = CutterLine(p1, p2)
- new_line.update_mid()
- lines.append(new_line)
- grid.add(new_line, new_line.mid)
- else:
- # Spawn FungusDot instead of a line
- fungus_dot = FungusDot(*p1)
- dots.append(fungus_dot)
- grid.add(fungus_dot, fungus_dot.pos)
- return True
- class MyceliumLine(Line):
- def __init__(self, p1, p2):
- super().__init__(p1, p2)
- self.color = COLORS['mycelium']
- # Cap length at MYCELIUM_SEARCH_RADIUS_MAX
- self.cap_length()
- def update_mid(self):
- if self.dot1 and self.dot2:
- self.p1, self.p2 = self.dot1.pos, self.dot2.pos
- # Cap length if dots move
- self.cap_length()
- self.mid = (self.p1 + self.p2) / 2
- def cap_length(self):
- # Ensure line length is at most MYCELIUM_SEARCH_RADIUS_MAX
- vec = self.p2 - self.p1
- length = np.linalg.norm(vec)
- if length > MYCELIUM_SEARCH_RADIUS_MAX:
- # Scale p2 to keep length at MYCELIUM_SEARCH_RADIUS_MAX
- direction = vec / length
- self.p2 = self.p1 + direction * MYCELIUM_SEARCH_RADIUS_MAX
- def get_wavy_points(self):
- p1, p2 = self.ends()
- p1, p2 = np.array(p1), np.array(p2)
- direction = p2 - p1
- length = np.linalg.norm(direction)
- if length < 1e-6:
- return [p1, p2]
- direction /= length
- perp = np.array([-direction[1], direction[0]])
- num_segments = int(length / 10) + 2
- points = []
- for i in range(num_segments):
- t = i / (num_segments - 1)
- base_pos = p1 + t * (p2 - p1)
- offset = MYCELIUM_WAVE_AMPLITUDE * np.sin(t * length * MYCELIUM_WAVE_FREQUENCY) * perp
- points.append(base_pos + offset)
- return points
- class SeedDot(Dot):
- def __init__(self, x, y):
- super().__init__(x, y)
- self.color = COLORS['seed']
- self.energy = 0
- self.lifetime = 1500 # Very long lifetime
- self.max_tree_lines = MAX_TREE_LINES # Use new constant
- def update(self, dt, sun, grid, lines, dots):
- self.lifetime -= dt
- if self.lifetime <= 0:
- # Remove all attached TreeLines and spawn FoodDots
- tree_lines = [line for line in self.lines if isinstance(line, TreeLine)]
- for tree_line in tree_lines:
- if tree_line in lines:
- lines.remove(tree_line)
- grid.remove(tree_line, tree_line.mid)
- if tree_line.dot1:
- tree_line.dot1.lines.discard(tree_line)
- if tree_line.dot2:
- tree_line.dot2.lines.discard(tree_line)
- # Spawn FoodDot with higher energy at tree line's midpoint
- food_dot = FoodDot(*tree_line.mid)
- food_dot.energy = TREE_FOOD_ENERGY # Set higher energy
- food_dot.is_tree_food = True # Flag to prevent mycelium connection
- dots.append(food_dot)
- grid.add(food_dot, food_dot.pos)
- # Remove the SeedDot itself
- if self in dots:
- dots.remove(self)
- grid.remove(self, self.pos)
- return
- # Gain energy if in sun's influence
- if sun and np.linalg.norm(self.pos - sun.pos) <= SUN_INFLUENCE_RADIUS:
- self.energy += SEED_ENERGY_GAIN * dt
- # Count current tree lines
- tree_line_count = sum(1 for line in self.lines if isinstance(line, TreeLine))
- # Sprout tree line only if under the cap and enough energy
- while self.energy >= SEED_ENERGY_PER_TREE and tree_line_count < self.max_tree_lines:
- self.energy -= SEED_ENERGY_PER_TREE
- angle = random.uniform(0, 2 * np.pi)
- length = random.uniform(*LINE_LEN_RANGE)
- p2 = self.pos + length * np.array([np.cos(angle), np.sin(angle)])
- tree_line = TreeLine(self.pos, p2)
- tree_line.dot1 = self
- self.lines.add(tree_line)
- tree_line.update_mid()
- lines.append(tree_line)
- grid.add(tree_line, tree_line.mid)
- tree_line_count += 1
- class Sun:
- def __init__(self):
- self.pos = np.array([-SUN_INFLUENCE_RADIUS, H], dtype=np.float32) # Start off-screen left
- self.height = H # Current height
- self.direction = 1 # 1 for right, -1 for left (though we only go right)
- def update(self, dt):
- # Move right
- self.pos[0] += SUN_SPEED * dt * self.direction
- # If off-screen right, reset to left and lower height
- if self.pos[0] > W + SUN_INFLUENCE_RADIUS:
- self.pos[0] = -SUN_INFLUENCE_RADIUS
- self.height -= SUN_HEIGHT_STEP
- # If too low, reset to top
- if self.height < 0:
- self.height = H
- self.pos[1] = self.height
- # Derived Classes
- class FungusLine(Line):
- def __init__(self, p1, p2):
- super().__init__(p1, p2)
- self.color = COLORS['fungus_line']
- self.base_length = random.uniform(*LINE_LEN_RANGE)
- self.immobile = False # Explicitly allow movement
- class FungusDot(Dot):
- def __init__(self, x, y):
- super().__init__(x, y)
- self.color = COLORS['fungus_dot']
- self.energy = 0
- self.lifetime = 1500 # Long lifetime, similar to SeedDot
- self.max_fungus_lines = MAX_FUNGUS_LINES
- def update(self, dt, sun, grid, lines, dots):
- self.lifetime -= dt
- if self.lifetime <= 0:
- # Remove all attached FungusLines and spawn FoodDots
- fungus_lines = [line for line in self.lines if isinstance(line, FungusLine)]
- for fungus_line in fungus_lines:
- if fungus_line in lines:
- lines.remove(fungus_line)
- grid.remove(fungus_line, fungus_line.mid)
- if fungus_line.dot1:
- fungus_line.dot1.lines.discard(fungus_line)
- if fungus_line.dot2:
- fungus_line.dot2.lines.discard(fungus_line)
- # Spawn FoodDot with regular energy
- food_dot = FoodDot(*fungus_line.mid)
- food_dot.energy = 5 # Regular 5-energy FoodDot
- food_dot.is_tree_food = False # Can connect to mycelium
- dots.append(food_dot)
- grid.add(food_dot, food_dot.pos)
- # Remove the FungusDot itself
- if self in dots:
- dots.remove(self)
- grid.remove(self, self.pos)
- return
- # Gain energy if in sun's influence
- if sun and np.linalg.norm(self.pos - sun.pos) <= SUN_INFLUENCE_RADIUS:
- self.energy += FUNGUS_ENERGY_GAIN * dt
- # Count current fungus lines
- fungus_line_count = sum(1 for line in self.lines if isinstance(line, FungusLine))
- # Grow fungus line if under the cap and enough energy
- while self.energy >= FUNGUS_ENERGY_PER_LINE and fungus_line_count < self.max_fungus_lines:
- self.energy -= FUNGUS_ENERGY_PER_LINE
- angle = random.uniform(0, 2 * np.pi)
- length = random.uniform(*LINE_LEN_RANGE)
- p2 = self.pos + length * np.array([np.cos(angle), np.sin(angle)])
- fungus_line = FungusLine(self.pos, p2)
- fungus_line.dot1 = self
- self.lines.add(fungus_line)
- fungus_line.update_mid()
- lines.append(fungus_line)
- grid.add(fungus_line, fungus_line.mid)
- fungus_line_count += 1
- class TreeLine(Line):
- def __init__(self, p1, p2):
- super().__init__(p1, p2)
- self.color = COLORS['tree']
- self.base_length = random.uniform(*LINE_LEN_RANGE)
- self.immobile = True
- def get_branch_points(self):
- p1, p2 = self.ends()
- p1, p2 = np.array(p1), np.array(p2)
- direction = p2 - p1
- length = np.linalg.norm(direction)
- if length < 1e-6:
- return [p1, p2]
- direction /= length
- perp = np.array([-direction[1], direction[0]])
- num_segments = max(4, int(length / 10)) # More segments for smoother waviness
- points = []
- t_values = np.linspace(0, 1, num_segments)
- for i, t in enumerate(t_values):
- base_pos = p1 + t * (p2 - p1)
- # Add waviness similar to MyceliumLineE
- wave_offset = TREE_WAVE_AMPLITUDE * np.sin(t * length * TREE_WAVE_FREQUENCY) * perp
- # Add fractal-like branching deviation
- if i % 2 == 0 and i < num_segments - 1: # Add deviation every other segment
- deviation = random.uniform(-np.pi / 12, np.pi / 12) # ±15 degrees for subtle branching
- deviation_vector = np.array([np.cos(deviation), np.sin(deviation)]) * length * 0.1
- base_pos += deviation_vector
- points.append(base_pos + wave_offset)
- # Ensure the last point is exactly p2 for accurate attachment
- points[-1] = p2
- return points
- class CutterLine(Line):
- def __init__(self, p1, p2):
- super().__init__(p1, p2)
- self.color = COLORS['cutter']
- self.base_length = random.uniform(*LINE_LEN_RANGE)
- def update(self, grid, lines, dots, shapes, data):
- nearby = grid.get_nearby(self.mid, CUTTER_RANGE)
- cuttable_lines = [l for l in nearby if isinstance(l, (TreeLine, FungusLine)) and l in lines]
- for cut_line in cuttable_lines:
- if np.linalg.norm(self.mid - cut_line.mid) < CUTTER_RANGE:
- # Remove the cut line
- lines.remove(cut_line)
- grid.remove(cut_line, cut_line.mid)
- if cut_line.dot1:
- cut_line.dot1.lines.discard(cut_line)
- if cut_line.dot2:
- cut_line.dot2.lines.discard(cut_line)
- # Spawn FoodDot
- food_dot = FoodDot(*cut_line.mid)
- food_dot.energy = TREE_FOOD_ENERGY if isinstance(cut_line, TreeLine) else 5
- food_dot.is_tree_food = isinstance(cut_line, TreeLine)
- dots.append(food_dot)
- grid.add(food_dot, food_dot.pos)
- # Update shape connectivity
- root = find(cut_line.dot1 or cut_line.dot2) if cut_line.dot1 or cut_line.dot2 else None
- if root in shapes:
- shape = shapes[root]
- if isinstance(cut_line, TreeLine) and any(isinstance(d, SeedDot) for d in shape['dots']):
- # Handle TreeLine cut: Recompute connectivity from SeedDot
- seed = next(d for d in shape['dots'] if isinstance(d, SeedDot))
- reachable = set()
- stack = [seed]
- while stack:
- dot = stack.pop()
- reachable.add(dot)
- for line in dot.lines:
- if isinstance(line, TreeLine) and line in lines:
- other_dot = line.dot2 if line.dot1 == dot else line.dot1
- if other_dot and other_dot not in reachable:
- stack.append(other_dot)
- # Remove disconnected TreeLines
- lines_to_remove = []
- for line in shape['lines'][:]:
- if isinstance(line, TreeLine):
- if line.dot1 and line.dot2:
- if line.dot1 not in reachable or line.dot2 not in reachable:
- lines_to_remove.append(line)
- for line in lines_to_remove:
- if line in lines:
- lines.remove(line)
- grid.remove(line, line.mid)
- if line.dot1:
- line.dot1.lines.discard(line)
- if line.dot2:
- line.dot2.lines.discard(line)
- food_dot = FoodDot(*line.mid)
- food_dot.energy = TREE_FOOD_ENERGY
- food_dot.is_tree_food = True
- dots.append(food_dot)
- grid.add(food_dot, food_dot.pos)
- recompute_connectivity(shape, shapes, data) # Pass shape, not self
- else:
- # Handle FungusLine or non-tree shapes: Split into components
- recompute_connectivity(shape, shapes, data) # Pass shape, not self
- class MoveDot(Dot):
- def __init__(self, x, y):
- super().__init__(x, y)
- self.color, self.vel = COLORS['move'], np.array([random.uniform(-5, 5), random.uniform(-5, 5)], dtype=np.float32)
- def update(self, dt):
- if not self.lines:
- self.pos += self.vel * dt
- self.pos[0] %= W + 2 * BUFFER
- self.pos[1] %= H + 2 * BUFFER
- class EnergyStorageDot(Dot):
- def __init__(self, x, y):
- super().__init__(x, y)
- self.color, self.energy = COLORS['storage'], 2
- class DigesterDot(Dot):
- def update(self, shape, grid, dots, lines, mycelium_networks):
- for obj in grid.get_nearby(self.pos, DIGESTER_RANGE):
- if isinstance(obj, Dot) and not obj.lines and obj != self and obj in dots:
- if isinstance(obj, SoilDot):
- continue
- if np.sum((self.pos - obj.pos) ** 2) < DIGESTER_RANGE ** 2:
- if isinstance(obj, (FoodDot, ShitDot)) and obj.network:
- obj.network.remove_dot(obj, grid, dots, mycelium_networks)
- dots.remove(obj)
- grid.remove(obj, obj.pos)
- if isinstance(obj, ShitDot):
- shape['life'] += SHIT_DOT_LIFE_EXTENSION
- else:
- # Grant higher energy for tree FoodDots
- energy = TREE_FOOD_ENERGY if getattr(obj, 'is_tree_food', False) else 5
- if shape['storage']:
- for s in shape['storage']:
- s.energy += energy / len(shape['storage'])
- elif isinstance(obj, Line) and not (obj.dot1 or obj.dot2) and obj in lines and not isinstance(obj, (VirusLine, TreeLine)):
- if np.sum((self.pos - obj.mid) ** 2) < DIGESTER_RANGE ** 2:
- lines.remove(obj)
- grid.remove(obj, obj.mid)
- if shape['storage']:
- for s in shape['storage']:
- s.energy += 10 / len(shape['storage'])
- class CopyDot(Dot):
- def __init__(self, x, y):
- super().__init__(x, y)
- self.color, self.egg_timer = COLORS['copy'], 0
- def update(self, shape, eggs):
- if self.egg_timer > 0:
- self.egg_timer = max(0, self.egg_timer - DT / SUB_STEPS)
- return
- if shape['lines']:
- has_parasite = any(isinstance(d, ParasiteDot) for d in shape['dots'])
- if not has_parasite:
- n, e = shape['n'], sum(d.energy for d in shape['storage'])
- if e >= n * 1.2 + 35:
- for s in shape['storage']:
- s.energy -= min(s.energy, n * 1.2 + 29 / len(shape['storage']))
- eggs.append(Egg(self.pos, snapshot_shape(shape)))
- self.egg_timer = 10
- shape['laid'] = True
- class FoodDot(Dot):
- def __init__(self, x, y):
- super().__init__(x, y)
- self.color = COLORS['food']
- self.energy = MYCELIUM_ENERGY_INITIAL # Default for non-tree FoodDots
- self.is_searching = True
- self.search_radius = MYCELIUM_SEARCH_RADIUS_START
- self.search_timer = 0
- self.network = None
- self.is_tree_food = False # Flag to identify tree-spawned FoodDots
- def update(self, dt, grid, dots, mycelium_networks):
- if self.is_tree_food:
- # Tree FoodDots don't connect to mycelium and don't decay
- return
- if self.network:
- return
- if self.energy < MYCELIUM_ENERGY_MIN:
- if self.network:
- self.network.remove_dot(self, grid, dots, mycelium_networks)
- if self in dots:
- dots.remove(self)
- grid.remove(self, self.pos)
- return
- self.energy -= MYCELIUM_SEARCH_ENERGY_COST * dt
- self.search_timer += dt
- if self.search_timer >= MYCELIUM_SEARCH_INTERVAL:
- self.search_timer = 0
- self.search_radius += MYCELIUM_SEARCH_RADIUS_STEP
- if self.search_radius > MYCELIUM_SEARCH_RADIUS_MAX:
- self.search_radius = MYCELIUM_SEARCH_RADIUS_START
- nearby = grid.get_nearby(self.pos, self.search_radius)
- for obj in nearby:
- if isinstance(obj, (FoodDot, ShitDot)) and obj != self and not obj.network and not getattr(obj, 'is_tree_food', False):
- distance = np.linalg.norm(self.pos - obj.pos)
- if distance <= self.search_radius:
- line = MyceliumLine(self.pos, obj.pos)
- line.dot1, line.dot2 = self, obj
- network = MyceliumNetwork(self)
- network.add_dot(obj, line)
- mycelium_networks.append(line)
- grid.add(line, line.mid)
- break
- class AssholeDot(Dot):
- def __init__(self, x, y):
- super().__init__(x, y)
- self.color = COLORS['asshole']
- self.shit_timer = 0
- def update(self, shape, grid, dots):
- self.shit_timer += DT / SUB_STEPS
- if self.shit_timer >= SHIT_RATE:
- self.shit_timer = 0
- if shape['storage']:
- total_energy = sum(d.energy for d in shape['storage'])
- energy_cost = ASSHOLE_ENERGY_COST
- if total_energy >= energy_cost:
- per_storage_cost = energy_cost / len(shape['storage'])
- for s in shape['storage']:
- s.energy = max(0, s.energy - per_storage_cost)
- shape['life'] += ASSHOLE_LIFE_EXTENSION
- offset = np.array([random.uniform(-20, 20), random.uniform(-20, 20)])
- shit_dot = ShitDot(self.pos[0] + offset[0], self.pos[1] + offset[1])
- dots.append(shit_dot)
- grid.add(shit_dot, shit_dot.pos)
- class ShitDot(Dot):
- def __init__(self, x, y):
- super().__init__(x, y)
- self.color = COLORS['shit']
- self.lifetime = SHIT_DOT_DECAY
- self.energy = SHIT_INITIAL_ENERGY
- self.is_searching = True
- self.search_radius = MYCELIUM_SEARCH_RADIUS_START
- self.search_timer = 0
- self.network = None
- def update(self, dt, grid, dots, mycelium_networks):
- if self.network:
- return
- self.lifetime -= dt
- if self.lifetime <= 0 or self.energy < MYCELIUM_ENERGY_MIN:
- if self.network:
- self.network.remove_dot(self, grid, dots, mycelium_networks)
- if self in dots:
- dots.remove(self)
- grid.remove(self, self.pos)
- return
- self.energy -= MYCELIUM_SEARCH_ENERGY_COST * dt
- self.search_timer += dt
- if self.search_timer >= MYCELIUM_SEARCH_INTERVAL:
- self.search_timer = 0
- self.search_radius += MYCELIUM_SEARCH_RADIUS_STEP
- if self.search_radius > MYCELIUM_SEARCH_RADIUS_MAX:
- self.search_radius = MYCELIUM_SEARCH_RADIUS_START
- nearby = grid.get_nearby(self.pos, self.search_radius)
- for obj in nearby:
- if isinstance(obj, (FoodDot, ShitDot)) and obj != self and not obj.network:
- # Ensure distance is within search_radius
- distance = np.linalg.norm(self.pos - obj.pos)
- if distance <= self.search_radius:
- line = MyceliumLine(self.pos, obj.pos)
- line.dot1, line.dot2 = self, obj
- network = MyceliumNetwork(self)
- network.add_dot(obj, line)
- mycelium_networks.append(line)
- grid.add(line, line.mid)
- break
- class OscillatorLine(Line):
- def __init__(self, p1, p2):
- super().__init__(p1, p2)
- self.color, self.phase = COLORS['oscillator'], random.uniform(0, 2 * np.pi)
- self.freq_factor = random.uniform(0.045, 1.5)
- @staticmethod
- def update_batch(oscillators, dt, shape):
- if not oscillators or not shape['storage']:
- return False
- total_energy = sum(d.energy for d in shape['storage'])
- energy_cost = 0.001 * dt * (1 + shape['n'] / 10) * len(oscillators)
- if total_energy < energy_cost:
- return False
- per_storage_cost = energy_cost / len(shape['storage'])
- for s in shape['storage']:
- s.energy = max(0, s.energy - per_storage_cost)
- dot1_pos = np.array([l.dot1.pos for l in oscillators])
- dot2_pos = np.array([l.dot2.pos for l in oscillators])
- base_lengths = np.array([l.base_length for l in oscillators])
- phases = np.array([l.phase for l in oscillators])
- freq_factors = np.array([l.freq_factor for l in oscillators])
- dx = dot2_pos - dot1_pos
- cl = np.maximum(np.sqrt(np.sum(dx ** 2, axis=1)), 1e-6)
- mp = (dot1_pos + dot2_pos) * 0.5
- dir = dx / cl[:, None]
- sin_vals, cos_vals = np.zeros_like(phases), np.zeros_like(phases)
- for i, phase in enumerate(phases):
- sin_vals[i], cos_vals[i] = get_trig(phase)
- scale = 1 + 0.35 * sin_vals
- tl = base_lengths * scale / 2
- target_pos1 = mp - dir * tl[:, None]
- target_pos2 = mp + dir * tl[:, None]
- dot1_pos += 0.5 * (target_pos1 - dot1_pos)
- dot2_pos += 0.5 * (target_pos2 - dot2_pos)
- for i, l in enumerate(oscillators):
- l.dot1.pos = dot1_pos[i]
- l.dot2.pos = dot2_pos[i]
- l.update_mid()
- l.phase = phases[i] + 2 * l.freq_factor * dt
- lf = np.where(base_lengths < LINE_LEN_RANGE[0] * 1.5, 1.2,
- np.where(base_lengths > LINE_LEN_RANGE[1] * 0.75, 0.833, 1))
- sf = max(1, shape['n'] / 10) ** -1.3
- lcr = 31 * base_lengths * lf * sf * cos_vals
- vel_contrib = 0.2 * np.abs(lcr) * dt * (np.maximum(freq_factors, 0.045) ** 1.5)
- shape['vel'] += np.sum(vel_contrib[:, None] * dir, axis=0)
- return True
- class VirusLine(Line):
- def __init__(self, p1, p2):
- super().__init__(p1, p2)
- self.color = (255, 0, 0)
- self.base_length = random.uniform(*LINE_LEN_RANGE)
- self.timer = 0
- def update(self, shape, lines, grid):
- shape['life'] -= VIRUS_LIFE_DAMAGE * (DT / SUB_STEPS)
- self.timer += DT / SUB_STEPS
- if self.timer >= VIRUS_REPRODUCTION_RATE:
- self.timer = 0
- if len(shape['dots']) > 1:
- target_dot = random.choice(shape['dots'])
- angle = random.uniform(0, 2 * np.pi)
- length = random.uniform(*LINE_LEN_RANGE)
- p2 = target_dot.pos + length * np.array([np.cos(angle), np.sin(angle)])
- new_virus = VirusLine(target_dot.pos, p2)
- new_virus.dot1 = target_dot
- target_dot.lines.add(new_virus)
- new_virus.update_mid()
- lines.append(new_virus)
- grid.add(new_virus, new_virus.mid)
- shape['lines'].append(new_virus)
- adjust_line(new_virus)
- class FilterLine(Line):
- def __init__(self, p1, p2):
- super().__init__(p1, p2)
- self.color = COLORS['filter']
- self.base_length = np.linalg.norm(self.p2 - self.p1)
- def update(self, grid, dots, lines, mycelium_networks):
- nearby = grid.get_nearby(self.mid, FILTER_RANGE)
- for obj in nearby:
- if isinstance(obj, ShitDot) and not obj.lines:
- if np.sum((self.mid - obj.pos) ** 2) < FILTER_RANGE ** 2:
- if obj.network:
- obj.network.remove_dot(obj, grid, dots, mycelium_networks)
- if obj in dots:
- dots.remove(obj)
- grid.remove(obj, obj.pos)
- if random.random() < 0.2:
- soil_dot = SoilDot(*obj.pos)
- dots.append(soil_dot)
- grid.add(soil_dot, soil_dot.pos)
- if random.random() < 0.025:
- parasite_dot = ParasiteDot(*obj.pos)
- dots.append(parasite_dot)
- grid.add(parasite_dot, parasite_dot.pos)
- angle = random.uniform(0, 2 * np.pi)
- length = random.uniform(20, 40)
- p1 = parasite_dot.pos + length * np.array([np.cos(angle), np.sin(angle)])
- parasite_line = ParasiteLine(p1, parasite_dot.pos)
- parasite_line.dot2 = parasite_dot
- parasite_dot.lines.add(parasite_line)
- parasite_dot.parasite_line = parasite_line
- parasite_line.update_mid()
- lines.append(parasite_line)
- grid.add(parasite_line, parasite_line.mid)
- class SoilDot(Dot):
- def __init__(self, x, y):
- super().__init__(x, y)
- self.color = COLORS['soil']
- self.lifetime = 200
- def update(self, dt, grid, dots):
- self.lifetime -= dt
- if self.lifetime <= 0:
- if self in dots:
- dots.remove(self)
- grid.remove(self, self.pos)
- class ParasiteDot(Dot):
- def __init__(self, x, y):
- super().__init__(x, y)
- self.color = COLORS['parasite']
- self.shit_timer = 0
- self.growth_line = None
- self.parasite_line = None
- def update(self, shape, grid, dots, lines):
- self.shit_timer += DT / SUB_STEPS
- if self.shit_timer >= SHIT_RATE:
- self.shit_timer = 0
- if shape['storage']:
- total_energy = sum(d.energy for d in shape['storage'])
- energy_cost = ASSHOLE_ENERGY_COST
- if total_energy >= energy_cost:
- per_storage_cost = energy_cost / len(shape['storage'])
- for s in shape['storage']:
- s.energy = max(0, s.energy - per_storage_cost)
- shape['life'] += ASSHOLE_LIFE_EXTENSION
- offset = np.array([random.uniform(-20, 20), random.uniform(-20, 20)])
- shit_dot = ShitDot(self.pos[0] + offset[0], self.pos[1] + offset[1])
- dots.append(shit_dot)
- grid.add(shit_dot, shit_dot.pos)
- if shape['storage'] and self.growth_line is None:
- n = shape['n']
- e = sum(d.energy for d in shape['storage'])
- if e >= n * 1.2 + 35:
- angle = random.uniform(0, 2 * np.pi)
- length = random.uniform(*LINE_LEN_RANGE)
- p2 = self.pos + length * np.array([np.cos(angle), np.sin(angle)])
- growth_line = ParasiteGrowthLine(self.pos, p2)
- growth_line.dot1 = self
- self.lines.add(growth_line)
- growth_line.update_mid()
- lines.append(growth_line)
- grid.add(growth_line, growth_line.mid)
- self.growth_line = growth_line
- for s in shape['storage']:
- s.energy -= min(s.energy, (n * 1.2 + 29) / len(shape['storage']))
- class ParasiteLine(OscillatorLine):
- def __init__(self, p1, p2):
- super().__init__(p1, p2)
- self.color = COLORS['parasite_line']
- self.base_length = random.uniform(20, 40)
- self.freq_factor = random.uniform(1.0, 2.0)
- class ParasiteGrowthLine(Line):
- def __init__(self, p1, p2):
- super().__init__(p1, p2)
- self.color = COLORS['parasite_growth']
- self.base_length = random.uniform(*LINE_LEN_RANGE)
- def update(self, grid, dots, lines):
- if not self.dot1 or self.dot2: # Only process if attached to ParasiteDot at dot1
- return
- nearby = grid.get_nearby(self.p2, ATTACH_DIST)
- for obj in nearby:
- if isinstance(obj, SoilDot) and not obj.lines:
- if np.linalg.norm(self.p2 - obj.pos) < ATTACH_DIST:
- # Delete the soil dot
- dots.remove(obj)
- grid.remove(obj, obj.pos)
- # Spawn a seed at the soil's position
- seed_dot = SeedDot(*obj.pos)
- dots.append(seed_dot)
- grid.add(seed_dot, seed_dot.pos)
- # Remove this growth line
- if self in lines:
- lines.remove(self)
- grid.remove(self, self.mid)
- if self.dot1:
- self.dot1.lines.discard(self)
- if isinstance(self.dot1, ParasiteDot):
- self.dot1.growth_line = None # Clear reference in ParasiteDot
- break # Only process one soil dot per update
- class Egg:
- def __init__(self, pos, snapshot):
- self.pos, self.snapshot, self.timer = pos, snapshot, 10
- def update(self, dt, eggs, dots, lines, shapes, grid, data):
- self.timer -= dt
- if self.timer <= 0:
- respawn_shape(self.snapshot, self.pos, dots, lines, shapes, grid, data)
- eggs.remove(self)
- # Functions (unchanged except where noted)
- def reset_component(c, keep=False):
- t = type(c).__name__
- p = PROPS.get(t, {})
- if keep:
- saved = {a: getattr(c, a) for a, v in p.get('inheritable', {}).items() if v and hasattr(c, a)}
- for a, v in p.get('defaults', {}).items():
- setattr(c, a, v() if callable(v) else v)
- if keep:
- for a, v in saved.items():
- setattr(c, a, v)
- def adjust_line(l):
- if l.dot1 and l.dot2:
- dx = l.dot2.pos - l.dot1.pos
- cl = max(np.linalg.norm(dx), 1e-6)
- mp = (l.dot1.pos + l.dot2.pos) * 0.5
- dir = dx / cl
- l.dot1.pos, l.dot2.pos = mp - dir * l.base_length / 2, mp + dir * l.base_length / 2
- elif l.dot1:
- l.p2 = l.dot1.pos + (l.p2 - l.dot1.pos) / max(np.linalg.norm(l.p2 - l.dot1.pos), 1e-6) * l.base_length
- elif l.dot2:
- l.p1 = l.dot2.pos + (l.p1 - l.dot2.pos) / max(np.linalg.norm(l.p1 - l.dot2.pos), 1e-6) * l.base_length
- else:
- dx = l.p2 - l.p1
- cl = max(np.linalg.norm(dx), 1e-6)
- mp = (l.p1 + l.p2) * 0.5
- dir = dx / cl
- l.p1, l.p2 = mp - dir * l.base_length / 2, mp + dir * l.base_length / 2
- l.update_mid()
- def spawn_dot():
- r = random.random()
- if r < 0.2:
- d = MoveDot(random.uniform(0, W), random.uniform(0, H))
- elif r < 0.4:
- d = EnergyStorageDot(random.uniform(0, W), random.uniform(0, H))
- elif r < 0.6:
- d = DigesterDot(random.uniform(0, W), random.uniform(0, H))
- elif r < 0.8:
- d = CopyDot(random.uniform(0, W), random.uniform(0, H))
- else:
- d = AssholeDot(random.uniform(0, W), random.uniform(0, H))
- reset_component(d)
- return d
- def spawn_line():
- l, a = random.uniform(*LINE_LEN_RANGE), random.uniform(0, 2 * np.pi)
- x, y = random.uniform(0, W), random.uniform(0, H)
- p1 = np.array([x, y], dtype=np.float32)
- p2 = p1 + l * np.array([np.cos(a), np.sin(a)])
- line = OscillatorLine(p1, p2) if random.random() < 0.5 else Line(p1, p2)
- reset_component(line)
- adjust_line(line)
- return line
- def join_prob(shape, root):
- if not shape:
- return 1
- n, g = shape['n'], shape['gen']
- return max(1 / (1 + n / 2) * 0.15 / (1 + 0.15 * g ** 2.5) * 3.75, 0.005)
- def find(d):
- if d.root != d:
- d.root = find(d.root)
- return d.root
- def union(d1, d2, shapes, data):
- r1, r2 = find(d1), find(d2)
- if r1 == r2:
- return
- # Prevent merging only for SeedDot or TreeLine shapes
- is_r1_tree = r1 in shapes and (any(isinstance(d, SeedDot) for d in shapes[r1]['dots']) or
- any(isinstance(l, TreeLine) for l in shapes[r1]['lines']))
- is_r2_tree = r2 in shapes and (any(isinstance(d, SeedDot) for d in shapes[r2]['dots']) or
- any(isinstance(l, TreeLine) for l in shapes[r2]['lines']))
- if is_r1_tree or is_r2_tree:
- return # Do not merge tree shapes
- if r1.rank < r2.rank:
- r1, r2 = r2, r1
- r2.root = r1
- if r1.rank == r2.rank:
- r1.rank += 1
- if r1 in shapes and r2 in shapes:
- n1 = shapes[r1]['n']
- n2 = shapes[r2]['n']
- total_n = n1 + n2
- if total_n > 0:
- shapes[r1]['life'] = (shapes[r1]['life'] * n1 + shapes[r2]['life'] * n2) / total_n
- else:
- shapes[r1]['life'] = shapes[r1]['life']
- shapes[r1]['laid'] |= shapes[r2]['laid']
- shapes[r1]['gen'] = max(shapes[r1]['gen'], shapes[r2]['gen'])
- if r1 in data and r2 in data:
- g1, g2 = data[r1]['graph'], data[r2]['graph']
- shapes[r1]['graph'] = {'nodes': g1['nodes'] + g2['nodes'], 'edges': g1['edges'] + g2['edges']}
- data[r1]['graph'] = shapes[r1]['graph']
- del data[r2]
- del shapes[r2]
- def snapshot_shape(s):
- c, d2i, l2i = s['c'], {d: i for i, d in enumerate(s['dots'])}, {l: i for i, l in enumerate(s['lines'])}
- edges = []
- for l in s['lines']:
- dot1_id = d2i.get(l.dot1) if l.dot1 else None
- dot2_id = d2i.get(l.dot2) if l.dot2 else None
- if (l.dot1 and dot1_id is None) or (l.dot2 and dot2_id is None):
- continue
- edges.append({
- 'id': l2i[l], 'type': type(l).__name__,
- 'dot1_id': dot1_id,
- 'dot2_id': dot2_id,
- 'base_length': l.base_length,
- **({'phase': l.phase, 'freq_factor': l.freq_factor} if isinstance(l, OscillatorLine) else {})
- })
- return {
- 'nodes': [{'id': d2i[d], 'type': type(d).__name__, 'rel_pos': d.pos - c} for d in s['dots']],
- 'edges': edges,
- 'generation': s['gen'],
- 'life': s['life']
- }
- def respawn_shape(snap, pos, dots, lines, shapes, grid, data):
- ang = random.uniform(0, 2 * np.pi)
- d2d = {}
- for n in snap['nodes']:
- p = pos + np.array([n['rel_pos'][0] * np.cos(ang) - n['rel_pos'][1] * np.sin(ang),
- n['rel_pos'][0] * np.sin(ang) + n['rel_pos'][1] * np.cos(ang)])
- d = {
- 'MoveDot': MoveDot,
- 'EnergyStorageDot': EnergyStorageDot,
- 'DigesterDot': DigesterDot,
- 'CopyDot': CopyDot,
- 'AssholeDot': AssholeDot,
- 'ShitDot': ShitDot,
- 'ParasiteDot': ParasiteDot,
- 'SoilDot': SoilDot,
- 'SeedDot': SeedDot,
- 'FungusDot': FungusDot # Added FungusDot
- }[n['type']](*p)
- reset_component(d, True)
- d2d[n['id']], d.root = d, d
- dots.append(d)
- for e in snap['edges']:
- line_class = {
- 'Line': Line,
- 'OscillatorLine': OscillatorLine,
- 'VirusLine': VirusLine,
- 'FilterLine': FilterLine,
- 'ParasiteLine': ParasiteLine,
- 'ParasiteGrowthLine': ParasiteGrowthLine,
- 'TreeLine': TreeLine,
- 'CutterLine': CutterLine,
- 'FungusLine': FungusLine # Added FungusLine
- }[e['type']]
- l = line_class([0, 0], [0, 0])
- l.base_length = e['base_length']
- reset_component(l, True)
- if e['dot1_id'] is not None:
- l.dot1, l.p1 = d2d[e['dot1_id']], d2d[e['dot1_id']].pos
- l.dot1.lines.add(l)
- if e['dot2_id'] is not None:
- l.dot2, l.p2 = d2d[e['dot2_id']], d2d[e['dot2_id']].pos
- l.dot2.lines.add(l)
- l.update_mid()
- if l.dot1 and l.dot2:
- union(l.dot1, l.dot2, shapes, data)
- adjust_line(l)
- lines.append(l)
- grid.add(l, l.mid)
- r = find(dots[-1]) if dots else None
- if r:
- data[r] = {
- 'life': LIFE_INITIAL,
- 'laid': False,
- 'gen': snap['generation'] + 1,
- 'osc': None,
- 'graph': snap,
- 'svel': np.zeros(2)
- }
- shapes[r] = {
- 'dots': [d for d in dots[-len(d2d):]],
- 'lines': lines[-len(snap['edges']):],
- 'vel': np.zeros(2),
- 'svel': np.zeros(2),
- 'life': LIFE_INITIAL,
- 'laid': False,
- 'gen': snap['generation'] + 1,
- 'osc': None,
- 'graph': snap
- }
- def recompute_shapes(dots, lines, shapes, data):
- old = {r: {'life': s['life'], 'laid': s['laid'], 'gen': s['gen'], 'osc': s['osc'], 'graph': s['graph'], 'svel': s['svel']}
- for r, s in shapes.items()}
- shapes.clear()
- for d in dots:
- if d.lines and not isinstance(d, (FoodDot, ShitDot)): # Remove FungusDot from exclusion
- r = find(d)
- shapes.setdefault(r, {'dots': [], 'lines': [], 'vel': np.zeros(2), 'svel': np.zeros(2),
- 'life': old.get(r, {'life': LIFE_INITIAL})['life'], 'laid': False, 'gen': 1, 'osc': None, 'graph': {'nodes': [], 'edges': []}})
- shapes[r]['dots'].append(d)
- for l in lines:
- r = None
- if l.dot1 and l.dot2 and find(l.dot1) == find(l.dot2):
- r = find(l.dot1)
- shape = shapes.setdefault(r, {'dots': [], 'lines': [], 'vel': np.zeros(2), 'svel': np.zeros(2),
- 'life': old.get(r, {'life': LIFE_INITIAL})['life'], 'laid': False, 'gen': 1, 'osc': None, 'graph': {'nodes': [], 'edges': []}})
- shape['lines'].append(l)
- if l.dot1 and l.dot1 not in shape['dots']:
- shape['dots'].append(l.dot1)
- if l.dot2 and l.dot2 not in shape['dots']:
- shape['dots'].append(l.dot2)
- else:
- if l.dot1:
- r = find(l.dot1)
- shape = shapes.setdefault(r, {'dots': [], 'lines': [], 'vel': np.zeros(2), 'svel': np.zeros(2),
- 'life': old.get(r, {'life': LIFE_INITIAL})['life'], 'laid': False, 'gen': 1, 'osc': None, 'graph': {'nodes': [], 'edges': []}})
- shape['lines'].append(l)
- if l.dot1 and l.dot1 not in shape['dots']:
- shape['dots'].append(l.dot1)
- if l.dot2:
- r = find(l.dot2)
- shape = shapes.setdefault(r, {'dots': [], 'lines': [], 'vel': np.zeros(2), 'svel': np.zeros(2),
- 'life': old.get(r, {'life': LIFE_INITIAL})['life'], 'laid': False, 'gen': 1, 'osc': None, 'graph': {'nodes': [], 'edges': []}})
- shape['lines'].append(l)
- if l.dot2 and l.dot2 not in shape['dots']:
- shape['dots'].append(l.dot2)
- for r in list(shapes):
- s = shapes[r]
- if not s['dots']:
- del shapes[r]
- continue
- s['c'] = np.mean([d.pos for d in s['dots']], axis=0)
- s['storage'] = [d for d in s['dots'] if isinstance(d, EnergyStorageDot)]
- s['n'] = len(s['dots']) + len(s['lines'])
- s['active'] = next((d for d in s['storage'] if d.energy >= 0.001 * DT * (1 + s['n'] / 10)), None)
- if r in old:
- s.update({k: old[r][k] for k in ['laid', 'gen', 'osc', 'graph', 'svel']})
- elif r not in data:
- data[r] = {'life': LIFE_INITIAL, 'laid': False, 'gen': 1, 'osc': None, 'graph': snapshot_shape(s), 'svel': np.zeros(2)}
- else:
- s.update({k: data[r][k] for k in ['laid', 'gen', 'osc', 'graph', 'svel']})
- data[r]['life'] = s['life']
- def recompute_connectivity(s, shapes, data):
- # Reset union-find structures
- for d in s['dots']:
- d.root, d.rank = d, 0
- # Rebuild connectivity using remaining lines
- for l in s['lines']:
- if l.dot1 and l.dot2:
- union(l.dot1, l.dot2, shapes, data)
- # Collect connected components
- root_to_dots = defaultdict(list)
- root_to_lines = defaultdict(list)
- for d in s['dots']:
- root = find(d)
- root_to_dots[root].append(d)
- for l in s['lines']:
- if l.dot1 and l.dot2 and find(l.dot1) == find(l.dot2):
- root = find(l.dot1)
- root_to_lines[root].append(l)
- elif l.dot1:
- root = find(l.dot1)
- root_to_lines[root].append(l)
- elif l.dot2:
- root = find(l.dot2)
- root_to_lines[root].append(l)
- # Remove the original shape
- original_root = find(s['dots'][0]) if s['dots'] else None
- if original_root in shapes:
- del shapes[original_root]
- if original_root in data:
- del data[original_root]
- # Create new shapes for each connected component
- for root, dots in root_to_dots.items():
- if not dots:
- continue
- new_shape = {
- 'dots': dots,
- 'lines': root_to_lines.get(root, []),
- 'vel': np.zeros(2),
- 'svel': np.zeros(2),
- 'life': s['life'],
- 'laid': s['laid'],
- 'gen': s['gen'],
- 'osc': None,
- 'graph': {'nodes': [], 'edges': []}
- }
- new_shape['c'] = np.mean([d.pos for d in new_shape['dots']], axis=0)
- new_shape['storage'] = [d for d in new_shape['dots'] if isinstance(d, EnergyStorageDot)]
- new_shape['n'] = len(new_shape['dots']) + len(new_shape['lines'])
- new_shape['active'] = next((d for d in new_shape['storage'] if d.energy >= 0.001 * DT * (1 + new_shape['n'] / 10)), None)
- shapes[root] = new_shape
- data[root] = {
- 'life': new_shape['life'],
- 'laid': new_shape['laid'],
- 'gen': new_shape['gen'],
- 'osc': None,
- 'graph': snapshot_shape(new_shape),
- 'svel': np.zeros(2)
- }
- def remove_shape(s, grid, dots, lines, data, shapes):
- r = find(s['dots'][0]) if s['dots'] else None
- if r in data:
- del data[r]
- is_tree_shape = any(isinstance(d, SeedDot) for d in s['dots']) or any(isinstance(l, TreeLine) for l in s['lines'])
- components_to_remove = []
- components_to_spawn = []
- if is_tree_shape:
- components_to_remove = [(d, d.pos) for d in s['dots'] if not isinstance(d, SeedDot)] + \
- [(l, l.mid) for l in s['lines'] if not isinstance(l, TreeLine)]
- components_to_spawn = random.sample(components_to_remove, len(components_to_remove) // 2) if components_to_remove else []
- s['dots'] = [d for d in s['dots'] if isinstance(d, SeedDot)]
- s['lines'] = [l for l in s['lines'] if isinstance(l, TreeLine)]
- if not s['dots'] and r is not None:
- del shapes[r]
- if r in data:
- del data[r]
- elif s['dots']:
- recompute_connectivity(s, shapes, data)
- else:
- components_to_remove = [(d, d.pos) for d in s['dots']] + \
- [(l, l.mid) for l in s['lines'] if not isinstance(l, TreeLine)]
- components_to_spawn = random.sample(components_to_remove, len(components_to_remove) // 2) if components_to_remove else []
- for comp, pos in components_to_remove:
- if isinstance(comp, Dot):
- if s['life'] <= 0 and (comp, pos) in components_to_spawn:
- fd = FoodDot(*pos)
- reset_component(fd)
- dots.append(fd)
- grid.add(fd, fd.pos)
- if comp in dots:
- dots.remove(comp)
- grid.remove(comp, comp.pos)
- comp.lines.clear()
- elif isinstance(comp, Line):
- if s['life'] <= 0 and (comp, pos) in components_to_spawn and not isinstance(comp, VirusLine):
- fd = FoodDot(*pos)
- reset_component(fd)
- dots.append(fd)
- grid.add(fd, fd.pos)
- if comp in lines:
- lines.remove(comp)
- grid.remove(comp, comp.mid)
- if r is not None and r in shapes and not shapes[r]['dots']:
- del shapes[r]
- def compute_shape_geometry(shape):
- dots = shape['dots']
- lines = shape['lines']
- dot_to_index = {d: i for i, d in enumerate(dots)}
- points = [(d.pos, d.color) for d in dots]
- edges = []
- for line in lines:
- if line.dot1 and line.dot2 and line.dot1 in dot_to_index and line.dot2 in dot_to_index:
- i1 = dot_to_index[line.dot1]
- i2 = dot_to_index[line.dot2]
- edges.append((i1, i2, line.color))
- return points, edges
- def render_shape(screen, shape, camera):
- points, edges = compute_shape_geometry(shape)
- if not points:
- return
- screen_points = [(camera.world_to_screen(np.array(pos)).tolist(), color) for pos, color in points]
- for i1, i2, edge_color in edges:
- pygame.draw.line(screen, edge_color, screen_points[i1][0], screen_points[i2][0], max(1, int(2 * camera.zoom)))
- for sp, dot_color in screen_points:
- radius = max(1, int(BASE_DOT_RADIUS * camera.zoom))
- pygame.draw.circle(screen, dot_color, sp, radius)
- def is_shape_visible(shape, min_x, min_y, max_x, max_y):
- if not shape['dots']:
- return False
- positions = [d.pos for d in shape['dots']]
- min_pos = np.min(positions, axis=0)
- max_pos = np.max(positions, axis=0)
- return (max_pos[0] >= min_x and min_pos[0] <= max_x and
- max_pos[1] >= min_y and min_pos[1] <= max_y)
- def update_shape_geometry(shape, camera):
- points, edges = compute_shape_geometry(shape)
- shape['render_points'] = [(camera.world_to_screen(np.array(pos)).tolist(), color) for pos, color in points]
- shape['render_edges'] = edges
- def rotate(p, c, a):
- r = p - c
- return c + np.array([r[0] * np.cos(a) - r[1] * np.sin(a), r[0] * np.sin(a) + r[1] * np.cos(a)])
- def check_overcrowding_and_spawn_viruses(tick, grid, lines, shapes, dots):
- if tick % 100 != 0:
- return
- OVERCROWD_THRESHOLD = 420 # Adjusted for entire world
- total_components = 0
- counted_dots = set()
- counted_lines = set()
- eligible_shapes = [] # Store (root, shape) for non-tree shapes
- # Count total non-tree components and collect eligible shapes
- for root, shape in shapes.items():
- if any(isinstance(d, SeedDot) for d in shape['dots']) or any(isinstance(l, TreeLine) for l in shape['lines']):
- continue # Skip tree shapes
- eligible_shapes.append((root, shape))
- for dot in shape['dots']:
- if dot not in counted_dots and not isinstance(dot, SeedDot):
- total_components += 1
- counted_dots.add(dot)
- for line in shape['lines']:
- if line not in counted_lines and not isinstance(line, TreeLine):
- total_components += 1
- counted_lines.add(line)
- if total_components <= OVERCROWD_THRESHOLD or not eligible_shapes:
- return
- # Shuffle eligible shapes to try them in random order
- random.shuffle(eligible_shapes)
- # Try each shape until a virus is spawned or all shapes are exhausted
- for root, target_shape in eligible_shapes:
- # Check if the shape already has a virus
- has_virus = any(isinstance(line, VirusLine) for line in target_shape['lines'])
- if has_virus:
- continue # Skip to the next shape
- # Choose a random dot from the shape
- eligible_dots = [d for d in target_shape['dots'] if not isinstance(d, SeedDot)]
- if eligible_dots:
- target_dot = random.choice(eligible_dots)
- # Spawn VirusLine attached to the target dot
- angle = random.uniform(0, 2 * np.pi)
- length = random.uniform(*LINE_LEN_RANGE)
- p2 = target_dot.pos + length * np.array([np.cos(angle), np.sin(angle)])
- virus = VirusLine(target_dot.pos, p2)
- virus.dot1 = target_dot
- target_dot.lines.add(virus)
- virus.update_mid()
- lines.append(virus)
- grid.add(virus, virus.mid)
- # Add the virus to the shape's lines
- target_shape['lines'].append(virus)
- adjust_line(virus)
- break # Exit after spawning one virus
- def main():
- screen = pygame.display.set_mode((SCREEN_W, SCREEN_H))
- clock = pygame.time.Clock()
- font = pygame.font.SysFont('arial', 16)
- camera = Camera(SCREEN_W, SCREEN_H)
- grid = SpatialGrid(W, H, GRID_CELL_SIZE)
- dots = [spawn_dot() for _ in range(INITIAL_DOTS)]
- lines = [spawn_line() for _ in range(INITIAL_LINES)]
- shapes, eggs, data = {}, [], {}
- mycelium_networks = []
- tick, ds, ls = 0, 0, 0
- sdt = DT / SUB_STEPS
- debug_shape_info = None
- DEBUG_DISPLAY_TIME = 5
- CLICK_RADIUS = ATTACH_DIST
- network_update_timer = 0
- network_batch_index = 0
- sun = Sun()
- for d in dots:
- grid.add(d, d.pos)
- for l in lines:
- grid.add(l, l.mid)
- while True:
- for e in pygame.event.get():
- if e.type == pygame.QUIT:
- pygame.quit()
- return
- elif e.type == pygame.MOUSEBUTTONDOWN and e.button == 3:
- click_pos = camera.screen_to_world(np.array(e.pos, dtype=np.float32))
- nearby_dots = grid.get_nearby(click_pos, CLICK_RADIUS)
- clicked_shape = None
- for dot in nearby_dots:
- if isinstance(dot, Dot) and dot.lines and not isinstance(dot, FoodDot) and not isinstance(dot, ShitDot):
- if np.sum((dot.pos - click_pos) ** 2) < CLICK_RADIUS ** 2:
- root = find(dot)
- if root in shapes:
- clicked_shape = shapes[root]
- break
- if clicked_shape:
- life_text = f"Life: {clicked_shape['life']:.1f} seconds"
- debug_shape_info = (clicked_shape, life_text, DEBUG_DISPLAY_TIME)
- camera.handle_event(e, grid, shapes)
- tick += 1
- if tick % SPAWN_INTERVAL == 0:
- ds = ls = 0
- network_update_timer += sdt
- for _ in range(SUB_STEPS):
- for d in dots:
- if isinstance(d, MoveDot) and not d.lines:
- d.update(sdt)
- grid.grid.clear()
- for d in dots:
- grid.add(d, d.pos)
- for l in lines:
- grid.add(l, l.mid)
- for l in mycelium_networks:
- grid.add(l, l.mid)
- # Batching for FoodDot and ShitDot updates
- individual_dots = [d for d in dots if isinstance(d, (FoodDot, ShitDot)) and (not hasattr(d, 'network') or not d.network)]
- if individual_dots:
- # Persistent index for queue-like cycling
- if not hasattr(main, 'dot_batch_index'):
- main.dot_batch_index = 0
- start_idx = main.dot_batch_index % len(individual_dots)
- end_idx = min(start_idx + INDIVIDUAL_DOT_BATCH_SIZE, len(individual_dots))
- batch_dots = individual_dots[start_idx:end_idx]
- update_dt = NETWORK_UPDATE_INTERVAL if network_update_timer >= NETWORK_UPDATE_INTERVAL else sdt
- for d in batch_dots:
- d.update(update_dt, grid, dots, mycelium_networks)
- # Advance the index for the next batch
- main.dot_batch_index = end_idx if end_idx < len(individual_dots) else 0
- # SoilDot updates (no batching needed)
- for d in dots[:]:
- if isinstance(d, SoilDot):
- d.update(update_dt, grid, dots)
- for d in dots[:]:
- if isinstance(d, (SeedDot, FungusDot)): # Include FungusDot
- d.update(sdt, sun, grid, lines, dots)
- ###
- for l in lines[:]:
- if isinstance(l, FilterLine):
- l.update(grid, dots, lines, mycelium_networks)
- elif isinstance(l, CutterLine):
- l.update(grid, lines, dots, shapes, data)
- elif isinstance(l, ParasiteGrowthLine):
- l.update(grid, dots, lines)
- active_networks = []
- for dot in dots:
- if isinstance(dot, (FoodDot, ShitDot)) and dot.network and dot.network not in active_networks:
- active_networks.append(dot.network)
- start_idx = network_batch_index * NETWORK_BATCH_SIZE
- end_idx = min(start_idx + NETWORK_BATCH_SIZE, len(active_networks))
- batch_networks = active_networks[start_idx:end_idx]
- update_dt = NETWORK_UPDATE_INTERVAL if network_update_timer >= NETWORK_UPDATE_INTERVAL else sdt
- for network in batch_networks[:]:
- if not network.update(update_dt, grid, dots, mycelium_networks):
- active_networks.remove(network)
- if network_update_timer >= NETWORK_UPDATE_INTERVAL:
- network.try_spawn_filter_line(grid, lines, mycelium_networks, dots)
- network_batch_index = (network_batch_index + 1) % max(1, (len(active_networks) + NETWORK_BATCH_SIZE - 1) // NETWORK_BATCH_SIZE)
- if network_update_timer >= NETWORK_UPDATE_INTERVAL:
- network_update_timer = 0
- if sum(1 for d in dots if isinstance(d, MoveDot) and not d.lines and not isinstance(d, FoodDot) and not isinstance(d, ShitDot)) < MIN_MOVEDOTS and ds < MAX_SPAWN:
- d = MoveDot(random.uniform(0, W), random.uniform(0, H))
- reset_component(d)
- dots.append(d)
- grid.add(d, d.pos)
- ds += 1
- if sum(1 for d in dots if not d.lines and not isinstance(d, FoodDot) and not isinstance(d, ShitDot)) < MIN_DOTS and ds < MAX_SPAWN:
- d = spawn_dot()
- dots.append(d)
- grid.add(d, d.pos)
- ds += 1
- if sum(1 for l in lines if not (l.dot1 or l.dot2)) < MIN_LINES and ls < MAX_SPAWN:
- l = spawn_line()
- lines.append(l)
- grid.add(l, l.mid)
- ls += 1
- for e in eggs[:]:
- e.update(sdt, eggs, dots, lines, shapes, grid, data)
- recompute_shapes(dots, lines, shapes, data)
- nearby = {l: grid.get_nearby(l.mid, ATTACH_DIST) for l in lines if not (l.dot1 and l.dot2)}
- check_overcrowding_and_spawn_viruses(tick, grid, lines, shapes, dots)
- for r, s in list(shapes.items()):
- is_tree_shape = any(isinstance(d, SeedDot) for d in s['dots']) or any(isinstance(l, TreeLine) for l in s['lines'])
- if not is_tree_shape:
- s['life'] -= sdt
- if s['life'] <= 0:
- remove_shape(s, grid, dots, lines, data, shapes)
- if r in shapes: # Check if shape still exists
- del shapes[r]
- continue
- for l in s['lines']:
- if isinstance(l, VirusLine):
- l.update(s, lines, grid)
- for d in s['dots']:
- if isinstance(d, DigesterDot):
- d.update(s, grid, dots, lines, mycelium_networks)
- elif isinstance(d, CopyDot):
- d.update(s, eggs)
- elif isinstance(d, AssholeDot):
- d.update(s, grid, dots)
- elif isinstance(d, ParasiteDot):
- d.update(s, grid, dots, lines)
- # Attachment logic
- # Attachment logic
- attached = set()
- for l in lines[:]:
- if l in attached or (l.dot1 and l.dot2):
- continue
- if isinstance(l, (TreeLine, ParasiteGrowthLine, MyceliumLine)):
- continue
- for d in nearby.get(l, set()):
- if not isinstance(d, Dot) or l not in lines:
- continue
- if isinstance(d, (FoodDot, ShitDot, SeedDot, SoilDot)):
- continue # Prevent attachment to these dots
- if isinstance(d, ParasiteDot):
- if l != d.parasite_line: # Only allow ParasiteDot to attach to its own ParasiteLine
- continue
- if isinstance(l, ParasiteLine):
- if not (isinstance(d, DigesterDot) or isinstance(d, AssholeDot)):
- continue
- dr, lr = find(d), find(l.dot1 or l.dot2) if l.dot1 or l.dot2 else None
- if lr and dr == lr:
- continue
- if random.random() > min(join_prob(shapes.get(dr, {}), dr), join_prob(shapes.get(lr, {}), lr) if lr else 1):
- continue
- if isinstance(l, FungusLine) and isinstance(d, FungusDot):
- # Allow FungusDot to attach to FungusLine
- pass
- if not l.dot1 and np.sum((d.pos - l.p1) ** 2) < ATTACH_DIST ** 2:
- l.dot1, l.p1 = d, d.pos
- d.lines.add(l)
- l.update_mid()
- attached.add(l)
- if dr in shapes and d not in shapes[dr]['dots']:
- shapes[dr]['dots'].append(d)
- if isinstance(l, FungusLine) and l.dot2 and isinstance(l.dot2, FungusDot):
- # Ensure FungusDot is included in the shape
- union(d, l.dot2, shapes, data)
- if not l.dot2 and np.sum((d.pos - l.p2) ** 2) < ATTACH_DIST ** 2:
- l.dot2, l.p2 = d, d.pos
- d.lines.add(l)
- l.update_mid()
- attached.add(l)
- if dr in shapes and d not in shapes[dr]['dots']:
- shapes[dr]['dots'].append(d)
- if isinstance(l, FungusLine) and l.dot1 and isinstance(l.dot1, FungusDot):
- # Ensure FungusDot is included in the shape
- union(d, l.dot1, shapes, data)
- if l.dot1 and l.dot2:
- union(l.dot1, l.dot2, shapes, data)
- r = find(l.dot1)
- if r in shapes:
- if l.dot1 not in shapes[r]['dots']:
- shapes[r]['dots'].append(l.dot1)
- if l.dot2 not in shapes[r]['dots']:
- shapes[r]['dots'].append(l.dot2)
- shapes[r]['graph'] = data[r]['graph'] = snapshot_shape(shapes[r])
- recompute_shapes(dots, lines, shapes, data)
- #
- # In the main loop, replace the existing attachment logic for tree lines with:
- # Tree line attachment logic: Only tree lines can attach to seed dots
- attached_lines = set()
- for tree_line in [l for l in lines if isinstance(l, TreeLine) and (not l.dot1 or not l.dot2)]:
- if tree_line in attached_lines:
- continue
- nearby = grid.get_nearby(tree_line.mid, ATTACH_DIST)
- for seed_dot in nearby:
- if not isinstance(seed_dot, SeedDot) or seed_dot in attached_lines:
- continue
- tree_end = tree_line.p1 if not tree_line.dot1 else tree_line.p2
- if np.linalg.norm(tree_end - seed_dot.pos) < ATTACH_DIST:
- # Attach tree line to seed dot
- if not tree_line.dot1:
- tree_line.dot1 = seed_dot
- tree_line.p1 = seed_dot.pos
- else:
- tree_line.dot2 = seed_dot
- tree_line.p2 = seed_dot.pos
- seed_dot.lines.add(tree_line)
- tree_line.update_mid()
- attached_lines.add(tree_line)
- # Update tree shape
- tree_root = find(seed_dot)
- if tree_root in shapes:
- if seed_dot not in shapes[tree_root]['dots']:
- shapes[tree_root]['dots'].append(seed_dot)
- if tree_line not in shapes[tree_root]['lines']:
- shapes[tree_root]['lines'].append(tree_line)
- recompute_shapes(dots, lines, shapes, data)
- # In the main loop, replace the shape update section with:
- for r, s in list(shapes.items()):
- is_tree_shape = any(isinstance(d, SeedDot) for d in s['dots']) or any(isinstance(l, TreeLine) for l in s['lines'])
- if not is_tree_shape:
- s['life'] -= sdt
- if s['life'] <= 0:
- remove_shape(s, grid, dots, lines, data, shapes)
- continue
- # Count non-tree components for overcrowding
- non_tree_dots = [d for d in s['dots'] if not isinstance(d, SeedDot)]
- non_tree_lines = [l for l in s['lines'] if not isinstance(l, TreeLine)]
- non_tree_n = len(non_tree_dots) + len(non_tree_lines)
- if non_tree_n > 30:
- eligible_lines = [l for l in s['lines'] if not isinstance(l, (ParasiteLine, ParasiteGrowthLine, TreeLine))]
- if eligible_lines:
- l = random.choice(eligible_lines)
- if l in lines:
- lines.remove(l)
- s['lines'].remove(l)
- if l.dot1:
- l.dot1.lines.discard(l)
- if l.dot2:
- l.dot2.lines.discard(l)
- recompute_connectivity(s, shapes, data)
- recompute_shapes(dots, lines, shapes, data)
- continue
- osc = [l for l in s['lines'] if isinstance(l, OscillatorLine) and l.dot1 and l.dot2]
- oc = np.mean([p for l in osc for p in [l.dot1.pos, l.dot2.pos]], axis=0) if len(osc) >= 2 else None
- s['vel'] = np.zeros(2)
- active = OscillatorLine.update_batch(osc, DT, s)
- s['c'] = np.mean([d.pos for d in s['dots']], axis=0)
- update_shape_geometry(s, camera)
- s['svel'] = 0.7 * s['svel'] + 0.3 * s['vel']
- ra = 0
- if len(osc) >= 2 and oc is not None:
- prev_oc = s['osc']
- if prev_oc is not None:
- d = oc - prev_oc
- dm = np.linalg.norm(d)
- if dm > 1e-6:
- ra = np.clip(dm * 0.05 * DT, -0.05, 0.05)
- rel_prev = prev_oc - s['c']
- rel_curr = oc - s['c']
- cp = rel_prev[0] * rel_curr[1] - rel_prev[1] * rel_curr[0]
- ra *= np.sign(cp) if cp != 0 else 1
- s['osc'] = oc.copy() if oc is not None else None
- if ra and not any(getattr(l, 'immobile', False) for l in s['lines']):
- for d in s['dots']:
- d.pos = rotate(d.pos, s['c'], ra)
- for l in s['lines']:
- if not l.dot1:
- l.p1 = rotate(l.p1, s['c'], ra)
- if not l.dot2:
- l.p2 = rotate(l.p2, s['c'], ra)
- l.update_mid()
- if not active and (md := [d for d in s['dots'] if isinstance(d, MoveDot)]):
- s['vel'] = np.mean([d.vel for d in md], axis=0)
- s['svel'] = 0.7 * s['svel'] + 0.3 * s['vel']
- if not any(getattr(l, 'immobile', False) for l in s['lines']):
- s['c'] += s['svel'] * DT
- # Boundary checks and position updates (unchanged)
- if s['c'][0] < -BUFFER:
- offset = W + 2 * BUFFER
- s['c'][0] += offset
- for d in s['dots']:
- d.pos[0] += offset
- for l in s['lines']:
- if not l.dot1:
- l.p1[0] += offset
- if not l.dot2:
- l.p2[0] += offset
- l.update_mid()
- if s['osc'] is not None:
- s['osc'][0] += offset
- elif s['c'][0] > W + BUFFER:
- offset = W + 2 * BUFFER
- s['c'][0] -= offset
- for d in s['dots']:
- d.pos[0] -= offset
- for l in s['lines']:
- if not l.dot1:
- l.p1[0] -= offset
- if not l.dot2:
- l.p2[0] -= offset
- l.update_mid()
- if s['osc'] is not None:
- s['osc'][0] -= offset
- if s['c'][1] < -BUFFER:
- offset = H + 2 * BUFFER
- s['c'][1] += offset
- for d in s['dots']:
- d.pos[1] += offset
- for l in s['lines']:
- if not l.dot1:
- l.p1[1] += offset
- if not l.dot2:
- l.p2[1] += offset
- l.update_mid()
- if s['osc'] is not None:
- s['osc'][1] += offset
- elif s['c'][1] > H + BUFFER:
- offset = H + 2 * BUFFER
- s['c'][1] -= offset
- for d in s['dots']:
- d.pos[1] -= offset
- for l in s['lines']:
- if not l.dot1:
- l.p1[1] -= offset
- if not l.dot2:
- l.p2[1] -= offset
- l.update_mid()
- if s['osc'] is not None:
- s['osc'][1] -= offset
- d = s['vel'] * DT
- for x in s['dots']:
- x.pos += d
- for l in s['lines']:
- l.p1 = l.dot1.pos if l.dot1 else l.p1 + d
- l.p2 = l.dot2.pos if l.dot2 else l.p2 + d
- l.update_mid()
- #sun updates
- sun.update(DT)
- for d in dots[:]:
- if isinstance(d, SeedDot):
- d.update(sdt, sun, grid, lines, dots)
- screen.fill(COLORS['black'])
- min_x, min_y, max_x, max_y = camera.get_visible_area()
- visible_objects = grid.get_in_area(min_x, min_y, max_x, max_y)
- for r, shape in shapes.items():
- if is_shape_visible(shape, min_x, min_y, max_x, max_y):
- render_shape(screen, shape, camera)
- # In main(), replace the visible_lines rendering with:
- # In the main loop, replace the visible_lines rendering section with:
- visible_lines = [l for l in lines if l in visible_objects and not (l.dot1 and l.dot2 and find(l.dot1) in shapes)]
- for l in visible_lines:
- if isinstance(l, TreeLine): # Only TreeLines get special (wavy) rendering
- points = l.get_branch_points()
- screen_points = [camera.world_to_screen(np.array(p)).tolist() for p in points]
- for i in range(len(screen_points) - 1):
- pygame.draw.line(screen, l.color, screen_points[i], screen_points[i + 1], max(1, int(2 * camera.zoom)))
- else:
- # Render FungusLines and other lines as straight lines
- p1, p2 = l.ends()
- sp1 = camera.world_to_screen(np.array(p1))
- sp2 = camera.world_to_screen(np.array(p2))
- pygame.draw.line(screen, l.color, sp1.tolist(), sp2.tolist(), max(1, int(2 * camera.zoom)))
- visible_mycelium_lines = [l for l in mycelium_networks if l in visible_objects]
- for l in visible_mycelium_lines:
- if l.dot1 and l.dot2:
- points = l.get_wavy_points()
- screen_points = [camera.world_to_screen(p).tolist() for p in points]
- for i in range(len(screen_points) - 1):
- pygame.draw.line(screen, l.color, screen_points[i], screen_points[i + 1], max(1, int(2 * camera.zoom)))
- visible_dots = [d for d in dots if d in visible_objects and not (d.lines and find(d) in shapes)]
- for d in visible_dots:
- sp = camera.world_to_screen(d.pos)
- radius = max(1, int(BASE_DOT_RADIUS * camera.zoom))
- pygame.draw.circle(screen, d.color, sp.tolist(), radius)
- visible_eggs = [e for e in eggs if min_x <= e.pos[0] <= max_x and min_y <= e.pos[1] <= max_y]
- for e in visible_eggs:
- sp = camera.world_to_screen(e.pos)
- radius = max(1, int(BASE_DOT_RADIUS * camera.zoom))
- pygame.draw.circle(screen, COLORS['egg'], sp.tolist(), radius)
- # After rendering eggs
- sp = camera.world_to_screen(sun.pos)
- radius = max(1, int(SUN_RADIUS * camera.zoom))
- pygame.draw.circle(screen, COLORS['sun'], sp.tolist(), radius)
- # Optional: Draw influence radius
- influence_radius = max(1, int(SUN_INFLUENCE_RADIUS * camera.zoom))
- pygame.draw.circle(screen, (255, 255, 0, 50), sp.tolist(), influence_radius, 1)
- fps = clock.get_fps()
- fps_text = font.render(f'FPS: {fps:.1f} | Zoom: {camera.zoom:.2f}x', True, (255, 255, 255))
- screen.blit(fps_text, (10, 10))
- if debug_shape_info:
- shape, life_text, timer = debug_shape_info
- timer -= DT
- if timer <= 0:
- debug_shape_info = None
- else:
- life_text = f"Life: {shape['life']:.1f} seconds"
- debug_shape_info = (shape, life_text, timer)
- debug_text = font.render(life_text, True, (255, 255, 255))
- screen.blit(debug_text, (10, 30))
- pygame.display.flip()
- clock.tick(FPS)
- if __name__ == "__main__":
- main()
Advertisement