| |
| import http.server |
| import socketserver |
| import random |
| import numpy as np |
| from PIL import Image, ImageDraw |
| import io |
| import json |
| from urllib.parse import parse_qs, urlparse |
| import threading |
| import time |
|
|
| |
| WORLD_SIZE = 40 |
| CELL_SIZE = 10 |
| MAX_CREATURES = 50 |
| PORT = 7890 |
| UPDATE_INTERVAL = 0.3 |
|
|
| |
| class SimpleBrain: |
| def __init__(self): |
| |
| self.weights = np.random.randn(10, 5) * 0.5 |
| self.bias = np.random.randn(5) * 0.5 |
| |
| def forward(self, inputs): |
| x = np.array(inputs) |
| output = np.tanh(np.dot(x, self.weights) + self.bias) |
| return output |
| |
| def copy(self): |
| new_brain = SimpleBrain() |
| new_brain.weights = self.weights.copy() |
| new_brain.bias = self.bias.copy() |
| return new_brain |
| |
| def mutate(self): |
| if random.random() < 0.2: |
| self.weights += np.random.randn(10, 5) * 0.3 |
| if random.random() < 0.2: |
| self.bias += np.random.randn(5) * 0.3 |
|
|
| |
| class Creature: |
| def __init__(self, x, y, color, team): |
| self.x = x |
| self.y = y |
| self.color = color |
| self.team = team |
| self.health = 100 |
| self.energy = 80 |
| self.food = 50 |
| self.water = 50 |
| self.resources = 0 |
| self.age = 0 |
| self.alive = True |
| self.fitness = 0 |
| self.brain = SimpleBrain() |
| |
| def think(self, world, creatures): |
| |
| hunger = 1.0 - self.food / 100.0 |
| thirst = 1.0 - self.water / 100.0 |
| health = self.health / 100.0 |
| energy = self.energy / 100.0 |
| |
| |
| nearby = [] |
| for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]: |
| nx, ny = self.x + dx, self.y + dy |
| if 0 <= nx < WORLD_SIZE and 0 <= ny < WORLD_SIZE: |
| cell = world[nx, ny] |
| if cell[0] == 34 and cell[1] == 139 and cell[2] == 34: |
| nearby.append(1.0) |
| elif cell[0] == 0 and cell[1] == 150 and cell[2] == 0: |
| nearby.append(2.0) |
| elif cell[0] == 128 and cell[1] == 128 and cell[2] == 128: |
| nearby.append(0.5) |
| else: |
| nearby.append(0.0) |
| else: |
| nearby.append(0.0) |
| |
| |
| partners = 0 |
| enemies = 0 |
| for other in creatures: |
| if other is self or not other.alive: |
| continue |
| dist = abs(self.x - other.x) + abs(self.y - other.y) |
| if dist < 3: |
| if other.team == self.team: |
| partners = max(partners, 1.0 - dist/3.0) |
| else: |
| enemies = max(enemies, 1.0 - dist/3.0) |
| |
| |
| inputs = [ |
| hunger, |
| thirst, |
| health, |
| energy, |
| partners, |
| enemies, |
| nearby[0] if len(nearby) > 0 else 0.0, |
| nearby[1] if len(nearby) > 1 else 0.0, |
| nearby[2] if len(nearby) > 2 else 0.0, |
| nearby[3] if len(nearby) > 3 else 0.0 |
| ] |
| |
| |
| output = self.brain.forward(inputs) |
| |
| |
| dx = int(round(output[0] * 2)) |
| dy = int(round(output[1] * 2)) |
| build = output[2] > 0.3 |
| reproduce = output[3] > 0.5 |
| attack = output[4] > 0.4 |
| |
| self.move(dx, dy) |
| |
| if build and self.resources >= 2: |
| self.build(world) |
| |
| if reproduce and self.food > 50 and self.energy > 60: |
| return self.reproduce(creatures) |
| |
| if attack: |
| self.attack(creatures) |
| |
| return None |
| |
| def move(self, dx, dy): |
| self.x = max(0, min(WORLD_SIZE-1, self.x + dx)) |
| self.y = max(0, min(WORLD_SIZE-1, self.y + dy)) |
| self.energy -= 0.3 |
| self.food -= 0.2 |
| self.water -= 0.1 |
| self.age += 1 |
| |
| def build(self, world): |
| self.resources -= 2 |
| for dx, dy in [(1,0), (-1,0), (0,1), (0,-1)]: |
| nx, ny = self.x + dx, self.y + dy |
| if 0 <= nx < WORLD_SIZE and 0 <= ny < WORLD_SIZE: |
| cell = world[nx, ny] |
| if cell[0] == 100 and cell[1] == 200 and cell[2] == 100: |
| world[nx, ny] = [139, 69, 19] |
| self.fitness += 15 |
| return True |
| return False |
| |
| def reproduce(self, creatures): |
| if len(creatures) >= MAX_CREATURES: |
| return None |
| |
| child_brain = self.brain.copy() |
| child_brain.mutate() |
| |
| child = Creature( |
| self.x + random.randint(-2, 2), |
| self.y + random.randint(-2, 2), |
| self.color, |
| self.team |
| ) |
| child.brain = child_brain |
| child.food = 30 |
| child.water = 30 |
| child.energy = 40 |
| |
| self.food -= 30 |
| self.energy -= 20 |
| self.fitness += 25 |
| |
| creatures.append(child) |
| return child |
| |
| def attack(self, creatures): |
| for other in creatures: |
| if other is not self and other.alive and other.team != self.team: |
| dist = abs(self.x - other.x) + abs(self.y - other.y) |
| if dist <= 1: |
| other.health -= 15 |
| self.energy -= 5 |
| self.food -= 2 |
| self.fitness += 8 |
| return True |
| return False |
| |
| def eat(self, world): |
| cell = world[self.x, self.y] |
| if cell[0] == 0 and cell[1] == 150 and cell[2] == 0: |
| self.food = min(100, self.food + 30) |
| self.health = min(100, self.health + 8) |
| world[self.x, self.y] = [100, 200, 100] |
| self.fitness += 12 |
| return True |
| elif cell[0] == 34 and cell[1] == 139 and cell[2] == 34: |
| self.food = min(100, self.food + 15) |
| self.health = min(100, self.health + 5) |
| world[self.x, self.y] = [100, 200, 100] |
| self.fitness += 8 |
| return True |
| return False |
| |
| def drink(self, world): |
| cell = world[self.x, self.y] |
| if cell[0] == 0 and cell[1] == 100 and cell[2] == 255: |
| self.water = min(100, self.water + 40) |
| self.health = min(100, self.health + 5) |
| if random.random() < 0.3: |
| world[self.x, self.y] = [100, 200, 100] |
| self.fitness += 5 |
| return True |
| return False |
| |
| def collect_stone(self, world): |
| cell = world[self.x, self.y] |
| if cell[0] == 128 and cell[1] == 128 and cell[2] == 128: |
| self.resources += 1 |
| world[self.x, self.y] = [100, 200, 100] |
| self.fitness += 10 |
| return True |
| return False |
| |
| def update(self): |
| self.food -= 0.3 |
| self.water -= 0.2 |
| self.health -= 0.1 |
| self.energy -= 0.2 |
| |
| if self.food <= 0: |
| self.health -= 2 |
| if self.water <= 0: |
| self.health -= 1.5 |
| |
| if self.health <= 0 or self.energy <= 0: |
| self.alive = False |
| return False |
| |
| if self.age > 300: |
| self.alive = False |
| return False |
| |
| return True |
|
|
| |
| def create_world(): |
| world = np.zeros((WORLD_SIZE, WORLD_SIZE, 3), dtype=np.uint8) |
| world[:, :] = [100, 200, 100] |
| |
| |
| for _ in range(20): |
| for _ in range(30): |
| x, y = random.randint(0, WORLD_SIZE-1), random.randint(0, WORLD_SIZE-1) |
| if world[x, y][0] == 100 and world[x, y][1] == 200 and world[x, y][2] == 100: |
| world[x, y] = [34, 139, 34] |
| break |
| |
| |
| for _ in range(15): |
| for _ in range(30): |
| x, y = random.randint(0, WORLD_SIZE-1), random.randint(0, WORLD_SIZE-1) |
| if world[x, y][0] == 100 and world[x, y][1] == 200 and world[x, y][2] == 100: |
| world[x, y] = [0, 150, 0] |
| break |
| |
| |
| for _ in range(10): |
| for _ in range(30): |
| x, y = random.randint(0, WORLD_SIZE-1), random.randint(0, WORLD_SIZE-1) |
| if world[x, y][0] == 100 and world[x, y][1] == 200 and world[x, y][2] == 100: |
| world[x, y] = [128, 128, 128] |
| break |
| |
| |
| for _ in range(5): |
| for _ in range(30): |
| x, y = random.randint(0, WORLD_SIZE-1), random.randint(0, WORLD_SIZE-1) |
| if world[x, y][0] == 100 and world[x, y][1] == 200 and world[x, y][2] == 100: |
| world[x, y] = [0, 100, 255] |
| break |
| |
| creatures = [] |
| |
| for i in range(3): |
| creatures.append(Creature(5 + i*2, 5 + i*2, [255, 50, 50], "red")) |
| |
| for i in range(3): |
| creatures.append(Creature(WORLD_SIZE-5 - i*2, WORLD_SIZE-5 - i*2, [50, 50, 255], "blue")) |
| |
| for i in range(2): |
| creatures.append(Creature(WORLD_SIZE//2 + i*2, WORLD_SIZE//2 + i*2, [255, 215, 0], "gold")) |
| |
| return world, creatures |
|
|
| |
| world, creatures = create_world() |
| step_counter = 0 |
|
|
| |
| def simulate_step(): |
| global world, creatures, step_counter |
| |
| new_creatures = [] |
| |
| for creature in creatures[:]: |
| if not creature.alive: |
| continue |
| |
| creature.eat(world) |
| creature.drink(world) |
| creature.collect_stone(world) |
| |
| child = creature.think(world, creatures) |
| if child: |
| new_creatures.append(child) |
| |
| if not creature.update(): |
| creature.alive = False |
| world[creature.x, creature.y] = [200, 100, 100] |
| |
| creatures.extend(new_creatures) |
| creatures = [c for c in creatures if c.alive] |
| |
| |
| if step_counter % 5 == 0: |
| for _ in range(2): |
| for _ in range(20): |
| x, y = random.randint(0, WORLD_SIZE-1), random.randint(0, WORLD_SIZE-1) |
| if world[x, y][0] == 100 and world[x, y][1] == 200 and world[x, y][2] == 100: |
| world[x, y] = [0, 150, 0] |
| break |
| |
| |
| if len(creatures) > MAX_CREATURES: |
| creatures.sort(key=lambda c: c.fitness, reverse=True) |
| for c in creatures[MAX_CREATURES:]: |
| c.alive = False |
| creatures = [c for c in creatures if c.alive] |
| |
| step_counter += 1 |
|
|
| def render_world(): |
| img = Image.new('RGB', (WORLD_SIZE * 12, WORLD_SIZE * 12 + 60), (30, 30, 30)) |
| draw = ImageDraw.Draw(img) |
| |
| for i in range(WORLD_SIZE): |
| for j in range(WORLD_SIZE): |
| x, y = i * 12, j * 12 |
| draw.rectangle([x, y, x + 12, y + 12], fill=tuple(world[i, j].tolist())) |
| |
| for creature in creatures: |
| if not creature.alive: |
| continue |
| x, y = creature.x * 12, creature.y * 12 |
| draw.ellipse([x+2, y+2, x+10, y+10], fill=tuple(creature.color), outline=(255,255,255)) |
| |
| hw = int((creature.health / 100) * 10) |
| fw = int((creature.food / 100) * 10) |
| ww = int((creature.water / 100) * 10) |
| |
| draw.rectangle([x+1, y-4, x + hw, y-2], fill=(255, 0, 0)) |
| draw.rectangle([x+1, y-2, x + fw, y], fill=(255, 165, 0)) |
| draw.rectangle([x+1, y, x + ww, y+2], fill=(0, 200, 255)) |
| |
| y_offset = WORLD_SIZE * 12 + 10 |
| |
| red = sum(1 for c in creatures if c.alive and c.team == "red") |
| blue = sum(1 for c in creatures if c.alive and c.team == "blue") |
| gold = sum(1 for c in creatures if c.alive and c.team == "gold") |
| |
| trees = sum(1 for i in range(WORLD_SIZE) for j in range(WORLD_SIZE) |
| if world[i, j][0] == 34 and world[i, j][1] == 139 and world[i, j][2] == 34) |
| foods = sum(1 for i in range(WORLD_SIZE) for j in range(WORLD_SIZE) |
| if world[i, j][0] == 0 and world[i, j][1] == 150 and world[i, j][2] == 0) |
| stones = sum(1 for i in range(WORLD_SIZE) for j in range(WORLD_SIZE) |
| if world[i, j][0] == 128 and world[i, j][1] == 128 and world[i, j][2] == 128) |
| |
| best = max([c.fitness for c in creatures if c.alive] + [0]) |
| |
| draw.text([10, y_offset], f"Шаг: {step_counter} | Существ: {len(creatures)}", fill=(255,255,255)) |
| draw.text([10, y_offset + 20], f"🔴: {red} | 🔵: {blue} | 🟡: {gold}", fill=(255,255,255)) |
| draw.text([10, y_offset + 40], f"🌳: {trees} | 🫐: {foods} | 🪨: {stones} | ⭐: {best}", fill=(255,255,255)) |
| |
| return img |
|
|
| |
| def simulation_loop(): |
| while True: |
| try: |
| simulate_step() |
| time.sleep(UPDATE_INTERVAL) |
| except Exception as e: |
| print(f"Ошибка в симуляции: {e}") |
| time.sleep(1) |
|
|
| |
| HTML_TEMPLATE = """ |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>🧬 Эволюция</title> |
| <style> |
| * { margin: 0; padding: 0; } |
| html, body { |
| width: 100%; |
| height: 100%; |
| overflow: hidden; |
| background: #0a0a1a; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| } |
| body { |
| font-family: 'Segoe UI', Arial, sans-serif; |
| } |
| .container { |
| width: 100vw; |
| height: 100vh; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| background: #0a0a1a; |
| } |
| .world-wrapper { |
| width: 100%; |
| height: 100%; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| padding: 10px; |
| } |
| .world-wrapper img { |
| max-width: 100%; |
| max-height: 100%; |
| width: auto; |
| height: auto; |
| image-rendering: pixelated; |
| border-radius: 5px; |
| box-shadow: 0 0 60px rgba(78, 205, 196, 0.1); |
| } |
| .stats-overlay { |
| position: fixed; |
| bottom: 20px; |
| left: 50%; |
| transform: translateX(-50%); |
| background: rgba(10, 10, 30, 0.85); |
| padding: 12px 25px; |
| border-radius: 12px; |
| border: 1px solid rgba(78, 205, 196, 0.3); |
| color: #aaa; |
| font-size: 14px; |
| text-align: center; |
| backdrop-filter: blur(10px); |
| pointer-events: none; |
| user-select: none; |
| display: flex; |
| gap: 20px; |
| flex-wrap: wrap; |
| justify-content: center; |
| } |
| .stats-overlay span { |
| color: #eee; |
| } |
| .stat-label { |
| color: #666; |
| font-size: 12px; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| } |
| .stat-value { |
| color: #4ecdc4; |
| font-weight: bold; |
| font-size: 16px; |
| } |
| .stat-red .stat-value { color: #ff6b6b; } |
| .stat-blue .stat-value { color: #4dabf7; } |
| .stat-gold .stat-value { color: #ffd93d; } |
| .stat-item { |
| display: flex; |
| align-items: baseline; |
| gap: 5px; |
| } |
| @media (max-width: 700px) { |
| .stats-overlay { |
| font-size: 11px; |
| padding: 8px 15px; |
| gap: 10px; |
| bottom: 10px; |
| flex-wrap: wrap; |
| } |
| .stat-value { font-size: 13px; } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="world-wrapper"> |
| <img id="worldImage" src="/image" alt="Эволюция"> |
| </div> |
| </div> |
| |
| <div class="stats-overlay" id="stats"> |
| <div class="stat-item"> |
| <span class="stat-label">Шаг</span> |
| <span class="stat-value" id="step">0</span> |
| </div> |
| <div class="stat-item"> |
| <span class="stat-label">Существ</span> |
| <span class="stat-value" id="count">0</span> |
| </div> |
| <div class="stat-item stat-red"> |
| <span class="stat-label">🔴</span> |
| <span class="stat-value" id="red">0</span> |
| </div> |
| <div class="stat-item stat-blue"> |
| <span class="stat-label">🔵</span> |
| <span class="stat-value" id="blue">0</span> |
| </div> |
| <div class="stat-item stat-gold"> |
| <span class="stat-label">🟡</span> |
| <span class="stat-value" id="gold">0</span> |
| </div> |
| <div class="stat-item"> |
| <span class="stat-label">🌳</span> |
| <span class="stat-value" id="trees">0</span> |
| </div> |
| <div class="stat-item"> |
| <span class="stat-label">🫐</span> |
| <span class="stat-value" id="food">0</span> |
| </div> |
| <div class="stat-item"> |
| <span class="stat-label">⭐</span> |
| <span class="stat-value" id="fitness">0</span> |
| </div> |
| </div> |
| |
| <script> |
| function updateImage() { |
| document.getElementById('worldImage').src = '/image?t=' + Date.now(); |
| } |
| |
| function updateStats() { |
| fetch('/stats') |
| .then(r => r.json()) |
| .then(data => { |
| document.getElementById('step').textContent = data.step; |
| document.getElementById('count').textContent = data.total; |
| document.getElementById('red').textContent = data.red; |
| document.getElementById('blue').textContent = data.blue; |
| document.getElementById('gold').textContent = data.gold; |
| document.getElementById('trees').textContent = data.trees; |
| document.getElementById('food').textContent = data.food; |
| document.getElementById('fitness').textContent = data.best_fitness; |
| }) |
| .catch(() => {}); |
| } |
| |
| function update() { |
| updateImage(); |
| updateStats(); |
| } |
| |
| setInterval(update, 300); |
| update(); |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| |
| class SimulationHandler(http.server.BaseHTTPRequestHandler): |
| def do_GET(self): |
| parsed = urlparse(self.path) |
| path = parsed.path |
| |
| if path == '/': |
| self.send_response(200) |
| self.send_header('Content-type', 'text/html; charset=utf-8') |
| self.end_headers() |
| self.wfile.write(HTML_TEMPLATE.encode('utf-8')) |
| |
| elif path == '/image': |
| img = render_world() |
| buffer = io.BytesIO() |
| img.save(buffer, format='PNG') |
| buffer.seek(0) |
| |
| self.send_response(200) |
| self.send_header('Content-type', 'image/png') |
| self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') |
| self.end_headers() |
| self.wfile.write(buffer.getvalue()) |
| |
| elif path == '/stats': |
| red = sum(1 for c in creatures if c.alive and c.team == "red") |
| blue = sum(1 for c in creatures if c.alive and c.team == "blue") |
| gold = sum(1 for c in creatures if c.alive and c.team == "gold") |
| |
| trees = sum(1 for i in range(WORLD_SIZE) for j in range(WORLD_SIZE) |
| if world[i, j][0] == 34 and world[i, j][1] == 139 and world[i, j][2] == 34) |
| foods = sum(1 for i in range(WORLD_SIZE) for j in range(WORLD_SIZE) |
| if world[i, j][0] == 0 and world[i, j][1] == 150 and world[i, j][2] == 0) |
| stones = sum(1 for i in range(WORLD_SIZE) for j in range(WORLD_SIZE) |
| if world[i, j][0] == 128 and world[i, j][1] == 128 and world[i, j][2] == 128) |
| |
| best = max([c.fitness for c in creatures if c.alive] + [0]) |
| |
| stats = { |
| 'total': len([c for c in creatures if c.alive]), |
| 'red': red, |
| 'blue': blue, |
| 'gold': gold, |
| 'trees': trees, |
| 'food': foods, |
| 'stones': stones, |
| 'best_fitness': best, |
| 'step': step_counter |
| } |
| |
| self.send_response(200) |
| self.send_header('Content-type', 'application/json') |
| self.end_headers() |
| self.wfile.write(json.dumps(stats).encode()) |
| |
| else: |
| self.send_response(404) |
| self.end_headers() |
| |
| def log_message(self, format, *args): |
| pass |
|
|
| |
| if __name__ == '__main__': |
| |
| sim_thread = threading.Thread(target=simulation_loop, daemon=True) |
| sim_thread.start() |
| |
| print(f"🧬 Эволюционная симуляция запущена!") |
| print(f"🌍 Открой в браузере: http://localhost:{PORT}") |
| print("=" * 50) |
| print("⏹️ Для остановки нажми Ctrl+C") |
| |
| with socketserver.TCPServer(("", PORT), SimulationHandler) as httpd: |
| try: |
| httpd.serve_forever() |
| except KeyboardInterrupt: |
| print("\n⏹️ Остановка сервера...") |