Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@ panel:
x: 50
y: 50
grid:
cell_size: 25
cell_size: 25
mating:
min_age_percent: 0.2
min_energy_level: 0.75
max_range: 5
mutation_chance: 0.05
mutation_multiply_border: 0.2
mutation_addding_border: 0.05
101 changes: 11 additions & 90 deletions src/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,6 @@
from environment import Environment
import uuid

def _clamp(x: float, lo: float, hi: float) -> float:
if x < lo:
return lo
if x > hi:
return hi
return x


def _tanh(x: float) -> float:
return math.tanh(x)


def _sqrt_scale(points: float, k: float) -> float:
return k * math.sqrt(max(0.0, points))


def _dist2(ax: float, ay: float, bx: float, by: float) -> float:
dx = ax - bx
dy = ay - by
return dx * dx + dy * dy



def _clamp(x: float, lo: float, hi: float) -> float:
"""Clamps value x between lo and hi."""
Expand All @@ -52,8 +30,8 @@ def _sqrt_scale(points: float, k: float) -> float:
return k * math.sqrt(max(0.0, points))


def _dist2(ax: float, ay: float, bx: float, by: float) -> float:
"""Returns squared Euclidean distance between (ax, ay) and (bx, by)."""
def dist2(ax: float, ay: float, bx: float, by: float) -> float:
"""Returns squared Euclidean dist2ance between (ax, ay) and (bx, by)."""
dx = ax - bx
dy = ay - by
return dx * dx + dy * dy
Expand All @@ -70,12 +48,12 @@ class Agent:
# 4 y_n -> y position normalized to world height (0..1)
# 5 head_x -> facing direction x (cos(angle))
# 6 head_y -> facing direction y (-sin(angle))
# 7 food_d_n -> distance to nearest food normalized by sight (0..1)
# 7 food_d_n -> dist2ance to nearest food normalized by sight (0..1)
# 8 food_dx_n -> x direction to nearest food (normalized)
# 9 food_dy_n -> y direction to nearest food (normalized)
# 10 friend_count_n -> nearby friends count (normalized)
# 11 enemy_count_n -> nearby enemies count (normalized)
# 12 enemy_d_n -> distance to nearest enemy normalized by sight (0..1)
# 12 enemy_d_n -> dist2ance to nearest enemy normalized by sight (0..1)
# 13 enemy_dx_n -> x direction to nearest enemy (normalized)
# 14 enemy_dy_n -> y direction to nearest enemy (normalized)
# 15 bias -> constant bias input (always 1.0)
Expand All @@ -92,17 +70,14 @@ class Agent:

body_points_total = 100

distance_to_partner_to_mate = 0.05
#parameters to reproduction
chance_for_mutation = 0.05
mutation_multiply_border = 0.2 # random.uniform(1-var,1+var)
mutation_addding_border = 0.05

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()

def __init__(self, position: tuple, environment : Environment,/, decision_matrix : typing.List[typing.List[int]] = None, genome = None ):
from mating import Mating

self.environment = environment
self.mate_module :Mating = Mating(self)
self.x = float(position[0])
self.y = float(position[1])

Expand Down Expand Up @@ -145,7 +120,7 @@ def __init__(self, position: tuple, environment : Environment,/, decision_matrix
self._last_food = None

def _random_body_points(self, total: int):
"""Randomly distribute total points across body stat keys."""
"""Randomly dist2ribute total points across body stat keys."""
keys = ["hp", "energy", "speed", "attack", "lifespan", "sight", "agility"]
pts = {k: 0 for k in keys}
for _ in range(total):
Expand Down Expand Up @@ -188,7 +163,7 @@ def sense(self, foods=None, agents=None):
for f in foods:
fx = float(getattr(f, "x", 0.0))
fy = float(getattr(f, "y", 0.0))
d2 = _dist2(self.x, self.y, fx, fy)
d2 = dist2(self.x, self.y, fx, fy)
if d2 < best_d2:
best_d2 = d2
best = (fx, fy)
Expand Down Expand Up @@ -222,7 +197,7 @@ def sense(self, foods=None, agents=None):
continue
ax = float(getattr(a, "x", 0.0))
ay = float(getattr(a, "y", 0.0))
d2 = _dist2(self.x, self.y, ax, ay)
d2 = dist2(self.x, self.y, ax, ay)
if d2 > r2:
continue

Expand Down Expand Up @@ -370,16 +345,7 @@ def _attack(self):
self.energy = max(0.0, self.energy - 0.08)

def _mate(self):
from environment import Environment
self.energy = max(0.0, self.energy - 0.3)
agents : typing.List[Agent] = self.environment.get_agents()
close_agents : typing.List[Agent] = []
for ag in agents:
if _dist2(ag.x, ag.y, self.x, self.y) < self.distance_to_partner_to_mate:
close_agents.append(ag)
if len(close_agents) > 0:
matrix, vector = self.create_new_genes( random.choice(close_agents))
self.environment.create_agent(Agent((self.x, self.y), self.environment, decision_matrix = matrix, genome = vector))
self.mate_module.mate()


def _tick_body(self):
Expand Down Expand Up @@ -424,51 +390,6 @@ def update(self, speed_modifier):

self._tick_body()

def create_new_genes(self, second : Agent):
matrix = self.create_new_decision_matrix(second)
vector = self.create_new_genome_vector(second)

numbers_to_change_in_matrix = int(len(matrix) * len(matrix[0]) / (1 / self.chance_for_mutation))

#Applying mutation - there could be multiply of value or adding

for _ in range(numbers_to_change_in_matrix):
row = random.choice(matrix)
i = random.randint(0, len(row)-1)
if random.random() < 0.2:
row[i] *= random.uniform(1 - self.mutation_multiply_border, 1 + self.mutation_multiply_border)
else:
row[i] += random.uniform(-self.mutation_addding_border, self.mutation_addding_border)

for _ in range(int(len(vector) / (1 / self.chance_for_mutation))):
item = random.choice(vector.keys())
if random.random() < 0.2:
vector[item] *= random.uniform(1 - self.mutation_multiply_border, 1 + self.mutation_multiply_border)
else:
vector[item] += random.uniform(-self.mutation_addding_border, self.mutation_addding_border)

#Normalisation of vector

normalisation = 100 / sum(vector.values())
for key,value in vector.items():
vector[key] = value * normalisation


return matrix, vector

def create_new_decision_matrix(self, second : Agent):
output = []
for i in range(len(self.weights)):
number = random.randint(0, len(self.weights[0]))
output.append(self.weights[i][:number] + self.weights[i][number:])
return output

def create_new_genome_vector(self, second : Agent):
genome = self.body_points.copy()
for item in genome.keys():
genome[item] = self.body_points[item] if random.randint(0,1) == 1 else second.body_points[item]
return genome

def render(self, window: pygame.window, cell_size: int, offset: tuple):
offset_x, offset_y = offset

Expand Down
7 changes: 7 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@
CELL_SIZE = cfg["grid"]["cell_size"]
GRID_WIDTH = PANEL_WIDTH // CELL_SIZE
GRID_HEIGHT = PANEL_HEIGHT // CELL_SIZE

MIN_AGE_PERCENT = cfg["mating"]["min_age_percent"]
MIN_ENERGY_LEVEL = cfg["mating"]["min_energy_level"]
MAX_RANGE = cfg["mating"]["max_range"]
MUTATION_CHANCE = cfg["mating"]["mutation_chance"]
MUTATION_MULTIPLY_BORDER = cfg["mating"]["mutation_multiply_border"]
MUTATION_ADDING_BORDER = cfg["mating"]["mutation_addding_border"]
12 changes: 7 additions & 5 deletions src/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def _spawn_initial_agents(self):
Agent.bound_y = self.pixel_height
Agent.cell_size = self.cell_size

for _ in range(50):
for _ in range(500):
pos_x = random.randint(0, Agent.bound_x)
pos_y = random.randint(0, Agent.bound_y)
agent = Agent((pos_x, pos_y), self)
Expand Down Expand Up @@ -212,13 +212,15 @@ def render(self, window: pygame.window, panel_x: int, panel_y: int, cell_size: i
agent.render(window, cell_size, (panel_x, panel_y))

font = pygame.font.Font(None, 32)
tick_text = font.render(f"Ticks: {self.tick_counter}", True, (255, 255, 255))
tick_text = font.render(f"Ticks: {self.tick_counter}", True, (255, 0, 0))
food_count_text = font.render(
f"Food items: {len(self.food_items)}", True, (255, 255, 255)
f"Food items: {len(self.food_items)}", True, (255, 0, 0)
)
agent_text = font.render(f"Agents: {len(self.agents)}", True, (255, 0, 0))

window.blit(tick_text, (panel_x + 10, panel_y + 10))
window.blit(food_count_text, (panel_x + 10, panel_y + 90))
window.blit(agent_text, (panel_x + 10, panel_y + 170))

def shutdown(self):
self.running = False
Expand All @@ -230,8 +232,8 @@ def get_agents(self):

#This function should deal with creating new agents
def create_agent(self, agent : Agent):
pass
# self.agents.append(agent)
self.agents.append(agent)

def _get_agent_area(self, agent: Agent) -> Area:
"""Maps agent pixel position to the underlying terrain cell."""
grid_x = min(self.grid_width - 1, max(0, int(agent.x // self.cell_size)))
Expand Down
76 changes: 74 additions & 2 deletions src/mating.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,76 @@

from __future__ import annotations
from agent import Agent, dist2
import typing
import config
from random import choice, randint, random, uniform

class Mating:
pass
def __init__(self, parent: Agent):
self.parent : Agent = parent

def mate(self):
if not self._checking_constrains():
return
close_agents = self._get_nearby_agents()
matrix, vector = self.get_new_genome(choice(close_agents))
new_agent = Agent((self.parent.x, self.parent.y), self.parent.environment, decision_matrix= matrix, genome= vector)
energy_level = self.parent.energy / self.parent.max_energy
new_agent.energy = new_agent.max_energy * energy_level / 2
self.parent.energy /= 2
self.parent.environment.create_agent(new_agent)


def _get_nearby_agents(self):
close_agents: typing.List[Agent] = []
for agent in self.parent.environment.get_agents():
if dist2(self.parent.x, self.parent.y, agent.x, agent.y) < config.MAX_RANGE:
close_agents.append(agent)
return close_agents

def _checking_constrains(self):
return not( self.parent.age < config.MIN_AGE_PERCENT * self.parent.max_age or self.parent.energy < config.MIN_ENERGY_LEVEL * self.parent.max_energy)

def get_new_genome(self, second : Agent):
matrix = self.create_new_decision_matrix(second)
vector = self.create_new_genome_vector(second)

numbers_to_change_in_matrix = int(len(matrix) * len(matrix[0]) / (1 / config.MUTATION_CHANCE))

#Applying mutation - there could be multiply of value or adding

for _ in range(numbers_to_change_in_matrix):
row =choice(matrix)
i = randint(0, len(row)-1)
if random() < 0.2:
row[i] *= uniform(1 - config.MUTATION_ADDING_BORDER, 1 + config.MUTATION_ADDING_BORDER)
else:
row[i] += uniform(-config.MUTATION_MULTIPLY_BORDER, config.MUTATION_MULTIPLY_BORDER)

for _ in range(int(len(vector) / (1 / config.MUTATION_CHANCE))):
item = choice(vector.keys())
if random() < 0.2:
vector[item] *= uniform(1 - config.MUTATION_ADDING_BORDER, 1 + config.MUTATION_ADDING_BORDER)
else:
vector[item] += uniform(-config.MUTATION_MULTIPLY_BORDER, config.MUTATION_MULTIPLY_BORDER)

#Normalisation of vector

normalisation = 100 / sum(vector.values())
for key,value in vector.items():
vector[key] = value * normalisation


return matrix, vector

def create_new_decision_matrix(self, second : Agent):
output = []
for i in range(len(self.parent.weights)):
number = randint(0, len(self.parent.weights[0]))
output.append(self.parent.weights[i][:number] + second.weights[i][number:])
return output

def create_new_genome_vector(self, second : Agent):
genome = self.parent.body_points.copy()
for item in genome.keys():
genome[item] = self.parent.body_points[item] if randint(0,1) == 1 else second.body_points[item]
return genome
Loading