Skip to content

Commit 7d0408b

Browse files
committed
initial commit
0 parents  commit 7d0408b

17 files changed

+449
-0
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.*
2+
!.gitignore
3+
4+
# pycahrm file
5+
*.iml
6+
7+
# virtual environment
8+
venv
9+
10+
__pycache__

Bird.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from __future__ import annotations
2+
3+
import numpy as np
4+
import pyglet
5+
from icecream import ic
6+
7+
import settings
8+
from NeuralNet import NeuralNet
9+
from settings import WINDOW_HEIGHT, NEURALNET_VISIBILITY_DIST, BIRD_JUMP_VEL, GRAVITY_ACCL
10+
11+
12+
class Bird(pyglet.sprite.Sprite):
13+
def __init__(self, brain: NeuralNet, bird_img, batch: pyglet.graphics.Batch, group: pyglet.graphics.group, x_offset: int, default_y: int):
14+
# anchor centre, for rotations...
15+
bird_img.anchor_x = bird_img.width // 2
16+
bird_img.anchor_y = bird_img.height // 2
17+
18+
super().__init__(img=bird_img, batch=batch, group=group)
19+
self.scale = settings.BIRD_IMG_SCALE
20+
self.brain = brain # the neural net object, backing the bird
21+
self.default_batch = batch
22+
self.batch = batch
23+
self.x_offset = x_offset
24+
self.default_y = default_y
25+
self.score = 0
26+
self.velocity_y = 0
27+
self.died = False
28+
self.x = x_offset
29+
self.y = default_y
30+
#self.color = (np.random.randint(low=0, high=255), np.random.randint(low=0, high=255), np.random.randint(low=0, high=255))
31+
32+
def take_decision(self, y_dist_to_top_pipe: float,
33+
y_dist_to_bottom_pipe: float,
34+
x_dist_to_pipe: float):
35+
max_vel = settings.WINDOW_HEIGHT // 2
36+
bird_vel = self.velocity_y / max_vel
37+
38+
y_dist_to_top_pipe = abs(y_dist_to_top_pipe / WINDOW_HEIGHT)
39+
y_dist_to_bottom_pipe = abs(y_dist_to_bottom_pipe / WINDOW_HEIGHT)
40+
if x_dist_to_pipe > NEURALNET_VISIBILITY_DIST:
41+
x_dist_to_pipe = 1
42+
else:
43+
x_dist_to_pipe = abs(x_dist_to_pipe / NEURALNET_VISIBILITY_DIST)
44+
45+
if y_dist_to_top_pipe < 0 or y_dist_to_top_pipe > 1:
46+
ic(y_dist_to_top_pipe, "out of bounds")
47+
if y_dist_to_bottom_pipe < 0 or y_dist_to_bottom_pipe > 1:
48+
ic(y_dist_to_bottom_pipe, "out of bounds")
49+
if x_dist_to_pipe > 1:
50+
ic(x_dist_to_pipe, "out of bounds")
51+
if bird_vel < -1 or bird_vel > 1:
52+
ic(bird_vel, "out of bounds")
53+
54+
if self.brain.get_choice(np.array([[bird_vel, y_dist_to_top_pipe, y_dist_to_bottom_pipe, x_dist_to_pipe]])) > 0.5:
55+
self._jump()
56+
57+
def _jump(self):
58+
if self.velocity_y <= 0:
59+
self.velocity_y += BIRD_JUMP_VEL
60+
61+
def update_movement(self):
62+
self.velocity_y -= GRAVITY_ACCL
63+
self.y += self.velocity_y
64+
65+
def mate(self, other: Bird) -> Bird:
66+
child = Bird(bird_img=self.image,
67+
batch=self.default_batch,
68+
group=self.group,
69+
x_offset=self.x_offset,
70+
default_y=self.default_y,
71+
brain=self.brain.mate(other.brain))
72+
return child
73+
74+
def get_clone(self) -> Bird:
75+
child = Bird(bird_img=self.image,
76+
batch=self.default_batch,
77+
group=self.group,
78+
x_offset=self.x_offset,
79+
default_y=self.default_y,
80+
brain=self.brain)
81+
return child
82+
83+
def die(self):
84+
self.died = True
85+
self.batch = None # remove from batch, so it won't draw...
86+
87+
def get_x_dist(self, coord: float) -> float:
88+
# FIXME: pos, neg dist...!!!
89+
# + self.width // 2, as it's anchored in the centre...
90+
return coord - (self.x + self.width // 2)
91+
92+
def get_y_dist(self, coord: float) -> float:
93+
# FIXME: pos, neg dist...!!!
94+
# + self.height // 2, as it's anchored in the centre...
95+
return coord - (self.y + self.height // 2)

Ecosystem.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import pyglet.graphics
2+
from icecream import ic
3+
4+
from Bird import Bird
5+
from NeuralNet import NeuralNet
6+
7+
import numpy as np
8+
9+
10+
class Ecosystem:
11+
def __init__(self, population_size: int, num_generations: int, holdout: int, bird_spawner_func):
12+
self.population_size = population_size
13+
self.num_generations = num_generations
14+
self.birds = []
15+
self.generation_id = 1
16+
self.max_score = 0
17+
self.holdout = holdout
18+
self.bird_spawner_func = bird_spawner_func
19+
for _ in range(population_size):
20+
self.birds.append(bird_spawner_func())
21+
22+
def new_generation(self):
23+
self.generation_id += 1
24+
25+
# arrange birds in descending order of their scores
26+
# TODO: any better way of doing this...?
27+
scores = [bird.score for bird in self.birds]
28+
self.birds = [self.birds[x] for x in np.argsort(scores)[::-1]]
29+
self.max_score = np.max(scores)
30+
31+
new_population = []
32+
33+
if self.max_score == 0 and self.generation_id % 15 == 0:
34+
# if for every 15 generations passed, no one scored above 0,
35+
# then demolish the species...
36+
for _ in range(self.population_size):
37+
new_population.append(self.bird_spawner_func())
38+
else:
39+
for i in range(self.population_size):
40+
parent_1_idx = i % self.holdout # parent_1_idx will be within 0 to self.holdout - 1
41+
# TODO: why np.random.exponential...?
42+
parent_2_idx = min(self.population_size - 1, int(np.random.exponential(self.holdout)))
43+
offspring = self.birds[parent_1_idx].mate(self.birds[parent_2_idx])
44+
new_population.append(offspring)
45+
46+
new_population[-1] = self.birds[0].get_clone() # Ensure best organism survives
47+
# new_population[-1].color = (255, 0, 0)
48+
self.birds = new_population
49+
50+
def max_gen_reached(self) -> bool:
51+
return self.num_generations == self.generation_id

NeuralNet.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from __future__ import annotations
2+
3+
import copy
4+
import numpy as np
5+
6+
7+
class NeuralNet:
8+
def __init__(self, dimensions: list):
9+
self.layers = []
10+
self.biases = []
11+
# TODO: do a study on these output activation funcs...
12+
self.output = self._activation()
13+
for i in range(len(dimensions) - 1):
14+
shape = (dimensions[i], dimensions[i + 1])
15+
std = np.sqrt(2 / sum(shape)) # TODO: why std dev is calculated like this...?
16+
layer = np.random.normal(0, std, shape)
17+
bias = np.random.normal(0, std, (1, dimensions[i + 1]))
18+
self.layers.append(layer)
19+
self.biases.append(bias)
20+
21+
def _activation(self, output: str = 'linear'):
22+
if output == 'sigmoid':
23+
return lambda X: (1 / (1 + np.exp(-X)))
24+
else:
25+
return lambda X: X # linear output
26+
27+
def mutate(self, stdev=0.03):
28+
# TODO: change stdev...?
29+
for i in range(len(self.layers)):
30+
self.layers[i] += np.random.normal(0, stdev, self.layers[i].shape)
31+
self.biases[i] += np.random.normal(0, stdev, self.biases[i].shape)
32+
33+
def mate(self, other: NeuralNet) -> NeuralNet:
34+
# TODO: remove the checks...?
35+
if not len(self.layers) == len(other.layers):
36+
raise ValueError('Both parents must have same number of layers')
37+
if not all(self.layers[x].shape == other.layers[x].shape for x in range(len(self.layers))):
38+
raise ValueError('Both parents must have same shape')
39+
40+
child = copy.deepcopy(self)
41+
for i in range(len(child.layers)):
42+
pass_on = np.random.rand(1, child.layers[i].shape[1]) < 0.5
43+
child.layers[i] = pass_on * self.layers[i] + ~pass_on * other.layers[i]
44+
child.biases[i] = pass_on * self.biases[i] + ~pass_on * other.biases[i]
45+
child.mutate()
46+
return child
47+
48+
def get_choice(self, X: np.ndarray) -> float:
49+
# TODO: remove the checks...?
50+
if not X.ndim == 2:
51+
raise ValueError(f'Input has {X.ndim} dimensions, expected 2')
52+
if not X.shape[1] == self.layers[0].shape[0]:
53+
raise ValueError(f'Input has {X.shape[1]} features, expected {self.layers[0].shape[0]}')
54+
for index, (layer, bias) in enumerate(zip(self.layers, self.biases)):
55+
X = X @ layer + np.ones((X.shape[0], 1)) @ bias
56+
if index == len(self.layers) - 1:
57+
X = self.output(X) # output activation
58+
else:
59+
X = np.clip(X, 0, np.inf) # ReLU
60+
61+
return X[0][0]
62+
# return np.argmax(X, axis=1).reshape((-1, 1)) # TODO: is it reshaping from -1 to +1?

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# AI plays-python
2+
3+
A simple flappy bird game, where the player is AI.
4+
5+
A flock of birds will try to survive in this game (rather environment), the best ones will continue, rest will die. Much like our evolution :)
6+
7+
It uses the concept of Evolutionary artificial neural networks (EANNs), or Evolving Neural Networks. I did this project mainly to get some idea of neural networks, machine learning in a somewhat interesting way. I used [this](https://towardsdatascience.com/evolving-neural-networks-b24517bb3701) article a lot, and also studied some free content whatever I could get on the internet :)
8+
9+
Wrote this a couple of months back, around May and uploading this on November. There are places, where this codebase can be improved.... Suggestions, bug-reports, bug-fixes are always welcome :)
10+
11+
## How to play
12+
13+
Assuming that you have `python` installed properly...
14+
15+
1. Clone this repo or use the code button in github and download the zip ([see this](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository#cloning-a-repository))
16+
17+
2. Create a virtual environment (recommended), so that there will be no conflicts between previously installed packages and the packages we will install now. At first `cd` into the downloaded repo, then issue the command: `python3 -m pip venv`
18+
19+
3. Depending on the OS, the command to activate your virtual environment will be: `.\venv\Scripts\activate.ps1` (issue a PR if something is wrong here) or `source venv\bin\activate`
20+
(On closing the terminal the virtual environment will be gone, you should do step 3 again)
21+
22+
4. To install libraries used in this project, issue the command: `pip install -r requirements.txt`
23+
24+
5. Last step: `python main.py`
25+
26+
6. **Use UP and DOWN arrow to increase the speed of the simulation and vice-versa.**
27+
28+
7. To change settings, like resolution etc, edit the `settings.py` file.

0 commit comments

Comments
 (0)