diff --git a/README.md b/README.md index 05da671..2ff20ac 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ Inspired by [https://github.com/ivanseidel/IAMDinosaur](https://github.com/ivans * Run the AI: `python ai.py` * After 5/8 generations, the dino should be a ninja! +## FAQ +* Try this first is you have problem installing pyHook: https://stackoverflow.com/questions/48327695/pyhook-for-python-3-6 +* You maybe should install this if you still cannot install pyHook: https://nchc.dl.sourceforge.net/project/pywin32/pywin32/Build%20221/pywin32-221.win-amd64-py3.6.exe + ## TODO - [ ] Find game automatically in the screen at the beginning diff --git a/dino-ml-win/ai.py b/dino-ml-win/ai.py new file mode 100644 index 0000000..b0e7838 --- /dev/null +++ b/dino-ml-win/ai.py @@ -0,0 +1,11 @@ +from generation import Generation + +def main(): + generation = Generation() + while True: + generation.execute() + generation.keep_best_genomes() + generation.mutations() + +if __name__ == '__main__': + main() diff --git a/dino-ml-win/generation.py b/dino-ml-win/generation.py new file mode 100644 index 0000000..a1df418 --- /dev/null +++ b/dino-ml-win/generation.py @@ -0,0 +1,70 @@ +from scanner import Scanner +from network import Network +from time import sleep +import numpy as np +import pykeyboard +import random +import copy + +class Generation: + def __init__(self): + self.__genomes = [Network() for i in range(12)] + self.__best_genomes = [] + + def execute(self): + k = pykeyboard.PyKeyboard() + scanner = Scanner() + scanner.find_game() + for genome in self.__genomes: + scanner.reset() + k.press_keys([k.control_l_key, 'r']) + k.release_key(k.control_l_key) + sleep(1) + k.press_key(k.space) + while True: + try: + obs = scanner.find_next_obstacle() + inputs = [obs['distance'] / 1000, obs['length'], obs['speed'] / 10] + outputs = genome.forward(np.array(inputs, dtype=float)) + if outputs[0] > 0.55: + k.press_key(k.space) + except: + break + genome.fitness = scanner.get_fitness() + + def keep_best_genomes(self): + self.__genomes.sort(key=lambda x: x.fitness, reverse=True) + self.__genomes = self.__genomes[:4] + self.__best_genomes = self.__genomes[:] + + def mutations(self): + while len(self.__genomes) < 10: + genome1 = random.choice(self.__best_genomes) + genome2 = random.choice(self.__best_genomes) + self.__genomes.append(self.mutate(self.cross_over(genome1, genome2))) + while len(self.__genomes) < 12: + genome = random.choice(self.__best_genomes) + self.__genomes.append(self.mutate(genome)) + + def cross_over(self, genome1, genome2): + new_genome = copy.deepcopy(genome1) + other_genome = copy.deepcopy(genome2) + cut_location = int(len(new_genome.W1) * random.uniform(0, 1)) + for i in range(cut_location): + new_genome.W1[i], other_genome.W1[i] = other_genome.W1[i], new_genome.W1[i] + cut_location = int(len(new_genome.W2) * random.uniform(0, 1)) + for i in range(cut_location): + new_genome.W2[i], other_genome.W2[i] = other_genome.W2[i], new_genome.W2[i] + return new_genome + + def __mutate_weights(self, weights): + if random.uniform(0, 1) < 0.2: + return weights * (random.uniform(0, 1) - 0.5) * 3 + (random.uniform(0, 1) - 0.5) + else: + return 0 + + def mutate(self, genome): + new_genome = copy.deepcopy(genome) + new_genome.W1 += self.__mutate_weights(new_genome.W1) + new_genome.W2 += self.__mutate_weights(new_genome.W2) + return new_genome diff --git a/dino-ml-win/network.py b/dino-ml-win/network.py new file mode 100644 index 0000000..9ab508f --- /dev/null +++ b/dino-ml-win/network.py @@ -0,0 +1,20 @@ +import numpy as np + +class Network: + def __init__(self): + self.input_size = 3 + self.hidden_size = 4 + self.output_size = 1 + self.W1 = np.random.randn(self.input_size, self.hidden_size) + self.W2 = np.random.randn(self.hidden_size, self.output_size) + self.fitness = 0 + + def forward(self, inputs): + self.z2 = np.dot(inputs, self.W1) + self.a2 = np.tanh(self.z2) + self.z3 = np.dot(self.a2, self.W2) + yHat = np.tanh(self.z3) + return yHat + + def sigmoid(self, z): + return 1 / (1 + np.exp(-z)) diff --git a/dino-ml-win/scanner.py b/dino-ml-win/scanner.py new file mode 100644 index 0000000..9e36504 --- /dev/null +++ b/dino-ml-win/scanner.py @@ -0,0 +1,95 @@ +from PIL import Image +from PIL import ImageGrab +from datetime import datetime +import os + +dino_color = (83, 83, 83) + +def screenshot(x, y, w, h): + # os.system("screencapture -R{},{},{},{} tmp.png".format(x, y, w, h)) + img = ImageGrab.grab() + img.save("tmp.png") + save = Image.open("tmp.png") + return save + +def is_dino_color(pixel): + return pixel == dino_color + +def obstacle(distance, length, speed, time): + return { 'distance': distance, 'length': length, 'speed': speed, 'time': time } + +class Scanner: + def __init__(self): + self.dino_start = (0, 0) + self.dino_end = (0, 0) + self.last_obstacle = {} + self.__current_fitness = 0 + self.__change_fitness = False + + def find_game(self): + image = screenshot(0, 0, 1500, 1500) + size = image.size + pixels = [] + for y in range(0, size[1], 10): + for x in range(0, size[0], 10): + color = image.getpixel((x, y)) + if is_dino_color(color): + pixels.append((x, y)) + + if not pixels: + raise Exception("Game not found!") + print("Game found!") + self.__find_dino(pixels) + + def __find_dino(self, pixels): + start = pixels[0] + end = pixels[1] + for pixel in pixels: + if pixel[0] < start[0] and pixel[1] > start[1]: + start = pixel + if pixel[0] > end[0] and pixel[1] > end[1]: + end = pixel + self.dino_start = start + self.dino_end = end + + def find_next_obstacle(self): + image = screenshot(210, 100, 500, 155) + dist = self.__next_obstacle_dist(image) + if dist < 50 and not self.__change_fitness: + self.__current_fitness += 1 + self.__change_fitness = True + elif dist > 50: + self.__change_fitness = False + time = datetime.now() + delta_dist = 0 + speed = 0 + if self.last_obstacle: + delta_dist = self.last_obstacle['distance'] - dist + speed = (delta_dist / ((time - self.last_obstacle['time']).microseconds)) * 10000 + self.last_obstacle = obstacle(dist, 1, speed, time) + return self.last_obstacle + + def __next_obstacle_dist(self, image): + s = 0 + for y in range(0, 250, 5): + for x in range(0, 1000, 5): + color = image.getpixel((x, y)) + if is_dino_color(color): + s += 1 + if s > 150: + raise Exception('Game over!') + + for x in range(0, 1000, 5): + for y in range(0, 310, 5): + color = image.getpixel((x, y)) + if is_dino_color(color): + return x + return 1000000 + + def reset(self): + self.last_obstacle = {} + self.__current_fitness = 0 + self.__change_fitness = False + + def get_fitness(self): + return self.__current_fitness diff --git a/pyHook-1.5.1-cp36-cp36m-win32.whl b/pyHook-1.5.1-cp36-cp36m-win32.whl new file mode 100644 index 0000000..b594d85 Binary files /dev/null and b/pyHook-1.5.1-cp36-cp36m-win32.whl differ diff --git a/pyHook-1.5.1-cp36-cp36m-win_amd64.whl b/pyHook-1.5.1-cp36-cp36m-win_amd64.whl new file mode 100644 index 0000000..e9c9a83 Binary files /dev/null and b/pyHook-1.5.1-cp36-cp36m-win_amd64.whl differ