12 Commits
10 changed files with 269 additions and 126 deletions
+2 -1
View File
@@ -1 +1,2 @@
__pycache__/
__pycache__/
.venv/
+8
View File
@@ -0,0 +1,8 @@
## How to run
1. Install Python 3.12
2. Install necessary libraries using `pip install -r requirements.txt`
3. Edit parametres in `main.py` (Optionally)
4. Run simulation using `py main.py` or `python3 main.py`
## Note
You can see the generated fitness statistics plot in fitness_plot.png after closing the game.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+140 -97
View File
@@ -3,62 +3,88 @@ import random
WIDTH, HEIGHT = 800, 600
HUD_HEIGHT = 150
BARRIER_SEED = 20
class GameObject:
"""
Base class for all game objects.
Handles image loading, positioning, drawing, and child objects.
"""
x = 0
y = 0
def __init__(self, image_path, x=0, y=0, offset = (0, 0)):
def __init__(self, image_path, x=0, y=0, offset=(0, 0)):
self.children = []
self.image = pygame.image.load(image_path) # Load the image
self.rect = self.image.get_rect(topleft=(x,y)) # Set the rectangle for the image
self.set_position(x, y) # Set initial position
self.image = pygame.image.load(image_path)
self.rect = self.image.get_rect(topleft=(x, y))
self.set_position(x, y)
self.offset = offset
def draw(self, surface):
surface.blit(self.image, self.rect) # Draw the image on the surface
if len(self.children) == 0: return
# Draw self and all children recursively
surface.blit(self.image, self.rect)
if not self.children:
return
for child in self.children:
child.draw(surface)
def set_position(self, x, y):
self.rect.x = x # Update rectangle position
# Set position and update children positions with offsets
self.rect.x = x
self.rect.y = y
self.x = x # Update object position
self.x = x
self.y = y
if len(self.children) == 0: return
if not self.children:
return
for child in self.children:
child.set_position(x + child.offset[0], y + child.offset[1])
def move(self, dx, dy):
self.set_position(self.rect.x + dx, self.rect.y + dy) # Move object by dx and dy
if len(self.children) == 0: return
# Move object by delta and update children accordingly
self.set_position(self.rect.x + dx, self.rect.y + dy)
if not self.children:
return
for child in self.children:
self.set_position(self.rect.x + dx, self.rect.y + dy)
def set_size(self, width, height):
self.image = pygame.transform.scale(self.image, (width,height)) # Resize the image
self.rect = self.image.get_rect(topleft=(self.x,self.y)) # Update rectangle size
# Resize image and update rect
self.image = pygame.transform.scale(self.image, (width, height))
self.rect = self.image.get_rect(topleft=(self.x, self.y))
def scale_by(self, scale):
self.image = pygame.transform.scale(self.image, (self.image.get_width() * scale, self.image.get_height() * scale)) # Scale the image
self.rect = self.image.get_rect(topleft=(self.x,self.y)) # Update rectangle size
# Scale image by a factor
new_size = (self.image.get_width() * scale, self.image.get_height() * scale)
self.image = pygame.transform.scale(self.image, new_size)
self.rect = self.image.get_rect(topleft=(self.x, self.y))
def get_position(self):
return (self.x, self.y) # Return current position
return (self.x, self.y)
def set_children(self, child):
# Add a child GameObject
self.children.append(child)
def recolor(self, color): #Method to recolor
def recolor(self, color):
# Apply color tint to image
self.image.fill(color, special_flags=pygame.BLEND_MULT)
self.image.set_colorkey(None) # Explicitly disable colorkey
self.image.set_colorkey(None)
class Horse(GameObject):
default_vacceleration = 0.2 # Default acceleration value
vspeed = 0 # Vertical speed
vacceleration = 0 # Vertical acceleration
stopped = False # Whether the horse is stopped
color = () # Color of the horse's mark
ribbon_fill = None
ribbon_out = None
"""
Represents a horse character with vertical movement, animation, and fitness tracking.
Supports acceleration, speed, and stopping.
"""
default_vacceleration = 0.2
def __init__(self, image_path, x, y):
super().__init__(image_path, x, y)
self.images = []
self.images.append(pygame.image.load("images/Horse_1.png"))
self.images.append(pygame.image.load("images/Horse_2.png"))
self.images.append(pygame.image.load("images/Horse_3.png"))
# Load animation frames
self.images = [
pygame.image.load("images/Horse_1.png"),
pygame.image.load("images/Horse_2.png"),
pygame.image.load("images/Horse_3.png")
]
self.animation_speed = 25
self.frame_counter = 0
self.current_frame = 0
@@ -66,126 +92,143 @@ class Horse(GameObject):
self.vacceleration = 0
self.stopped = False
self.fitness = 0
self.color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) # Random color for the mark
# Assign random color for ribbons
self.color = (random.randint(0,255), random.randint(0,255), random.randint(0,255))
self.ribbon_fill = GameObject("images/Ribbon_fill.png", self.x, self.y, (16, -1))
self.ribbon_fill.recolor(self.color)
self.ribbon_out = GameObject("images/Ribbon_out.png", self.x, self.y, (14, -2))
self.set_children(self.ribbon_fill)
self.set_children(self.ribbon_out)
# self.set_size(75, 75) # Set the size of the horse
def apply_vspeed(self):
pos = self.get_position()
self.set_position(pos[0], pos[1] + self.vspeed) # Apply vertical speed to position
# Move vertically by current speed
self.set_position(self.x, self.y + self.vspeed)
def apply_vacceleration(self):
self.vspeed += self.vacceleration # Apply acceleration to speed
# Update speed by acceleration
self.vspeed += self.vacceleration
def set_vspeed(self, speed):
self.vspeed = speed # Set vertical speed
self.vspeed = speed
def add_vspeed(self, delta):
self.vspeed += delta # Add to vertical speed
self.vspeed += delta
def set_vacceleration(self, vacceleration):
self.vacceleration = vacceleration # Set vertical acceleration
self.vacceleration = vacceleration
def up(self):
self.set_vacceleration(-self.default_vacceleration) # Move up
# Accelerate upward
self.set_vacceleration(-self.default_vacceleration)
def down(self):
self.set_vacceleration(self.default_vacceleration) # Move down
# Accelerate downward
self.set_vacceleration(self.default_vacceleration)
def stay(self):
self.set_vacceleration(0) # Stop vertical movement
# No vertical acceleration
self.set_vacceleration(0)
def stop(self):
self.stopped = True # Stop the horse
# Stop all movement
self.stopped = True
self.set_vacceleration(0)
self.set_vspeed(0)
def draw(self, surface):
super().draw(surface) # Draw the horse
#pygame.draw.rect(surface, self.color, (self.x + 40, self.y + 20, 25, 25)) # Draw the horse's mark
def update_animation(self):
# Animate horse by cycling frames
self.frame_counter += 1
if self.frame_counter >= self.animation_speed:
self.frame_counter = 0
self.current_frame = (self.current_frame + 1) % len(self.images)
self.image = self.images[self.current_frame]
def count_fitness(self):
if self.stopped == False:
# Increase fitness if moving; penalize if speed zero
if not self.stopped:
self.fitness += 1
if self.vspeed == 0: self.fitness -= 0.9
if self.vspeed == 0:
self.fitness -= 0.9
class Background(GameObject):
"""
Background image for the game scene.
"""
def __init__(self, image_path, x, y):
super().__init__(image_path, x, y) # Initialize background
super().__init__(image_path, x, y)
class Barrier(GameObject):
"""
Obstacles that move horizontally and interact with horses.
"""
def __init__(self, image_path, x, y):
super().__init__(image_path, x, y) # Initialize barrier
self.scale_by(5)
super().__init__(image_path, x, y)
self.scale_by(5) # Make barrier larger for visibility
class UI:
"""
Handles HUD display and horse status markers.
"""
iteration_num = 0
horses = [] # List of child objects
horizontal_offset = 25 # Horizontal spacing between marks
vertical_offset = -25 # Vertical spacing between marks
mark_size = 25 # Size of the marks
horses = []
horizontal_offset = 25
vertical_offset = -25
mark_size = 25
def __init__(self, hud_image_path, x=0, y=HEIGHT):
self.horses = [] # Initialize children list
self.horses = []
self.hud_image = pygame.image.load(hud_image_path)
self.hud_rect = self.hud_image.get_rect(topleft=(x,y))
self.hud_rect = self.hud_image.get_rect(topleft=(x, y))
def add_horses(self, children):
new_horses = [child for child in children if child not in self.horses] # Add new children
self.horses.extend(new_horses)
# Add horses to UI tracking list
self.horses.extend([child for child in children if child not in self.horses])
def draw_marks(self, screen):
active_marks = [horse for horse in self.horses if not horse.stopped]
unactive_marks = [horse for horse in self.horses if horse.stopped]
width = 10 # Number of marks per row
# Отрисовка активных меток
xa = 0 # Horizontal position for active marks
ya = HEIGHT + HUD_HEIGHT - self.mark_size # Vertical position for active marks
for i, horse in enumerate(active_marks):
# Draw colored squares representing active and stopped horses
active = [h for h in self.horses if not h.stopped]
inactive = [h for h in self.horses if h.stopped]
xa, ya = 0, HEIGHT + HUD_HEIGHT - self.mark_size
for i, horse in enumerate(active):
pygame.draw.rect(screen, horse.color, (xa, ya, self.mark_size, self.mark_size))
xa += self.horizontal_offset # Move to the next position
# Move to the next row if the row is full
if (i + 1) % width == 0: # Check if the next mark would be on a new row
xa += self.horizontal_offset
if (i+1) % 10 == 0:
ya += self.vertical_offset
xa = 0 # Reset horizontal position for the new row
# Отрисовка неактивных меток
xu = WIDTH - self.mark_size # Horizontal position for inactive marks
yu = HEIGHT + HUD_HEIGHT - self.mark_size # Vertical position for inactive marks
for i, horse in enumerate(unactive_marks):
xa = 0
xu, yu = WIDTH - self.mark_size, HEIGHT + HUD_HEIGHT - self.mark_size
for i, horse in enumerate(inactive):
pygame.draw.rect(screen, horse.color, (xu, yu, self.mark_size, self.mark_size))
xu -= self.horizontal_offset # Move to the next position
# Move to the next row if the row is full
if (i + 1) % width == 0: # Check if the next mark would be on a new row
xu -= self.horizontal_offset
if (i+1) % 10 == 0:
yu += self.vertical_offset
xu = WIDTH - self.mark_size # Reset horizontal position for the new row
xu = WIDTH - self.mark_size
def draw(self, surface):
# Draw HUD image
surface.blit(self.hud_image, self.hud_rect)
class Spawner:
active = True # Whether the spawner is active
delay = 500 # Number of ticks between spawns
tick_counter = 0 # Counter for ticks
"""
Spawns barriers at intervals to challenge horses.
"""
active = True
delay = 500
tick_counter = 0
def __init__(self, barrier_image_path, delay):
self.barrier_image_path = barrier_image_path # Path to the barrier image
self.barrier_image_path = barrier_image_path
self.delay = delay
def handle(self):
# Increment tick counter and spawn barrier when delay reached
if self.active:
self.tick_counter += 1 # Increment tick counter
if self.tick_counter >= self.delay: # Check if it's time to spawn
self.tick_counter += 1
if self.tick_counter >= self.delay:
self.spawn()
self.tick_counter = 0 # Reset tick counter
self.tick_counter = 0
def spawn(self):
random_y = random.randint(0, HEIGHT - 100) # Random Y position for the barrier
new_barrier = Barrier(self.barrier_image_path, WIDTH, random_y) # Create a new barrier
return new_barrier
# Create a new barrier at a random vertical position on the right edge
return Barrier(self.barrier_image_path, WIDTH, random.randint(0, HEIGHT - 100))
+9
View File
@@ -29,12 +29,15 @@ class GeneticAlgorithm:
print(self.device)
self.initialize_population()
# create random individual
def create_ind(self):
return NeuralNetwork(self.inputSize).to(self.device)
# create random population
def initialize_population(self):
self.population = [self.create_ind() for _ in range(self.populationSize)]
# perform crossofer operation over 2 parents
def crossover(self, parent1: NeuralNetwork, parent2: NeuralNetwork):
child1 = self.create_ind()
child2 = self.create_ind()
@@ -55,22 +58,28 @@ class GeneticAlgorithm:
param.data += torch.randn_like(param.data) * 0.1
return model
# learn using given fitness for all neural networks
def learn(self, fitness: list):
sortedFitnessArg = np.argsort(fitness)[::-1]
self.fitnessBest.append(fitness[sortedFitnessArg[0]])
# print best fitness for this population
print(self.fitnessBest[-1])
self.population = [self.population[x] for x in sortedFitnessArg]
numBest = int(self.populationSize * self.percentageBest)
# left only best individuals
self.population = self.population[:numBest]
# perform crossover
while len(self.population) < self.populationSize - self.populationSize * self.percentageNew:
parent1, parent2 = np.random.choice(self.population), np.random.choice(self.population)
child1, child2 = self.crossover(parent1, parent2)
child1 = self.mutate(child1)
child2 = self.mutate(child2)
self.population.extend([child1, child2])
# add new individuals
while len(self.population) < self.populationSize: self.population.append(self.create_ind())
while len(self.population) > self.populationSize: self.population.pop()
# return predicted direction for desired horse
def predict(self, data: list, i):
data = torch.tensor(data, requires_grad=False).float().to(self.device)
return self.population[i](data)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 892 B

After

Width:  |  Height:  |  Size: 1.3 KiB

+74 -28
View File
@@ -2,23 +2,36 @@ import pygame
import sys
from gameobjects import *
from genetic_alg import GeneticAlgorithm
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import random
POPULATION_SIZE = 100
POPULATION_SIZE = 50 # Number of horses in population
MUTATION_RATE = 0.5
POPULATION_NEW = 0.1
POPULATION_BEST = 0.3
FRAME_RATE = 300 # TODO: FIX THE INCORRECT FRAME RATE CORRELATION
FRAME_RATE = 300
BARRIER_SPEED = 10
BARRIER_DELAY = 100
MAX_ITERATIONS = 50
BARRIER_SEED = 42
current_iteration = 1
best_fitnesses = []
running_fitnesses = []
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT + HUD_HEIGHT)) # Initialize the screen
pygame.display.set_caption("NIC_Project") # Set window title
screen = pygame.display.set_mode((WIDTH, HEIGHT + HUD_HEIGHT))
pygame.display.set_caption("NIC_Project")
font = pygame.font.SysFont("Arial", 36)
WHITE = (255, 255, 255) # Define white color
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
TEXT_COLOUR = (10, 20, 200)
UI = UI("images/HUD.png") # Initialize UI
UI = UI("images/HUD.png")
gameobjects = []
horses = []
barriers = []
@@ -30,15 +43,17 @@ last_barrier = None
def init_game():
global gameobjects, horses, barriers, upper_bound_rect, lower_bound_rect, spawner, last_barrier, BARRIER_SPEED
grass = Background("images/Grass.jpg", 0, 0)
# Initialize background and game objects for a new iteration
#random.seed(BARRIER_SEED) #SET SEED
grass = Background("images/Grass.png", 0, 0)
grass.set_size(WIDTH, HEIGHT)
gameobjects = [grass]
barriers = []
# Reset horses to starting position and state
for horse in horses:
horse.set_position(50, HEIGHT/2)
horse.set_position(50, HEIGHT / 2)
horse.stopped = False
horse.set_vacceleration(0)
horse.set_vspeed(0)
@@ -52,6 +67,7 @@ def init_game():
gameobjects.extend(horses)
gameobjects.extend(barriers)
# Define upper and lower bounds for horse movement
upper_bound_rect = pygame.Rect(0, 0, WIDTH, -10)
lower_bound_rect = pygame.Rect(0, HEIGHT, WIDTH, 10)
@@ -61,6 +77,7 @@ def init_game():
def get_features(horse: Horse):
# Extract normalized features for neural network input
features = [last_barrier.rect.topleft[0], last_barrier.rect.topleft[1],
last_barrier.rect.bottomright[0], last_barrier.rect.bottomright[1],
horse.rect.topleft[0], horse.rect.topleft[1],
@@ -70,7 +87,7 @@ def get_features(horse: Horse):
return features
horses = [Horse("images/Horse_1.png", 50, HEIGHT/2 + 0 * i) for i in range(POPULATION_SIZE)]
horses = [Horse("images/Horse_1.png", 50, HEIGHT / 2 + 0 * i) for i in range(POPULATION_SIZE)]
init_game()
@@ -79,16 +96,23 @@ genecticAlg = GeneticAlgorithm(POPULATION_SIZE, MUTATION_RATE, POPULATION_BEST,
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT: # Handle window close event
if event.type == pygame.QUIT:
# Save fitness plot on exit
plt.plot(running_fitnesses)
plt.xlabel("Iteration")
plt.ylabel("Current Fitness")
plt.title("Current Fitness per Iteration")
plt.savefig('fitness_plot.png')
pygame.quit()
sys.exit()
keys = pygame.key.get_pressed() # Get pressed keys
keys = pygame.key.get_pressed()
for i, horse in enumerate(horses):
if not horse.stopped: # If the horse is not stopped
if not horse.stopped:
data = get_features(horse)
res = genecticAlg.predict(data, i)
# Control horse movement based on neural network output
if res == 0:
horse.up()
elif res == 1:
@@ -96,26 +120,29 @@ while True:
else:
horse.stay()
else:
horse.move(-BARRIER_SPEED, 0) # Move stopped horse to the left
# Move stopped horses left with barriers
horse.move(-BARRIER_SPEED, 0)
horse.apply_vacceleration() # Apply acceleration to horse
horse.apply_vspeed() # Apply speed to horse
horse.draw(screen) # Draw the horse
horse.apply_vacceleration()
horse.apply_vspeed()
horse.draw(screen)
spawner.handle() # Handle spawner logic
if spawner.tick_counter == 0: # If a new barrier is spawned
new_barrier = spawner.spawn() # Spawn a new barrier
barriers.append(new_barrier) # Add barrier to barriers list
gameobjects.append(new_barrier) # Add barrier to game objects
spawner.handle()
if spawner.tick_counter == 0:
# Spawn new barrier periodically
new_barrier = spawner.spawn()
barriers.append(new_barrier)
gameobjects.append(new_barrier)
last_barrier = new_barrier
for barrier in barriers:
barrier.move(-BARRIER_SPEED, 0) # Move barriers to the left
barrier.move(-BARRIER_SPEED, 0)
for horse in horses:
horse.update_animation()
if not horse.stopped:
# Check collisions with barriers and bounds
for barrier in barriers:
if horse.rect.colliderect(barrier.rect):
horse.stop()
@@ -123,17 +150,36 @@ while True:
horse.stop()
horse.count_fitness()
for object in gameobjects:
object.draw(screen) # Draw all game objects
object.draw(screen)
current_fitnesses = [horse.fitness for horse in horses]
current_fitness = max(current_fitnesses)
if all(horse.stopped for horse in horses):
# All horses stopped: evolve population and start new iteration
UI.iteration_num += 1
genecticAlg.learn([x.fitness for x in horses])
local_best_fitness = max(horse.fitness for horse in horses)
best_fitnesses.append(local_best_fitness)
running_fitnesses.append(current_fitness)
current_iteration += 1
init_game()
UI.draw(screen)
UI.draw_marks(screen)
pygame.display.flip() # Update the display
pygame.time.Clock().tick(FRAME_RATE) # Limit the frame rate to 160 FPS
# Display iteration and fitness info
iteration_num_txt = font.render(f"Iteration: {current_iteration}", True, TEXT_COLOUR)
current_fitness_txt = font.render(
f"Current Fitness: {current_fitness}", True, TEXT_COLOUR)
best_fitness_txt = font.render(
f"Best Fitness: {best_fitnesses[-1] if best_fitnesses else 0}", True, TEXT_COLOUR)
screen.blit(iteration_num_txt, (WIDTH / 2 - 90, HEIGHT + HUD_HEIGHT - 100))
screen.blit(best_fitness_txt, (10, 5))
screen.blit(current_fitness_txt, (10, 40))
pygame.display.flip()
pygame.time.Clock().tick(FRAME_RATE)
+36
View File
@@ -0,0 +1,36 @@
contourpy==1.3.1
cycler==0.12.1
filelock==3.18.0
fonttools==4.57.0
fsspec==2025.3.2
Jinja2==3.1.6
kiwisolver==1.4.8
MarkupSafe==3.0.2
matplotlib==3.10.1
mpmath==1.3.0
networkx==3.4.2
numpy==2.2.4
nvidia-cublas-cu12==12.4.5.8
nvidia-cuda-cupti-cu12==12.4.127
nvidia-cuda-nvrtc-cu12==12.4.127
nvidia-cuda-runtime-cu12==12.4.127
nvidia-cudnn-cu12==9.1.0.70
nvidia-cufft-cu12==11.2.1.3
nvidia-curand-cu12==10.3.5.147
nvidia-cusolver-cu12==11.6.1.9
nvidia-cusparse-cu12==12.3.1.170
nvidia-cusparselt-cu12==0.6.2
nvidia-nccl-cu12==2.21.5
nvidia-nvjitlink-cu12==12.4.127
nvidia-nvtx-cu12==12.4.127
packaging==24.2
pillow==11.2.1
pygame==2.6.1
pyparsing==3.2.3
python-dateutil==2.9.0.post0
setuptools==78.1.0
six==1.17.0
sympy==1.13.1
torch==2.6.0
triton==3.2.0
typing_extensions==4.13.2