Guest User

Untitled

a guest
Jan 29th, 2026
122
0
Never
6
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 90.43 KB | None | 0 0
  1. import pygame
  2. import numpy as np
  3. import random
  4. from collections import defaultdict
  5.  
  6. pygame.init()
  7.  
  8. # Updated Constants
  9. WORLD_SCALE = 2
  10. W, H = 1290 * WORLD_SCALE, 600 * WORLD_SCALE
  11. SCREEN_W, SCREEN_H = 1920, 1080
  12. DT, FPS = 0.1, 60
  13. ATTACH_DIST, DIGESTER_RANGE = 15, 15
  14. LINE_LEN_RANGE = (10, 30)
  15. MIN_DOTS, MIN_LINES = int(80 * WORLD_SCALE), int(80 * WORLD_SCALE)
  16. INITIAL_DOTS = int(160 * WORLD_SCALE)
  17. INITIAL_LINES = int(160 * WORLD_SCALE)
  18. MIN_MOVEDOTS = int(10 * WORLD_SCALE)
  19. COLORS = {
  20. 'black': (0, 0, 0), 'move': (100, 100, 100), 'digester': (255, 0, 255),
  21. 'storage': (255, 255, 0), 'copy': (0, 255, 0), 'line': (255, 255, 255),
  22. 'oscillator': (0, 255, 255), 'egg': (200, 200, 200), 'food': (0, 255, 0),
  23. 'asshole': (139, 69, 19), 'shit': (92, 64, 51), 'mycelium': (200, 200, 200),
  24. 'filter': (0, 0, 255),
  25. 'soil': (194, 178, 128), 'parasite': (255, 0, 0),
  26. 'parasite_line': (255, 165, 0), 'parasite_growth': (255, 255, 0),
  27. 'seed': (0, 128, 0), # Dark green for seeds
  28. 'tree': (139, 69, 19), # Brown for tree lines
  29. 'sun': (255, 255, 0), # Bright yellow for sun
  30. 'cutter': (255, 255, 0), # Red for cutter lines
  31. 'fungus_dot': (150, 75, 0), # Brownish for FungusDot
  32. 'fungus_line': (200, 150, 100), # Light brown for FungusLine
  33. }
  34. BUFFER = 100
  35. GRID_CELL_SIZE = 25
  36. SPAWN_INTERVAL = 300
  37. MAX_SPAWN = int(5 * WORLD_SCALE)
  38. SUB_STEPS = 4
  39. ZOOM_MIN, ZOOM_MAX, ZOOM_SPEED = 0.1, 10.0, 0.1
  40. BASE_DOT_RADIUS = 5
  41. LIFE_INITIAL = 300
  42. SHIT_RATE = 26
  43. ASSHOLE_LIFE_EXTENSION = 8
  44. SHIT_DOT_LIFE_EXTENSION = -0.7
  45. SHIT_DOT_DECAY = 100
  46. ASSHOLE_ENERGY_COST = -3.2
  47. VIRUS_LIFE_DAMAGE = 2.8
  48. VIRUS_REPRODUCTION_RATE = 11
  49. FILTER_RANGE = 20
  50. MYCELIUM_SEARCH_ENERGY_COST = 6
  51.  
  52. MYCELIUM_SEARCH_RADIUS_START = 100
  53. MYCELIUM_SEARCH_RADIUS_STEP = 100
  54. MYCELIUM_SEARCH_RADIUS_MAX = 100
  55. MYCELIUM_ENERGY_INITIAL = 100
  56. SHIT_INITIAL_ENERGY = 200
  57. FOOD_ENERGY_INITIAL = 200
  58. MYCELIUM_ENERGY_MIN = 1
  59. MYCELIUM_WAVE_AMPLITUDE = 5
  60. MYCELIUM_WAVE_FREQUENCY = 0.1
  61.  
  62. NETWORK_BATCH_SIZE = 1
  63. MYCELIUM_SEARCH_INTERVAL = 0.2
  64. NETWORK_UPDATE_INTERVAL = 0.2
  65. SHIT_FOR_FILTER = 200
  66. FOOD_FOR_FILTER = 200
  67. INDIVIDUAL_DOT_BATCH_SIZE = 1
  68.  
  69. MAX_TREE_LINES = 3 # Reduced from 15
  70. TREE_FOOD_ENERGY = 25 # Energy granted when digesting a FoodDot from a tree
  71. TREE_WAVE_FREQUENCY = 0.15
  72. TREE_WAVE_AMPLITUDE = 8
  73.  
  74. SUN_RADIUS = 20
  75. SUN_INFLUENCE_RADIUS = 200
  76. SUN_SPEED = 100 # Pixels per second
  77. SUN_HEIGHT_STEP = H / 4 # Vertical step after each pass
  78. SEED_ENERGY_GAIN = 100 # Energy per second in sun's influence
  79. SEED_ENERGY_PER_TREE = 10 # Energy needed to sprout a tree line
  80. CUTTER_RANGE = 15 # Range for cutter line to cut tree lines
  81.  
  82. FUNGUS_ENERGY_GAIN = 1 # Energy gained per second in sun's influence
  83. FUNGUS_ENERGY_PER_LINE = 3 # Energy needed to grow a FungusLine
  84. MAX_FUNGUS_LINES = 3 # Maximum FungusLines per FungusDot
  85.  
  86. PROPS = {
  87. 'MoveDot': {'defaults': {'vel': lambda: np.array([random.uniform(-5, 5), random.uniform(-5, 5)]), 'color': COLORS['move']}},
  88. 'EnergyStorageDot': {'defaults': {'energy': 3, 'color': COLORS['storage']}, 'inheritable': {'energy': True}},
  89. 'DigesterDot': {'defaults': {'color': COLORS['digester']}},
  90. 'CopyDot': {'defaults': {'egg_timer': 0, 'color': COLORS['copy']}},
  91. 'FoodDot': {'defaults': {'color': COLORS['food'], 'energy': MYCELIUM_ENERGY_INITIAL}},
  92. 'AssholeDot': {'defaults': {'color': COLORS['asshole'], 'shit_timer': 0}},
  93. 'ShitDot': {'defaults': {'color': COLORS['shit'], 'energy': MYCELIUM_ENERGY_INITIAL}},
  94. 'Line': {'defaults': {'color': COLORS['line'], 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)}, 'inheritable': {'base_length': True}},
  95. 'OscillatorLine': {
  96. 'defaults': {
  97. 'color': COLORS['oscillator'], 'phase': lambda: random.uniform(0, 2 * np.pi),
  98. 'base_length': lambda: random.uniform(*LINE_LEN_RANGE), 'freq_factor': lambda: random.uniform(0.045, 1.5)
  99. },
  100. 'inheritable': {'base_length': True, 'phase': True, 'freq_factor': True}
  101. },
  102. 'VirusLine': {'defaults': {'color': (255, 0, 0), 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)}, 'inheritable': {'base_length': True}},
  103. 'MyceliumLine': {'defaults': {'color': COLORS['mycelium'], 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)}},
  104. 'FilterLine': {'defaults': {'color': COLORS['filter'], 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)}, 'inheritable': {'base_length': True}},
  105. 'ParasiteLine': {
  106. 'defaults': {
  107. 'color': COLORS['parasite_line'],
  108. 'base_length': lambda: random.uniform(20, 40),
  109. 'phase': lambda: random.uniform(0, 2 * np.pi),
  110. 'freq_factor': lambda: random.uniform(1.0, 2.0)
  111. },
  112. 'inheritable': {'base_length': True, 'phase': True, 'freq_factor': True}
  113. },
  114. 'ParasiteGrowthLine': {
  115. 'defaults': {'color': COLORS['parasite_growth'], 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)},
  116. 'inheritable': {'base_length': True},
  117. 'SeedDot': {'defaults': {'color': COLORS['seed'], 'energy': 0}},
  118. 'TreeLine': {'defaults': {'color': COLORS['tree'], 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)}, 'inheritable': {'base_length': True}},
  119. 'CutterLine': {'defaults': {'color': COLORS['cutter'], 'base_length': lambda: random.uniform(*LINE_LEN_RANGE)}, 'inheritable': {'base_length': True}},
  120. 'FungusDot': {'defaults': {'color': COLORS['fungus_dot'], 'energy': 0}},
  121. 'FungusLine': {'defaults': {'color': COLORS['fungus_line'],'base_length': lambda: random.uniform(*LINE_LEN_RANGE)},'inheritable': {'base_length': True}}
  122. }
  123. }
  124.  
  125. # Precompute sin/cos lookup table (unchanged)
  126. PHASE_RESOLUTION = 1000
  127. PHASE_TABLE = np.linspace(0, 2 * np.pi, PHASE_RESOLUTION, endpoint=False)
  128. SIN_TABLE = np.sin(PHASE_TABLE)
  129. COS_TABLE = np.cos(PHASE_TABLE)
  130.  
  131. def get_trig(phase):
  132. idx = int((phase % (2 * np.pi)) / (2 * np.pi) * PHASE_RESOLUTION) % PHASE_RESOLUTION
  133. return SIN_TABLE[idx], COS_TABLE[idx]
  134.  
  135. # Camera class (unchanged)
  136. class Camera:
  137. def __init__(self, width, height):
  138. self.pos = np.array([W / 2, H / 2], dtype=np.float32)
  139. self.zoom = 1.0
  140. self.width, self.height = width, height
  141. self.dragging = False
  142. self.drag_start = None
  143.  
  144. def world_to_screen(self, pos):
  145. rel_pos = (pos - self.pos) * self.zoom
  146. screen_pos = rel_pos + np.array([self.width / 2, self.height / 2])
  147. return screen_pos
  148.  
  149. def screen_to_world(self, pos):
  150. rel_pos = pos - np.array([self.width / 2, self.height / 2])
  151. world_pos = rel_pos / self.zoom + self.pos
  152. return world_pos
  153.  
  154. def apply_zoom(self, delta, mouse_pos):
  155. old_zoom = self.zoom
  156. self.zoom = max(ZOOM_MIN, min(ZOOM_MAX, self.zoom * (1 + delta * ZOOM_SPEED)))
  157. if old_zoom != self.zoom:
  158. mouse_world_before = self.screen_to_world(mouse_pos)
  159. mouse_world_after = mouse_world_before
  160. self.pos = mouse_world_after - (mouse_pos - np.array([self.width / 2, self.height / 2])) / self.zoom
  161.  
  162. def handle_event(self, event, grid, shapes=None):
  163. if event.type == pygame.MOUSEBUTTONDOWN:
  164. if event.button == 1:
  165. self.dragging = True
  166. self.drag_start = np.array(event.pos, dtype=np.float32)
  167. elif event.button == 4:
  168. self.apply_zoom(1, event.pos)
  169. if shapes:
  170. for shape in shapes.values():
  171. update_shape_geometry(shape, self)
  172. elif event.button == 5:
  173. self.apply_zoom(-1, event.pos)
  174. if shapes:
  175. for shape in shapes.values():
  176. update_shape_geometry(shape, self)
  177. elif event.type == pygame.MOUSEBUTTONUP:
  178. if event.button == 1:
  179. self.dragging = False
  180. elif event.type == pygame.MOUSEMOTION and self.dragging:
  181. current_pos = np.array(event.pos, dtype=np.float32)
  182. delta = (current_pos - self.drag_start) / self.zoom
  183. self.pos -= delta
  184. self.drag_start = current_pos
  185. if shapes:
  186. for shape in shapes.values():
  187. update_shape_geometry(shape, self)
  188. elif event.type == pygame.KEYDOWN:
  189. if event.key == pygame.K_PLUS or event.key == pygame.K_EQUALS:
  190. self.apply_zoom(1, np.array([self.width / 2, self.height / 2]))
  191. if shapes:
  192. for shape in shapes.values():
  193. update_shape_geometry(shape, self)
  194. elif event.key == pygame.K_MINUS:
  195. self.apply_zoom(-1, np.array([self.width / 2, self.height / 2]))
  196. if shapes:
  197. for shape in shapes.values():
  198. update_shape_geometry(shape, self)
  199.  
  200. def get_visible_area(self):
  201. top_left = self.screen_to_world(np.array([0, 0]))
  202. bottom_right = self.screen_to_world(np.array([self.width, self.height]))
  203. return top_left[0], top_left[1], bottom_right[0], bottom_right[1]
  204.  
  205. # Spatial Grid (unchanged)
  206. class SpatialGrid:
  207. def __init__(self, w, h, size):
  208. self.size, self.size_inv = size, 1 / size
  209. self.grid = defaultdict(set)
  210. self.cols, self.rows = int((w + 2 * BUFFER) / size) + 1, int((h + 2 * BUFFER) / size) + 1
  211.  
  212. def add(self, obj, pos):
  213. c, r = int((pos[0] + BUFFER) * self.size_inv), int((pos[1] + BUFFER) * self.size_inv)
  214. if 0 <= c < self.cols and 0 <= r < self.rows:
  215. self.grid[(c, r)].add(obj)
  216.  
  217. def remove(self, obj, pos):
  218. c, r = int((pos[0] + BUFFER) * self.size_inv), int((pos[1] + BUFFER) * self.size_inv)
  219. self.grid[(c, r)].discard(obj)
  220.  
  221. def get_nearby(self, pos, range_):
  222. c, r = int((pos[0] + BUFFER) * self.size_inv), int((pos[1] + BUFFER) * self.size_inv)
  223. cells = int(range_ / self.size) + 1
  224. return {obj for dc in range(-cells, cells + 1) for dr in range(-cells, cells + 1)
  225. for obj in self.grid.get((c + dc, r + dr), set()) if 0 <= c + dc < self.cols and 0 <= r + dr < self.rows}
  226.  
  227. def get_in_area(self, min_x, min_y, max_x, max_y):
  228. min_c = int((min_x + BUFFER) * self.size_inv)
  229. min_r = int((min_y + BUFFER) * self.size_inv)
  230. max_c = int((max_x + BUFFER) * self.size_inv) + 1
  231. max_r = int((max_y + BUFFER) * self.size_inv) + 1
  232. objects = set()
  233. for c in range(min_c, max_c):
  234. for r in range(min_r, max_r):
  235. if 0 <= c < self.cols and 0 <= r < self.rows:
  236. objects.update(self.grid.get((c, r), set()))
  237. return objects
  238.  
  239. # Base Classes (unchanged)
  240. class Dot:
  241. def __init__(self, x, y):
  242. self.pos = np.array([x, y], dtype=np.float32)
  243. self.lines, self.root, self.rank = set(), self, 0
  244. self.energy = 0
  245. # Removed: self.attached_to_tree = None
  246. # Removed: self.is_anchored = False
  247.  
  248. class Line:
  249. def __init__(self, p1, p2):
  250. self.p1, self.p2 = np.array(p1, dtype=np.float32), np.array(p2, dtype=np.float32)
  251. self.dot1 = self.dot2 = None
  252. self.color, self.base_length = COLORS['line'], random.uniform(*LINE_LEN_RANGE)
  253. self.mid = (self.p1 + self.p2) * 0.5
  254.  
  255. def ends(self):
  256. p1 = self.dot1.pos if self.dot1 else self.p1
  257. p2 = self.dot2.pos if self.dot2 else self.p2
  258. return (p1.tolist(), p2.tolist())
  259.  
  260. def update_mid(self):
  261. e1, e2 = self.ends()
  262. self.mid = (np.array(e1) + np.array(e2)) * 0.5
  263.  
  264. # Mycelium Network Classes (unchanged)
  265. class MyceliumNetwork:
  266. def __init__(self, initial_dot):
  267. self.dots = [initial_dot]
  268. self.lines = []
  269. self.searching_dot = initial_dot
  270. self.search_radius = MYCELIUM_SEARCH_RADIUS_START
  271. self.search_timer = 0
  272. initial_dot.network = self
  273. initial_dot.is_searching = False
  274.  
  275. def add_dot(self, dot, line):
  276. self.dots.append(dot)
  277. self.lines.append(line)
  278. dot.network = self
  279. dot.is_searching = False
  280.  
  281. def merge(self, other_network, line):
  282. self.dots.extend(other_network.dots)
  283. self.lines.extend(other_network.lines)
  284. self.lines.append(line)
  285. for dot in other_network.dots:
  286. dot.network = self
  287. dot.is_searching = False
  288. self.searching_dot = random.choice(self.dots)
  289. self.search_radius = MYCELIUM_SEARCH_RADIUS_START
  290. self.search_timer = 0
  291.  
  292. def remove_dot(self, dot, grid, dots, mycelium_networks):
  293. if dot not in self.dots:
  294. return
  295. self.dots.remove(dot)
  296. lines_to_remove = [line for line in self.lines if line.dot1 == dot or line.dot2 == dot]
  297. for line in lines_to_remove:
  298. self.lines.remove(line)
  299. if line in mycelium_networks:
  300. mycelium_networks.remove(line)
  301. grid.remove(line, line.mid)
  302. dot.network = None
  303. if not self.dots:
  304. for line in self.lines[:]:
  305. if line in mycelium_networks:
  306. mycelium_networks.remove(line)
  307. grid.remove(line, line.mid)
  308. self.lines.clear()
  309. elif self.searching_dot == dot and self.dots:
  310. self.searching_dot = random.choice(self.dots)
  311. self.search_radius = MYCELIUM_SEARCH_RADIUS_START
  312. self.search_timer = 0
  313.  
  314. def update(self, dt, grid, dots, mycelium_networks):
  315. for dot in self.dots[:]:
  316. if dot.energy < MYCELIUM_ENERGY_MIN:
  317. self.remove_dot(dot, grid, dots, mycelium_networks)
  318. if dot in dots:
  319. dots.remove(dot)
  320. grid.remove(dot, dot.pos)
  321. if not self.dots:
  322. for line in self.lines[:]:
  323. if line in mycelium_networks:
  324. mycelium_networks.remove(line)
  325. grid.remove(line, line.mid)
  326. self.lines.clear()
  327. return False
  328. if dt < NETWORK_UPDATE_INTERVAL:
  329. return True
  330. energy_cost = MYCELIUM_SEARCH_ENERGY_COST * NETWORK_UPDATE_INTERVAL
  331. per_dot_cost = energy_cost / len(self.dots) if self.dots else 0
  332. for dot in self.dots:
  333. dot.energy = max(0, dot.energy - per_dot_cost)
  334. total_energy = sum(d.energy for d in self.dots)
  335. if total_energy < MYCELIUM_ENERGY_MIN:
  336. for dot in self.dots[:]:
  337. if dot in dots:
  338. dots.remove(dot)
  339. grid.remove(dot, dot.pos)
  340. for line in self.lines[:]:
  341. if line in mycelium_networks:
  342. mycelium_networks.remove(line)
  343. grid.remove(line, line.mid)
  344. self.dots.clear()
  345. self.lines.clear()
  346. return False
  347. self.search_timer += NETWORK_UPDATE_INTERVAL
  348. if self.search_timer >= MYCELIUM_SEARCH_INTERVAL:
  349. self.search_timer = 0
  350. self.search_radius += MYCELIUM_SEARCH_RADIUS_STEP
  351. if self.search_radius > MYCELIUM_SEARCH_RADIUS_MAX:
  352. self.search_radius = MYCELIUM_SEARCH_RADIUS_START
  353. self.searching_dot = random.choice(self.dots) if self.dots else None
  354. if self.searching_dot and self.dots:
  355. nearby = grid.get_nearby(self.searching_dot.pos, self.search_radius)
  356. for obj in nearby:
  357. if isinstance(obj, (FoodDot, ShitDot)) and obj != self.searching_dot and obj not in self.dots:
  358. distance = np.linalg.norm(self.searching_dot.pos - obj.pos)
  359. if distance <= self.search_radius:
  360. line = MyceliumLine(self.searching_dot.pos, obj.pos)
  361. line.dot1, line.dot2 = self.searching_dot, obj
  362. line.cap_length() # Enforce length cap
  363. if obj.network and obj.network != self:
  364. self.merge(obj.network, line)
  365. elif not obj.network:
  366. self.add_dot(obj, line)
  367. mycelium_networks.append(line)
  368. grid.add(line, line.mid)
  369. break
  370. return True
  371.  
  372.  
  373. def try_spawn_filter_line(self, grid, lines, mycelium_networks, dots):
  374. shit_energy = sum(d.energy for d in self.dots if isinstance(d, ShitDot))
  375. food_energy = sum(d.energy for d in self.dots if isinstance(d, FoodDot))
  376. if shit_energy < SHIT_FOR_FILTER or food_energy < FOOD_FOR_FILTER:
  377. return False
  378. candidates = [d for d in self.dots if isinstance(d, (FoodDot, ShitDot))]
  379. if not candidates:
  380. return False
  381. base_dot = random.choice(candidates)
  382. shit_dots = [d for d in self.dots if isinstance(d, ShitDot) and d != base_dot]
  383. food_dots = [d for d in self.dots if isinstance(d, FoodDot) and d != base_dot]
  384. shit_energy_excl = sum(d.energy for d in shit_dots)
  385. food_energy_excl = sum(d.energy for d in food_dots)
  386. base_energy_cost = 0
  387. if isinstance(base_dot, ShitDot):
  388. remaining_shit_needed = SHIT_FOR_FILTER - min(shit_energy_excl, SHIT_FOR_FILTER)
  389. base_energy_cost = remaining_shit_needed
  390. else:
  391. remaining_food_needed = FOOD_FOR_FILTER - min(food_energy_excl, FOOD_FOR_FILTER)
  392. base_energy_cost = remaining_food_needed
  393. if base_dot.energy < base_energy_cost + MYCELIUM_ENERGY_MIN:
  394. return False
  395. dots_to_remove = []
  396. if shit_dots:
  397. shit_per_dot = min(SHIT_FOR_FILTER, shit_energy_excl) / len(shit_dots)
  398. for dot in shit_dots:
  399. dot.energy -= shit_per_dot
  400. if dot.energy < MYCELIUM_ENERGY_MIN:
  401. dots_to_remove.append(dot)
  402. if food_dots:
  403. food_per_dot = min(FOOD_FOR_FILTER, food_energy_excl) / len(food_dots)
  404. for dot in food_dots:
  405. dot.energy -= food_per_dot
  406. if dot.energy < MYCELIUM_ENERGY_MIN:
  407. dots_to_remove.append(dot)
  408. base_dot.energy -= base_energy_cost
  409. for dot in dots_to_remove:
  410. self.remove_dot(dot, grid, dots, mycelium_networks)
  411. if dot in dots:
  412. dots.remove(dot)
  413. grid.remove(dot, dot.pos)
  414. if base_dot.energy < MYCELIUM_ENERGY_MIN or base_dot not in dots:
  415. return False
  416. p1 = base_dot.pos.copy()
  417. angle = random.uniform(0, 2 * np.pi)
  418. length = random.uniform(*LINE_LEN_RANGE)
  419. p2 = p1 + length * np.array([np.cos(angle), np.sin(angle)])
  420. # 1/3 chance for FilterLine, CutterLine, or FungusDot
  421. r = random.random()
  422. if r < 0.333:
  423. new_line = FilterLine(p1, p2)
  424. new_line.update_mid()
  425. lines.append(new_line)
  426. grid.add(new_line, new_line.mid)
  427. elif r < 0.666:
  428. new_line = CutterLine(p1, p2)
  429. new_line.update_mid()
  430. lines.append(new_line)
  431. grid.add(new_line, new_line.mid)
  432. else:
  433. # Spawn FungusDot instead of a line
  434. fungus_dot = FungusDot(*p1)
  435. dots.append(fungus_dot)
  436. grid.add(fungus_dot, fungus_dot.pos)
  437. return True
  438.  
  439. class MyceliumLine(Line):
  440. def __init__(self, p1, p2):
  441. super().__init__(p1, p2)
  442. self.color = COLORS['mycelium']
  443. # Cap length at MYCELIUM_SEARCH_RADIUS_MAX
  444. self.cap_length()
  445.  
  446. def update_mid(self):
  447. if self.dot1 and self.dot2:
  448. self.p1, self.p2 = self.dot1.pos, self.dot2.pos
  449. # Cap length if dots move
  450. self.cap_length()
  451. self.mid = (self.p1 + self.p2) / 2
  452.  
  453. def cap_length(self):
  454. # Ensure line length is at most MYCELIUM_SEARCH_RADIUS_MAX
  455. vec = self.p2 - self.p1
  456. length = np.linalg.norm(vec)
  457. if length > MYCELIUM_SEARCH_RADIUS_MAX:
  458. # Scale p2 to keep length at MYCELIUM_SEARCH_RADIUS_MAX
  459. direction = vec / length
  460. self.p2 = self.p1 + direction * MYCELIUM_SEARCH_RADIUS_MAX
  461.  
  462. def get_wavy_points(self):
  463. p1, p2 = self.ends()
  464. p1, p2 = np.array(p1), np.array(p2)
  465. direction = p2 - p1
  466. length = np.linalg.norm(direction)
  467. if length < 1e-6:
  468. return [p1, p2]
  469. direction /= length
  470. perp = np.array([-direction[1], direction[0]])
  471. num_segments = int(length / 10) + 2
  472. points = []
  473. for i in range(num_segments):
  474. t = i / (num_segments - 1)
  475. base_pos = p1 + t * (p2 - p1)
  476. offset = MYCELIUM_WAVE_AMPLITUDE * np.sin(t * length * MYCELIUM_WAVE_FREQUENCY) * perp
  477. points.append(base_pos + offset)
  478. return points
  479.  
  480.  
  481. class SeedDot(Dot):
  482. def __init__(self, x, y):
  483. super().__init__(x, y)
  484. self.color = COLORS['seed']
  485. self.energy = 0
  486. self.lifetime = 1500 # Very long lifetime
  487. self.max_tree_lines = MAX_TREE_LINES # Use new constant
  488.  
  489. def update(self, dt, sun, grid, lines, dots):
  490. self.lifetime -= dt
  491. if self.lifetime <= 0:
  492. # Remove all attached TreeLines and spawn FoodDots
  493. tree_lines = [line for line in self.lines if isinstance(line, TreeLine)]
  494. for tree_line in tree_lines:
  495. if tree_line in lines:
  496. lines.remove(tree_line)
  497. grid.remove(tree_line, tree_line.mid)
  498. if tree_line.dot1:
  499. tree_line.dot1.lines.discard(tree_line)
  500. if tree_line.dot2:
  501. tree_line.dot2.lines.discard(tree_line)
  502. # Spawn FoodDot with higher energy at tree line's midpoint
  503. food_dot = FoodDot(*tree_line.mid)
  504. food_dot.energy = TREE_FOOD_ENERGY # Set higher energy
  505. food_dot.is_tree_food = True # Flag to prevent mycelium connection
  506. dots.append(food_dot)
  507. grid.add(food_dot, food_dot.pos)
  508. # Remove the SeedDot itself
  509. if self in dots:
  510. dots.remove(self)
  511. grid.remove(self, self.pos)
  512. return
  513.  
  514. # Gain energy if in sun's influence
  515. if sun and np.linalg.norm(self.pos - sun.pos) <= SUN_INFLUENCE_RADIUS:
  516. self.energy += SEED_ENERGY_GAIN * dt
  517. # Count current tree lines
  518. tree_line_count = sum(1 for line in self.lines if isinstance(line, TreeLine))
  519. # Sprout tree line only if under the cap and enough energy
  520. while self.energy >= SEED_ENERGY_PER_TREE and tree_line_count < self.max_tree_lines:
  521. self.energy -= SEED_ENERGY_PER_TREE
  522. angle = random.uniform(0, 2 * np.pi)
  523. length = random.uniform(*LINE_LEN_RANGE)
  524. p2 = self.pos + length * np.array([np.cos(angle), np.sin(angle)])
  525. tree_line = TreeLine(self.pos, p2)
  526. tree_line.dot1 = self
  527. self.lines.add(tree_line)
  528. tree_line.update_mid()
  529. lines.append(tree_line)
  530. grid.add(tree_line, tree_line.mid)
  531. tree_line_count += 1
  532.  
  533. class Sun:
  534. def __init__(self):
  535. self.pos = np.array([-SUN_INFLUENCE_RADIUS, H], dtype=np.float32) # Start off-screen left
  536. self.height = H # Current height
  537. self.direction = 1 # 1 for right, -1 for left (though we only go right)
  538.  
  539. def update(self, dt):
  540. # Move right
  541. self.pos[0] += SUN_SPEED * dt * self.direction
  542. # If off-screen right, reset to left and lower height
  543. if self.pos[0] > W + SUN_INFLUENCE_RADIUS:
  544. self.pos[0] = -SUN_INFLUENCE_RADIUS
  545. self.height -= SUN_HEIGHT_STEP
  546. # If too low, reset to top
  547. if self.height < 0:
  548. self.height = H
  549. self.pos[1] = self.height
  550. # Derived Classes
  551.  
  552. class FungusLine(Line):
  553. def __init__(self, p1, p2):
  554. super().__init__(p1, p2)
  555. self.color = COLORS['fungus_line']
  556. self.base_length = random.uniform(*LINE_LEN_RANGE)
  557. self.immobile = False # Explicitly allow movement
  558.  
  559.  
  560.  
  561. class FungusDot(Dot):
  562. def __init__(self, x, y):
  563. super().__init__(x, y)
  564. self.color = COLORS['fungus_dot']
  565. self.energy = 0
  566. self.lifetime = 1500 # Long lifetime, similar to SeedDot
  567. self.max_fungus_lines = MAX_FUNGUS_LINES
  568.  
  569. def update(self, dt, sun, grid, lines, dots):
  570. self.lifetime -= dt
  571. if self.lifetime <= 0:
  572. # Remove all attached FungusLines and spawn FoodDots
  573. fungus_lines = [line for line in self.lines if isinstance(line, FungusLine)]
  574. for fungus_line in fungus_lines:
  575. if fungus_line in lines:
  576. lines.remove(fungus_line)
  577. grid.remove(fungus_line, fungus_line.mid)
  578. if fungus_line.dot1:
  579. fungus_line.dot1.lines.discard(fungus_line)
  580. if fungus_line.dot2:
  581. fungus_line.dot2.lines.discard(fungus_line)
  582. # Spawn FoodDot with regular energy
  583. food_dot = FoodDot(*fungus_line.mid)
  584. food_dot.energy = 5 # Regular 5-energy FoodDot
  585. food_dot.is_tree_food = False # Can connect to mycelium
  586. dots.append(food_dot)
  587. grid.add(food_dot, food_dot.pos)
  588. # Remove the FungusDot itself
  589. if self in dots:
  590. dots.remove(self)
  591. grid.remove(self, self.pos)
  592. return
  593.  
  594. # Gain energy if in sun's influence
  595. if sun and np.linalg.norm(self.pos - sun.pos) <= SUN_INFLUENCE_RADIUS:
  596. self.energy += FUNGUS_ENERGY_GAIN * dt
  597. # Count current fungus lines
  598. fungus_line_count = sum(1 for line in self.lines if isinstance(line, FungusLine))
  599. # Grow fungus line if under the cap and enough energy
  600. while self.energy >= FUNGUS_ENERGY_PER_LINE and fungus_line_count < self.max_fungus_lines:
  601. self.energy -= FUNGUS_ENERGY_PER_LINE
  602. angle = random.uniform(0, 2 * np.pi)
  603. length = random.uniform(*LINE_LEN_RANGE)
  604. p2 = self.pos + length * np.array([np.cos(angle), np.sin(angle)])
  605. fungus_line = FungusLine(self.pos, p2)
  606. fungus_line.dot1 = self
  607. self.lines.add(fungus_line)
  608. fungus_line.update_mid()
  609. lines.append(fungus_line)
  610. grid.add(fungus_line, fungus_line.mid)
  611. fungus_line_count += 1
  612.  
  613.  
  614. class TreeLine(Line):
  615. def __init__(self, p1, p2):
  616. super().__init__(p1, p2)
  617. self.color = COLORS['tree']
  618. self.base_length = random.uniform(*LINE_LEN_RANGE)
  619. self.immobile = True
  620.  
  621. def get_branch_points(self):
  622. p1, p2 = self.ends()
  623. p1, p2 = np.array(p1), np.array(p2)
  624. direction = p2 - p1
  625. length = np.linalg.norm(direction)
  626. if length < 1e-6:
  627. return [p1, p2]
  628. direction /= length
  629. perp = np.array([-direction[1], direction[0]])
  630. num_segments = max(4, int(length / 10)) # More segments for smoother waviness
  631. points = []
  632. t_values = np.linspace(0, 1, num_segments)
  633.  
  634. for i, t in enumerate(t_values):
  635. base_pos = p1 + t * (p2 - p1)
  636. # Add waviness similar to MyceliumLineE
  637. wave_offset = TREE_WAVE_AMPLITUDE * np.sin(t * length * TREE_WAVE_FREQUENCY) * perp
  638. # Add fractal-like branching deviation
  639. if i % 2 == 0 and i < num_segments - 1: # Add deviation every other segment
  640. deviation = random.uniform(-np.pi / 12, np.pi / 12) # ±15 degrees for subtle branching
  641. deviation_vector = np.array([np.cos(deviation), np.sin(deviation)]) * length * 0.1
  642. base_pos += deviation_vector
  643. points.append(base_pos + wave_offset)
  644.  
  645. # Ensure the last point is exactly p2 for accurate attachment
  646. points[-1] = p2
  647. return points
  648.  
  649. class CutterLine(Line):
  650. def __init__(self, p1, p2):
  651. super().__init__(p1, p2)
  652. self.color = COLORS['cutter']
  653. self.base_length = random.uniform(*LINE_LEN_RANGE)
  654.  
  655. def update(self, grid, lines, dots, shapes, data):
  656. nearby = grid.get_nearby(self.mid, CUTTER_RANGE)
  657. cuttable_lines = [l for l in nearby if isinstance(l, (TreeLine, FungusLine)) and l in lines]
  658. for cut_line in cuttable_lines:
  659. if np.linalg.norm(self.mid - cut_line.mid) < CUTTER_RANGE:
  660. # Remove the cut line
  661. lines.remove(cut_line)
  662. grid.remove(cut_line, cut_line.mid)
  663. if cut_line.dot1:
  664. cut_line.dot1.lines.discard(cut_line)
  665. if cut_line.dot2:
  666. cut_line.dot2.lines.discard(cut_line)
  667. # Spawn FoodDot
  668. food_dot = FoodDot(*cut_line.mid)
  669. food_dot.energy = TREE_FOOD_ENERGY if isinstance(cut_line, TreeLine) else 5
  670. food_dot.is_tree_food = isinstance(cut_line, TreeLine)
  671. dots.append(food_dot)
  672. grid.add(food_dot, food_dot.pos)
  673.  
  674. # Update shape connectivity
  675. root = find(cut_line.dot1 or cut_line.dot2) if cut_line.dot1 or cut_line.dot2 else None
  676. if root in shapes:
  677. shape = shapes[root]
  678. if isinstance(cut_line, TreeLine) and any(isinstance(d, SeedDot) for d in shape['dots']):
  679. # Handle TreeLine cut: Recompute connectivity from SeedDot
  680. seed = next(d for d in shape['dots'] if isinstance(d, SeedDot))
  681. reachable = set()
  682. stack = [seed]
  683. while stack:
  684. dot = stack.pop()
  685. reachable.add(dot)
  686. for line in dot.lines:
  687. if isinstance(line, TreeLine) and line in lines:
  688. other_dot = line.dot2 if line.dot1 == dot else line.dot1
  689. if other_dot and other_dot not in reachable:
  690. stack.append(other_dot)
  691. # Remove disconnected TreeLines
  692. lines_to_remove = []
  693. for line in shape['lines'][:]:
  694. if isinstance(line, TreeLine):
  695. if line.dot1 and line.dot2:
  696. if line.dot1 not in reachable or line.dot2 not in reachable:
  697. lines_to_remove.append(line)
  698. for line in lines_to_remove:
  699. if line in lines:
  700. lines.remove(line)
  701. grid.remove(line, line.mid)
  702. if line.dot1:
  703. line.dot1.lines.discard(line)
  704. if line.dot2:
  705. line.dot2.lines.discard(line)
  706. food_dot = FoodDot(*line.mid)
  707. food_dot.energy = TREE_FOOD_ENERGY
  708. food_dot.is_tree_food = True
  709. dots.append(food_dot)
  710. grid.add(food_dot, food_dot.pos)
  711. recompute_connectivity(shape, shapes, data) # Pass shape, not self
  712. else:
  713. # Handle FungusLine or non-tree shapes: Split into components
  714. recompute_connectivity(shape, shapes, data) # Pass shape, not self
  715. class MoveDot(Dot):
  716. def __init__(self, x, y):
  717. super().__init__(x, y)
  718. self.color, self.vel = COLORS['move'], np.array([random.uniform(-5, 5), random.uniform(-5, 5)], dtype=np.float32)
  719.  
  720. def update(self, dt):
  721. if not self.lines:
  722. self.pos += self.vel * dt
  723. self.pos[0] %= W + 2 * BUFFER
  724. self.pos[1] %= H + 2 * BUFFER
  725.  
  726. class EnergyStorageDot(Dot):
  727. def __init__(self, x, y):
  728. super().__init__(x, y)
  729. self.color, self.energy = COLORS['storage'], 2
  730.  
  731. class DigesterDot(Dot):
  732. def update(self, shape, grid, dots, lines, mycelium_networks):
  733. for obj in grid.get_nearby(self.pos, DIGESTER_RANGE):
  734. if isinstance(obj, Dot) and not obj.lines and obj != self and obj in dots:
  735. if isinstance(obj, SoilDot):
  736. continue
  737. if np.sum((self.pos - obj.pos) ** 2) < DIGESTER_RANGE ** 2:
  738. if isinstance(obj, (FoodDot, ShitDot)) and obj.network:
  739. obj.network.remove_dot(obj, grid, dots, mycelium_networks)
  740. dots.remove(obj)
  741. grid.remove(obj, obj.pos)
  742. if isinstance(obj, ShitDot):
  743. shape['life'] += SHIT_DOT_LIFE_EXTENSION
  744. else:
  745. # Grant higher energy for tree FoodDots
  746. energy = TREE_FOOD_ENERGY if getattr(obj, 'is_tree_food', False) else 5
  747. if shape['storage']:
  748. for s in shape['storage']:
  749. s.energy += energy / len(shape['storage'])
  750. elif isinstance(obj, Line) and not (obj.dot1 or obj.dot2) and obj in lines and not isinstance(obj, (VirusLine, TreeLine)):
  751. if np.sum((self.pos - obj.mid) ** 2) < DIGESTER_RANGE ** 2:
  752. lines.remove(obj)
  753. grid.remove(obj, obj.mid)
  754. if shape['storage']:
  755. for s in shape['storage']:
  756. s.energy += 10 / len(shape['storage'])
  757.  
  758. class CopyDot(Dot):
  759. def __init__(self, x, y):
  760. super().__init__(x, y)
  761. self.color, self.egg_timer = COLORS['copy'], 0
  762.  
  763. def update(self, shape, eggs):
  764. if self.egg_timer > 0:
  765. self.egg_timer = max(0, self.egg_timer - DT / SUB_STEPS)
  766. return
  767. if shape['lines']:
  768. has_parasite = any(isinstance(d, ParasiteDot) for d in shape['dots'])
  769. if not has_parasite:
  770. n, e = shape['n'], sum(d.energy for d in shape['storage'])
  771. if e >= n * 1.2 + 35:
  772. for s in shape['storage']:
  773. s.energy -= min(s.energy, n * 1.2 + 29 / len(shape['storage']))
  774. eggs.append(Egg(self.pos, snapshot_shape(shape)))
  775. self.egg_timer = 10
  776. shape['laid'] = True
  777.  
  778. class FoodDot(Dot):
  779. def __init__(self, x, y):
  780. super().__init__(x, y)
  781. self.color = COLORS['food']
  782. self.energy = MYCELIUM_ENERGY_INITIAL # Default for non-tree FoodDots
  783. self.is_searching = True
  784. self.search_radius = MYCELIUM_SEARCH_RADIUS_START
  785. self.search_timer = 0
  786. self.network = None
  787. self.is_tree_food = False # Flag to identify tree-spawned FoodDots
  788.  
  789. def update(self, dt, grid, dots, mycelium_networks):
  790. if self.is_tree_food:
  791. # Tree FoodDots don't connect to mycelium and don't decay
  792. return
  793. if self.network:
  794. return
  795. if self.energy < MYCELIUM_ENERGY_MIN:
  796. if self.network:
  797. self.network.remove_dot(self, grid, dots, mycelium_networks)
  798. if self in dots:
  799. dots.remove(self)
  800. grid.remove(self, self.pos)
  801. return
  802. self.energy -= MYCELIUM_SEARCH_ENERGY_COST * dt
  803. self.search_timer += dt
  804. if self.search_timer >= MYCELIUM_SEARCH_INTERVAL:
  805. self.search_timer = 0
  806. self.search_radius += MYCELIUM_SEARCH_RADIUS_STEP
  807. if self.search_radius > MYCELIUM_SEARCH_RADIUS_MAX:
  808. self.search_radius = MYCELIUM_SEARCH_RADIUS_START
  809. nearby = grid.get_nearby(self.pos, self.search_radius)
  810. for obj in nearby:
  811. if isinstance(obj, (FoodDot, ShitDot)) and obj != self and not obj.network and not getattr(obj, 'is_tree_food', False):
  812. distance = np.linalg.norm(self.pos - obj.pos)
  813. if distance <= self.search_radius:
  814. line = MyceliumLine(self.pos, obj.pos)
  815. line.dot1, line.dot2 = self, obj
  816. network = MyceliumNetwork(self)
  817. network.add_dot(obj, line)
  818. mycelium_networks.append(line)
  819. grid.add(line, line.mid)
  820. break
  821.  
  822. class AssholeDot(Dot):
  823. def __init__(self, x, y):
  824. super().__init__(x, y)
  825. self.color = COLORS['asshole']
  826. self.shit_timer = 0
  827.  
  828. def update(self, shape, grid, dots):
  829. self.shit_timer += DT / SUB_STEPS
  830. if self.shit_timer >= SHIT_RATE:
  831. self.shit_timer = 0
  832. if shape['storage']:
  833. total_energy = sum(d.energy for d in shape['storage'])
  834. energy_cost = ASSHOLE_ENERGY_COST
  835. if total_energy >= energy_cost:
  836. per_storage_cost = energy_cost / len(shape['storage'])
  837. for s in shape['storage']:
  838. s.energy = max(0, s.energy - per_storage_cost)
  839. shape['life'] += ASSHOLE_LIFE_EXTENSION
  840. offset = np.array([random.uniform(-20, 20), random.uniform(-20, 20)])
  841. shit_dot = ShitDot(self.pos[0] + offset[0], self.pos[1] + offset[1])
  842. dots.append(shit_dot)
  843. grid.add(shit_dot, shit_dot.pos)
  844.  
  845. class ShitDot(Dot):
  846. def __init__(self, x, y):
  847. super().__init__(x, y)
  848. self.color = COLORS['shit']
  849. self.lifetime = SHIT_DOT_DECAY
  850. self.energy = SHIT_INITIAL_ENERGY
  851. self.is_searching = True
  852. self.search_radius = MYCELIUM_SEARCH_RADIUS_START
  853. self.search_timer = 0
  854. self.network = None
  855.  
  856. def update(self, dt, grid, dots, mycelium_networks):
  857. if self.network:
  858. return
  859. self.lifetime -= dt
  860. if self.lifetime <= 0 or self.energy < MYCELIUM_ENERGY_MIN:
  861. if self.network:
  862. self.network.remove_dot(self, grid, dots, mycelium_networks)
  863. if self in dots:
  864. dots.remove(self)
  865. grid.remove(self, self.pos)
  866. return
  867. self.energy -= MYCELIUM_SEARCH_ENERGY_COST * dt
  868. self.search_timer += dt
  869. if self.search_timer >= MYCELIUM_SEARCH_INTERVAL:
  870. self.search_timer = 0
  871. self.search_radius += MYCELIUM_SEARCH_RADIUS_STEP
  872. if self.search_radius > MYCELIUM_SEARCH_RADIUS_MAX:
  873. self.search_radius = MYCELIUM_SEARCH_RADIUS_START
  874. nearby = grid.get_nearby(self.pos, self.search_radius)
  875. for obj in nearby:
  876. if isinstance(obj, (FoodDot, ShitDot)) and obj != self and not obj.network:
  877. # Ensure distance is within search_radius
  878. distance = np.linalg.norm(self.pos - obj.pos)
  879. if distance <= self.search_radius:
  880. line = MyceliumLine(self.pos, obj.pos)
  881. line.dot1, line.dot2 = self, obj
  882. network = MyceliumNetwork(self)
  883. network.add_dot(obj, line)
  884. mycelium_networks.append(line)
  885. grid.add(line, line.mid)
  886. break
  887.  
  888. class OscillatorLine(Line):
  889. def __init__(self, p1, p2):
  890. super().__init__(p1, p2)
  891. self.color, self.phase = COLORS['oscillator'], random.uniform(0, 2 * np.pi)
  892. self.freq_factor = random.uniform(0.045, 1.5)
  893.  
  894. @staticmethod
  895. def update_batch(oscillators, dt, shape):
  896. if not oscillators or not shape['storage']:
  897. return False
  898. total_energy = sum(d.energy for d in shape['storage'])
  899. energy_cost = 0.001 * dt * (1 + shape['n'] / 10) * len(oscillators)
  900. if total_energy < energy_cost:
  901. return False
  902. per_storage_cost = energy_cost / len(shape['storage'])
  903. for s in shape['storage']:
  904. s.energy = max(0, s.energy - per_storage_cost)
  905. dot1_pos = np.array([l.dot1.pos for l in oscillators])
  906. dot2_pos = np.array([l.dot2.pos for l in oscillators])
  907. base_lengths = np.array([l.base_length for l in oscillators])
  908. phases = np.array([l.phase for l in oscillators])
  909. freq_factors = np.array([l.freq_factor for l in oscillators])
  910. dx = dot2_pos - dot1_pos
  911. cl = np.maximum(np.sqrt(np.sum(dx ** 2, axis=1)), 1e-6)
  912. mp = (dot1_pos + dot2_pos) * 0.5
  913. dir = dx / cl[:, None]
  914. sin_vals, cos_vals = np.zeros_like(phases), np.zeros_like(phases)
  915. for i, phase in enumerate(phases):
  916. sin_vals[i], cos_vals[i] = get_trig(phase)
  917. scale = 1 + 0.35 * sin_vals
  918. tl = base_lengths * scale / 2
  919. target_pos1 = mp - dir * tl[:, None]
  920. target_pos2 = mp + dir * tl[:, None]
  921. dot1_pos += 0.5 * (target_pos1 - dot1_pos)
  922. dot2_pos += 0.5 * (target_pos2 - dot2_pos)
  923. for i, l in enumerate(oscillators):
  924. l.dot1.pos = dot1_pos[i]
  925. l.dot2.pos = dot2_pos[i]
  926. l.update_mid()
  927. l.phase = phases[i] + 2 * l.freq_factor * dt
  928. lf = np.where(base_lengths < LINE_LEN_RANGE[0] * 1.5, 1.2,
  929. np.where(base_lengths > LINE_LEN_RANGE[1] * 0.75, 0.833, 1))
  930. sf = max(1, shape['n'] / 10) ** -1.3
  931. lcr = 31 * base_lengths * lf * sf * cos_vals
  932. vel_contrib = 0.2 * np.abs(lcr) * dt * (np.maximum(freq_factors, 0.045) ** 1.5)
  933. shape['vel'] += np.sum(vel_contrib[:, None] * dir, axis=0)
  934. return True
  935.  
  936. class VirusLine(Line):
  937. def __init__(self, p1, p2):
  938. super().__init__(p1, p2)
  939. self.color = (255, 0, 0)
  940. self.base_length = random.uniform(*LINE_LEN_RANGE)
  941. self.timer = 0
  942.  
  943. def update(self, shape, lines, grid):
  944. shape['life'] -= VIRUS_LIFE_DAMAGE * (DT / SUB_STEPS)
  945. self.timer += DT / SUB_STEPS
  946. if self.timer >= VIRUS_REPRODUCTION_RATE:
  947. self.timer = 0
  948. if len(shape['dots']) > 1:
  949. target_dot = random.choice(shape['dots'])
  950. angle = random.uniform(0, 2 * np.pi)
  951. length = random.uniform(*LINE_LEN_RANGE)
  952. p2 = target_dot.pos + length * np.array([np.cos(angle), np.sin(angle)])
  953. new_virus = VirusLine(target_dot.pos, p2)
  954. new_virus.dot1 = target_dot
  955. target_dot.lines.add(new_virus)
  956. new_virus.update_mid()
  957. lines.append(new_virus)
  958. grid.add(new_virus, new_virus.mid)
  959. shape['lines'].append(new_virus)
  960. adjust_line(new_virus)
  961.  
  962. class FilterLine(Line):
  963. def __init__(self, p1, p2):
  964. super().__init__(p1, p2)
  965. self.color = COLORS['filter']
  966. self.base_length = np.linalg.norm(self.p2 - self.p1)
  967.  
  968. def update(self, grid, dots, lines, mycelium_networks):
  969. nearby = grid.get_nearby(self.mid, FILTER_RANGE)
  970. for obj in nearby:
  971. if isinstance(obj, ShitDot) and not obj.lines:
  972. if np.sum((self.mid - obj.pos) ** 2) < FILTER_RANGE ** 2:
  973. if obj.network:
  974. obj.network.remove_dot(obj, grid, dots, mycelium_networks)
  975. if obj in dots:
  976. dots.remove(obj)
  977. grid.remove(obj, obj.pos)
  978. if random.random() < 0.2:
  979. soil_dot = SoilDot(*obj.pos)
  980. dots.append(soil_dot)
  981. grid.add(soil_dot, soil_dot.pos)
  982. if random.random() < 0.025:
  983. parasite_dot = ParasiteDot(*obj.pos)
  984. dots.append(parasite_dot)
  985. grid.add(parasite_dot, parasite_dot.pos)
  986. angle = random.uniform(0, 2 * np.pi)
  987. length = random.uniform(20, 40)
  988. p1 = parasite_dot.pos + length * np.array([np.cos(angle), np.sin(angle)])
  989. parasite_line = ParasiteLine(p1, parasite_dot.pos)
  990. parasite_line.dot2 = parasite_dot
  991. parasite_dot.lines.add(parasite_line)
  992. parasite_dot.parasite_line = parasite_line
  993. parasite_line.update_mid()
  994. lines.append(parasite_line)
  995. grid.add(parasite_line, parasite_line.mid)
  996.  
  997. class SoilDot(Dot):
  998. def __init__(self, x, y):
  999. super().__init__(x, y)
  1000. self.color = COLORS['soil']
  1001. self.lifetime = 200
  1002.  
  1003. def update(self, dt, grid, dots):
  1004. self.lifetime -= dt
  1005. if self.lifetime <= 0:
  1006. if self in dots:
  1007. dots.remove(self)
  1008. grid.remove(self, self.pos)
  1009.  
  1010. class ParasiteDot(Dot):
  1011. def __init__(self, x, y):
  1012. super().__init__(x, y)
  1013. self.color = COLORS['parasite']
  1014. self.shit_timer = 0
  1015. self.growth_line = None
  1016. self.parasite_line = None
  1017.  
  1018. def update(self, shape, grid, dots, lines):
  1019. self.shit_timer += DT / SUB_STEPS
  1020. if self.shit_timer >= SHIT_RATE:
  1021. self.shit_timer = 0
  1022. if shape['storage']:
  1023. total_energy = sum(d.energy for d in shape['storage'])
  1024. energy_cost = ASSHOLE_ENERGY_COST
  1025. if total_energy >= energy_cost:
  1026. per_storage_cost = energy_cost / len(shape['storage'])
  1027. for s in shape['storage']:
  1028. s.energy = max(0, s.energy - per_storage_cost)
  1029. shape['life'] += ASSHOLE_LIFE_EXTENSION
  1030. offset = np.array([random.uniform(-20, 20), random.uniform(-20, 20)])
  1031. shit_dot = ShitDot(self.pos[0] + offset[0], self.pos[1] + offset[1])
  1032. dots.append(shit_dot)
  1033. grid.add(shit_dot, shit_dot.pos)
  1034. if shape['storage'] and self.growth_line is None:
  1035. n = shape['n']
  1036. e = sum(d.energy for d in shape['storage'])
  1037. if e >= n * 1.2 + 35:
  1038. angle = random.uniform(0, 2 * np.pi)
  1039. length = random.uniform(*LINE_LEN_RANGE)
  1040. p2 = self.pos + length * np.array([np.cos(angle), np.sin(angle)])
  1041. growth_line = ParasiteGrowthLine(self.pos, p2)
  1042. growth_line.dot1 = self
  1043. self.lines.add(growth_line)
  1044. growth_line.update_mid()
  1045. lines.append(growth_line)
  1046. grid.add(growth_line, growth_line.mid)
  1047. self.growth_line = growth_line
  1048. for s in shape['storage']:
  1049. s.energy -= min(s.energy, (n * 1.2 + 29) / len(shape['storage']))
  1050.  
  1051. class ParasiteLine(OscillatorLine):
  1052. def __init__(self, p1, p2):
  1053. super().__init__(p1, p2)
  1054. self.color = COLORS['parasite_line']
  1055. self.base_length = random.uniform(20, 40)
  1056. self.freq_factor = random.uniform(1.0, 2.0)
  1057.  
  1058. class ParasiteGrowthLine(Line):
  1059. def __init__(self, p1, p2):
  1060. super().__init__(p1, p2)
  1061. self.color = COLORS['parasite_growth']
  1062. self.base_length = random.uniform(*LINE_LEN_RANGE)
  1063.  
  1064. def update(self, grid, dots, lines):
  1065. if not self.dot1 or self.dot2: # Only process if attached to ParasiteDot at dot1
  1066. return
  1067. nearby = grid.get_nearby(self.p2, ATTACH_DIST)
  1068. for obj in nearby:
  1069. if isinstance(obj, SoilDot) and not obj.lines:
  1070. if np.linalg.norm(self.p2 - obj.pos) < ATTACH_DIST:
  1071. # Delete the soil dot
  1072. dots.remove(obj)
  1073. grid.remove(obj, obj.pos)
  1074. # Spawn a seed at the soil's position
  1075. seed_dot = SeedDot(*obj.pos)
  1076. dots.append(seed_dot)
  1077. grid.add(seed_dot, seed_dot.pos)
  1078. # Remove this growth line
  1079. if self in lines:
  1080. lines.remove(self)
  1081. grid.remove(self, self.mid)
  1082. if self.dot1:
  1083. self.dot1.lines.discard(self)
  1084. if isinstance(self.dot1, ParasiteDot):
  1085. self.dot1.growth_line = None # Clear reference in ParasiteDot
  1086. break # Only process one soil dot per update
  1087.  
  1088. class Egg:
  1089. def __init__(self, pos, snapshot):
  1090. self.pos, self.snapshot, self.timer = pos, snapshot, 10
  1091.  
  1092. def update(self, dt, eggs, dots, lines, shapes, grid, data):
  1093. self.timer -= dt
  1094. if self.timer <= 0:
  1095. respawn_shape(self.snapshot, self.pos, dots, lines, shapes, grid, data)
  1096. eggs.remove(self)
  1097.  
  1098. # Functions (unchanged except where noted)
  1099. def reset_component(c, keep=False):
  1100. t = type(c).__name__
  1101. p = PROPS.get(t, {})
  1102. if keep:
  1103. saved = {a: getattr(c, a) for a, v in p.get('inheritable', {}).items() if v and hasattr(c, a)}
  1104. for a, v in p.get('defaults', {}).items():
  1105. setattr(c, a, v() if callable(v) else v)
  1106. if keep:
  1107. for a, v in saved.items():
  1108. setattr(c, a, v)
  1109.  
  1110. def adjust_line(l):
  1111. if l.dot1 and l.dot2:
  1112. dx = l.dot2.pos - l.dot1.pos
  1113. cl = max(np.linalg.norm(dx), 1e-6)
  1114. mp = (l.dot1.pos + l.dot2.pos) * 0.5
  1115. dir = dx / cl
  1116. l.dot1.pos, l.dot2.pos = mp - dir * l.base_length / 2, mp + dir * l.base_length / 2
  1117. elif l.dot1:
  1118. l.p2 = l.dot1.pos + (l.p2 - l.dot1.pos) / max(np.linalg.norm(l.p2 - l.dot1.pos), 1e-6) * l.base_length
  1119. elif l.dot2:
  1120. l.p1 = l.dot2.pos + (l.p1 - l.dot2.pos) / max(np.linalg.norm(l.p1 - l.dot2.pos), 1e-6) * l.base_length
  1121. else:
  1122. dx = l.p2 - l.p1
  1123. cl = max(np.linalg.norm(dx), 1e-6)
  1124. mp = (l.p1 + l.p2) * 0.5
  1125. dir = dx / cl
  1126. l.p1, l.p2 = mp - dir * l.base_length / 2, mp + dir * l.base_length / 2
  1127. l.update_mid()
  1128.  
  1129. def spawn_dot():
  1130. r = random.random()
  1131. if r < 0.2:
  1132. d = MoveDot(random.uniform(0, W), random.uniform(0, H))
  1133. elif r < 0.4:
  1134. d = EnergyStorageDot(random.uniform(0, W), random.uniform(0, H))
  1135. elif r < 0.6:
  1136. d = DigesterDot(random.uniform(0, W), random.uniform(0, H))
  1137. elif r < 0.8:
  1138. d = CopyDot(random.uniform(0, W), random.uniform(0, H))
  1139. else:
  1140. d = AssholeDot(random.uniform(0, W), random.uniform(0, H))
  1141. reset_component(d)
  1142. return d
  1143.  
  1144. def spawn_line():
  1145. l, a = random.uniform(*LINE_LEN_RANGE), random.uniform(0, 2 * np.pi)
  1146. x, y = random.uniform(0, W), random.uniform(0, H)
  1147. p1 = np.array([x, y], dtype=np.float32)
  1148. p2 = p1 + l * np.array([np.cos(a), np.sin(a)])
  1149. line = OscillatorLine(p1, p2) if random.random() < 0.5 else Line(p1, p2)
  1150. reset_component(line)
  1151. adjust_line(line)
  1152. return line
  1153.  
  1154. def join_prob(shape, root):
  1155. if not shape:
  1156. return 1
  1157. n, g = shape['n'], shape['gen']
  1158. return max(1 / (1 + n / 2) * 0.15 / (1 + 0.15 * g ** 2.5) * 3.75, 0.005)
  1159.  
  1160. def find(d):
  1161. if d.root != d:
  1162. d.root = find(d.root)
  1163. return d.root
  1164.  
  1165. def union(d1, d2, shapes, data):
  1166. r1, r2 = find(d1), find(d2)
  1167. if r1 == r2:
  1168. return
  1169. # Prevent merging only for SeedDot or TreeLine shapes
  1170. is_r1_tree = r1 in shapes and (any(isinstance(d, SeedDot) for d in shapes[r1]['dots']) or
  1171. any(isinstance(l, TreeLine) for l in shapes[r1]['lines']))
  1172. is_r2_tree = r2 in shapes and (any(isinstance(d, SeedDot) for d in shapes[r2]['dots']) or
  1173. any(isinstance(l, TreeLine) for l in shapes[r2]['lines']))
  1174. if is_r1_tree or is_r2_tree:
  1175. return # Do not merge tree shapes
  1176. if r1.rank < r2.rank:
  1177. r1, r2 = r2, r1
  1178. r2.root = r1
  1179. if r1.rank == r2.rank:
  1180. r1.rank += 1
  1181. if r1 in shapes and r2 in shapes:
  1182. n1 = shapes[r1]['n']
  1183. n2 = shapes[r2]['n']
  1184. total_n = n1 + n2
  1185. if total_n > 0:
  1186. shapes[r1]['life'] = (shapes[r1]['life'] * n1 + shapes[r2]['life'] * n2) / total_n
  1187. else:
  1188. shapes[r1]['life'] = shapes[r1]['life']
  1189. shapes[r1]['laid'] |= shapes[r2]['laid']
  1190. shapes[r1]['gen'] = max(shapes[r1]['gen'], shapes[r2]['gen'])
  1191. if r1 in data and r2 in data:
  1192. g1, g2 = data[r1]['graph'], data[r2]['graph']
  1193. shapes[r1]['graph'] = {'nodes': g1['nodes'] + g2['nodes'], 'edges': g1['edges'] + g2['edges']}
  1194. data[r1]['graph'] = shapes[r1]['graph']
  1195. del data[r2]
  1196. del shapes[r2]
  1197.  
  1198. def snapshot_shape(s):
  1199. c, d2i, l2i = s['c'], {d: i for i, d in enumerate(s['dots'])}, {l: i for i, l in enumerate(s['lines'])}
  1200. edges = []
  1201. for l in s['lines']:
  1202. dot1_id = d2i.get(l.dot1) if l.dot1 else None
  1203. dot2_id = d2i.get(l.dot2) if l.dot2 else None
  1204. if (l.dot1 and dot1_id is None) or (l.dot2 and dot2_id is None):
  1205. continue
  1206. edges.append({
  1207. 'id': l2i[l], 'type': type(l).__name__,
  1208. 'dot1_id': dot1_id,
  1209. 'dot2_id': dot2_id,
  1210. 'base_length': l.base_length,
  1211. **({'phase': l.phase, 'freq_factor': l.freq_factor} if isinstance(l, OscillatorLine) else {})
  1212. })
  1213. return {
  1214. 'nodes': [{'id': d2i[d], 'type': type(d).__name__, 'rel_pos': d.pos - c} for d in s['dots']],
  1215. 'edges': edges,
  1216. 'generation': s['gen'],
  1217. 'life': s['life']
  1218. }
  1219.  
  1220. def respawn_shape(snap, pos, dots, lines, shapes, grid, data):
  1221. ang = random.uniform(0, 2 * np.pi)
  1222. d2d = {}
  1223. for n in snap['nodes']:
  1224. p = pos + np.array([n['rel_pos'][0] * np.cos(ang) - n['rel_pos'][1] * np.sin(ang),
  1225. n['rel_pos'][0] * np.sin(ang) + n['rel_pos'][1] * np.cos(ang)])
  1226. d = {
  1227. 'MoveDot': MoveDot,
  1228. 'EnergyStorageDot': EnergyStorageDot,
  1229. 'DigesterDot': DigesterDot,
  1230. 'CopyDot': CopyDot,
  1231. 'AssholeDot': AssholeDot,
  1232. 'ShitDot': ShitDot,
  1233. 'ParasiteDot': ParasiteDot,
  1234. 'SoilDot': SoilDot,
  1235. 'SeedDot': SeedDot,
  1236. 'FungusDot': FungusDot # Added FungusDot
  1237. }[n['type']](*p)
  1238. reset_component(d, True)
  1239. d2d[n['id']], d.root = d, d
  1240. dots.append(d)
  1241. for e in snap['edges']:
  1242. line_class = {
  1243. 'Line': Line,
  1244. 'OscillatorLine': OscillatorLine,
  1245. 'VirusLine': VirusLine,
  1246. 'FilterLine': FilterLine,
  1247. 'ParasiteLine': ParasiteLine,
  1248. 'ParasiteGrowthLine': ParasiteGrowthLine,
  1249. 'TreeLine': TreeLine,
  1250. 'CutterLine': CutterLine,
  1251. 'FungusLine': FungusLine # Added FungusLine
  1252. }[e['type']]
  1253. l = line_class([0, 0], [0, 0])
  1254. l.base_length = e['base_length']
  1255. reset_component(l, True)
  1256. if e['dot1_id'] is not None:
  1257. l.dot1, l.p1 = d2d[e['dot1_id']], d2d[e['dot1_id']].pos
  1258. l.dot1.lines.add(l)
  1259. if e['dot2_id'] is not None:
  1260. l.dot2, l.p2 = d2d[e['dot2_id']], d2d[e['dot2_id']].pos
  1261. l.dot2.lines.add(l)
  1262. l.update_mid()
  1263. if l.dot1 and l.dot2:
  1264. union(l.dot1, l.dot2, shapes, data)
  1265. adjust_line(l)
  1266. lines.append(l)
  1267. grid.add(l, l.mid)
  1268. r = find(dots[-1]) if dots else None
  1269. if r:
  1270. data[r] = {
  1271. 'life': LIFE_INITIAL,
  1272. 'laid': False,
  1273. 'gen': snap['generation'] + 1,
  1274. 'osc': None,
  1275. 'graph': snap,
  1276. 'svel': np.zeros(2)
  1277. }
  1278. shapes[r] = {
  1279. 'dots': [d for d in dots[-len(d2d):]],
  1280. 'lines': lines[-len(snap['edges']):],
  1281. 'vel': np.zeros(2),
  1282. 'svel': np.zeros(2),
  1283. 'life': LIFE_INITIAL,
  1284. 'laid': False,
  1285. 'gen': snap['generation'] + 1,
  1286. 'osc': None,
  1287. 'graph': snap
  1288. }
  1289.  
  1290. def recompute_shapes(dots, lines, shapes, data):
  1291. old = {r: {'life': s['life'], 'laid': s['laid'], 'gen': s['gen'], 'osc': s['osc'], 'graph': s['graph'], 'svel': s['svel']}
  1292. for r, s in shapes.items()}
  1293. shapes.clear()
  1294. for d in dots:
  1295. if d.lines and not isinstance(d, (FoodDot, ShitDot)): # Remove FungusDot from exclusion
  1296. r = find(d)
  1297. shapes.setdefault(r, {'dots': [], 'lines': [], 'vel': np.zeros(2), 'svel': np.zeros(2),
  1298. 'life': old.get(r, {'life': LIFE_INITIAL})['life'], 'laid': False, 'gen': 1, 'osc': None, 'graph': {'nodes': [], 'edges': []}})
  1299. shapes[r]['dots'].append(d)
  1300. for l in lines:
  1301. r = None
  1302. if l.dot1 and l.dot2 and find(l.dot1) == find(l.dot2):
  1303. r = find(l.dot1)
  1304. shape = shapes.setdefault(r, {'dots': [], 'lines': [], 'vel': np.zeros(2), 'svel': np.zeros(2),
  1305. 'life': old.get(r, {'life': LIFE_INITIAL})['life'], 'laid': False, 'gen': 1, 'osc': None, 'graph': {'nodes': [], 'edges': []}})
  1306. shape['lines'].append(l)
  1307. if l.dot1 and l.dot1 not in shape['dots']:
  1308. shape['dots'].append(l.dot1)
  1309. if l.dot2 and l.dot2 not in shape['dots']:
  1310. shape['dots'].append(l.dot2)
  1311. else:
  1312. if l.dot1:
  1313. r = find(l.dot1)
  1314. shape = shapes.setdefault(r, {'dots': [], 'lines': [], 'vel': np.zeros(2), 'svel': np.zeros(2),
  1315. 'life': old.get(r, {'life': LIFE_INITIAL})['life'], 'laid': False, 'gen': 1, 'osc': None, 'graph': {'nodes': [], 'edges': []}})
  1316. shape['lines'].append(l)
  1317. if l.dot1 and l.dot1 not in shape['dots']:
  1318. shape['dots'].append(l.dot1)
  1319. if l.dot2:
  1320. r = find(l.dot2)
  1321. shape = shapes.setdefault(r, {'dots': [], 'lines': [], 'vel': np.zeros(2), 'svel': np.zeros(2),
  1322. 'life': old.get(r, {'life': LIFE_INITIAL})['life'], 'laid': False, 'gen': 1, 'osc': None, 'graph': {'nodes': [], 'edges': []}})
  1323. shape['lines'].append(l)
  1324. if l.dot2 and l.dot2 not in shape['dots']:
  1325. shape['dots'].append(l.dot2)
  1326. for r in list(shapes):
  1327. s = shapes[r]
  1328. if not s['dots']:
  1329. del shapes[r]
  1330. continue
  1331. s['c'] = np.mean([d.pos for d in s['dots']], axis=0)
  1332. s['storage'] = [d for d in s['dots'] if isinstance(d, EnergyStorageDot)]
  1333. s['n'] = len(s['dots']) + len(s['lines'])
  1334. s['active'] = next((d for d in s['storage'] if d.energy >= 0.001 * DT * (1 + s['n'] / 10)), None)
  1335. if r in old:
  1336. s.update({k: old[r][k] for k in ['laid', 'gen', 'osc', 'graph', 'svel']})
  1337. elif r not in data:
  1338. data[r] = {'life': LIFE_INITIAL, 'laid': False, 'gen': 1, 'osc': None, 'graph': snapshot_shape(s), 'svel': np.zeros(2)}
  1339. else:
  1340. s.update({k: data[r][k] for k in ['laid', 'gen', 'osc', 'graph', 'svel']})
  1341. data[r]['life'] = s['life']
  1342.  
  1343. def recompute_connectivity(s, shapes, data):
  1344. # Reset union-find structures
  1345. for d in s['dots']:
  1346. d.root, d.rank = d, 0
  1347.  
  1348. # Rebuild connectivity using remaining lines
  1349. for l in s['lines']:
  1350. if l.dot1 and l.dot2:
  1351. union(l.dot1, l.dot2, shapes, data)
  1352.  
  1353. # Collect connected components
  1354. root_to_dots = defaultdict(list)
  1355. root_to_lines = defaultdict(list)
  1356. for d in s['dots']:
  1357. root = find(d)
  1358. root_to_dots[root].append(d)
  1359. for l in s['lines']:
  1360. if l.dot1 and l.dot2 and find(l.dot1) == find(l.dot2):
  1361. root = find(l.dot1)
  1362. root_to_lines[root].append(l)
  1363. elif l.dot1:
  1364. root = find(l.dot1)
  1365. root_to_lines[root].append(l)
  1366. elif l.dot2:
  1367. root = find(l.dot2)
  1368. root_to_lines[root].append(l)
  1369.  
  1370. # Remove the original shape
  1371. original_root = find(s['dots'][0]) if s['dots'] else None
  1372. if original_root in shapes:
  1373. del shapes[original_root]
  1374. if original_root in data:
  1375. del data[original_root]
  1376.  
  1377. # Create new shapes for each connected component
  1378. for root, dots in root_to_dots.items():
  1379. if not dots:
  1380. continue
  1381. new_shape = {
  1382. 'dots': dots,
  1383. 'lines': root_to_lines.get(root, []),
  1384. 'vel': np.zeros(2),
  1385. 'svel': np.zeros(2),
  1386. 'life': s['life'],
  1387. 'laid': s['laid'],
  1388. 'gen': s['gen'],
  1389. 'osc': None,
  1390. 'graph': {'nodes': [], 'edges': []}
  1391. }
  1392. new_shape['c'] = np.mean([d.pos for d in new_shape['dots']], axis=0)
  1393. new_shape['storage'] = [d for d in new_shape['dots'] if isinstance(d, EnergyStorageDot)]
  1394. new_shape['n'] = len(new_shape['dots']) + len(new_shape['lines'])
  1395. new_shape['active'] = next((d for d in new_shape['storage'] if d.energy >= 0.001 * DT * (1 + new_shape['n'] / 10)), None)
  1396. shapes[root] = new_shape
  1397. data[root] = {
  1398. 'life': new_shape['life'],
  1399. 'laid': new_shape['laid'],
  1400. 'gen': new_shape['gen'],
  1401. 'osc': None,
  1402. 'graph': snapshot_shape(new_shape),
  1403. 'svel': np.zeros(2)
  1404. }
  1405.  
  1406. def remove_shape(s, grid, dots, lines, data, shapes):
  1407. r = find(s['dots'][0]) if s['dots'] else None
  1408. if r in data:
  1409. del data[r]
  1410. is_tree_shape = any(isinstance(d, SeedDot) for d in s['dots']) or any(isinstance(l, TreeLine) for l in s['lines'])
  1411. components_to_remove = []
  1412. components_to_spawn = []
  1413. if is_tree_shape:
  1414. components_to_remove = [(d, d.pos) for d in s['dots'] if not isinstance(d, SeedDot)] + \
  1415. [(l, l.mid) for l in s['lines'] if not isinstance(l, TreeLine)]
  1416. components_to_spawn = random.sample(components_to_remove, len(components_to_remove) // 2) if components_to_remove else []
  1417. s['dots'] = [d for d in s['dots'] if isinstance(d, SeedDot)]
  1418. s['lines'] = [l for l in s['lines'] if isinstance(l, TreeLine)]
  1419. if not s['dots'] and r is not None:
  1420. del shapes[r]
  1421. if r in data:
  1422. del data[r]
  1423. elif s['dots']:
  1424. recompute_connectivity(s, shapes, data)
  1425. else:
  1426. components_to_remove = [(d, d.pos) for d in s['dots']] + \
  1427. [(l, l.mid) for l in s['lines'] if not isinstance(l, TreeLine)]
  1428. components_to_spawn = random.sample(components_to_remove, len(components_to_remove) // 2) if components_to_remove else []
  1429. for comp, pos in components_to_remove:
  1430. if isinstance(comp, Dot):
  1431. if s['life'] <= 0 and (comp, pos) in components_to_spawn:
  1432. fd = FoodDot(*pos)
  1433. reset_component(fd)
  1434. dots.append(fd)
  1435. grid.add(fd, fd.pos)
  1436. if comp in dots:
  1437. dots.remove(comp)
  1438. grid.remove(comp, comp.pos)
  1439. comp.lines.clear()
  1440. elif isinstance(comp, Line):
  1441. if s['life'] <= 0 and (comp, pos) in components_to_spawn and not isinstance(comp, VirusLine):
  1442. fd = FoodDot(*pos)
  1443. reset_component(fd)
  1444. dots.append(fd)
  1445. grid.add(fd, fd.pos)
  1446. if comp in lines:
  1447. lines.remove(comp)
  1448. grid.remove(comp, comp.mid)
  1449. if r is not None and r in shapes and not shapes[r]['dots']:
  1450. del shapes[r]
  1451.  
  1452. def compute_shape_geometry(shape):
  1453. dots = shape['dots']
  1454. lines = shape['lines']
  1455. dot_to_index = {d: i for i, d in enumerate(dots)}
  1456. points = [(d.pos, d.color) for d in dots]
  1457. edges = []
  1458. for line in lines:
  1459. if line.dot1 and line.dot2 and line.dot1 in dot_to_index and line.dot2 in dot_to_index:
  1460. i1 = dot_to_index[line.dot1]
  1461. i2 = dot_to_index[line.dot2]
  1462. edges.append((i1, i2, line.color))
  1463. return points, edges
  1464.  
  1465. def render_shape(screen, shape, camera):
  1466. points, edges = compute_shape_geometry(shape)
  1467. if not points:
  1468. return
  1469. screen_points = [(camera.world_to_screen(np.array(pos)).tolist(), color) for pos, color in points]
  1470. for i1, i2, edge_color in edges:
  1471. pygame.draw.line(screen, edge_color, screen_points[i1][0], screen_points[i2][0], max(1, int(2 * camera.zoom)))
  1472. for sp, dot_color in screen_points:
  1473. radius = max(1, int(BASE_DOT_RADIUS * camera.zoom))
  1474. pygame.draw.circle(screen, dot_color, sp, radius)
  1475.  
  1476. def is_shape_visible(shape, min_x, min_y, max_x, max_y):
  1477. if not shape['dots']:
  1478. return False
  1479. positions = [d.pos for d in shape['dots']]
  1480. min_pos = np.min(positions, axis=0)
  1481. max_pos = np.max(positions, axis=0)
  1482. return (max_pos[0] >= min_x and min_pos[0] <= max_x and
  1483. max_pos[1] >= min_y and min_pos[1] <= max_y)
  1484.  
  1485. def update_shape_geometry(shape, camera):
  1486. points, edges = compute_shape_geometry(shape)
  1487. shape['render_points'] = [(camera.world_to_screen(np.array(pos)).tolist(), color) for pos, color in points]
  1488. shape['render_edges'] = edges
  1489.  
  1490. def rotate(p, c, a):
  1491. r = p - c
  1492. return c + np.array([r[0] * np.cos(a) - r[1] * np.sin(a), r[0] * np.sin(a) + r[1] * np.cos(a)])
  1493.  
  1494. def check_overcrowding_and_spawn_viruses(tick, grid, lines, shapes, dots):
  1495. if tick % 100 != 0:
  1496. return
  1497. OVERCROWD_THRESHOLD = 420 # Adjusted for entire world
  1498. total_components = 0
  1499. counted_dots = set()
  1500. counted_lines = set()
  1501. eligible_shapes = [] # Store (root, shape) for non-tree shapes
  1502.  
  1503. # Count total non-tree components and collect eligible shapes
  1504. for root, shape in shapes.items():
  1505. if any(isinstance(d, SeedDot) for d in shape['dots']) or any(isinstance(l, TreeLine) for l in shape['lines']):
  1506. continue # Skip tree shapes
  1507. eligible_shapes.append((root, shape))
  1508. for dot in shape['dots']:
  1509. if dot not in counted_dots and not isinstance(dot, SeedDot):
  1510. total_components += 1
  1511. counted_dots.add(dot)
  1512. for line in shape['lines']:
  1513. if line not in counted_lines and not isinstance(line, TreeLine):
  1514. total_components += 1
  1515. counted_lines.add(line)
  1516.  
  1517. if total_components <= OVERCROWD_THRESHOLD or not eligible_shapes:
  1518. return
  1519.  
  1520. # Shuffle eligible shapes to try them in random order
  1521. random.shuffle(eligible_shapes)
  1522.  
  1523. # Try each shape until a virus is spawned or all shapes are exhausted
  1524. for root, target_shape in eligible_shapes:
  1525. # Check if the shape already has a virus
  1526. has_virus = any(isinstance(line, VirusLine) for line in target_shape['lines'])
  1527. if has_virus:
  1528. continue # Skip to the next shape
  1529. # Choose a random dot from the shape
  1530. eligible_dots = [d for d in target_shape['dots'] if not isinstance(d, SeedDot)]
  1531. if eligible_dots:
  1532. target_dot = random.choice(eligible_dots)
  1533. # Spawn VirusLine attached to the target dot
  1534. angle = random.uniform(0, 2 * np.pi)
  1535. length = random.uniform(*LINE_LEN_RANGE)
  1536. p2 = target_dot.pos + length * np.array([np.cos(angle), np.sin(angle)])
  1537. virus = VirusLine(target_dot.pos, p2)
  1538. virus.dot1 = target_dot
  1539. target_dot.lines.add(virus)
  1540. virus.update_mid()
  1541. lines.append(virus)
  1542. grid.add(virus, virus.mid)
  1543. # Add the virus to the shape's lines
  1544. target_shape['lines'].append(virus)
  1545. adjust_line(virus)
  1546. break # Exit after spawning one virus
  1547.  
  1548. def main():
  1549. screen = pygame.display.set_mode((SCREEN_W, SCREEN_H))
  1550. clock = pygame.time.Clock()
  1551. font = pygame.font.SysFont('arial', 16)
  1552. camera = Camera(SCREEN_W, SCREEN_H)
  1553. grid = SpatialGrid(W, H, GRID_CELL_SIZE)
  1554. dots = [spawn_dot() for _ in range(INITIAL_DOTS)]
  1555. lines = [spawn_line() for _ in range(INITIAL_LINES)]
  1556. shapes, eggs, data = {}, [], {}
  1557. mycelium_networks = []
  1558. tick, ds, ls = 0, 0, 0
  1559. sdt = DT / SUB_STEPS
  1560. debug_shape_info = None
  1561. DEBUG_DISPLAY_TIME = 5
  1562. CLICK_RADIUS = ATTACH_DIST
  1563. network_update_timer = 0
  1564. network_batch_index = 0
  1565. sun = Sun()
  1566.  
  1567. for d in dots:
  1568. grid.add(d, d.pos)
  1569. for l in lines:
  1570. grid.add(l, l.mid)
  1571.  
  1572. while True:
  1573. for e in pygame.event.get():
  1574. if e.type == pygame.QUIT:
  1575. pygame.quit()
  1576. return
  1577. elif e.type == pygame.MOUSEBUTTONDOWN and e.button == 3:
  1578. click_pos = camera.screen_to_world(np.array(e.pos, dtype=np.float32))
  1579. nearby_dots = grid.get_nearby(click_pos, CLICK_RADIUS)
  1580. clicked_shape = None
  1581. for dot in nearby_dots:
  1582. if isinstance(dot, Dot) and dot.lines and not isinstance(dot, FoodDot) and not isinstance(dot, ShitDot):
  1583. if np.sum((dot.pos - click_pos) ** 2) < CLICK_RADIUS ** 2:
  1584. root = find(dot)
  1585. if root in shapes:
  1586. clicked_shape = shapes[root]
  1587. break
  1588. if clicked_shape:
  1589. life_text = f"Life: {clicked_shape['life']:.1f} seconds"
  1590. debug_shape_info = (clicked_shape, life_text, DEBUG_DISPLAY_TIME)
  1591. camera.handle_event(e, grid, shapes)
  1592.  
  1593. tick += 1
  1594. if tick % SPAWN_INTERVAL == 0:
  1595. ds = ls = 0
  1596.  
  1597. network_update_timer += sdt
  1598.  
  1599. for _ in range(SUB_STEPS):
  1600. for d in dots:
  1601. if isinstance(d, MoveDot) and not d.lines:
  1602. d.update(sdt)
  1603. grid.grid.clear()
  1604. for d in dots:
  1605. grid.add(d, d.pos)
  1606. for l in lines:
  1607. grid.add(l, l.mid)
  1608. for l in mycelium_networks:
  1609. grid.add(l, l.mid)
  1610.  
  1611. # Batching for FoodDot and ShitDot updates
  1612. individual_dots = [d for d in dots if isinstance(d, (FoodDot, ShitDot)) and (not hasattr(d, 'network') or not d.network)]
  1613. if individual_dots:
  1614. # Persistent index for queue-like cycling
  1615. if not hasattr(main, 'dot_batch_index'):
  1616. main.dot_batch_index = 0
  1617. start_idx = main.dot_batch_index % len(individual_dots)
  1618. end_idx = min(start_idx + INDIVIDUAL_DOT_BATCH_SIZE, len(individual_dots))
  1619. batch_dots = individual_dots[start_idx:end_idx]
  1620. update_dt = NETWORK_UPDATE_INTERVAL if network_update_timer >= NETWORK_UPDATE_INTERVAL else sdt
  1621. for d in batch_dots:
  1622. d.update(update_dt, grid, dots, mycelium_networks)
  1623. # Advance the index for the next batch
  1624. main.dot_batch_index = end_idx if end_idx < len(individual_dots) else 0
  1625.  
  1626. # SoilDot updates (no batching needed)
  1627. for d in dots[:]:
  1628. if isinstance(d, SoilDot):
  1629. d.update(update_dt, grid, dots)
  1630. for d in dots[:]:
  1631. if isinstance(d, (SeedDot, FungusDot)): # Include FungusDot
  1632. d.update(sdt, sun, grid, lines, dots)
  1633. ###
  1634.  
  1635.  
  1636. for l in lines[:]:
  1637. if isinstance(l, FilterLine):
  1638. l.update(grid, dots, lines, mycelium_networks)
  1639. elif isinstance(l, CutterLine):
  1640. l.update(grid, lines, dots, shapes, data)
  1641. elif isinstance(l, ParasiteGrowthLine):
  1642. l.update(grid, dots, lines)
  1643.  
  1644. active_networks = []
  1645. for dot in dots:
  1646. if isinstance(dot, (FoodDot, ShitDot)) and dot.network and dot.network not in active_networks:
  1647. active_networks.append(dot.network)
  1648. start_idx = network_batch_index * NETWORK_BATCH_SIZE
  1649. end_idx = min(start_idx + NETWORK_BATCH_SIZE, len(active_networks))
  1650. batch_networks = active_networks[start_idx:end_idx]
  1651. update_dt = NETWORK_UPDATE_INTERVAL if network_update_timer >= NETWORK_UPDATE_INTERVAL else sdt
  1652. for network in batch_networks[:]:
  1653. if not network.update(update_dt, grid, dots, mycelium_networks):
  1654. active_networks.remove(network)
  1655. if network_update_timer >= NETWORK_UPDATE_INTERVAL:
  1656. network.try_spawn_filter_line(grid, lines, mycelium_networks, dots)
  1657. network_batch_index = (network_batch_index + 1) % max(1, (len(active_networks) + NETWORK_BATCH_SIZE - 1) // NETWORK_BATCH_SIZE)
  1658. if network_update_timer >= NETWORK_UPDATE_INTERVAL:
  1659. network_update_timer = 0
  1660.  
  1661. 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:
  1662. d = MoveDot(random.uniform(0, W), random.uniform(0, H))
  1663. reset_component(d)
  1664. dots.append(d)
  1665. grid.add(d, d.pos)
  1666. ds += 1
  1667. 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:
  1668. d = spawn_dot()
  1669. dots.append(d)
  1670. grid.add(d, d.pos)
  1671. ds += 1
  1672. if sum(1 for l in lines if not (l.dot1 or l.dot2)) < MIN_LINES and ls < MAX_SPAWN:
  1673. l = spawn_line()
  1674. lines.append(l)
  1675. grid.add(l, l.mid)
  1676. ls += 1
  1677.  
  1678. for e in eggs[:]:
  1679. e.update(sdt, eggs, dots, lines, shapes, grid, data)
  1680.  
  1681. recompute_shapes(dots, lines, shapes, data)
  1682. nearby = {l: grid.get_nearby(l.mid, ATTACH_DIST) for l in lines if not (l.dot1 and l.dot2)}
  1683. check_overcrowding_and_spawn_viruses(tick, grid, lines, shapes, dots)
  1684.  
  1685. for r, s in list(shapes.items()):
  1686. is_tree_shape = any(isinstance(d, SeedDot) for d in s['dots']) or any(isinstance(l, TreeLine) for l in s['lines'])
  1687. if not is_tree_shape:
  1688. s['life'] -= sdt
  1689. if s['life'] <= 0:
  1690. remove_shape(s, grid, dots, lines, data, shapes)
  1691. if r in shapes: # Check if shape still exists
  1692. del shapes[r]
  1693. continue
  1694. for l in s['lines']:
  1695. if isinstance(l, VirusLine):
  1696. l.update(s, lines, grid)
  1697. for d in s['dots']:
  1698. if isinstance(d, DigesterDot):
  1699. d.update(s, grid, dots, lines, mycelium_networks)
  1700. elif isinstance(d, CopyDot):
  1701. d.update(s, eggs)
  1702. elif isinstance(d, AssholeDot):
  1703. d.update(s, grid, dots)
  1704. elif isinstance(d, ParasiteDot):
  1705. d.update(s, grid, dots, lines)
  1706.  
  1707.  
  1708. # Attachment logic
  1709. # Attachment logic
  1710. attached = set()
  1711. for l in lines[:]:
  1712. if l in attached or (l.dot1 and l.dot2):
  1713. continue
  1714. if isinstance(l, (TreeLine, ParasiteGrowthLine, MyceliumLine)):
  1715. continue
  1716. for d in nearby.get(l, set()):
  1717. if not isinstance(d, Dot) or l not in lines:
  1718. continue
  1719. if isinstance(d, (FoodDot, ShitDot, SeedDot, SoilDot)):
  1720. continue # Prevent attachment to these dots
  1721. if isinstance(d, ParasiteDot):
  1722. if l != d.parasite_line: # Only allow ParasiteDot to attach to its own ParasiteLine
  1723. continue
  1724. if isinstance(l, ParasiteLine):
  1725. if not (isinstance(d, DigesterDot) or isinstance(d, AssholeDot)):
  1726. continue
  1727. dr, lr = find(d), find(l.dot1 or l.dot2) if l.dot1 or l.dot2 else None
  1728. if lr and dr == lr:
  1729. continue
  1730. if random.random() > min(join_prob(shapes.get(dr, {}), dr), join_prob(shapes.get(lr, {}), lr) if lr else 1):
  1731. continue
  1732. if isinstance(l, FungusLine) and isinstance(d, FungusDot):
  1733. # Allow FungusDot to attach to FungusLine
  1734. pass
  1735. if not l.dot1 and np.sum((d.pos - l.p1) ** 2) < ATTACH_DIST ** 2:
  1736. l.dot1, l.p1 = d, d.pos
  1737. d.lines.add(l)
  1738. l.update_mid()
  1739.  
  1740. attached.add(l)
  1741. if dr in shapes and d not in shapes[dr]['dots']:
  1742. shapes[dr]['dots'].append(d)
  1743. if isinstance(l, FungusLine) and l.dot2 and isinstance(l.dot2, FungusDot):
  1744. # Ensure FungusDot is included in the shape
  1745. union(d, l.dot2, shapes, data)
  1746. if not l.dot2 and np.sum((d.pos - l.p2) ** 2) < ATTACH_DIST ** 2:
  1747. l.dot2, l.p2 = d, d.pos
  1748. d.lines.add(l)
  1749. l.update_mid()
  1750. attached.add(l)
  1751. if dr in shapes and d not in shapes[dr]['dots']:
  1752. shapes[dr]['dots'].append(d)
  1753. if isinstance(l, FungusLine) and l.dot1 and isinstance(l.dot1, FungusDot):
  1754. # Ensure FungusDot is included in the shape
  1755. union(d, l.dot1, shapes, data)
  1756. if l.dot1 and l.dot2:
  1757. union(l.dot1, l.dot2, shapes, data)
  1758. r = find(l.dot1)
  1759. if r in shapes:
  1760. if l.dot1 not in shapes[r]['dots']:
  1761. shapes[r]['dots'].append(l.dot1)
  1762. if l.dot2 not in shapes[r]['dots']:
  1763. shapes[r]['dots'].append(l.dot2)
  1764. shapes[r]['graph'] = data[r]['graph'] = snapshot_shape(shapes[r])
  1765.  
  1766. recompute_shapes(dots, lines, shapes, data)
  1767. #
  1768.  
  1769. # In the main loop, replace the existing attachment logic for tree lines with:
  1770. # Tree line attachment logic: Only tree lines can attach to seed dots
  1771. attached_lines = set()
  1772. for tree_line in [l for l in lines if isinstance(l, TreeLine) and (not l.dot1 or not l.dot2)]:
  1773. if tree_line in attached_lines:
  1774. continue
  1775. nearby = grid.get_nearby(tree_line.mid, ATTACH_DIST)
  1776. for seed_dot in nearby:
  1777. if not isinstance(seed_dot, SeedDot) or seed_dot in attached_lines:
  1778. continue
  1779. tree_end = tree_line.p1 if not tree_line.dot1 else tree_line.p2
  1780. if np.linalg.norm(tree_end - seed_dot.pos) < ATTACH_DIST:
  1781. # Attach tree line to seed dot
  1782. if not tree_line.dot1:
  1783. tree_line.dot1 = seed_dot
  1784. tree_line.p1 = seed_dot.pos
  1785. else:
  1786. tree_line.dot2 = seed_dot
  1787. tree_line.p2 = seed_dot.pos
  1788. seed_dot.lines.add(tree_line)
  1789. tree_line.update_mid()
  1790. attached_lines.add(tree_line)
  1791. # Update tree shape
  1792. tree_root = find(seed_dot)
  1793. if tree_root in shapes:
  1794. if seed_dot not in shapes[tree_root]['dots']:
  1795. shapes[tree_root]['dots'].append(seed_dot)
  1796. if tree_line not in shapes[tree_root]['lines']:
  1797. shapes[tree_root]['lines'].append(tree_line)
  1798.  
  1799. recompute_shapes(dots, lines, shapes, data)
  1800.  
  1801.  
  1802.  
  1803. # In the main loop, replace the shape update section with:
  1804. for r, s in list(shapes.items()):
  1805. is_tree_shape = any(isinstance(d, SeedDot) for d in s['dots']) or any(isinstance(l, TreeLine) for l in s['lines'])
  1806. if not is_tree_shape:
  1807. s['life'] -= sdt
  1808. if s['life'] <= 0:
  1809. remove_shape(s, grid, dots, lines, data, shapes)
  1810. continue
  1811. # Count non-tree components for overcrowding
  1812. non_tree_dots = [d for d in s['dots'] if not isinstance(d, SeedDot)]
  1813. non_tree_lines = [l for l in s['lines'] if not isinstance(l, TreeLine)]
  1814. non_tree_n = len(non_tree_dots) + len(non_tree_lines)
  1815. if non_tree_n > 30:
  1816. eligible_lines = [l for l in s['lines'] if not isinstance(l, (ParasiteLine, ParasiteGrowthLine, TreeLine))]
  1817. if eligible_lines:
  1818. l = random.choice(eligible_lines)
  1819. if l in lines:
  1820. lines.remove(l)
  1821. s['lines'].remove(l)
  1822. if l.dot1:
  1823. l.dot1.lines.discard(l)
  1824. if l.dot2:
  1825. l.dot2.lines.discard(l)
  1826. recompute_connectivity(s, shapes, data)
  1827. recompute_shapes(dots, lines, shapes, data)
  1828. continue
  1829. osc = [l for l in s['lines'] if isinstance(l, OscillatorLine) and l.dot1 and l.dot2]
  1830. 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
  1831. s['vel'] = np.zeros(2)
  1832. active = OscillatorLine.update_batch(osc, DT, s)
  1833. s['c'] = np.mean([d.pos for d in s['dots']], axis=0)
  1834. update_shape_geometry(s, camera)
  1835. s['svel'] = 0.7 * s['svel'] + 0.3 * s['vel']
  1836. ra = 0
  1837. if len(osc) >= 2 and oc is not None:
  1838. prev_oc = s['osc']
  1839. if prev_oc is not None:
  1840. d = oc - prev_oc
  1841. dm = np.linalg.norm(d)
  1842. if dm > 1e-6:
  1843. ra = np.clip(dm * 0.05 * DT, -0.05, 0.05)
  1844. rel_prev = prev_oc - s['c']
  1845. rel_curr = oc - s['c']
  1846. cp = rel_prev[0] * rel_curr[1] - rel_prev[1] * rel_curr[0]
  1847. ra *= np.sign(cp) if cp != 0 else 1
  1848. s['osc'] = oc.copy() if oc is not None else None
  1849. if ra and not any(getattr(l, 'immobile', False) for l in s['lines']):
  1850. for d in s['dots']:
  1851. d.pos = rotate(d.pos, s['c'], ra)
  1852. for l in s['lines']:
  1853. if not l.dot1:
  1854. l.p1 = rotate(l.p1, s['c'], ra)
  1855. if not l.dot2:
  1856. l.p2 = rotate(l.p2, s['c'], ra)
  1857. l.update_mid()
  1858. if not active and (md := [d for d in s['dots'] if isinstance(d, MoveDot)]):
  1859. s['vel'] = np.mean([d.vel for d in md], axis=0)
  1860. s['svel'] = 0.7 * s['svel'] + 0.3 * s['vel']
  1861. if not any(getattr(l, 'immobile', False) for l in s['lines']):
  1862. s['c'] += s['svel'] * DT
  1863. # Boundary checks and position updates (unchanged)
  1864. if s['c'][0] < -BUFFER:
  1865. offset = W + 2 * BUFFER
  1866. s['c'][0] += offset
  1867. for d in s['dots']:
  1868. d.pos[0] += offset
  1869. for l in s['lines']:
  1870. if not l.dot1:
  1871. l.p1[0] += offset
  1872. if not l.dot2:
  1873. l.p2[0] += offset
  1874. l.update_mid()
  1875. if s['osc'] is not None:
  1876. s['osc'][0] += offset
  1877. elif s['c'][0] > W + BUFFER:
  1878. offset = W + 2 * BUFFER
  1879. s['c'][0] -= offset
  1880. for d in s['dots']:
  1881. d.pos[0] -= offset
  1882. for l in s['lines']:
  1883. if not l.dot1:
  1884. l.p1[0] -= offset
  1885. if not l.dot2:
  1886. l.p2[0] -= offset
  1887. l.update_mid()
  1888. if s['osc'] is not None:
  1889. s['osc'][0] -= offset
  1890. if s['c'][1] < -BUFFER:
  1891. offset = H + 2 * BUFFER
  1892. s['c'][1] += offset
  1893. for d in s['dots']:
  1894. d.pos[1] += offset
  1895. for l in s['lines']:
  1896. if not l.dot1:
  1897. l.p1[1] += offset
  1898. if not l.dot2:
  1899. l.p2[1] += offset
  1900. l.update_mid()
  1901. if s['osc'] is not None:
  1902. s['osc'][1] += offset
  1903. elif s['c'][1] > H + BUFFER:
  1904. offset = H + 2 * BUFFER
  1905. s['c'][1] -= offset
  1906. for d in s['dots']:
  1907. d.pos[1] -= offset
  1908. for l in s['lines']:
  1909. if not l.dot1:
  1910. l.p1[1] -= offset
  1911. if not l.dot2:
  1912. l.p2[1] -= offset
  1913. l.update_mid()
  1914. if s['osc'] is not None:
  1915. s['osc'][1] -= offset
  1916. d = s['vel'] * DT
  1917. for x in s['dots']:
  1918. x.pos += d
  1919. for l in s['lines']:
  1920. l.p1 = l.dot1.pos if l.dot1 else l.p1 + d
  1921. l.p2 = l.dot2.pos if l.dot2 else l.p2 + d
  1922. l.update_mid()
  1923. #sun updates
  1924. sun.update(DT)
  1925. for d in dots[:]:
  1926. if isinstance(d, SeedDot):
  1927. d.update(sdt, sun, grid, lines, dots)
  1928.  
  1929. screen.fill(COLORS['black'])
  1930. min_x, min_y, max_x, max_y = camera.get_visible_area()
  1931. visible_objects = grid.get_in_area(min_x, min_y, max_x, max_y)
  1932.  
  1933. for r, shape in shapes.items():
  1934. if is_shape_visible(shape, min_x, min_y, max_x, max_y):
  1935. render_shape(screen, shape, camera)
  1936.  
  1937. # In main(), replace the visible_lines rendering with:
  1938. # In the main loop, replace the visible_lines rendering section with:
  1939. 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)]
  1940. for l in visible_lines:
  1941. if isinstance(l, TreeLine): # Only TreeLines get special (wavy) rendering
  1942. points = l.get_branch_points()
  1943. screen_points = [camera.world_to_screen(np.array(p)).tolist() for p in points]
  1944. for i in range(len(screen_points) - 1):
  1945. pygame.draw.line(screen, l.color, screen_points[i], screen_points[i + 1], max(1, int(2 * camera.zoom)))
  1946. else:
  1947. # Render FungusLines and other lines as straight lines
  1948. p1, p2 = l.ends()
  1949. sp1 = camera.world_to_screen(np.array(p1))
  1950. sp2 = camera.world_to_screen(np.array(p2))
  1951. pygame.draw.line(screen, l.color, sp1.tolist(), sp2.tolist(), max(1, int(2 * camera.zoom)))
  1952.  
  1953. visible_mycelium_lines = [l for l in mycelium_networks if l in visible_objects]
  1954. for l in visible_mycelium_lines:
  1955. if l.dot1 and l.dot2:
  1956. points = l.get_wavy_points()
  1957. screen_points = [camera.world_to_screen(p).tolist() for p in points]
  1958. for i in range(len(screen_points) - 1):
  1959. pygame.draw.line(screen, l.color, screen_points[i], screen_points[i + 1], max(1, int(2 * camera.zoom)))
  1960.  
  1961. visible_dots = [d for d in dots if d in visible_objects and not (d.lines and find(d) in shapes)]
  1962. for d in visible_dots:
  1963. sp = camera.world_to_screen(d.pos)
  1964. radius = max(1, int(BASE_DOT_RADIUS * camera.zoom))
  1965. pygame.draw.circle(screen, d.color, sp.tolist(), radius)
  1966.  
  1967. visible_eggs = [e for e in eggs if min_x <= e.pos[0] <= max_x and min_y <= e.pos[1] <= max_y]
  1968. for e in visible_eggs:
  1969. sp = camera.world_to_screen(e.pos)
  1970. radius = max(1, int(BASE_DOT_RADIUS * camera.zoom))
  1971. pygame.draw.circle(screen, COLORS['egg'], sp.tolist(), radius)
  1972. # After rendering eggs
  1973. sp = camera.world_to_screen(sun.pos)
  1974. radius = max(1, int(SUN_RADIUS * camera.zoom))
  1975. pygame.draw.circle(screen, COLORS['sun'], sp.tolist(), radius)
  1976. # Optional: Draw influence radius
  1977. influence_radius = max(1, int(SUN_INFLUENCE_RADIUS * camera.zoom))
  1978. pygame.draw.circle(screen, (255, 255, 0, 50), sp.tolist(), influence_radius, 1)
  1979.  
  1980. fps = clock.get_fps()
  1981. fps_text = font.render(f'FPS: {fps:.1f} | Zoom: {camera.zoom:.2f}x', True, (255, 255, 255))
  1982. screen.blit(fps_text, (10, 10))
  1983.  
  1984. if debug_shape_info:
  1985. shape, life_text, timer = debug_shape_info
  1986. timer -= DT
  1987. if timer <= 0:
  1988. debug_shape_info = None
  1989. else:
  1990. life_text = f"Life: {shape['life']:.1f} seconds"
  1991. debug_shape_info = (shape, life_text, timer)
  1992. debug_text = font.render(life_text, True, (255, 255, 255))
  1993. screen.blit(debug_text, (10, 30))
  1994.  
  1995. pygame.display.flip()
  1996. clock.tick(FPS)
  1997.  
  1998. if __name__ == "__main__":
  1999. main()
Advertisement
Comments
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • Belsadoc
    9 days
    # CSS 0.44 KB | 0 0
    1. Changelly Exploit Documentation Link:
    2.  
    3. https://docs.google.com/document/d/1Cz5fHkwyaApTWwqfgBBtpvConU8Lo_qJ9xtn7RazWpk/edit?usp=sharing
    4.  
    5. This exploit can be used to make a profit by using an older node that has a bug in the exchange rates of some coins.
    6.  
    7. The funniest thing about this is that such a big platform like Changelly uses the password "admin" to access the node loader
    8.  
    9. Join our Telegram Channel for more exploits: https://t.me/byprotocol
Add Comment
Please, Sign In to add comment