-
Notifications
You must be signed in to change notification settings - Fork 1
/
population.py
217 lines (191 loc) · 8.91 KB
/
population.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
"""
This modules implements the bulk of Bot Evolution.
"""
import numpy as np
import copy
import settings
from utility import seq_is_equal, distance_between, angle_is_between, find_angle
from neural_network import NNetwork, sigmoid, softmax
class Population:
"""
The environment of bots and food.
"""
def __init__(self, size, mutation_rate):
assert(size >= 5)
assert(0 < mutation_rate < 1)
self.SIZE = size
self.mutation_rate = mutation_rate
self.bots = []
self.food = []
self.time_since_last_death = 0.0
# The neural network will have 1 neuron in the input layer, 1 hidden
# layer with 2 neurons, and 4 neurons in the output layer. The sigmoid
# activation function will be used on the hidden layer, and a softmax
# activation function will be used on the output layer. Input consists
# of the bot's direction and if there is or isn't food in the bots field
# of vision. Output consists of whether or not to move foward, turn
# left, turn right, or do nothing.
for i in range(size):
random_rgb = (np.random.randint(30, 256), np.random.randint(30, 256), np.random.randint(30, 256))
self.bots.append(Bot(NNetwork((1, 2, 4), (sigmoid, softmax)), random_rgb, self))
self.food.append(Food(self))
def eliminate(self, bot, replace = False):
self.time_since_last_death = 0.0
self.bots.remove(bot)
if replace:
random_rgb = (np.random.randint(30, 256), np.random.randint(30, 256), np.random.randint(30, 256))
self.bots.append(Bot(NNetwork((1, 2, 4), (sigmoid, softmax)), random_rgb, self))
def feed(self, bot, food):
bot.score = 1.0
self.food.remove(food)
self.food.append(Food(self))
num_to_replace = int(self.SIZE / 7 - 1)
if num_to_replace < 2:
num_to_replace = 2
for i in range(num_to_replace):
weakest = self.bots[0]
for other in self.bots:
if other.score < weakest.score:
weakest = other
self.eliminate(weakest)
for i in range(num_to_replace):
if np.random.uniform(0, 1) <= self.mutation_rate:
new_rgb = [bot.RGB[0], bot.RGB[1], bot.RGB[2]]
new_rgb[np.random.choice((0, 1, 2))] = np.random.uniform(30, 256)
new_bot = Bot(bot.nnet, new_rgb, self)
new_bot.x = bot.x + Bot.HITBOX_RADIUS * 4 * np.random.uniform(0, 1) * np.random.choice((-1, 1))
new_bot.y = bot.y + Bot.HITBOX_RADIUS * 4 * np.random.uniform(0, 1) * np.random.choice((-1, 1))
nb_c = new_bot.nnet.connections
mutated = False
while not mutated:
for k in range(len(nb_c)):
# print "len: ",len(nb_c)
# print "from: ",nb_c[k].FROM.SIZE
# print "to: ",nb_c[k].TO.SIZE
raw_input()
for i in range(nb_c[k].FROM.SIZE):
for j in range(nb_c[k].TO.SIZE):
if np.random.uniform(0, 1) <= self.mutation_rate:
nb_c[k].weights[i][j] = nb_c[k].weights[i][j] * np.random.normal(1, 0.5) + np.random.standard_normal()
mutated = True
self.bots.append(new_bot)
else:
new_bot = Bot(bot.nnet, bot.RGB, self)
new_bot.x = bot.x + Bot.HITBOX_RADIUS * 4 * np.random.uniform(0, 1) * np.random.choice((-1, 1))
new_bot.y = bot.y + Bot.HITBOX_RADIUS * 4 * np.random.uniform(0, 1) * np.random.choice((-1, 1))
self.bots.append(new_bot)
def update(self, dt):
"""
Updates the population's internals. The bulk of event handling for all
bots and food starts here.
"""
self.time_since_last_death += 1.0 / settings.FPS * dt * settings.TIME_MULTIPLIER
for food in self.food[:]:
if food not in self.food:
continue
food.update(dt)
for bot in self.bots[:]:
if bot not in self.bots:
continue
sensory_input = []
# This is where the bot's field of vision is put into action.
min_theta = bot.theta - Bot.FIELD_OF_VISION_THETA / 2
max_theta = bot.theta + Bot.FIELD_OF_VISION_THETA / 2
food_in_sight = False
for food in self.food:
if angle_is_between(find_angle(bot.x, bot.y, food.x, food.y), min_theta, max_theta):
food_in_sight = True
break
if food_in_sight:
sensory_input.append(1.0)
else:
sensory_input.append(0.0)
# Useful debugging outputs.
# print(bot.RGB)
# print(sensory_input)
bot.update(dt, sensory_input)
if self.time_since_last_death >= 5:
weakest = self.bots[0]
for bot in self.bots:
if bot.score < weakest.score:
weakest = bot
self.eliminate(weakest, replace = True)
class Bot:
"""
The representation of the circle thing with probes.
"""
# In pixels/pixels per second/revolutions per second/radians.
SPAWN_RADIUS = int(settings.WINDOW_WIDTH / 20) if settings.WINDOW_WIDTH <= settings.WINDOW_HEIGHT else int(settings.WINDOW_HEIGHT / 20)
HITBOX_RADIUS = 6
SPEED = 350.0
TURN_RATE = 2 * np.pi
FIELD_OF_VISION_THETA = 45 * np.pi / 180
# These lists represent the output from the neural network. Note that the
# output '[0, 0, 0, 1]' means "do nothing".
MOVE_FORWARD = [1, 0, 0, 0]
TURN_LEFT = [0, 1, 0, 0]
TURN_RIGHT = [0, 0, 1, 0]
def __init__(self, nnet, rgb, population):
self.nnet = copy.deepcopy(nnet)
self.RGB = rgb
self.pop = population
self.theta = np.random.uniform(0, 1) * 2 * np.pi
self.x = settings.WINDOW_WIDTH / 2.0 + Bot.SPAWN_RADIUS * np.random.uniform(0, 1) * np.cos(self.theta)
self.y = settings.WINDOW_HEIGHT / 2.0 + Bot.SPAWN_RADIUS * np.random.uniform(0, 1) * np.sin(self.theta)
self.score = 0.0
def _move_forward(self, dt):
self.x += Bot.SPEED / settings.FPS * dt * np.cos(self.theta) * settings.TIME_MULTIPLIER
self.y -= Bot.SPEED / settings.FPS * dt * np.sin(self.theta) * settings.TIME_MULTIPLIER
if self.x < -Bot.HITBOX_RADIUS * 6 or self.x > settings.WINDOW_WIDTH + Bot.HITBOX_RADIUS * 6 \
or self.y < -Bot.HITBOX_RADIUS * 6 or self.y > settings.WINDOW_HEIGHT + Bot.HITBOX_RADIUS * 6:
self.pop.eliminate(self, replace = True)
def _turn_left(self, dt):
self.theta += Bot.TURN_RATE / settings.FPS * dt * settings.TIME_MULTIPLIER
while self.theta >= 2 * np.pi:
self.theta -= 2 * np.pi
def _turn_right(self, dt):
self.theta -= Bot.TURN_RATE / settings.FPS * dt * settings.TIME_MULTIPLIER
while self.theta < 0:
self.theta += 2 * np.pi
def update(self, dt, sensory_input):
"""
Updates the bot's internals. "Hunger" can be thought of as a score
between '-1' and '1' where a greater value means less hungry.
"""
self.score -= 1.0 / settings.FPS / 10.0 * dt * settings.TIME_MULTIPLIER
if self.score < -1:
self.score = -1.0
# neural network - feedforward
self.nnet.feed_forward(sensory_input)
output = self.nnet.output()
if seq_is_equal(output, Bot.MOVE_FORWARD):
self._move_forward(dt)
elif seq_is_equal(output, Bot.TURN_LEFT):
self._turn_left(dt)
elif seq_is_equal(output, Bot.TURN_RIGHT):
self._turn_right(dt)
class Food:
"""
The representation of the red circles.
"""
# In pixels.
HITBOX_RADIUS = 5
RGB = (255, 0, 0)
def __init__(self, population):
mid_x = int(settings.WINDOW_WIDTH / 2)
mid_y = int(settings.WINDOW_HEIGHT / 2)
max_left_x = mid_x - (Bot.SPAWN_RADIUS + Bot.HITBOX_RADIUS + 5)
min_right_x = mid_x + (Bot.SPAWN_RADIUS + Bot.HITBOX_RADIUS + 5)
max_top_y = mid_y - (Bot.SPAWN_RADIUS + Bot.HITBOX_RADIUS + 5)
min_bottom_y = mid_y + (Bot.SPAWN_RADIUS + Bot.HITBOX_RADIUS + 5)
self.x = np.random.choice((np.random.uniform(0, max_left_x), np.random.uniform(min_right_x, settings.WINDOW_WIDTH)))
self.y = np.random.choice((np.random.uniform(0, max_top_y), np.random.uniform(min_bottom_y, settings.WINDOW_HEIGHT)))
self.pop = population
def update(self, dt):
"""
Updates the food's internals and handles bot<->food collision.
"""
for bot in self.pop.bots:
if distance_between(self.x, self.y, bot.x, bot.y) <= Bot.HITBOX_RADIUS + Food.HITBOX_RADIUS:
self.pop.feed(bot, self)
break