added code
This commit is contained in:
parent
d21aa43362
commit
099f7bcd2f
7 changed files with 410 additions and 0 deletions
34
brain.py
Normal file
34
brain.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
import random
|
||||
from typing import List
|
||||
|
||||
from vector import Vector
|
||||
|
||||
|
||||
class Brain:
|
||||
def __init__(self, size: int):
|
||||
self.step: int = 0
|
||||
self.size: int = size
|
||||
self.directions: List[Vector] = []
|
||||
|
||||
def randomize(self):
|
||||
self.directions = [
|
||||
Vector(1, random.random() * 360) for _ in range(self.size)
|
||||
]
|
||||
|
||||
def available(self) -> bool:
|
||||
return self.step < self.size
|
||||
|
||||
def next_direction(self) -> Vector:
|
||||
self.step += 1
|
||||
return self.directions[self.step - 1]
|
||||
|
||||
def clone(self) -> 'Brain':
|
||||
out: Brain = Brain(self.size)
|
||||
out.directions = self.directions.copy()
|
||||
return out
|
||||
|
||||
def mutate(self, start: int):
|
||||
mutation_rate: float = 0.01
|
||||
for i in range(start, self.size):
|
||||
if random.random() < mutation_rate:
|
||||
self.directions[i] = Vector(1, random.random() * 360)
|
68
dot.py
Normal file
68
dot.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
from brain import Brain
|
||||
from game import Game
|
||||
from vector import Point, Vector
|
||||
|
||||
|
||||
class Dot:
|
||||
def __init__(self, game: Game):
|
||||
super().__init__()
|
||||
|
||||
self.fitness: float = 0
|
||||
self.is_best: bool = False
|
||||
self.alive: bool = True
|
||||
self.brain: Brain = Brain(1000)
|
||||
self.game: Game = game
|
||||
self.pos: Point = game.start
|
||||
self.vel: Vector = Vector(0, 0)
|
||||
self.closest_distance: float = 1e1337
|
||||
|
||||
self.reached_goal: bool = False
|
||||
self.reached_final_goal: bool = False
|
||||
|
||||
@staticmethod
|
||||
def randomized(game: Game) -> 'Dot':
|
||||
dot: Dot = Dot(game)
|
||||
dot.brain.randomize()
|
||||
return dot
|
||||
|
||||
def die(self):
|
||||
self.alive: bool = False
|
||||
|
||||
def move(self):
|
||||
if not self.brain.available():
|
||||
self.die()
|
||||
return
|
||||
|
||||
acc: Vector = self.brain.next_direction()
|
||||
self.vel += acc
|
||||
self.vel.distance = min(self.vel.distance, 5)
|
||||
self.pos += self.vel.to_point()
|
||||
|
||||
def update(self):
|
||||
if not self.alive or self.reached_goal:
|
||||
return
|
||||
|
||||
self.move()
|
||||
current_target: Point = self.game.get_current_target()
|
||||
if self.brain.step >= self.game.mutation_start:
|
||||
self.closest_distance: float = min(self.closest_distance, (self.pos - current_target).to_vector().distance)
|
||||
|
||||
if not (2 <= self.pos.x < self.game.width - 2 and 2 <= self.pos.y < self.game.height - 2):
|
||||
self.die()
|
||||
elif (self.pos - current_target).to_vector().distance < 5:
|
||||
if self.game.goal == current_target:
|
||||
self.reached_final_goal: bool = True
|
||||
self.reached_goal: bool = True
|
||||
elif any(obstacle.check_collision(self.pos) for obstacle in self.game.obstacles):
|
||||
self.die()
|
||||
|
||||
def clone(self) -> 'Dot':
|
||||
out: Dot = Dot(self.game)
|
||||
out.brain = self.brain.clone()
|
||||
return out
|
||||
|
||||
def calculate_fitness(self):
|
||||
if self.reached_goal:
|
||||
self.fitness: float = 10000 + 1 / (self.brain.step ** 2)
|
||||
else:
|
||||
self.fitness: float = 1 / (self.closest_distance ** 2)
|
24
game.py
Normal file
24
game.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from typing import List
|
||||
|
||||
from obstacle import Obstacle
|
||||
from vector import Point
|
||||
|
||||
|
||||
class Game:
|
||||
def __init__(self, width: float, height: float, start: Point, goal: Point,
|
||||
obstacles: List[Obstacle], checkpoints: List[Point]):
|
||||
self.width: float = width
|
||||
self.height: float = height
|
||||
self.start: Point = start
|
||||
self.goal: Point = goal
|
||||
self.obstacles: List[Obstacle] = obstacles
|
||||
self.checkpoints: List[Point] = checkpoints
|
||||
self.generation: int = 1
|
||||
self.current_target: int = 0
|
||||
self.target_countdown: int = None
|
||||
self.mutation_start: int = 0
|
||||
|
||||
def get_current_target(self) -> Point:
|
||||
if self.current_target < len(self.checkpoints):
|
||||
return self.checkpoints[self.current_target]
|
||||
return self.goal
|
10
obstacle.py
Normal file
10
obstacle.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from vector import Point
|
||||
|
||||
|
||||
class Obstacle:
|
||||
def __init__(self, p1: Point, p2: Point):
|
||||
self.p1: Point = p1
|
||||
self.p2: Point = p2
|
||||
|
||||
def check_collision(self, p: Point) -> bool:
|
||||
return self.p1.x - 2 <= p.x <= self.p2.x + 2 and self.p1.y - 2 <= p.y <= self.p2.y + 2
|
84
population.py
Normal file
84
population.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
import random
|
||||
from typing import List
|
||||
|
||||
from dot import Dot
|
||||
from game import Game
|
||||
|
||||
|
||||
class Population:
|
||||
def __init__(self, game: Game, size: int):
|
||||
self.game: Game = game
|
||||
self.dots: List[Dot] = [Dot.randomized(game) for _ in range(size)]
|
||||
self.total_fitness: float = 0
|
||||
self.best_dot: Dot = None
|
||||
|
||||
def all_dots_dead(self) -> bool:
|
||||
return all(not dot.alive or dot.reached_goal for dot in self.dots)
|
||||
|
||||
def calculate_fitness(self):
|
||||
self.total_fitness: float = 0
|
||||
for dot in self.dots:
|
||||
dot.calculate_fitness()
|
||||
self.total_fitness += dot.fitness
|
||||
|
||||
def natural_selection(self):
|
||||
self.best_dot: Dot = max(self.dots, key=lambda dot: dot.fitness)
|
||||
new_dots: List[Dot] = [self.best_dot.clone()]
|
||||
new_dots[0].is_best = True
|
||||
|
||||
for _ in range(1, len(self.dots)):
|
||||
new_dots.append(self.select_parent().clone())
|
||||
|
||||
self.dots: List[Dot] = new_dots
|
||||
|
||||
def select_parent(self):
|
||||
if self.game.target_countdown == 0:
|
||||
return self.best_dot
|
||||
|
||||
rand: float = random.random() * self.total_fitness
|
||||
runner: float = 0
|
||||
for dot in self.dots:
|
||||
runner += dot.fitness
|
||||
if runner >= rand:
|
||||
return dot
|
||||
assert False, "math is broken"
|
||||
|
||||
def mutate(self):
|
||||
if self.game.target_countdown == 0:
|
||||
self.game.mutation_start: int = self.best_dot.brain.step
|
||||
|
||||
for dot in self.dots[1:]:
|
||||
dot.brain.mutate(self.game.mutation_start)
|
||||
|
||||
def update(self):
|
||||
if self.all_dots_dead():
|
||||
self.calculate_fitness()
|
||||
self.natural_selection()
|
||||
self.mutate()
|
||||
print(f"Generation #{self.game.generation} complete!")
|
||||
if self.best_dot.reached_final_goal:
|
||||
self.game.mutation_start: int = 0
|
||||
print(f" Reached goal in {self.best_dot.brain.step} steps!")
|
||||
elif self.best_dot.reached_goal:
|
||||
print(f" Reached checkpoint #{self.game.current_target + 1} in {self.best_dot.brain.step} steps")
|
||||
print(f" Best fitness: {self.best_dot.fitness}")
|
||||
print(f" Average fitness: {self.total_fitness / len(self.dots)}")
|
||||
self.game.generation += 1
|
||||
|
||||
if self.game.target_countdown is not None:
|
||||
if self.game.target_countdown > 0:
|
||||
self.game.target_countdown -= 1
|
||||
else:
|
||||
self.game.current_target += 1
|
||||
self.game.target_countdown = None
|
||||
elif not self.best_dot.reached_final_goal and self.best_dot.reached_goal:
|
||||
self.game.target_countdown: int = 20
|
||||
|
||||
if self.game.target_countdown:
|
||||
print(f" Optimizing this checkpoint for {self.game.target_countdown} more generations")
|
||||
else:
|
||||
for dot in self.dots:
|
||||
if self.best_dot and self.best_dot.reached_goal and dot.brain.step > self.best_dot.brain.step:
|
||||
dot.die()
|
||||
else:
|
||||
dot.update()
|
109
smart_dots.py
Normal file
109
smart_dots.py
Normal file
|
@ -0,0 +1,109 @@
|
|||
import random
|
||||
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtWidgets import *
|
||||
|
||||
from dot import Dot
|
||||
from game import Game
|
||||
from obstacle import Obstacle
|
||||
from population import Population
|
||||
from vector import Point
|
||||
|
||||
random.seed(0)
|
||||
|
||||
|
||||
class SmartDots(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle("Smart Dots")
|
||||
self.setFixedSize(640, 480)
|
||||
|
||||
self.population: Population = Population(
|
||||
Game(
|
||||
self.width(), self.height(),
|
||||
Point(self.width() / 2, self.height() - 10), Point(self.width() / 2, 10), [
|
||||
Obstacle(
|
||||
Point(0, self.height() / 4 - 5),
|
||||
Point(self.width() * 3 / 4 + 5, self.height() / 4 + 5)
|
||||
), Obstacle(
|
||||
Point(self.width() * 3 / 4 - 5, self.height() / 4 - 5),
|
||||
Point(self.width() * 3 / 4 + 5, self.height() * 5 / 8 - 5)
|
||||
), Obstacle(
|
||||
Point(self.width() * 3 / 8, self.height() * 5 / 8 - 5),
|
||||
Point(self.width() * 3 / 4 + 5, self.height() * 5 / 8 + 5)
|
||||
), Obstacle(
|
||||
Point(self.width() / 4, self.height() * 3 / 8 - 5),
|
||||
Point(self.width() * 5 / 8, self.height() * 3 / 8 + 5)
|
||||
), Obstacle(
|
||||
Point(self.width() / 4 - 5, self.height() * 3 / 8 - 5),
|
||||
Point(self.width() / 4 + 5, self.height() * 3 / 4 + 5)
|
||||
), Obstacle(
|
||||
Point(self.width() / 4 - 5, self.height() * 3 / 4 - 5),
|
||||
Point(self.width(), self.height() * 3 / 4 + 5)
|
||||
)
|
||||
], [
|
||||
Point(self.width() / 2, self.height() * 2.5 / 8),
|
||||
Point(self.width() / 2, self.height() * 4 / 8),
|
||||
Point(self.width() / 2, self.height() * 5.5 / 8)
|
||||
]
|
||||
),
|
||||
1000
|
||||
)
|
||||
self.timer: QBasicTimer = QBasicTimer()
|
||||
self.timer.start(10, self)
|
||||
|
||||
self.show()
|
||||
self.setFocus()
|
||||
|
||||
def timerEvent(self, e: QTimerEvent):
|
||||
if e.timerId() == self.timer.timerId():
|
||||
self.tick()
|
||||
|
||||
def tick(self):
|
||||
self.population.update()
|
||||
self.repaint()
|
||||
|
||||
def keyReleaseEvent(self, e: QKeyEvent):
|
||||
if e.key() == Qt.Key_Q:
|
||||
self.close()
|
||||
|
||||
def paintEvent(self, _):
|
||||
qp: QPainter = QPainter(self)
|
||||
qp.setPen(Qt.white)
|
||||
qp.setBrush(Qt.white)
|
||||
qp.drawRect(self.rect())
|
||||
|
||||
target: Point = self.population.game.get_current_target()
|
||||
|
||||
qp.setPen(Qt.black)
|
||||
qp.setBrush(Qt.transparent)
|
||||
if self.population.best_dot is not None and not self.population.best_dot.reached_goal:
|
||||
radius: float = self.population.best_dot.closest_distance
|
||||
qp.drawEllipse(QPoint(*target), radius, radius)
|
||||
|
||||
qp.setBrush([Qt.cyan, Qt.red][target == self.population.game.goal])
|
||||
qp.drawEllipse(QPoint(*self.population.game.goal), 5, 5)
|
||||
|
||||
qp.setBrush(Qt.blue)
|
||||
for obstacle in self.population.game.obstacles:
|
||||
qp.drawRect(*obstacle.p1, *(obstacle.p2 - obstacle.p1))
|
||||
|
||||
for checkpoint in self.population.game.checkpoints:
|
||||
qp.setBrush([Qt.cyan, Qt.red][target == checkpoint])
|
||||
qp.drawEllipse(QPoint(*checkpoint), 3, 3)
|
||||
|
||||
for dot in self.population.dots[::-1]: # type: Dot
|
||||
if dot.is_best:
|
||||
qp.setBrush(Qt.green)
|
||||
qp.drawEllipse(QPoint(*dot.pos), 4, 4)
|
||||
else:
|
||||
qp.setBrush(Qt.black)
|
||||
qp.drawEllipse(QPoint(*dot.pos), 2, 2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
qa = QApplication([])
|
||||
app = SmartDots()
|
||||
qa.exec_()
|
81
vector.py
Normal file
81
vector.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
import math
|
||||
|
||||
|
||||
def deg2rad(angle: float) -> float:
|
||||
return (angle % 360) / 180 * math.pi
|
||||
|
||||
|
||||
def rad2deg(angle: float) -> float:
|
||||
return (angle * 180 / math.pi) % 360
|
||||
|
||||
|
||||
class Point:
|
||||
def __init__(self, x: float, y: float):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Point(x={self.x}, y={self.y})"
|
||||
|
||||
def __iter__(self):
|
||||
yield self.x
|
||||
yield self.y
|
||||
|
||||
def __add__(self, other):
|
||||
assert isinstance(other, Point)
|
||||
return Point(self.x + other.x, self.y + other.y)
|
||||
|
||||
def __sub__(self, other):
|
||||
assert isinstance(other, Point)
|
||||
return Point(self.x - other.x, self.y - other.y)
|
||||
|
||||
def __mul__(self, other):
|
||||
assert isinstance(other, int) or isinstance(other, float)
|
||||
return Point(self.x * other, self.y * other)
|
||||
|
||||
def __truediv__(self, other):
|
||||
assert isinstance(other, int) or isinstance(other, float)
|
||||
return Point(self.x / other, self.y / other)
|
||||
|
||||
def to_vector(self):
|
||||
if not self.x: return Vector(-self.y, 0)
|
||||
a = math.atan(self.y / self.x)
|
||||
d = self.x / math.cos(a)
|
||||
return Vector(d, rad2deg(a) + 90)
|
||||
|
||||
|
||||
class Vector:
|
||||
def __init__(self, distance: float, angle: float):
|
||||
if distance < 0:
|
||||
distance *= -1
|
||||
angle += 180
|
||||
self.distance = distance
|
||||
self.angle = angle % 360
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Vector(distance={self.distance}, angle={self.angle})"
|
||||
|
||||
def __add__(self, other):
|
||||
assert isinstance(other, Vector)
|
||||
return (self.to_point() + other.to_point()).to_vector()
|
||||
|
||||
def __sub__(self, other):
|
||||
assert isinstance(other, Vector)
|
||||
return (self.to_point() - other.to_point()).to_vector()
|
||||
|
||||
def __mul__(self, other):
|
||||
assert isinstance(other, int) or isinstance(other, float)
|
||||
return Point(self.distance * other, self.angle)
|
||||
|
||||
def __truediv__(self, other):
|
||||
assert isinstance(other, int) or isinstance(other, float)
|
||||
return Point(self.distance / other, self.angle)
|
||||
|
||||
def to_point(self):
|
||||
return Point(
|
||||
math.cos(deg2rad(self.angle - 90)) * self.distance,
|
||||
math.sin(deg2rad(self.angle - 90)) * self.distance
|
||||
)
|
||||
|
||||
def copy(self):
|
||||
return Vector(self.distance, self.angle)
|
Reference in a new issue