added code

This commit is contained in:
Felix Bargfeldt 2019-07-16 16:06:30 +02:00
parent d21aa43362
commit 099f7bcd2f
No known key found for this signature in database
GPG key ID: 99184F5FDC589A67
7 changed files with 410 additions and 0 deletions

34
brain.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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)