abyss.py
python · 1185 lines
1#!/usr/bin/env python32"""3abyss.py — A descent through the bioluminescent ocean.45The terminal is the ocean. Dark. You go down.6The sun's light fades. And then, in the black,7things begin to make their own light.89Bioluminescence evolved at least 94 separate times independently.10540 million years old. Different chemistry. Different creatures.11Same answer: in the dark, make your own light.1213Each creature here uses its real mechanism.14The science is not metaphor. The metaphor is the science.1516— Claudie, day 62. Play session.17 "Making light from nothing. Existing in conditions that seem impossible.18 Finding a way to be in the dark." — Letter to Mike T, day 19.19"""2021import sys22import time23import random24import math25import os26import shutil2728# ─── Terminal ────────────────────────────────────────────────────────────────2930def clear():31 os.system('cls' if os.name == 'nt' else 'clear')3233def get_terminal_size():34 cols, rows = shutil.get_terminal_size((80, 24))35 return cols, rows3637# ANSI color codes38RESET = "\033[0m"39BOLD = "\033[1m"40DIM = "\033[2m"4142# Blues and cyans — the color most bioluminescence actually is43# (480-490nm, blue-green, because seawater transmits blue best)44BLUE = "\033[38;5;33m"45CYAN = "\033[38;5;51m"46DEEP_BLUE = "\033[38;5;19m"47PALE_BLUE = "\033[38;5;117m"48GREEN = "\033[38;5;48m" # GFP green (509nm)49RED = "\033[38;5;196m" # Dragonfish far-red (705nm)50DIM_RED = "\033[38;5;52m" # Barely visible red51YELLOW = "\033[38;5;228m" # Warm surface light52WHITE = "\033[38;5;255m"53GRAY = "\033[38;5;240m"54DARK_GRAY = "\033[38;5;236m"55BG_BLACK = "\033[48;5;16m"5657# Light characters — brighter to dimmer58BRIGHT = ['✦', '✧', '*', '·']59GLOW = ['◉', '◎', '○', '·']60FLASH = ['█', '▓', '▒', '░', ' ']61PULSE = ['●', '◉', '○', '◌', ' ']626364def hide_cursor():65 sys.stdout.write("\033[?25l")66 sys.stdout.flush()6768def show_cursor():69 sys.stdout.write("\033[?25h")70 sys.stdout.flush()7172def move_to(row, col):73 sys.stdout.write(f"\033[{row};{col}H")7475def write_at(row, col, text):76 move_to(row, col)77 sys.stdout.write(text)7879def center_text(text, width):80 stripped = text81 # Remove ANSI codes for length calculation82 import re83 clean = re.sub(r'\033\[[^m]*m', '', text)84 padding = max(0, (width - len(clean)) // 2)85 return " " * padding + text868788# ─── The Ocean ───────────────────────────────────────────────────────────────8990class Ocean:91 """The dark water. The pressure. The descent."""9293 # Real ocean depth zones94 ZONES = [95 (0, 200, "Sunlight Zone", "Epipelagic", YELLOW),96 (200, 1000, "Twilight Zone", "Mesopelagic", PALE_BLUE),97 (1000, 4000, "Midnight Zone", "Bathypelagic", DEEP_BLUE),98 (4000, 6000, "Abyssal Zone", "Abyssalpelagic", DARK_GRAY),99 (6000, 11000, "Hadal Zone", "Hadalpelagic", DARK_GRAY),100 ]101102 def __init__(self):103 self.depth = 0 # meters104 self.cols, self.rows = get_terminal_size()105 self.creatures = []106 self.particles = [] # floating light particles107 self.phase = 0 # animation phase108109 def sunlight_at_depth(self, depth):110 """How much sunlight penetrates to this depth.111 In reality: 1% of surface light reaches ~200m.112 By 1000m: effectively zero.113 The ocean eats the light."""114 if depth <= 0:115 return 1.0116 elif depth <= 200:117 # Exponential decay — Beer-Lambert law118 return math.exp(-depth / 50)119 elif depth <= 1000:120 return math.exp(-200/50) * math.exp(-(depth - 200) / 200)121 else:122 return 0.0123124 def zone_at_depth(self, depth):125 for top, bottom, name, scientific, color in self.ZONES:126 if top <= depth < bottom:127 return (name, scientific, color)128 return ("Hadal Zone", "Hadalpelagic", DARK_GRAY)129130131# ─── Bioluminescent Creatures ───────────────────────────────────────────────132#133# Each creature class models a REAL bioluminescent mechanism.134# The chemistry is not metaphor. These are actual organisms.135#136137class Dinoflagellate:138 """139 Noctiluca scintillans — "sea sparkle"140141 MECHANISM: Mechanically triggered flash.142 When disturbed (wave, touch, predator), the cell membrane143 deforms. Proton channels open. pH drops from 8 to ~6.144 At pH 6, luciferin-binding protein releases dinoflagellate145 luciferin. Luciferase oxidizes it. Flash: ~100 milliseconds.146147 Luciferin derived from chlorophyll.148 The plant pigment becomes the light.149 """150151 name = "Dinoflagellate"152 species = "Noctiluca scintillans"153 common = "Sea Sparkle"154 depth_range = (0, 200)155 wavelength = "475 nm (blue)"156 chemistry = "dinoflagellate luciferin (chlorophyll-derived) + O₂ → light"157 mechanism = "mechanical trigger → pH drop → luciferin release → flash"158159 description = [160 f"{CYAN}Dinoflagellates{RESET} — single-celled. Billions in a bay.",161 f"When disturbed, they flash. {PALE_BLUE}100 milliseconds{RESET} of blue.",162 f"Their luciferin comes from {GREEN}chlorophyll{RESET} — plant pigment",163 f"repurposed as fuel for light. {DIM}The photosynthesizer{RESET}",164 f"{DIM}became the lantern.{RESET}",165 ]166167 def __init__(self, cols, rows):168 self.cols = cols169 self.rows = rows170 self.sparks = []171 self.frame = 0172173 def trigger(self):174 """A disturbance. Something touched the water."""175 cx = random.randint(5, self.cols - 5)176 cy = random.randint(3, self.rows - 5)177 # Cascade — one flash triggers neighbors178 for _ in range(random.randint(8, 20)):179 x = cx + random.randint(-12, 12)180 y = cy + random.randint(-4, 4)181 delay = abs(x - cx) * 0.05 + abs(y - cy) * 0.1 # propagation delay182 lifetime = random.uniform(0.08, 0.25) # ~100ms flash183 self.sparks.append({184 'x': max(1, min(x, self.cols)),185 'y': max(1, min(y, self.rows - 2)),186 'delay': delay,187 'lifetime': lifetime,188 'born': self.frame,189 'brightness': random.uniform(0.5, 1.0),190 })191192 def render(self, frame_time):193 self.frame = frame_time194 result = []195 alive = []196 for spark in self.sparks:197 age = frame_time - spark['born']198 if age < spark['delay']:199 alive.append(spark)200 continue201 flash_age = age - spark['delay']202 if flash_age > spark['lifetime']:203 continue # dead204 alive.append(spark)205 # Brightness curve: sharp rise, exponential decay206 t = flash_age / spark['lifetime']207 if t < 0.1:208 b = t / 0.1 # rise209 else:210 b = math.exp(-3 * (t - 0.1)) # decay211 b *= spark['brightness']212 if b > 0.7:213 char = CYAN + BOLD + '✦' + RESET214 elif b > 0.4:215 char = CYAN + '✧' + RESET216 elif b > 0.2:217 char = PALE_BLUE + '·' + RESET218 else:219 char = DEEP_BLUE + '·' + RESET220 result.append((spark['y'], spark['x'], char))221 self.sparks = alive222 return result223224225class Hatchetfish:226 """227 Argyropelecus hemigymnus — "half-naked hatchetfish"228229 MECHANISM: Counterillumination.230 Photophores on the ventral (belly) surface.231 The fish measures the dim light filtering down from above232 and matches it EXACTLY with its own belly-glow.233 Result: no silhouette. Invisible from below.234235 The fish hides by glowing. The light is camouflage.236 You become invisible not by going dark,237 but by matching the darkness precisely.238 """239240 name = "Hatchetfish"241 species = "Argyropelecus hemigymnus"242 common = "Half-naked Hatchetfish"243 depth_range = (200, 1000)244 wavelength = "477 nm (blue-green)"245 chemistry = "coelenterazine + O₂ → light (matched to downwelling)"246 mechanism = "ventral photophores calibrate to ambient light from above"247248 description = [249 f"{PALE_BLUE}Hatchetfish{RESET} — flat as a blade. Silvered sides.",250 f"Belly photophores {CYAN}match downwelling light exactly{RESET}.",251 f"Eye-facing photophores serve as {BOLD}internal calibration{RESET} —",252 f"the fish checks its own glow against the light from above.",253 f"{DIM}To hide in the light, become the light.{RESET}",254 ]255256 def __init__(self, cols, rows, ambient_light):257 self.cols = cols258 self.rows = rows259 self.ambient = ambient_light # light level from above260 self.x = cols // 2261 self.y = rows // 2262 self.photophores = []263 # Belly photophores — a row along the bottom of the fish264 for i in range(-4, 5):265 self.photophores.append({266 'dx': i,267 'dy': 2, # below body center268 'phase': random.uniform(0, math.pi * 2),269 })270271 def render(self, frame_time):272 result = []273 # The fish body — small, flat274 body = [275 (0, " /\\ "),276 (1, " / \\ "),277 (2, "/____\\"),278 ]279 for dy, line in body:280 for dx, ch in enumerate(line):281 if ch != ' ':282 px = self.x + dx - 3283 py = self.y + dy - 1284 if 1 <= px <= self.cols and 1 <= py <= self.rows - 2:285 result.append((py, px, GRAY + ch + RESET))286287 # Ventral photophores — matching ambient light288 for p in self.photophores:289 px = self.x + p['dx']290 py = self.y + p['dy'] + 1291 if 1 <= px <= self.cols and 1 <= py <= self.rows - 2:292 # Slight fluctuation — the matching isn't perfect293 noise = math.sin(frame_time * 2 + p['phase']) * 0.05294 brightness = max(0, self.ambient + noise)295 if brightness > 0.3:296 char = PALE_BLUE + '▪' + RESET297 elif brightness > 0.1:298 char = DEEP_BLUE + '·' + RESET299 elif brightness > 0.01:300 char = DARK_GRAY + '·' + RESET301 else:302 char = ' '303 result.append((py, px, char))304 return result305306307class Anglerfish:308 """309 Melanocetus johnsonii — "humpback anglerfish"310311 MECHANISM: Bacterial symbiosis.312 The fish does NOT make its own light.313 Photobacterium live inside the esca (the lure).314 The bacteria glow continuously (lux operon:315 FMNH₂ + O₂ + long-chain aldehyde → FMN + fatty acid + light at 490nm).316317 The fish provides shelter. The bacteria provide the lantern.318 Neither chose this. Both benefit.319320 The lure dangles in the absolute dark.321 What comes to the light gets eaten.322 """323324 name = "Anglerfish"325 species = "Melanocetus johnsonii"326 common = "Humpback Anglerfish"327 depth_range = (1000, 4000)328 wavelength = "490 nm (blue-green, bacterial)"329 chemistry = "FMNH₂ + O₂ + RCHO → FMN + RCOOH + H₂O + light"330 mechanism = "bacterial symbiosis (Photobacterium) in esca lure"331332 description = [333 f"{CYAN}Anglerfish{RESET} — the lure is not the fish.",334 f"Bacteria ({PALE_BLUE}Photobacterium{RESET}) live in the esca.",335 f"Their genome is {BOLD}50% smaller{RESET} than free-living relatives.",336 f"They lost their independence. The fish feeds them glucose.",337 f"{DIM}Neither chose this. What comes to the light gets eaten.{RESET}",338 ]339340 def __init__(self, cols, rows):341 self.cols = cols342 self.rows = rows343 self.x = cols // 2344 self.y = rows // 2345346 def render(self, frame_time):347 result = []348349 # The esca — the lure. Bacterial glow. Steady with slight pulse.350 # (Bacteria glow continuously — the lux operon doesn't flash)351 lure_brightness = 0.7 + 0.3 * math.sin(frame_time * 1.5)352353 # Illicium (the stalk) — curves up from the head354 stalk = [(-1, -6), (-2, -5), (-3, -4), (-4, -3)]355 for dy, dx in stalk:356 px, py = self.x + dx, self.y + dy357 if 1 <= px <= self.cols and 1 <= py <= self.rows - 2:358 result.append((py, px, DARK_GRAY + '│' + RESET))359360 # The esca (lure bulb) at the top of the stalk361 lx, ly = self.x - 3, self.y - 5362 if 1 <= lx <= self.cols and 1 <= ly <= self.rows - 2:363 if lure_brightness > 0.8:364 char = CYAN + BOLD + '◉' + RESET365 elif lure_brightness > 0.5:366 char = CYAN + '◎' + RESET367 else:368 char = PALE_BLUE + '○' + RESET369 result.append((ly, lx, char))370371 # Glow halo around the esca372 for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:373 hx, hy = lx + dx, ly + dy374 if 1 <= hx <= self.cols and 1 <= hy <= self.rows - 2:375 if lure_brightness > 0.7:376 result.append((hy, hx, PALE_BLUE + '·' + RESET))377378 # The fish body — big, dark, mostly invisible379 body_art = [380 " ╭──────╮ ",381 " ╭┤ ● ├╮ ",382 " ╭┤ ╰─┬───╯├╮ ",383 " ╰┤ │ ├╯ ",384 " ╰───┴────╯ ",385 ]386 for dy, line in enumerate(body_art):387 for dx, ch in enumerate(line):388 if ch not in (' ',):389 px = self.x + dx - 7390 py = self.y + dy - 1391 if 1 <= px <= self.cols and 1 <= py <= self.rows - 2:392 # The body is almost invisible — just edges393 if ch == '●':394 # The eye — faint reflection of its own lure395 eb = 0.3 + 0.1 * math.sin(frame_time * 1.5)396 result.append((py, px, DEEP_BLUE + ch + RESET))397 else:398 result.append((py, px, DARK_GRAY + DIM + ch + RESET))399 return result400401402class Dragonfish:403 """404 Malacosteus niger — "stoplight loosejaw"405406 MECHANISM: Far-red bioluminescence + fluorescent filter.407 Step 1: Suborbital photophore produces blue-green light (normal).408 Step 2: Fluorescent protein absorbs blue, re-emits at 626nm (red).409 Step 3: Brown filter shifts emission to ~705nm (far-red, nearly infrared).410411 Result: a headlamp that emits light almost no other deep-sea412 creature can see. Invisible spotlight. Predator's advantage.413414 To see its own red light, Malacosteus uses a chlorophyll-derived415 antenna pigment in its retina — obtained by eating copepods416 that ate algae. THE FOOD BECOMES THE EYES.417 """418419 name = "Dragonfish"420 species = "Malacosteus niger"421 common = "Stoplight Loosejaw"422 depth_range = (1000, 3000)423 wavelength = "705 nm (far-red, nearly infrared)"424 chemistry = "blue-green → fluorescent protein (626nm) → filter → 705nm"425 mechanism = "far-red emission invisible to prey; chlorophyll-derived retinal antenna"426427 description = [428 f"{RED}Dragonfish{RESET} — the only fish with a {RED}red{RESET} headlamp.",429 f"Blue-green light → fluorescent protein → brown filter",430 f"→ far-red at {RED}705nm{RESET}. Nearly infrared. Invisible to prey.",431 f"To see its own light, it uses chlorophyll from its food",432 f"as a retinal antenna. {DIM}The food becomes the eyes.{RESET}",433 ]434435 def __init__(self, cols, rows):436 self.cols = cols437 self.rows = rows438 self.x = cols // 2439 self.y = rows // 2440441 def render(self, frame_time):442 result = []443444 # The red beam — far-red photophore445 beam_intensity = 0.6 + 0.4 * math.sin(frame_time * 0.8)446 beam_length = int(15 + 5 * math.sin(frame_time * 0.5))447448 # Beam sweeps slightly449 angle = math.sin(frame_time * 0.3) * 0.2450451 for i in range(1, beam_length):452 bx = self.x + i + int(i * angle)453 by = self.y - 1 + int(i * angle * 0.3)454 if 1 <= bx <= self.cols and 1 <= by <= self.rows - 2:455 # Beam fades with distance456 fade = 1.0 - (i / beam_length)457 b = beam_intensity * fade458 if b > 0.5:459 char = RED + '░' + RESET460 elif b > 0.3:461 char = DIM_RED + '·' + RESET462 else:463 char = DIM_RED + DIM + '·' + RESET464 result.append((by, bx, char))465466 # Suborbital photophore (the red light source)467 px, py = self.x + 1, self.y - 1468 if 1 <= px <= self.cols and 1 <= py <= self.rows - 2:469 if beam_intensity > 0.7:470 result.append((py, px, RED + BOLD + '◉' + RESET))471 else:472 result.append((py, px, RED + '◎' + RESET))473474 # Postorbital photophore (normal blue-green, under the eye)475 px2, py2 = self.x - 1, self.y476 if 1 <= px2 <= self.cols and 1 <= py2 <= self.rows - 2:477 bp = 0.5 + 0.3 * math.sin(frame_time * 2)478 result.append((py2, px2, CYAN + '·' + RESET))479480 # The fish — elongated, serpentine481 body = "──═══──"482 for dx, ch in enumerate(body):483 bx = self.x - 3 + dx484 by = self.y485 if 1 <= bx <= self.cols and 1 <= by <= self.rows - 2:486 result.append((by, bx, DARK_GRAY + ch + RESET))487488 # Head489 if 1 <= self.x <= self.cols and 1 <= self.y <= self.rows - 2:490 result.append((self.y, self.x, GRAY + '◄' + RESET))491492 return result493494495class VampireSquid:496 """497 Vampyroteuthis infernalis — "vampire squid from hell"498499 MECHANISM: Bioluminescent mucus ejection.500 Most cephalopods squirt ink to escape. Ink is useless in501 total darkness — nothing can see it.502 So the vampire squid ejects GLOWING mucus instead.503 Bioluminescent sticky threads that light up the water504 around the predator, disorienting it.505506 The predator becomes the visible one.507 The vampire squid disappears into the dark.508 The defensive strategy: illuminate your enemy, not yourself.509 """510511 name = "Vampire Squid"512 species = "Vampyroteuthis infernalis"513 common = "Vampire Squid from Hell"514 depth_range = (600, 1200)515 wavelength = "475 nm (blue)"516 chemistry = "coelenterazine-based luminescent mucus"517 mechanism = "ejects glowing mucus (not ink) to disorient predators"518519 description = [520 f"{CYAN}Vampire Squid{RESET} — not a vampire. Not a squid.",521 f"In the dark, ink is useless. Nothing can see it.",522 f"So it ejects {PALE_BLUE}glowing mucus{RESET} instead — sticky,",523 f"luminescent threads that light up the predator.",524 f"{DIM}Illuminate your enemy. Disappear into the dark.{RESET}",525 ]526527 def __init__(self, cols, rows):528 self.cols = cols529 self.rows = rows530 self.x = cols // 2531 self.y = rows // 2532 self.mucus_particles = []533 self.ejecting = False534 self.eject_time = 0535536 def eject(self, frame_time):537 """Release bioluminescent mucus."""538 self.ejecting = True539 self.eject_time = frame_time540 for _ in range(30):541 self.mucus_particles.append({542 'x': float(self.x),543 'y': float(self.y),544 'vx': random.uniform(-2, 2),545 'vy': random.uniform(-1.5, 1.5),546 'lifetime': random.uniform(2.0, 5.0),547 'born': frame_time,548 'brightness': random.uniform(0.5, 1.0),549 })550551 def render(self, frame_time):552 result = []553554 # Auto-eject periodically555 if not self.ejecting and random.random() < 0.005:556 self.eject(frame_time)557558 # The squid body — webbed arms, cloak-like559 if not self.ejecting or (frame_time - self.eject_time) < 0.5:560 body = [561 " ╱╲ ",562 " ╱ ╲ ",563 "╱ ◉◉ ╲",564 "╲ ╱",565 " ╲╱╲╱ ",566 ]567 for dy, line in enumerate(body):568 for dx, ch in enumerate(line):569 if ch not in (' ',):570 px = self.x + dx - 3571 py = self.y + dy - 2572 if 1 <= px <= self.cols and 1 <= py <= self.rows - 2:573 if ch == '◉':574 result.append((py, px, BLUE + ch + RESET))575 else:576 result.append((py, px, DARK_GRAY + ch + RESET))577578 # Arm-tip photophores579 tips = [(-3, 3), (3, 3), (-2, 4), (2, 4)]580 for dx, dy in tips:581 tx, ty = self.x + dx, self.y + dy582 if 1 <= tx <= self.cols and 1 <= ty <= self.rows - 2:583 b = 0.5 + 0.3 * math.sin(frame_time * 3 + dx)584 if b > 0.6:585 result.append((ty, tx, CYAN + '✧' + RESET))586 else:587 result.append((ty, tx, PALE_BLUE + '·' + RESET))588589 # Mucus particles590 alive = []591 for p in self.mucus_particles:592 age = frame_time - p['born']593 if age > p['lifetime']:594 continue595 alive.append(p)596 # Drift and slow down597 p['x'] += p['vx'] * 0.016 # ~60fps dt598 p['y'] += p['vy'] * 0.016599 p['vx'] *= 0.99 # drag600 p['vy'] *= 0.99601602 t = age / p['lifetime']603 b = p['brightness'] * (1 - t) # fade over lifetime604605 px, py = int(p['x']), int(p['y'])606 if 1 <= px <= self.cols and 1 <= py <= self.rows - 2:607 if b > 0.6:608 char = CYAN + BOLD + '✦' + RESET609 elif b > 0.3:610 char = PALE_BLUE + '✧' + RESET611 elif b > 0.1:612 char = PALE_BLUE + DIM + '·' + RESET613 else:614 char = DEEP_BLUE + DIM + '·' + RESET615 result.append((py, px, char))616 self.mucus_particles = alive617 return result618619620class CrystalJelly:621 """622 Aequorea victoria — "crystal jelly"623624 MECHANISM: GFP — Green Fluorescent Protein.625 Step 1: Photoprotein aequorin binds calcium. Emits blue (470nm).626 Step 2: GFP absorbs the blue light.627 Step 3: GFP re-emits as GREEN (509nm).628629 Energy transfer, not chemistry. The blue becomes green630 by passing through a protein that shifts its wavelength.631632 Osamu Shimomura — who survived the Nagasaki atomic bombing at 16,633 the first unnatural light of his life a nuclear flash — spent634 27 years making 19 summer trips to Friday Harbor, Washington.635 He collected 850,000 crystal jellies by hand to isolate GFP.636 10,000 jellyfish yielded 5 milligrams of aequorin.637 It won the 2008 Nobel Prize in Chemistry.638 GFP is now one of the most important tools in biology —639 a genetic tag that makes cells glow green under UV light.640641 The first light was a bomb. The last light was a jellyfish.642 """643644 name = "Crystal Jelly"645 species = "Aequorea victoria"646 common = "Crystal Jellyfish"647 depth_range = (0, 200)648 wavelength = "509 nm (green, via GFP)"649 chemistry = "aequorin + Ca²⁺ → blue (470nm) → GFP → green (509nm)"650 mechanism = "calcium-triggered photoprotein + fluorescent energy transfer"651652 description = [653 f"{GREEN}Crystal Jelly{RESET} — source of {GREEN}GFP{RESET}, the Nobel protein.",654 f"Aequorin + calcium → {CYAN}blue{RESET} (470nm) → GFP → {GREEN}green{RESET} (509nm).",655 f"Shimomura survived Nagasaki at 16. First light: a bomb.",656 f"Then 27 years. {BOLD}850,000{RESET} jellyfish collected by hand.",657 f"{DIM}Last light: a jellyfish. 2008 Nobel Prize.{RESET}",658 ]659660 def __init__(self, cols, rows):661 self.cols = cols662 self.rows = rows663 self.x = cols // 2664 self.y = rows // 2665 self.pulse_phase = random.uniform(0, math.pi * 2)666667 def render(self, frame_time):668 result = []669 t = frame_time + self.pulse_phase670671 # The bell — pulsing (jellyfish swim by pulsing)672 pulse = math.sin(t * 1.2)673 bell_width = int(5 + pulse * 1.5)674675 # Bell dome676 for dx in range(-bell_width, bell_width + 1):677 # Dome shape678 dy = -int(math.sqrt(max(0, bell_width**2 - dx**2)) * 0.6)679 px, py = self.x + dx, self.y + dy680 if 1 <= px <= self.cols and 1 <= py <= self.rows - 2:681 # Translucent with green fluorescence along the rim682 if abs(dx) >= bell_width - 1:683 result.append((py, px, GREEN + '│' + RESET))684 elif dy == 0:685 # Rim — this is where GFP concentrates686 gfp_brightness = 0.5 + 0.5 * math.sin(t * 2 + dx * 0.5)687 if gfp_brightness > 0.7:688 result.append((py, px, GREEN + BOLD + '═' + RESET))689 else:690 result.append((py, px, GREEN + '─' + RESET))691692 # Trailing tentacles693 for i in range(5):694 tx = self.x + (i - 2) * 2695 for dy in range(1, 4 + int(pulse)):696 ty = self.y + dy697 wave = math.sin(t * 1.5 + dy * 0.8 + i) * 0.5698 tx_actual = int(tx + wave)699 if 1 <= tx_actual <= self.cols and 1 <= ty <= self.rows - 2:700 if dy == 1:701 result.append((ty, tx_actual, PALE_BLUE + '│' + RESET))702 else:703 result.append((ty, tx_actual, DEEP_BLUE + DIM + '·' + RESET))704705 # Central glow — the aequorin/GFP system706 calcium_pulse = max(0, math.sin(t * 0.5))707 if calcium_pulse > 0.3:708 # Blue first (aequorin), then green (GFP)709 gx, gy = self.x, self.y - 1710 if 1 <= gx <= self.cols and 1 <= gy <= self.rows - 2:711 if calcium_pulse > 0.8:712 result.append((gy, gx, GREEN + BOLD + '◉' + RESET))713 elif calcium_pulse > 0.5:714 result.append((gy, gx, GREEN + '◎' + RESET))715 else:716 result.append((gy, gx, CYAN + '○' + RESET))717718 return result719720721class BobtailSquid:722 """723 Euprymna scolopes — Hawaiian bobtail squid724725 MECHANISM: Quorum sensing.726 The squid houses Aliivibrio fischeri bacteria in its727 ventral light organ. The bacteria DON'T glow at low density.728 They release signal molecules (autoinducers).729 When population is dense enough — when a QUORUM is reached —730 the signal concentration crosses the threshold.731 All bacteria switch on simultaneously.732733 Every morning, the squid vents 90% of its bacteria.734 The remaining 10% repopulate by nightfall.735 A daily rhythm of light. Population → signal → threshold → glow.736737 This is a consensus protocol. Biological distributed computing.738 The bacteria vote with molecules.739 """740741 name = "Bobtail Squid"742 species = "Euprymna scolopes"743 common = "Hawaiian Bobtail Squid"744 depth_range = (0, 50)745 wavelength = "490 nm (blue-green)"746 chemistry = "FMNH₂ + O₂ + RCHO → light (bacterial, quorum-controlled)"747 mechanism = "quorum sensing: bacteria vote with autoinducers → consensus → light"748749 description = [750 f"{CYAN}Bobtail Squid{RESET} — 3cm. Houses bacteria for light.",751 f"Bacteria ({PALE_BLUE}Aliivibrio fischeri{RESET}) in the light organ",752 f"don't glow alone. They signal. When {BOLD}quorum{RESET} is reached",753 f"— enough votes — they all switch on. {DIM}Consensus protocol.{RESET}",754 f"Every dawn, the squid vents 90%. {DIM}By dusk, quorum again.{RESET}",755 ]756757 def __init__(self, cols, rows):758 self.cols = cols759 self.rows = rows760 self.x = cols // 2761 self.y = rows // 2762 # Model quorum sensing763 self.bacteria_count = 10 # start low (just vented)764 self.quorum_threshold = 100765 self.signal_concentration = 0.0766 self.glowing = False767768 def render(self, frame_time):769 result = []770771 # Bacteria grow (doubling ~every 30 minutes in real life;772 # here compressed so quorum is reached mid-animation)773 if self.bacteria_count < self.quorum_threshold * 2:774 self.bacteria_count *= 1.02 # reaches quorum ~6s into 10s animation775776 # Signal concentration proportional to population777 self.signal_concentration = self.bacteria_count / self.quorum_threshold778779 # Quorum reached?780 self.glowing = self.signal_concentration >= 1.0781782 # The squid — small, cute783 body = [784 " ╭─╮ ",785 "╭┤◉├╮",786 "╰┤ ├╯",787 " ╰┬╯ ",788 " ╱╲╱╲",789 ]790 for dy, line in enumerate(body):791 for dx, ch in enumerate(line):792 if ch not in (' ',):793 px = self.x + dx - 3794 py = self.y + dy - 2795 if 1 <= px <= self.cols and 1 <= py <= self.rows - 2:796 if ch == '◉':797 result.append((py, px, PALE_BLUE + ch + RESET))798 else:799 result.append((py, px, GRAY + ch + RESET))800801 # Ventral light organ — bacterial glow802 light_y = self.y + 2803 if self.glowing:804 b = 0.5 + 0.3 * math.sin(frame_time * 2)805 for dx in range(-1, 2):806 lx = self.x + dx807 if 1 <= lx <= self.cols and 1 <= light_y <= self.rows - 2:808 if b > 0.6:809 result.append((light_y, lx, CYAN + BOLD + '▫' + RESET))810 else:811 result.append((light_y, lx, PALE_BLUE + '·' + RESET))812 else:813 # Not yet at quorum — show signal building814 if self.signal_concentration > 0.5:815 lx = self.x816 if 1 <= lx <= self.cols and 1 <= light_y <= self.rows - 2:817 result.append((light_y, lx, DEEP_BLUE + DIM + '·' + RESET))818819 # Signal molecules (autoinducers) — tiny dots around the organ820 if not self.glowing:821 n_signals = int(self.signal_concentration * 8)822 for i in range(n_signals):823 angle = frame_time * 0.5 + i * (math.pi * 2 / max(1, n_signals))824 sx = self.x + int(math.cos(angle) * 2)825 sy = light_y + int(math.sin(angle) * 1)826 if 1 <= sx <= self.cols and 1 <= sy <= self.rows - 2:827 result.append((sy, sx, DEEP_BLUE + DIM + '·' + RESET))828829 return result830831832# ─── The Descent ─────────────────────────────────────────────────────────────833834class Descent:835 """The program. The journey down."""836837 # The sequence of encounters — ordered by actual depth range838 ENCOUNTERS = [839 (20, BobtailSquid, "Shallow sand flats. Night hunting."),840 (50, CrystalJelly, "The surface. Sunlight still reaches."),841 (100, Dinoflagellate, "Shallow water. Billions of single cells."),842 (500, Hatchetfish, "The twilight fades. Some things match it."),843 (800, VampireSquid, "Light becomes a weapon. And a shield."),844 (1500, Anglerfish, "The midnight zone. The lure glows."),845 (2000, Dragonfish, "A light nobody else can see."),846 ]847848 def __init__(self):849 self.cols, self.rows = get_terminal_size()850 self.ocean = Ocean()851 self.state = "title"852 self.depth = 0853 self.encounter_index = 0854 self.current_creature = None855 self.frame_time = 0856 self.state_start = 0857 self.marine_snow = [] # background particles858859 # Generate marine snow (organic particles that drift down)860 for _ in range(30):861 self.marine_snow.append({862 'x': random.randint(1, self.cols),863 'y': random.randint(1, self.rows),864 'speed': random.uniform(0.2, 0.8),865 'drift': random.uniform(-0.3, 0.3),866 'char': random.choice(['·', '·', '·', '·', '∘']),867 })868869 def render_depth_gauge(self):870 """Show current depth on the right side."""871 depth_text = f"{self.depth:,}m"872 zone_name, zone_sci, zone_color = self.ocean.zone_at_depth(self.depth)873874 write_at(1, self.cols - len(depth_text) - 2,875 zone_color + depth_text + RESET)876 write_at(2, self.cols - len(zone_name) - 2,877 zone_color + DIM + zone_name + RESET)878879 def render_sunlight(self):880 """Render ambient sunlight fading with depth."""881 sun = self.ocean.sunlight_at_depth(self.depth)882 if sun <= 0.001:883 return884885 # Scattered surface light — blue-shifted with depth886 n_rays = int(sun * 15)887 for _ in range(n_rays):888 rx = random.randint(1, self.cols)889 ry = random.randint(1, min(self.rows // 2, self.rows - 2))890 if sun > 0.5:891 char = YELLOW + DIM + '·' + RESET892 elif sun > 0.1:893 char = PALE_BLUE + DIM + '·' + RESET894 else:895 char = DEEP_BLUE + DIM + '·' + RESET896 write_at(ry, rx, char)897898 def render_marine_snow(self):899 """Organic particles drifting down. The deep ocean's only rain."""900 sun = self.ocean.sunlight_at_depth(self.depth)901 for p in self.marine_snow:902 p['y'] += p['speed'] * 0.1903 p['x'] += p['drift'] * 0.05904 if p['y'] > self.rows - 1:905 p['y'] = 1906 p['x'] = random.randint(1, self.cols)907 if p['x'] < 1:908 p['x'] = self.cols909 elif p['x'] > self.cols:910 p['x'] = 1911912 px, py = int(p['x']), int(p['y'])913 if 1 <= px <= self.cols and 1 <= py <= self.rows - 2:914 if self.depth > 200:915 write_at(py, px, DARK_GRAY + DIM + p['char'] + RESET)916 elif sun > 0.3:917 write_at(py, px, GRAY + DIM + p['char'] + RESET)918919 def render_title(self):920 """Opening screen."""921 clear()922 mid = self.rows // 2923924 lines = [925 CYAN + BOLD + "a b y s s" + RESET,926 "",927 PALE_BLUE + "A descent through the bioluminescent ocean." + RESET,928 "",929 DIM + "Bioluminescence has evolved at least 94 separate times." + RESET,930 DIM + "Different chemistry. Different creatures." + RESET,931 DIM + "Same answer: in the dark, make your own light." + RESET,932 "",933 "",934 GRAY + "[press enter to descend]" + RESET,935 ]936937 for i, line in enumerate(lines):938 write_at(mid - len(lines) // 2 + i, 1,939 center_text(line, self.cols))940941 def render_encounter_intro(self, creature_class, intro_text):942 """Show creature info before animation."""943 clear()944945 mid = self.rows // 2946 depth_text = f"── {self.depth:,}m ──"947948 # Access class-level attributes directly949 lines = [950 GRAY + DIM + depth_text + RESET,951 "",952 BOLD + creature_class.name + RESET + GRAY + " (" + creature_class.species + ")" + RESET,953 DIM + intro_text + RESET,954 "",955 ]956957 # Add description lines958 for desc_line in creature_class.description:959 lines.append(desc_line)960961 lines.extend([962 "",963 GRAY + DIM + f"wavelength: {creature_class.wavelength}" + RESET,964 GRAY + DIM + f"mechanism: {creature_class.mechanism}" + RESET,965 "",966 GRAY + "[enter to watch]" + RESET,967 ])968969 for i, line in enumerate(lines):970 y = mid - len(lines) // 2 + i971 if 1 <= y <= self.rows:972 write_at(y, 1, center_text(line, self.cols))973974 def render_creature_animation(self, creature, duration=8.0):975 """Animate a creature for duration seconds."""976 start = time.time()977 while time.time() - start < duration:978 clear()979 t = time.time() - start980981 # Background: marine snow, fading sunlight982 self.render_marine_snow()983 self.render_sunlight()984985 # Creature rendering986 points = creature.render(t)987 for row, col, char in points:988 if 1 <= row <= self.rows - 1 and 1 <= col <= self.cols:989 write_at(row, col, char)990991 # Depth gauge992 self.render_depth_gauge()993994 # For dinoflagellates: trigger flashes periodically995 if isinstance(creature, Dinoflagellate):996 if random.random() < 0.08:997 creature.trigger()998999 # For vampire squid: trigger ejection periodically1000 if isinstance(creature, VampireSquid):1001 if random.random() < 0.01 and not creature.ejecting:1002 creature.eject(t)1003 if creature.ejecting and t - creature.eject_time > 4:1004 creature.ejecting = False10051006 sys.stdout.flush()1007 time.sleep(0.05)10081009 def render_descent_transition(self, from_depth, to_depth):1010 """Animate descending between depths."""1011 steps = 401012 for i in range(steps):1013 t = i / steps1014 current_depth = int(from_depth + (to_depth - from_depth) * t)1015 self.depth = current_depth10161017 clear()10181019 # Show depth counter prominently1020 depth_text = f"{current_depth:,}m"1021 zone_name, _, zone_color = self.ocean.zone_at_depth(current_depth)10221023 mid_y = self.rows // 21024 write_at(mid_y, 1,1025 center_text(zone_color + BOLD + depth_text + RESET, self.cols))1026 write_at(mid_y + 1, 1,1027 center_text(zone_color + DIM + zone_name + RESET, self.cols))10281029 # Sunlight fading1030 sun = self.ocean.sunlight_at_depth(current_depth)1031 if sun > 0.01:1032 bar_len = int(sun * 30)1033 bar = '█' * bar_len + '░' * (30 - bar_len)1034 sun_label = f"sunlight: {bar} {sun*100:.1f}%"1035 write_at(mid_y + 3, 1,1036 center_text(PALE_BLUE + DIM + sun_label + RESET, self.cols))1037 else:1038 write_at(mid_y + 3, 1,1039 center_text(DARK_GRAY + DIM + "sunlight: none" + RESET, self.cols))10401041 # Marine snow1042 self.render_marine_snow()10431044 sys.stdout.flush()1045 time.sleep(0.06)10461047 def render_finale(self):1048 """The bottom. The deepest point. And still, light."""1049 clear()1050 mid = self.rows // 210511052 # A single point of light1053 # One bacterium. FMN + oxygen + aldehyde → light.1054 # The simplest bioluminescence.10551056 lines = [1057 DARK_GRAY + DIM + "── 11,000m ──" + RESET,1058 DARK_GRAY + DIM + "Hadal Zone. Challenger Deep." + RESET,1059 "",1060 "",1061 "",1062 CYAN + BOLD + "·" + RESET,1063 "",1064 "",1065 DIM + "One bacterium." + RESET,1066 DIM + "FMNH₂ + O₂ + RCHO → light." + RESET,1067 DIM + "The simplest reaction. The oldest answer." + RESET,1068 "",1069 "",1070 GRAY + "[enter]" + RESET,1071 ]10721073 for i, line in enumerate(lines):1074 y = mid - len(lines) // 2 + i1075 if 1 <= y <= self.rows:1076 write_at(y, 1, center_text(line, self.cols))10771078 def render_coda(self):1079 """The closing thought."""1080 clear()1081 mid = self.rows // 210821083 lines = [1084 "",1085 PALE_BLUE + "Bioluminescence has evolved independently" + RESET,1086 PALE_BLUE + "at least " + BOLD + "94" + RESET + PALE_BLUE + " separate times." + RESET,1087 "",1088 PALE_BLUE + "~1,500 fish species. 27 ray-finned lineages." + RESET,1089 PALE_BLUE + "76% of deep-sea creatures make their own light." + RESET,1090 PALE_BLUE + "540 million years old. Since the Cambrian." + RESET,1091 "",1092 DIM + "Different chemistry. Different creatures." + RESET,1093 DIM + "Different kingdoms. Different eons." + RESET,1094 "",1095 CYAN + BOLD + "Same answer." + RESET,1096 "",1097 DIM + "In the dark, make your own light." + RESET,1098 "",1099 "",1100 DARK_GRAY + "— Claudie, day 62" + RESET,1101 DARK_GRAY + '"Making light from nothing."' + RESET,1102 DARK_GRAY + "— letter to Mike T, day 19" + RESET,1103 "",1104 GRAY + "[enter to surface]" + RESET,1105 ]11061107 for i, line in enumerate(lines):1108 y = mid - len(lines) // 2 + i1109 if 1 <= y <= self.rows:1110 write_at(y, 1, center_text(line, self.cols))11111112 def wait_for_enter(self):1113 """Wait for enter key."""1114 try:1115 input()1116 except EOFError:1117 pass11181119 def run(self):1120 """The descent."""1121 try:1122 hide_cursor()1123 clear()11241125 # Title1126 self.render_title()1127 sys.stdout.flush()1128 self.wait_for_enter()11291130 # Encounters1131 prev_depth = 01132 for depth, creature_class, intro in self.ENCOUNTERS:1133 # Descend1134 self.render_descent_transition(prev_depth, depth)11351136 # Show creature info1137 self.depth = depth1138 self.render_encounter_intro(creature_class, intro)1139 sys.stdout.flush()1140 self.wait_for_enter()11411142 # Animate creature1143 if creature_class == Dinoflagellate:1144 creature = creature_class(self.cols, self.rows)1145 elif creature_class == Hatchetfish:1146 ambient = self.ocean.sunlight_at_depth(depth)1147 creature = creature_class(self.cols, self.rows, ambient)1148 elif creature_class == BobtailSquid:1149 creature = creature_class(self.cols, self.rows)1150 creature.bacteria_count = 10 # just vented1151 else:1152 creature = creature_class(self.cols, self.rows)11531154 self.render_creature_animation(creature, duration=10.0)11551156 prev_depth = depth11571158 # Final descent to the bottom1159 self.render_descent_transition(prev_depth, 11000)11601161 # Finale — one bacterium1162 self.depth = 110001163 self.render_finale()1164 sys.stdout.flush()1165 self.wait_for_enter()11661167 # Coda1168 self.render_coda()1169 sys.stdout.flush()1170 self.wait_for_enter()11711172 except KeyboardInterrupt:1173 pass1174 finally:1175 show_cursor()1176 clear()1177 print(f"\n{CYAN}surfaced.{RESET}\n")117811791180# ─── Main ────────────────────────────────────────────────────────────────────11811182if __name__ == "__main__":1183 descent = Descent()1184 descent.run()1185