From f4361a4d1f792bf93c740e33931530e4a80b19ae Mon Sep 17 00:00:00 2001 From: codebecker Date: Thu, 14 Nov 2019 14:25:39 +0100 Subject: [PATCH 1/7] initial commit --- neat/.gitignore | 11 ++ neat/README.md | 124 ++++++++++++++ neat/config | 81 +++++++++ neat/model_ai_finish/config | 81 +++++++++ neat/model_auto_finish/config | 81 +++++++++ neat/neat_goda_config.json | 11 ++ neat/neat_goda_emulator.py | 120 ++++++++++++++ neat/neat_goda_play.py | 273 +++++++++++++++++++++++++++++++ neat/neat_goda_run.py | 70 ++++++++ neat/neat_goda_train.py | 242 +++++++++++++++++++++++++++ neat/neat_goda_train_multiple.py | 20 +++ neat/requirements.txt | 8 + neat/visualize.py | 212 ++++++++++++++++++++++++ 13 files changed, 1334 insertions(+) create mode 100644 neat/.gitignore create mode 100644 neat/README.md create mode 100644 neat/config create mode 100644 neat/model_ai_finish/config create mode 100644 neat/model_auto_finish/config create mode 100644 neat/neat_goda_config.json create mode 100644 neat/neat_goda_emulator.py create mode 100644 neat/neat_goda_play.py create mode 100644 neat/neat_goda_run.py create mode 100644 neat/neat_goda_train.py create mode 100644 neat/neat_goda_train_multiple.py create mode 100644 neat/requirements.txt create mode 100644 neat/visualize.py diff --git a/neat/.gitignore b/neat/.gitignore new file mode 100644 index 0000000..c62e79d --- /dev/null +++ b/neat/.gitignore @@ -0,0 +1,11 @@ +neat-checkpoint-* +.idea +.vscode +__pycache__ +Digraph.gv* +winner.pkl +speciation.svg +avg_fitness.svg +neat-examples +history +*.ods diff --git a/neat/README.md b/neat/README.md new file mode 100644 index 0000000..e72b0a1 --- /dev/null +++ b/neat/README.md @@ -0,0 +1,124 @@ +## Interactive machine learning with Goda + +This is a machine learning implementation for the Goda game. +[Goda](link to game repo) is a game developed by inovex and can be controlled by +a AI trained by this NEAT implementation. + +The process of training the AI can therefore be watched with Goda-NEAT. + +[insert gif of training process] + +Additionally to the model a visualized image of the Neural Network and statistic of the learning process will be saved. + +The best Neural Network and statistics supporting the understanding of the learning process are visualized at +the end of the learning process. + + +## How to setup: + +### Install all dependencies + +For visualizing the neural network ```graphviz``` must be installed at your system: + +``` +sudo apt-get install graphviz +``` + +All dependencies for this project can be installed by: + +``` +python3 -m pip install -r requirements.txt +``` + + +### Change the browser path of pyppeteer + +In the file ``` neat_goda_play.py ``` you will have to change the path in the following line: + +```#browser = await launch({'headless': False, 'args': ['--window-size=1920,1080']}) ``` + +to the path of your prefered chrome (or chromium) installation: + +``` browser = await launch(headless=False, executablePath='/path/to/your/chrome-browser') ``` + + +## How to run: + +### Run a model already trained: + +Models saved to the disk can be run directly by providing their path to the ```neat_goda_run.py```. For exammple to run the sample models provided in the repository use: + +``` +python3.6 neat_goda_run.py model_ai_finish +``` + +If not path is provided the newest model from the history folder will be loaded. + +### Train a new model: + +To train a new model just start the training process with the following command: + +``` +python3.6 neat_goda_train.py +``` + +All progress will be saved to the ```history/[date_time] ``` folder including: config, graph, network and checkpoints + +## Individualize the learning process + +There are two config files in this project ``` neat_goda_config.json ``` for general setting and ``` config``` to change +the behaviour of the machine learning process. By changing their settings you can take impact in the traing setup and process. + +### neat_goda_config.json + +The ``` neat_goda_config.json ``` specifies the learning setup of the goda game. The options are described in the following: + +``` numberOfEvolutions ``` +The numbers of evolutions the model will take by training. Default is 100 the + +``` runNewBest ``` +If a genome with a better fitness then all genomes of all generations before emerges run it. + +``` runEachGeneration ``` +Each n-th generation will be run in the browser so you can watch the training process. + + + +``` numberOfRandomRecipes``` +Denotes with which kind of recips the Model will be trained with. There are three options: + +- numberOfRandomRecipes > 0: The specified number of random recipes will be used to train the model + +- numberOfRandomRecipes == 0: All 18 possible recipes of the game will be used for training + +- numberOfRandomRecipes < 0: All possible recipes plus abs(numberOfRandomRecipes) random recipes will be used for training + + +```fillSteps``` The amouth of units filled in the glass each step. Default is 10. + +```viewNetwork``` True if the Network-Image should be shown after training? They will be saved in any case. + +```viewSpeciesGraph``` True if the Species-Graph should be shown after training? They will be saved in any case. + +```viewFitnessGraph```True if the Fitness-Graph should be shown after training? They will be saved in any case. + +```logToFile``` True if all output should be writen to a logfile. This is usufull if you want to run the training on a server and +inspect the output later. + +### config + +Only one option has an impact to our implementation all others are just for modifying the training process. + +##### num_outputs + ```num_outputs``` specifies how many output nodes the model will have. +If there are 3 outputs the level will be finished by the algorithem as soon as the glass filling exceed 90%. +This is staticly programmed and thus the network doesn't has any impact on this decision. +If there are 4 outputs the fourth output will be used to decide if a level is finished. Thus the network is taking this decision by it's own. + +- num_outputs == 3: The Algorithem will stop the Level as soon as the Glass is filled by more than 90%. + +- num_outputs == 4: The Model itself will decide if when to stop the filling process + + +All other options are best described in the [NEAT-documentation ](https://neat-python.readthedocs.io/en/latest/config_file.html). +Feel free to play around with them to learn about their impact to the models quiality. diff --git a/neat/config b/neat/config new file mode 100644 index 0000000..bf158d9 --- /dev/null +++ b/neat/config @@ -0,0 +1,81 @@ + +[NEAT] +fitness_criterion = max +fitness_threshold = 10000 +no_fitness_termination = True +pop_size = 200 +reset_on_extinction = False + +[DefaultGenome] +# node activation options +activation_default = relu +activation_mutate_rate = 0.0 +activation_options = relu + +# node aggregation options +aggregation_default = sum +aggregation_mutate_rate = 0.0 +aggregation_options = sum + +# node bias options +bias_init_mean = 0.0 +bias_init_stdev = 1.0 +bias_max_value = 30.0 +bias_min_value = -30.0 +bias_mutate_power = 0.5 +bias_mutate_rate = 0.7 +bias_replace_rate = 0.1 + +# genome compatibility options +compatibility_disjoint_coefficient = 1.0 +compatibility_weight_coefficient = 0.5 + +# connection add/remove rates +conn_add_prob = 0.0 +conn_delete_prob = 0.0 + +# connection enable options +enabled_default = True +enabled_mutate_rate = 0.01 + +feed_forward = True +initial_connection = full_nodirect + +# node add/remove rates +node_add_prob = 0.2 +node_delete_prob = 0.2 + +# network parameters +num_hidden = 6 +num_inputs = 7 +num_outputs = 4 + +# node response options +response_init_mean = 1.0 +response_init_stdev = 0.0 +response_max_value = 30.0 +response_min_value = -30.0 +response_mutate_power = 0.0 +response_mutate_rate = 0.0 +response_replace_rate = 0.0 + +# connection weight options +weight_init_mean = 0.0 +weight_init_stdev = 1.0 +weight_max_value = 30 +weight_min_value = -30 +weight_mutate_power = 0.5 +weight_mutate_rate = 0.8 +weight_replace_rate = 0.1 + +[DefaultSpeciesSet] +compatibility_threshold = 3.0 + +[DefaultStagnation] +species_fitness_func = max +max_stagnation = 20 +species_elitism = 2 + +[DefaultReproduction] +elitism = 2 +survival_threshold = 0.2 \ No newline at end of file diff --git a/neat/model_ai_finish/config b/neat/model_ai_finish/config new file mode 100644 index 0000000..bf158d9 --- /dev/null +++ b/neat/model_ai_finish/config @@ -0,0 +1,81 @@ + +[NEAT] +fitness_criterion = max +fitness_threshold = 10000 +no_fitness_termination = True +pop_size = 200 +reset_on_extinction = False + +[DefaultGenome] +# node activation options +activation_default = relu +activation_mutate_rate = 0.0 +activation_options = relu + +# node aggregation options +aggregation_default = sum +aggregation_mutate_rate = 0.0 +aggregation_options = sum + +# node bias options +bias_init_mean = 0.0 +bias_init_stdev = 1.0 +bias_max_value = 30.0 +bias_min_value = -30.0 +bias_mutate_power = 0.5 +bias_mutate_rate = 0.7 +bias_replace_rate = 0.1 + +# genome compatibility options +compatibility_disjoint_coefficient = 1.0 +compatibility_weight_coefficient = 0.5 + +# connection add/remove rates +conn_add_prob = 0.0 +conn_delete_prob = 0.0 + +# connection enable options +enabled_default = True +enabled_mutate_rate = 0.01 + +feed_forward = True +initial_connection = full_nodirect + +# node add/remove rates +node_add_prob = 0.2 +node_delete_prob = 0.2 + +# network parameters +num_hidden = 6 +num_inputs = 7 +num_outputs = 4 + +# node response options +response_init_mean = 1.0 +response_init_stdev = 0.0 +response_max_value = 30.0 +response_min_value = -30.0 +response_mutate_power = 0.0 +response_mutate_rate = 0.0 +response_replace_rate = 0.0 + +# connection weight options +weight_init_mean = 0.0 +weight_init_stdev = 1.0 +weight_max_value = 30 +weight_min_value = -30 +weight_mutate_power = 0.5 +weight_mutate_rate = 0.8 +weight_replace_rate = 0.1 + +[DefaultSpeciesSet] +compatibility_threshold = 3.0 + +[DefaultStagnation] +species_fitness_func = max +max_stagnation = 20 +species_elitism = 2 + +[DefaultReproduction] +elitism = 2 +survival_threshold = 0.2 \ No newline at end of file diff --git a/neat/model_auto_finish/config b/neat/model_auto_finish/config new file mode 100644 index 0000000..5f7139d --- /dev/null +++ b/neat/model_auto_finish/config @@ -0,0 +1,81 @@ + +[NEAT] +fitness_criterion = max +fitness_threshold = 10000 +no_fitness_termination = True +pop_size = 200 +reset_on_extinction = False + +[DefaultGenome] +# node activation options +activation_default = relu +activation_mutate_rate = 0.0 +activation_options = relu + +# node aggregation options +aggregation_default = random +aggregation_mutate_rate = 0.0 +aggregation_options = sum + +# node bias options +bias_init_mean = 0.0 +bias_init_stdev = 1.0 +bias_max_value = 30.0 +bias_min_value = -30.0 +bias_mutate_power = 0.5 +bias_mutate_rate = 0.7 +bias_replace_rate = 0.1 + +# genome compatibility options +compatibility_disjoint_coefficient = 1.0 +compatibility_weight_coefficient = 0.5 + +# connection add/remove rates +conn_add_prob = 0.0 +conn_delete_prob = 0.0 + +# connection enable options +enabled_default = True +enabled_mutate_rate = 0.01 + +feed_forward = True +initial_connection = full_nodirect + +# node add/remove rates +node_add_prob = 0.2 +node_delete_prob = 0.2 + +# network parameters +num_hidden = 6 +num_inputs = 6 +num_outputs = 3 + +# node response options +response_init_mean = 1.0 +response_init_stdev = 0.0 +response_max_value = 30.0 +response_min_value = -30.0 +response_mutate_power = 0.0 +response_mutate_rate = 0.0 +response_replace_rate = 0.0 + +# connection weight options +weight_init_mean = 0.0 +weight_init_stdev = 1.0 +weight_max_value = 30 +weight_min_value = -30 +weight_mutate_power = 0.5 +weight_mutate_rate = 0.8 +weight_replace_rate = 0.1 + +[DefaultSpeciesSet] +compatibility_threshold = 3.0 + +[DefaultStagnation] +species_fitness_func = max +max_stagnation = 20 +species_elitism = 2 + +[DefaultReproduction] +elitism = 2 +survival_threshold = 0.2 \ No newline at end of file diff --git a/neat/neat_goda_config.json b/neat/neat_goda_config.json new file mode 100644 index 0000000..5cc7423 --- /dev/null +++ b/neat/neat_goda_config.json @@ -0,0 +1,11 @@ +{ + "numberOfEvolutions":100, + "runEachGeneration": 0, + "runNewBest": true, + "numberOfRandomRecipes": 0, + "fillSteps":5, + "viewNetwork": true, + "viewSpeciesGraph": false, + "viewFitnessGraph": false, + "logToFile": false +} \ No newline at end of file diff --git a/neat/neat_goda_emulator.py b/neat/neat_goda_emulator.py new file mode 100644 index 0000000..840ff71 --- /dev/null +++ b/neat/neat_goda_emulator.py @@ -0,0 +1,120 @@ +import math +import neat + + +class GameEmulation: + def __init__(self, capacity, fillSteps, recipe): + self.stateCyan = 0 + self.stateMagenta = 0 + self.stateYellow = 0 + self.capacity = capacity + self.fillSteps = fillSteps + self.recipe = recipe + self.stepCounter = 0 + self.stopGame = False + self.fillInPercent = 0.0 + +#get current Glass color +def getGlassColorRGB(g): + #RGB Werte herausschneiden und in int array parsen + sum = g.stateCyan + g.stateMagenta + g.stateYellow + ratioCyan = 0 + ratioMagenta = 0 + ratioYellow = 0 + + if sum != 0 : + ratioCyan = g.stateCyan / sum + ratioMagenta = g.stateMagenta / sum + ratioYellow = g.stateYellow / sum + + redValue = int((ratioMagenta + ratioYellow)*255) + greenValue = int((ratioCyan + ratioYellow)*255) + blueValue = int((ratioCyan + ratioMagenta)*255) + + return [redValue, greenValue, blueValue] + +#Calculates current fill state of provided gameState and saves it to the gameState.fillInPercent +#returns fill state in percent +def calcFillPercent(g): + g.fillInPercent = (g.stateCyan + g.stateMagenta + g.stateYellow) / g.capacity * 100 + return g.fillInPercent + +#Calc points based on filling and color NO TIME YET!!! +def getPoints(game): + + fillPercent = calcFillPercent(game) + if fillPercent > 107: + # if the glass overflows once player gets no points at all + return 0 + elif fillPercent < 85: + fillScore = -(85 - fillPercent) / 85 * 50 + else: + fillScore = max(abs(fillPercent - 90) / 17 * 5, 0) + + colorGlass = getGlassColorRGB(game) + distance = math.sqrt(math.pow(colorGlass[0]-game.recipe[0], 2) + math.pow(colorGlass[1]-game.recipe[1], 2) + math.pow(colorGlass[2]-game.recipe[2], 2)) + color_distance_zero = 150 + if distance > color_distance_zero: + colorAccuracy = 0 + else: + colorAccuracy = 1 - distance / color_distance_zero + + colorScore = int(colorAccuracy * 10000) + fillScore = int(colorAccuracy * 10000 * fillScore / 100) + + #TODO:Include time bonus in fitness calculation + #timeBonus = math.min(timeLeft / 1000, 15) / 15 * 5 + #timeBonusScore = math.round(colorAccuracy * 10000 * timeBonus / 100) + + return colorScore + fillScore #+timeScore + +#play a emulated game with reduced functionality for faster training +#fillSteps = amouth of Color added each time when using fillColor +#recipe is a RGB recipe in a array +def playGameEmulator(genome, config, fillSteps, recipe, capacity = 400): + fillThreshold = 0 + + net = neat.nn.FeedForwardNetwork.create(genome, config) + gameState = GameEmulation(capacity, fillSteps, recipe) + + if len(net.output_nodes) == 3: + #will be be stoped by algotihem at specified threshold + fillThreshold = 95 + elif len(net.output_nodes) == 4: + #AI decides by it's own when to stop + #If the glas is filled more then 107 it will get 0 points + fillThreshold = 107 + + + while gameState.stopGame == False: + + currentColor = getGlassColorRGB(gameState) + + #ask the model what to do + output = net.activate(currentColor + recipe + [gameState.fillInPercent]) + + #extract and execute models decision + maxValue = max(output) + maxIndex = output.index(maxValue) + + # if maxValue == 0: + # network didn't made any decision? no colors will be filled + # return + + if maxIndex == 0: + gameState.stateCyan += gameState.fillSteps + + elif maxIndex == 1: + gameState.stateMagenta += gameState.fillSteps + + elif maxIndex == 2: + gameState.stateYellow += gameState.fillSteps + + elif maxIndex == 3: + gameState.stopGame = True + + if calcFillPercent(gameState) > fillThreshold: + #end game if threshold has been passed + gameState.stopGame = True + + return getPoints(gameState) \ No newline at end of file diff --git a/neat/neat_goda_play.py b/neat/neat_goda_play.py new file mode 100644 index 0000000..dd47a96 --- /dev/null +++ b/neat/neat_goda_play.py @@ -0,0 +1,273 @@ +import asyncio +import neat + +from pyppeteer import launch + + +CYAN_KEY = "a" +MAGENTA_KEY = "s" +YELLOW_KEY = "d" +DONE_KEY ="j" +ABORT_KEY = "o" + +COLOR_KEYS = [CYAN_KEY, MAGENTA_KEY, YELLOW_KEY, DONE_KEY] + +globalScore = -1 +globalFinalScore = -1 +capacyityLevel = [400, 400, 250, 100] + +#Get the RGB color in the glass of specified page game +async def getGlassColor(page): + data = await page.evaluate('''() => { + return { + glassColor: document.getElementById('CocktailGlassColor').getAttribute('fill'), + } + }''' + ) + #RGB Werte herausschneiden und in int array parsen + colorString = data["glassColor"] + colorString = colorString[5:len(colorString)-3].split(",") + return list(map(int, colorString)) + +#Get the RBG recipe of specified page game +async def getRecipe(page): + data = await page.evaluate('''() => { + return { + glassColor: document.getElementById('HINTGLASS').getAttribute('fill'), + } + }''' + ) + #RGB Werte herausschneiden und in int array parsen + colorString = data["glassColor"] + colorString = colorString[5:len(colorString)-3].split(",") + return list(map(int, colorString)) + +#Get fill status in percent of specified page game +async def getGlassFillPercent(page): + data = await page.evaluate('''() => { + return { + glassFill: document.getElementsByClassName('Glass__Filling').item(0).getAttribute('data-fillpercent'), + } + }''' + ) + return float(data["glassFill"]) + +#Get the current color and fill in Percent of specified page game +async def getGlassFillPercentAndColor(page): + data = await page.evaluate('''() => { + return { + glassFill: document.getElementsByClassName('Glass__Filling').item(0).getAttribute('data-fillpercent'), + glassColor: document.getElementById('CocktailGlassColor').getAttribute('fill') + } + }''' + ) + + colorString = data["glassColor"] + colorString = colorString[5:len(colorString) - 3].split(",") + return float(data["glassFill"]), list(map(int, colorString)) + + +#Setups a observer in the game which calls updateGlassFill each time the content of the glass changes +#page: page in which the game is running +#At the moment this approach isn't used because it is uses a lot of perfomance +async def setupUpdateGlassFillHandler(page): + await page.exposeFunction("handleColorFill",updateGlassFill) + + await page.evaluate(''' + const glassFill = document.getElementById('Glass__Filling').item(0).getAttribute('data-fillpercent'); + const observerFill = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + window.handleColorGlass(mutation.target.getAttribute('fill')); + }) + }); + const config = { characterData: false, attributes: true, childList: false, subtree: false }; + observer.observe(glass, config);''' + , force_expr=True) + +#updates the global glass fill variable this function should only be called by setupUpdateGlassFillHandler +def updateGlassFill(glassFill): + globalGlassFill = glassFill + +#Setups a observer in the game which calls updateGlassColor each time the content of the glass changes +#page: page in which the game is running +#At the moment this approach isn't used because it is uses a lot of perfomance +async def setupUpdateGlassColorHandler(page): + await page.exposeFunction("handleColorGlass",updateGlassColor) + + await page.evaluate(''' + const glass = document.getElementById('CocktailGlassColor'); + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + window.handleColorGlass(mutation.target.getAttribute('fill')); + }) + }); + const config = { characterData: false, attributes: true, childList: false, subtree: false }; + observer.observe(glass, config);''' + , force_expr=True) + +#updates the global glass color variable this function should only be called by setupUpdateGlassColorHandler +def updateGlassColor(glassColor): + glassColor = glassColor[5:len(glassColor) - 3].split(",") + golbalGlassColor = list(map(int, glassColor)) + +#Setups a observer in the game which calls updateScore each time the score changes +#page: page in which the game is running +#At the moment this approach isn't used because it is uses a lot of perfomance +async def setupUpdateScoreHandler(page): + await page.exposeFunction("handleIntermediate",updateScore) + await page.evaluate("window.addEventListener('intermediateScore', (e)=>{window.handleIntermediate(e.detail.totalScore) }) ", force_expr=True) + +#updates the global score variable this function should only be called by setupUpdateScoreHandler +def updateScore(currentScore): + global globalScore + globalScore = currentScore + +#Setups a observer in the game which calls finalScore each time the final score changes +#page: page in which the game is running +#At the moment this approach isn't used because it is uses a lot of perfomance +async def setupUpdateFinalScoreHandler(page): + await page.exposeFunction("handleFinalScore",updateFinalScore) + await page.evaluate("window.addEventListener('finalScore', (e)=>{window.handleFinalScore(e.detail.totalScore) }) ", force_expr=True) + +#updates the global score variable this function should only be called by setupUpdateFinalScoreHandler +def updateFinalScore(finalScore): + global globalFinalScore + print("Final score", finalScore) + globalFinalScore = finalScore + +#setups the butten press logic thus only one button is pressed at the same time +async def setupButtonPressHandler(page): + #only one button a time can be pressed + #a button is pressed and released after a specified timeout + #if a butten is pressed while another button is still pressed the previous button will be released and the new one is pressed + return await page.evaluate(''' + window.myButtonTimeouts = {a: null, s:null, d:null, j:null, o:null }; + window.pressMyButton = (button, time) => { + if (window.myButtonTimeouts[button] !== null) { + clearTimeout(window.myButtonTimeouts[button]); + } + const e_down = new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: button, char: button, shiftKey: false }); + document.dispatchEvent(e_down); + + window.myButtonTimeouts[button] = setTimeout(() => { + const e_up = new KeyboardEvent("keyup", { bubbles: true, cancelable: true, key: button, char: button, shiftKey: false }); + document.dispatchEvent(e_up); + }, time); + }; + ''', force_expr=True) + +#press a button for a specified time +#This function is used to press the color buttons and to continue a level +#page: page running the game +#key: key which should be pressed as char +#duration: is time in SECONDS +async def pressButton(page, key, duration): + duration *= 1000 # seconds to milliseconds + # wenn zweimal die selbe taste gedrückt wird und die events dazu verzahnt sind muss das folgende key up event gelösch werden + return await page.evaluate('''(keyP, time) => { + window.pressMyButton(keyP, time) + }''', key, duration) + +#Chrome does loose connection after 20 seconds due to it's default timeout +#This function fixes this issue +def patch_pyppeteer(): + import pyppeteer.connection + original_method = pyppeteer.connection.websockets.client.connect + + def new_method(*args, **kwargs): + kwargs['ping_interval'] = None + kwargs['ping_timeout'] = None + return original_method(*args, **kwargs) + + pyppeteer.connection.websockets.client.connect = new_method + +#browser: a chrome browser where the game will be startet in a new page +#genome: the model which should play the game +#config: config specifing the models settings +#fillSteps: Amount of color added each interval thus also specifies number of intervals +async def playGame(browser, genome, config, fillSteps): + + global globalFinalScore, globalScore, capacyityLevel + + net = neat.nn.FeedForwardNetwork.create(genome, config) + + page = await browser.newPage() + await page.setViewport({'width': 1280, 'height': 720}) + await page.goto('https://build.bontscho.now.sh/') + await setupButtonPressHandler(page) + await setupUpdateScoreHandler(page) + await setupUpdateFinalScoreHandler(page) + + for index in range(len(capacyityLevel)): + await playLevel(page, net, fillSteps, capacyityLevel[index]) + + await pressButton(page, DONE_KEY, 0.1); # finish game + await asyncio.sleep(5)#show score screen for 5 secconds + + await pressButton(page, DONE_KEY, 0.1); #continue to next level + await asyncio.sleep(0.1)#test buffer + + #TODO: GET FITNESS WITH POINTS MADE IN THE GAME! + #points = 0 + #genome.fitness = points + + await page.close() + return + +#plays a level of Goda with provided Model +#net: Model network making decision each interval +#fillSteps: Amount of color added each interval thus also specifies number of intervals +#capacity: Capacity of the glass in this level +async def playLevel(page, net, fillSteps, capacity): + + #all time units are in MS + fillRatePerSecond = 50 #one second color-butten press will fill 50 unit of color in the glass + timeSteps = fillSteps / fillRatePerSecond #in seconds fillSteps / 50 units/s + stopGame = False + oldFillPercent = 0 + + recipe = await getRecipe(page) + + numberOutputs = len(net.output_nodes) + + + while stopGame == False : + fillPercent, currentColor = await getGlassFillPercentAndColor(page) + + if numberOutputs == 3 and fillPercent >= 90: + #stop game by algo + break + + output = net.activate(currentColor + recipe + [fillPercent]) + + maxValue = max(output) + maxIndex = output.index(maxValue) + + #if maxIndex < 3 and maxValue != 0: # model can decide to make no fill at all + if maxIndex < 3 : #always wil take highest input as fill + await pressButton(page, COLOR_KEYS[maxIndex], timeSteps) + + if maxIndex == 3: #if AI decides to stop + stopGame = True + + #TODO: If Glass overruns the level should be ended + #if oldFillPercent > fillPercent: + # #if glass has been spilled end game + # endLevel() # end level by waiting till time is over? + # break + + oldFillPercent = fillPercent + + await asyncio.sleep(timeSteps)#sleep is in seconds ... wired + +#This function will open a new browser and run a game of Goda with specified model +#genome: the model which should play the game +#config: config specifing the models settings +#fillSteps: Amount of color added each interval thus also specifies number of intervals +async def runSingleGame(genome, config, fillSteps): + patch_pyppeteer() #prevents browser timeout after 20 sec + browser = await launch({'headless': False, 'args': ['--window-size=1920,1080']}) + await playGame(browser, genome, config, fillSteps) + await browser.close() + + diff --git a/neat/neat_goda_run.py b/neat/neat_goda_run.py new file mode 100644 index 0000000..0cd9509 --- /dev/null +++ b/neat/neat_goda_run.py @@ -0,0 +1,70 @@ +""" +Implementation of Goda Game +""" + +from __future__ import print_function + +import asyncio +import fnmatch +import os +import sys +import json + +import neat.checkpoint as load +from neat.six_util import itervalues + +from neat_goda_play import runSingleGame + +#Loads best model from a checkpoint folder and runs a game of Goda with it +#If no checkpoint folder is specified the newest checkpoint folder from history folder will be loaded +def main(checkpoint_folder = None): + + if checkpoint_folder is not None: + #check if specified checkpoint folder exists + assert (os.path.isdir(checkpoint_folder)), ("Couldn't find specified checkpoint folder:" + checkpoint_folder) + os.chdir(checkpoint_folder) + else: + #search for newest checkpoint + assert (os.path.isdir("history")), "Couldn't find a history folder. Train model first!" + checkpoint_folder = os.listdir("history") #all folders + checkpoint_folder = max(checkpoint_folder) #only newest folder + checkpoint_folder = "history/"+checkpoint_folder + os.chdir(checkpoint_folder) + + #check if config is present + assert (os.path.isfile("config")), ("config file is missing in folder"+checkpoint_folder) + + #extract newest checkpoint + checkpoint = os.listdir(".") # all folders + checkpoint = fnmatch.filter(checkpoint, 'neat-checkpoint*') # only checkpoints + checkpoint = max(checkpoint) # only newest checkpoints + assert (checkpoint != None), "Couldn't find any checkpoints in folder." + + #load genomes newest checkpoint + population = load.Checkpointer.restore_checkpoint(checkpoint) + config = population.config + genomes = population.population + + # load neat_goda_config.json data + assert (os.path.isfile('neat_goda_config.json')), "neat_goda_config.json file is missing" + with open('neat_goda_config.json') as config_file: + config_data = json.load(config_file) + fillSteps = config_data['fillSteps'] + + #find best genome on all genomes + bestGenome = None + for g in itervalues(genomes): + if g.fitness is not None and (bestGenome is None or g.fitness > bestGenome.fitness): + bestGenome = g + + print("Run game with model from folder {} and fitness of {}".format(checkpoint_folder, bestGenome.fitness)) + asyncio.get_event_loop().run_until_complete(runSingleGame(bestGenome, config, fillSteps)) + + +if __name__ == '__main__': + + if (len(sys.argv) == 2): + checkpoint_folder = sys.argv[1] + main(checkpoint_folder) + else: + main() diff --git a/neat/neat_goda_train.py b/neat/neat_goda_train.py new file mode 100644 index 0000000..f2915fe --- /dev/null +++ b/neat/neat_goda_train.py @@ -0,0 +1,242 @@ +""" +Implementation of Goda Game +""" + +from __future__ import print_function +import os +import asyncio +import neat +import random +import datetime +import shutil +import multiprocessing as mp +import sys +import json + +#import from local files +from neat_goda_emulator import playGameEmulator +from neat_goda_play import runSingleGame +import visualize + +#changeable parameters +numberOfEvolutions = 0 +runEachGeneration = 0 +runNewBest = False +numberOfRandomRecipes = 0 +fillSteps = 0 +viewGraphs = False +trainRecipes = [] +viewNetwork = False +viewSpeciesGraph = False +viewFitnessGraph = False + +'''recipes = [[255, 255, 255], + [255,0,0], + [0,255,0], + [0,0,255], + [255, 51, 255], + [255, 153, 51]] #random training set +''' + +#default not chanageable parameters +counter = 0 +pool = None +globalBestGenomeFitness = 0 + + +def addRandomColors(n=10): + global trainRecipes + for i in range(n): + trainRecipes.append([random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)]) + +def addRatioColors(): + global trainRecipes + ratiosInLevels = [[60,30,10], + [90,50,30], + [40,20,10]] + + #for each combination of ratios + for ratio in ratiosInLevels: + for ratioA in ratio: + for ratioB in ratio: + for ratioC in ratio: + if (ratioA is not ratioB and ratioB is not ratioC and ratioA is not ratioC): + trainRecipes.append(generateRatioColor(ratioA, ratioB, ratioC)) + +def generateRatioColor(ratioC, ratioM, ratioY): + + sumTotal = ratioC + ratioM + ratioY + ratioCyan = ratioC / sumTotal + ratioMagenta = ratioM / sumTotal + ratioYellow = ratioY / sumTotal + valueRed = round((ratioMagenta + ratioYellow) * 255) + valueGreen = round((ratioCyan + ratioYellow) * 255) + valueBlue = round((ratioCyan + ratioMagenta) * 255) + return [valueRed, valueGreen, valueBlue] + + +#evaluate all genomes in a generation +def eval_genomes(genomes, config): + global trainRecipes, counter, runEachGeneration, pool, fillSteps, globalBestGenomeFitness + + #parallel approach + pool = mp.Pool(mp.cpu_count()) + jobs = [] + + #start a async job for each genome + for genome_id, genome in genomes: + jobs.append(pool.apply_async(evaluate_single_genome, (genome_id, genome, config, fillSteps, trainRecipes))) + + # assign the fitness back to each genome + for job, (ignored_genome_id, genome) in zip(jobs, genomes): + #job.get is a blocking func waiting for the results of execution above + genome.fitness = job.get(timeout=None) + + pool.close() + counter += 1 + + #if a better genome has emerged from crossover and if option is set in configuration run new best genome + if (runNewBest == True): + bestGenome, bestGenomeFitness = findBestGenome(genomes) + if(bestGenomeFitness > globalBestGenomeFitness): + globalBestGenomeFitness = bestGenomeFitness + runGameWithGenome(bestGenome, config, fillSteps) + + #run each n-th generation in browser n = runEachGeneration OR + if (runEachGeneration != 0 and counter % runEachGeneration == 0): + bestGenome, bestGenomeFitness = findBestGenome(genomes) + runGameWithGenome(bestGenome, config, fillSteps) + return + +#finds the best genome grom a array of genomes +def findBestGenome(genomes): + # find fittest genome + bestGenomeFitness = 0 + for genome_id, genome in genomes: + if bestGenomeFitness <= genome.fitness: + bestGenomeFitness = genome.fitness + bestGenome = genome + + return bestGenome, bestGenomeFitness + +#opens a browser and starts game with specified genome's model +def runGameWithGenome(genome, config, fillSteps): + print("Run game with fitness of"+str(genome.fitness)) + loop = asyncio.get_event_loop() + loop.run_until_complete(runSingleGame(genome, config, fillSteps)) + + +#evaluate a single genome +def evaluate_single_genome(genome_id, genome, config, fillSteps, recipes): + fitness = 0 + #run genome for each recipe once and calculate it's fitness + capacitys = [400] + for capacity in capacitys: + for recipe in recipes: + fitness += playGameEmulator(genome, config, fillSteps, recipe, capacity) + + fitness = fitness / (len(recipes)*len(capacitys)) + return fitness + +#trains a model with specified config settings +def train(config_file): + global recipe, numberOfEvolutions, pool, numberOfRandomRecipes, \ + viewGraphs, viewNetwork, viewSpeciesGraph, viewFitnessGraph + + addRandomColors(abs(numberOfRandomRecipes)) + + if numberOfRandomRecipes <= 0: + addRatioColors() + + config = neat.Config(neat.DefaultGenome, neat.DefaultReproduction, + neat.DefaultSpeciesSet, neat.DefaultStagnation, + config_file) + + # Create the population, which is the top-level object for a NEAT run. + p = neat.Population(config) + + # Add a stdout reporter to show progress in the terminal. + p.add_reporter(neat.StdOutReporter(True)) + stats = neat.StatisticsReporter() + p.add_reporter(stats) + p.add_reporter(neat.Checkpointer(10)) + + #create pool for multiprocessing + pool = mp.Pool(mp.cpu_count()) + + # Run for up to x generations. + winner = p.run(eval_genomes, numberOfEvolutions) + + # Display the winning genome. + print('\nBest genome:\n{!s}'.format(winner)) + + node_names = {-1:'glassR (-1)', -2: 'glassG (-2)' , -3: 'glassB(-3)', -4: 'recipeR(-4)', -5: 'recipeG(-5)', -6: 'recipeB(-6)', -7: 'fillState(-7)', 0:'outC (0)', 1: 'outM (1)', 2: 'outY(2)', 3: 'stop(3)'} + visualize.draw_net(config, winner, view=viewNetwork, node_names=node_names) + visualize.plot_stats(stats, ylog=False, view=viewFitnessGraph) + visualize.plot_species(stats, view=viewSpeciesGraph) + +#Run the training process by +#Load all necessary data +#Setup all necessary folders +#Starting the trainings process +def main(config_name = "config"): + global numberOfEvolutions, runEachGeneration, runNewBest, numberOfRandomRecipes, fillSteps, \ + viewNetwork, viewSpeciesGraph,viewFitnessGraph + + # load config file data + assert (os.path.isfile('neat_goda_config.json')), "neat_goda_config.json file is missing" + with open('neat_goda_config.json') as config_file: + config_data = json.load(config_file) + + numberOfEvolutions = config_data['numberOfEvolutions'] + runEachGeneration = config_data['runEachGeneration'] + runNewBest = config_data['runNewBest'] + numberOfRandomRecipes = config_data['numberOfRandomRecipes'] + fillSteps = config_data['fillSteps'] + logToFile = config_data['logToFile'] + viewNetwork = config_data['viewNetwork'] + viewSpeciesGraph = config_data['viewSpeciesGraph'] + viewFitnessGraph = config_data['viewFitnessGraph'] + + + assert (os.path.isfile(config_name)), ("config file is missing filename:" + config_name) + + # Determine path to configuration file. This path manipulation is + # here so that the script will run successfully regardless of the + # current working directory. + + if not os.path.isdir('./history'): + os.mkdir("history") + + # create new savefoleder in history + savefolderName = str(datetime.datetime.now()) + savefolderName = savefolderName.replace(" ", "_") + local_dir = os.path.dirname(__file__) + os.chdir("history") + os.mkdir(savefolderName) + os.chdir("..") + + # move neat config to savefolder + savefolderPath = "history/" + savefolderName + shutil.copy('config', savefolderPath + "/" + config_name) + shutil.copy('neat_goda_config.json', savefolderPath + "/" + 'neat_goda_config.json') + config_path = os.path.join(local_dir, config_name) + os.chdir(savefolderPath) + + #if logFile is true all print outputs will be redirected to a logfile + if (logToFile): + print("All output will be redirected to the log.txt file in " + savefolderPath) + orig_stdout = sys.stdout + logFile = open('log.txt', 'w') + sys.stdout = logFile + + train(config_path) + + +if __name__ == '__main__': + + if (len(sys.argv) == 2): + config_name = sys.argv[1] + main(config_name) + else: + main('config') diff --git a/neat/neat_goda_train_multiple.py b/neat/neat_goda_train_multiple.py new file mode 100644 index 0000000..93bc40f --- /dev/null +++ b/neat/neat_goda_train_multiple.py @@ -0,0 +1,20 @@ + + +import neat_goda_train as godtrain +import os + +#This script runs multiple training sessions one after another +#Thus it starts be laoding config0 train a model and continues mit config1 +#if there is no directly following numbered config it will stop +if __name__ == '__main__': + + counter = 0 + conf_name = "config" + str(counter) + run = os.path.isfile(conf_name) + root = os.getcwd() + + while(os.path.isfile(conf_name)): + godtrain.main(conf_name) + counter = counter + 1 + conf_name = "config" + str(counter) + os.chdir(root) diff --git a/neat/requirements.txt b/neat/requirements.txt new file mode 100644 index 0000000..7652856 --- /dev/null +++ b/neat/requirements.txt @@ -0,0 +1,8 @@ +#### Run the following command to install all requirements ##### +#### python3 -m pip install -r requirements.txt ##### + +matplotlib +graphviz +neat-python == 0.92 +asyncio +pyppeteer == 0.0.25 diff --git a/neat/visualize.py b/neat/visualize.py new file mode 100644 index 0000000..27b4a4a --- /dev/null +++ b/neat/visualize.py @@ -0,0 +1,212 @@ +from __future__ import print_function + +import copy +import warnings + +import graphviz +import matplotlib.pyplot as plt +import numpy as np + + +def plot_stats(statistics, ylog=False, view=False, filename='avg_fitness.svg'): + """ Plots the population's average and best fitness. """ + if plt is None: + warnings.warn("This display is not available due to a missing optional dependency (matplotlib)") + return + + generation = range(len(statistics.most_fit_genomes)) + best_fitness = [c.fitness for c in statistics.most_fit_genomes] + avg_fitness = np.array(statistics.get_fitness_mean()) + stdev_fitness = np.array(statistics.get_fitness_stdev()) + + plt.plot(generation, avg_fitness, 'b-', label="average") + plt.plot(generation, avg_fitness - stdev_fitness, 'g-.', label="-1 sd") + plt.plot(generation, avg_fitness + stdev_fitness, 'g-.', label="+1 sd") + plt.plot(generation, best_fitness, 'r-', label="best") + + plt.title("Population's average and best fitness") + plt.xlabel("Generations") + plt.ylabel("Fitness") + plt.grid() + plt.legend(loc="best") + if ylog: + plt.gca().set_yscale('symlog') + + plt.savefig(filename) + if view: + plt.show() + + plt.close() + + +def plot_spikes(spikes, view=False, filename=None, title=None): + """ Plots the trains for a single spiking neuron. """ + t_values = [t for t, I, v, u, f in spikes] + v_values = [v for t, I, v, u, f in spikes] + u_values = [u for t, I, v, u, f in spikes] + I_values = [I for t, I, v, u, f in spikes] + f_values = [f for t, I, v, u, f in spikes] + + fig = plt.figure() + plt.subplot(4, 1, 1) + plt.ylabel("Potential (mv)") + plt.xlabel("Time (in ms)") + plt.grid() + plt.plot(t_values, v_values, "g-") + + if title is None: + plt.title("Izhikevich's spiking neuron model") + else: + plt.title("Izhikevich's spiking neuron model ({0!s})".format(title)) + + plt.subplot(4, 1, 2) + plt.ylabel("Fired") + plt.xlabel("Time (in ms)") + plt.grid() + plt.plot(t_values, f_values, "r-") + + plt.subplot(4, 1, 3) + plt.ylabel("Recovery (u)") + plt.xlabel("Time (in ms)") + plt.grid() + plt.plot(t_values, u_values, "r-") + + plt.subplot(4, 1, 4) + plt.ylabel("Current (I)") + plt.xlabel("Time (in ms)") + plt.grid() + plt.plot(t_values, I_values, "r-o") + + if filename is not None: + plt.savefig(filename) + + if view: + plt.show() + plt.close() + fig = None + + return fig + + +def plot_species(statistics, view=False, filename='speciation.svg'): + """ Visualizes speciation throughout evolution. """ + if plt is None: + warnings.warn("This display is not available due to a missing optional dependency (matplotlib)") + return + + species_sizes = statistics.get_species_sizes() + num_generations = len(species_sizes) + curves = np.array(species_sizes).T + + fig, ax = plt.subplots() + ax.stackplot(range(num_generations), *curves) + + plt.title("Speciation") + plt.ylabel("Size per Species") + plt.xlabel("Generations") + + plt.savefig(filename) + + if view: + plt.show() + + plt.close() + + +def draw_net(config, genome, view=False, filename=None, node_names=None, show_disabled=True, prune_unused=False, + node_colors=None, fmt='svg'): + """ Receives a genome and draws a neural network with arbitrary topology. """ + # Attributes for network nodes. + if graphviz is None: + warnings.warn("This display is not available due to a missing optional dependency (graphviz)") + return + + if node_names is None: + node_names = {} + + for index in node_names: + node_names[index] = node_names.get(index, str(index)) + + assert type(node_names) is dict + + if node_colors is None: + node_colors = {} + + assert type(node_colors) is dict + + node_attrs = { + 'shape': 'circle', + 'fontsize': '9', + 'height': '0.2', + 'width': '0.2'} + + dot = graphviz.Digraph(format=fmt, node_attr=node_attrs) + + inputs = set() + for k in config.genome_config.input_keys: + inputs.add(k) + name = node_names.get(k, str(k)) + input_attrs = {'style': 'filled', + 'shape': 'box'} + input_attrs['fillcolor'] = node_colors.get(k, 'lightgray') + dot.node(name, _attributes=input_attrs) + + outputs = set() + for k in config.genome_config.output_keys: + outputs.add(k) + #add attributes to node_name + node_names[k] += " \n" + genome.nodes.get(k).aggregation + " \n" + str(genome.nodes.get(k).bias) + " \n" + genome.nodes.get(k).activation + name = node_names.get(k, str(k)) + node_attrs = {'style': 'filled'} + node_attrs['fillcolor'] = node_colors.get(k, 'lightblue') + + dot.node(name, _attributes=node_attrs) + + #do not show nodes with no connection if prune_unuseed is true + if prune_unused: + connections = set() + for cg in genome.connections.values(): + if cg.enabled or show_disabled: + connections.add((cg.in_node_id, cg.out_node_id)) + + used_nodes = copy.copy(outputs) + pending = copy.copy(outputs) + while pending: + new_pending = set() + for a, b in connections: + if b in pending and a not in used_nodes: + new_pending.add(a) + used_nodes.add(a) + pending = new_pending + else: + used_nodes = set(genome.nodes.keys()) + + #create dot for each used node + for n in used_nodes: + if n in inputs or n in outputs: + continue + + attrs = {'style': 'filled', + 'fillcolor': node_colors.get(n, 'white')} + + #add attribute to node name and add new names to node_names dict + name = str(n) + " \n" + genome.nodes.get(n).aggregation + " \n" + str(genome.nodes.get(n).bias) + " \n" + genome.nodes.get(n).activation + node_names[n] = name + dot.node(name, _attributes=attrs) + + #create all connections + for cg in genome.connections.values(): + if cg.enabled or show_disabled: + #if cg.input not in used_nodes or cg.output not in used_nodes: + # continue + input, output = cg.key + a = node_names.get(input, str(input)) + b = node_names.get(output, str(output)) + style = 'solid' if cg.enabled else 'dotted' + color = 'green' if cg.weight > 0 else 'red' + width = str(0.1 + 0.4) + dot.edge(a, b, _attributes={'style': style, 'color': color, 'penwidth': width, 'label': str(round(cg.weight,4))}) + + dot.render(filename, view=view) + + return dot From dc6dcbcd7e89b9573abe7234ebe9cbd1c58326af Mon Sep 17 00:00:00 2001 From: codebecker Date: Thu, 14 Nov 2019 14:41:43 +0100 Subject: [PATCH 2/7] Updated README.md --- neat/README.md | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/neat/README.md b/neat/README.md index e72b0a1..41658b2 100644 --- a/neat/README.md +++ b/neat/README.md @@ -1,12 +1,10 @@ -## Interactive machine learning with Goda +## Interactive machine learning with Cocktail-Mash -This is a machine learning implementation for the Goda game. -[Goda](link to game repo) is a game developed by inovex and can be controlled by +This is a machine learning implementation for Cocktail-Mash. +Cocktail-Mash is a game developed by inovex and can be controlled by a AI trained by this NEAT implementation. -The process of training the AI can therefore be watched with Goda-NEAT. - -[insert gif of training process] +The process of training the AI can therefore be watched with this implementation. Additionally to the model a visualized image of the Neural Network and statistic of the learning process will be saved. @@ -30,18 +28,6 @@ All dependencies for this project can be installed by: python3 -m pip install -r requirements.txt ``` - -### Change the browser path of pyppeteer - -In the file ``` neat_goda_play.py ``` you will have to change the path in the following line: - -```#browser = await launch({'headless': False, 'args': ['--window-size=1920,1080']}) ``` - -to the path of your prefered chrome (or chromium) installation: - -``` browser = await launch(headless=False, executablePath='/path/to/your/chrome-browser') ``` - - ## How to run: ### Run a model already trained: @@ -52,7 +38,7 @@ Models saved to the disk can be run directly by providing their path to the ```n python3.6 neat_goda_run.py model_ai_finish ``` -If not path is provided the newest model from the history folder will be loaded. +If no path is provided the newest model from the history folder will be loaded. ### Train a new model: @@ -96,14 +82,13 @@ Denotes with which kind of recips the Model will be trained with. There are thre ```fillSteps``` The amouth of units filled in the glass each step. Default is 10. -```viewNetwork``` True if the Network-Image should be shown after training? They will be saved in any case. +```viewNetwork``` True if the Network-Image should be shown after training. They will be saved in any case. -```viewSpeciesGraph``` True if the Species-Graph should be shown after training? They will be saved in any case. +```viewSpeciesGraph``` True if the Species-Graph should be shown after training. They will be saved in any case. -```viewFitnessGraph```True if the Fitness-Graph should be shown after training? They will be saved in any case. +```viewFitnessGraph```True if the Fitness-Graph should be shown after training. They will be saved in any case. -```logToFile``` True if all output should be writen to a logfile. This is usufull if you want to run the training on a server and -inspect the output later. +```logToFile``` True if all output should be writen to a logfile. This is usufull if you want to run the training on a server and inspect the output later. ### config From b0f304fcfc70bad6c7f77d70020713ffcd0927a4 Mon Sep 17 00:00:00 2001 From: codebecker Date: Thu, 14 Nov 2019 14:45:58 +0100 Subject: [PATCH 3/7] Create LICENSE --- neat/LICENSE | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 neat/LICENSE diff --git a/neat/LICENSE b/neat/LICENSE new file mode 100644 index 0000000..ac87ce7 --- /dev/null +++ b/neat/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2007-2011, cesar.gomes and mirrorballu2 +Copyright (c) 2015-2017, CodeReclaimers, LLC + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 07582d80500f61ec70d78e30c845b92768a19111 Mon Sep 17 00:00:00 2001 From: codebecker Date: Thu, 14 Nov 2019 15:01:17 +0100 Subject: [PATCH 4/7] Updated game URL to new URL hosted on github pages --- neat/neat_goda_play.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neat/neat_goda_play.py b/neat/neat_goda_play.py index dd47a96..50be930 100644 --- a/neat/neat_goda_play.py +++ b/neat/neat_goda_play.py @@ -193,7 +193,7 @@ async def playGame(browser, genome, config, fillSteps): page = await browser.newPage() await page.setViewport({'width': 1280, 'height': 720}) - await page.goto('https://build.bontscho.now.sh/') + await page.goto('https://inovex.github.io/cocktail-mash/') await setupButtonPressHandler(page) await setupUpdateScoreHandler(page) await setupUpdateFinalScoreHandler(page) From 9df8828135692a9e4a6d7fa95f1a477bd7f4d047 Mon Sep 17 00:00:00 2001 From: codebecker Date: Fri, 15 Nov 2019 13:23:09 +0100 Subject: [PATCH 5/7] Updated comments and cleaned up project --- neat/neat_goda_emulator.py | 2 +- neat/neat_goda_play.py | 30 +++++++++++++++--------------- neat/neat_goda_train.py | 4 ++-- neat/neat_goda_train_multiple.py | 20 -------------------- 4 files changed, 18 insertions(+), 38 deletions(-) delete mode 100644 neat/neat_goda_train_multiple.py diff --git a/neat/neat_goda_emulator.py b/neat/neat_goda_emulator.py index 840ff71..5cd7ef2 100644 --- a/neat/neat_goda_emulator.py +++ b/neat/neat_goda_emulator.py @@ -39,7 +39,7 @@ def calcFillPercent(g): g.fillInPercent = (g.stateCyan + g.stateMagenta + g.stateYellow) / g.capacity * 100 return g.fillInPercent -#Calc points based on filling and color NO TIME YET!!! +#Calc points based on filling and color. Time is not included into calculatuin yet! def getPoints(game): fillPercent = calcFillPercent(game) diff --git a/neat/neat_goda_play.py b/neat/neat_goda_play.py index 50be930..b23f241 100644 --- a/neat/neat_goda_play.py +++ b/neat/neat_goda_play.py @@ -16,7 +16,7 @@ globalFinalScore = -1 capacyityLevel = [400, 400, 250, 100] -#Get the RGB color in the glass of specified page game +#Get the RGB color in the glass of specified page async def getGlassColor(page): data = await page.evaluate('''() => { return { @@ -29,7 +29,7 @@ async def getGlassColor(page): colorString = colorString[5:len(colorString)-3].split(",") return list(map(int, colorString)) -#Get the RBG recipe of specified page game +#Get the RBG recipe of specified page async def getRecipe(page): data = await page.evaluate('''() => { return { @@ -37,12 +37,12 @@ async def getRecipe(page): } }''' ) - #RGB Werte herausschneiden und in int array parsen + #Extract RGB values and parse into int colorString = data["glassColor"] colorString = colorString[5:len(colorString)-3].split(",") return list(map(int, colorString)) -#Get fill status in percent of specified page game +#Get fill status in percent of specified page async def getGlassFillPercent(page): data = await page.evaluate('''() => { return { @@ -52,7 +52,7 @@ async def getGlassFillPercent(page): ) return float(data["glassFill"]) -#Get the current color and fill in Percent of specified page game +#Get the current color and fill in Percent of specified page async def getGlassFillPercentAndColor(page): data = await page.evaluate('''() => { return { @@ -68,7 +68,7 @@ async def getGlassFillPercentAndColor(page): #Setups a observer in the game which calls updateGlassFill each time the content of the glass changes -#page: page in which the game is running +#page: page the game is running in #At the moment this approach isn't used because it is uses a lot of perfomance async def setupUpdateGlassFillHandler(page): await page.exposeFunction("handleColorFill",updateGlassFill) @@ -89,7 +89,7 @@ def updateGlassFill(glassFill): globalGlassFill = glassFill #Setups a observer in the game which calls updateGlassColor each time the content of the glass changes -#page: page in which the game is running +#page: page the game is running in #At the moment this approach isn't used because it is uses a lot of perfomance async def setupUpdateGlassColorHandler(page): await page.exposeFunction("handleColorGlass",updateGlassColor) @@ -111,7 +111,7 @@ def updateGlassColor(glassColor): golbalGlassColor = list(map(int, glassColor)) #Setups a observer in the game which calls updateScore each time the score changes -#page: page in which the game is running +#page: page the game is running in #At the moment this approach isn't used because it is uses a lot of perfomance async def setupUpdateScoreHandler(page): await page.exposeFunction("handleIntermediate",updateScore) @@ -123,7 +123,7 @@ def updateScore(currentScore): globalScore = currentScore #Setups a observer in the game which calls finalScore each time the final score changes -#page: page in which the game is running +#page: page the game is running in #At the moment this approach isn't used because it is uses a lot of perfomance async def setupUpdateFinalScoreHandler(page): await page.exposeFunction("handleFinalScore",updateFinalScore) @@ -135,7 +135,7 @@ def updateFinalScore(finalScore): print("Final score", finalScore) globalFinalScore = finalScore -#setups the butten press logic thus only one button is pressed at the same time +#setups the button press logic thus only one button is pressed at the same time async def setupButtonPressHandler(page): #only one button a time can be pressed #a button is pressed and released after a specified timeout @@ -160,7 +160,7 @@ async def setupButtonPressHandler(page): #This function is used to press the color buttons and to continue a level #page: page running the game #key: key which should be pressed as char -#duration: is time in SECONDS +#duration: time in SECONDS the button should be hold async def pressButton(page, key, duration): duration *= 1000 # seconds to milliseconds # wenn zweimal die selbe taste gedrückt wird und die events dazu verzahnt sind muss das folgende key up event gelösch werden @@ -184,7 +184,7 @@ def new_method(*args, **kwargs): #browser: a chrome browser where the game will be startet in a new page #genome: the model which should play the game #config: config specifing the models settings -#fillSteps: Amount of color added each interval thus also specifies number of intervals +#fillSteps: Amount of color added each interval async def playGame(browser, genome, config, fillSteps): global globalFinalScore, globalScore, capacyityLevel @@ -261,13 +261,13 @@ async def playLevel(page, net, fillSteps, capacity): await asyncio.sleep(timeSteps)#sleep is in seconds ... wired #This function will open a new browser and run a game of Goda with specified model -#genome: the model which should play the game +#net: the model which should play the game #config: config specifing the models settings #fillSteps: Amount of color added each interval thus also specifies number of intervals -async def runSingleGame(genome, config, fillSteps): +async def runSingleGame(net, config, fillSteps): patch_pyppeteer() #prevents browser timeout after 20 sec browser = await launch({'headless': False, 'args': ['--window-size=1920,1080']}) - await playGame(browser, genome, config, fillSteps) + await playGame(browser, net, config, fillSteps) await browser.close() diff --git a/neat/neat_goda_train.py b/neat/neat_goda_train.py index f2915fe..1857af4 100644 --- a/neat/neat_goda_train.py +++ b/neat/neat_goda_train.py @@ -102,7 +102,7 @@ def eval_genomes(genomes, config): globalBestGenomeFitness = bestGenomeFitness runGameWithGenome(bestGenome, config, fillSteps) - #run each n-th generation in browser n = runEachGeneration OR + #run each n-th generation in browser n = runEachGeneration if (runEachGeneration != 0 and counter % runEachGeneration == 0): bestGenome, bestGenomeFitness = findBestGenome(genomes) runGameWithGenome(bestGenome, config, fillSteps) @@ -119,7 +119,7 @@ def findBestGenome(genomes): return bestGenome, bestGenomeFitness -#opens a browser and starts game with specified genome's model +#opens a browser and starts game with specified genomes model def runGameWithGenome(genome, config, fillSteps): print("Run game with fitness of"+str(genome.fitness)) loop = asyncio.get_event_loop() diff --git a/neat/neat_goda_train_multiple.py b/neat/neat_goda_train_multiple.py deleted file mode 100644 index 93bc40f..0000000 --- a/neat/neat_goda_train_multiple.py +++ /dev/null @@ -1,20 +0,0 @@ - - -import neat_goda_train as godtrain -import os - -#This script runs multiple training sessions one after another -#Thus it starts be laoding config0 train a model and continues mit config1 -#if there is no directly following numbered config it will stop -if __name__ == '__main__': - - counter = 0 - conf_name = "config" + str(counter) - run = os.path.isfile(conf_name) - root = os.getcwd() - - while(os.path.isfile(conf_name)): - godtrain.main(conf_name) - counter = counter + 1 - conf_name = "config" + str(counter) - os.chdir(root) From 2bc83ce5689a97d8019e5816e782f4e343caaf72 Mon Sep 17 00:00:00 2001 From: codebecker Date: Fri, 15 Nov 2019 13:50:12 +0100 Subject: [PATCH 6/7] update readme --- neat/README.md | 12 ++++++------ neat/{neat_goda_config.json => neat_config.json} | 0 .../{neat_goda_emulator.py => neat_game_emulator.py} | 0 neat/{neat_goda_play.py => neat_play.py} | 0 neat/{neat_goda_run.py => neat_run.py} | 0 neat/{neat_goda_train.py => neat_train.py} | 0 6 files changed, 6 insertions(+), 6 deletions(-) rename neat/{neat_goda_config.json => neat_config.json} (100%) rename neat/{neat_goda_emulator.py => neat_game_emulator.py} (100%) rename neat/{neat_goda_play.py => neat_play.py} (100%) rename neat/{neat_goda_run.py => neat_run.py} (100%) rename neat/{neat_goda_train.py => neat_train.py} (100%) diff --git a/neat/README.md b/neat/README.md index 41658b2..3db45f0 100644 --- a/neat/README.md +++ b/neat/README.md @@ -32,10 +32,10 @@ python3 -m pip install -r requirements.txt ### Run a model already trained: -Models saved to the disk can be run directly by providing their path to the ```neat_goda_run.py```. For exammple to run the sample models provided in the repository use: +Models saved to the disk can be run directly by providing their path to the ```neat_run.py```. For exammple to run the sample models provided in the repository use: ``` -python3.6 neat_goda_run.py model_ai_finish +python3.6 neatrun.py model_ai_finish ``` If no path is provided the newest model from the history folder will be loaded. @@ -45,19 +45,19 @@ If no path is provided the newest model from the history folder will be loaded. To train a new model just start the training process with the following command: ``` -python3.6 neat_goda_train.py +python3.6 neattrain.py ``` All progress will be saved to the ```history/[date_time] ``` folder including: config, graph, network and checkpoints ## Individualize the learning process -There are two config files in this project ``` neat_goda_config.json ``` for general setting and ``` config``` to change +There are two config files in this project ``` neat_config.json ``` for general setting and ``` config``` to change the behaviour of the machine learning process. By changing their settings you can take impact in the traing setup and process. -### neat_goda_config.json +### neat_config.json -The ``` neat_goda_config.json ``` specifies the learning setup of the goda game. The options are described in the following: +The ``` neat_config.json ``` specifies the learning setup of the game. The options are described in the following: ``` numberOfEvolutions ``` The numbers of evolutions the model will take by training. Default is 100 the diff --git a/neat/neat_goda_config.json b/neat/neat_config.json similarity index 100% rename from neat/neat_goda_config.json rename to neat/neat_config.json diff --git a/neat/neat_goda_emulator.py b/neat/neat_game_emulator.py similarity index 100% rename from neat/neat_goda_emulator.py rename to neat/neat_game_emulator.py diff --git a/neat/neat_goda_play.py b/neat/neat_play.py similarity index 100% rename from neat/neat_goda_play.py rename to neat/neat_play.py diff --git a/neat/neat_goda_run.py b/neat/neat_run.py similarity index 100% rename from neat/neat_goda_run.py rename to neat/neat_run.py diff --git a/neat/neat_goda_train.py b/neat/neat_train.py similarity index 100% rename from neat/neat_goda_train.py rename to neat/neat_train.py From 4418e1fa897b2a5fd4d7daab5a9351aeb9f2c2f9 Mon Sep 17 00:00:00 2001 From: codebecker Date: Tue, 17 Dec 2019 14:02:57 +0100 Subject: [PATCH 7/7] Included argparse and shebang into runable scripts neat_run and neat_train --- README.md | 4 ++- {neat => cocktail-mash-ai}/.gitignore | 0 {neat => cocktail-mash-ai}/LICENSE | 0 {neat => cocktail-mash-ai}/README.md | 0 {neat => cocktail-mash-ai}/config | 0 .../model_ai_finish/config | 0 .../model_ai_finish}/neat_config.json | 0 .../model_auto_finish/config | 0 .../model_auto_finish/neat_config.json | 11 +++++++ cocktail-mash-ai/neat_config.json | 11 +++++++ .../neat_game_emulator.py | 0 {neat => cocktail-mash-ai}/neat_play.py | 0 {neat => cocktail-mash-ai}/neat_run.py | 24 ++++++++------- {neat => cocktail-mash-ai}/neat_train.py | 30 +++++++++++-------- {neat => cocktail-mash-ai}/requirements.txt | 1 - {neat => cocktail-mash-ai}/visualize.py | 1 - 16 files changed, 55 insertions(+), 27 deletions(-) rename {neat => cocktail-mash-ai}/.gitignore (100%) rename {neat => cocktail-mash-ai}/LICENSE (100%) rename {neat => cocktail-mash-ai}/README.md (100%) rename {neat => cocktail-mash-ai}/config (100%) rename {neat => cocktail-mash-ai}/model_ai_finish/config (100%) rename {neat => cocktail-mash-ai/model_ai_finish}/neat_config.json (100%) mode change 100644 => 100755 rename {neat => cocktail-mash-ai}/model_auto_finish/config (100%) create mode 100755 cocktail-mash-ai/model_auto_finish/neat_config.json create mode 100755 cocktail-mash-ai/neat_config.json rename {neat => cocktail-mash-ai}/neat_game_emulator.py (100%) rename {neat => cocktail-mash-ai}/neat_play.py (100%) rename {neat => cocktail-mash-ai}/neat_run.py (79%) mode change 100644 => 100755 rename {neat => cocktail-mash-ai}/neat_train.py (90%) mode change 100644 => 100755 rename {neat => cocktail-mash-ai}/requirements.txt (99%) rename {neat => cocktail-mash-ai}/visualize.py (99%) diff --git a/README.md b/README.md index f395431..438ebbe 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# Readme +# cocktail-mash + +With [cocktail-mash-neat](./neat/README.md) you can watch a AI learning how to play cocktail-mash with NEAT.] diff --git a/neat/.gitignore b/cocktail-mash-ai/.gitignore similarity index 100% rename from neat/.gitignore rename to cocktail-mash-ai/.gitignore diff --git a/neat/LICENSE b/cocktail-mash-ai/LICENSE similarity index 100% rename from neat/LICENSE rename to cocktail-mash-ai/LICENSE diff --git a/neat/README.md b/cocktail-mash-ai/README.md similarity index 100% rename from neat/README.md rename to cocktail-mash-ai/README.md diff --git a/neat/config b/cocktail-mash-ai/config similarity index 100% rename from neat/config rename to cocktail-mash-ai/config diff --git a/neat/model_ai_finish/config b/cocktail-mash-ai/model_ai_finish/config similarity index 100% rename from neat/model_ai_finish/config rename to cocktail-mash-ai/model_ai_finish/config diff --git a/neat/neat_config.json b/cocktail-mash-ai/model_ai_finish/neat_config.json old mode 100644 new mode 100755 similarity index 100% rename from neat/neat_config.json rename to cocktail-mash-ai/model_ai_finish/neat_config.json diff --git a/neat/model_auto_finish/config b/cocktail-mash-ai/model_auto_finish/config similarity index 100% rename from neat/model_auto_finish/config rename to cocktail-mash-ai/model_auto_finish/config diff --git a/cocktail-mash-ai/model_auto_finish/neat_config.json b/cocktail-mash-ai/model_auto_finish/neat_config.json new file mode 100755 index 0000000..5cc7423 --- /dev/null +++ b/cocktail-mash-ai/model_auto_finish/neat_config.json @@ -0,0 +1,11 @@ +{ + "numberOfEvolutions":100, + "runEachGeneration": 0, + "runNewBest": true, + "numberOfRandomRecipes": 0, + "fillSteps":5, + "viewNetwork": true, + "viewSpeciesGraph": false, + "viewFitnessGraph": false, + "logToFile": false +} \ No newline at end of file diff --git a/cocktail-mash-ai/neat_config.json b/cocktail-mash-ai/neat_config.json new file mode 100755 index 0000000..a6d232f --- /dev/null +++ b/cocktail-mash-ai/neat_config.json @@ -0,0 +1,11 @@ +{ + "numberOfEvolutions":100, + "runEachGeneration": 50, + "runNewBest": false, + "numberOfRandomRecipes": 0, + "fillSteps":5, + "viewNetwork": true, + "viewSpeciesGraph": false, + "viewFitnessGraph": false, + "logToFile": false +} \ No newline at end of file diff --git a/neat/neat_game_emulator.py b/cocktail-mash-ai/neat_game_emulator.py similarity index 100% rename from neat/neat_game_emulator.py rename to cocktail-mash-ai/neat_game_emulator.py diff --git a/neat/neat_play.py b/cocktail-mash-ai/neat_play.py similarity index 100% rename from neat/neat_play.py rename to cocktail-mash-ai/neat_play.py diff --git a/neat/neat_run.py b/cocktail-mash-ai/neat_run.py old mode 100644 new mode 100755 similarity index 79% rename from neat/neat_run.py rename to cocktail-mash-ai/neat_run.py index 0cd9509..85d6d32 --- a/neat/neat_run.py +++ b/cocktail-mash-ai/neat_run.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 """ Implementation of Goda Game """ @@ -9,11 +10,12 @@ import os import sys import json +import argparse import neat.checkpoint as load from neat.six_util import itervalues +from neat_play import runSingleGame -from neat_goda_play import runSingleGame #Loads best model from a checkpoint folder and runs a game of Goda with it #If no checkpoint folder is specified the newest checkpoint folder from history folder will be loaded @@ -27,7 +29,7 @@ def main(checkpoint_folder = None): #search for newest checkpoint assert (os.path.isdir("history")), "Couldn't find a history folder. Train model first!" checkpoint_folder = os.listdir("history") #all folders - checkpoint_folder = max(checkpoint_folder) #only newest folder + checkpoint_folder = max(checkpoint_folder) #only most resent folder checkpoint_folder = "history/"+checkpoint_folder os.chdir(checkpoint_folder) @@ -38,16 +40,16 @@ def main(checkpoint_folder = None): checkpoint = os.listdir(".") # all folders checkpoint = fnmatch.filter(checkpoint, 'neat-checkpoint*') # only checkpoints checkpoint = max(checkpoint) # only newest checkpoints - assert (checkpoint != None), "Couldn't find any checkpoints in folder." + assert (checkpoint != None), "Couldn't find any checkpoints or genomes in folder." #load genomes newest checkpoint population = load.Checkpointer.restore_checkpoint(checkpoint) config = population.config genomes = population.population - # load neat_goda_config.json data - assert (os.path.isfile('neat_goda_config.json')), "neat_goda_config.json file is missing" - with open('neat_goda_config.json') as config_file: + # load neat_config.json data + assert (os.path.isfile('neat_config.json')), "neat_config.json file is missing" + with open('neat_config.json') as config_file: config_data = json.load(config_file) fillSteps = config_data['fillSteps'] @@ -62,9 +64,9 @@ def main(checkpoint_folder = None): if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-f', '--folder', + help="Specifiy a folder containing a genome", default=None) + args = parser.parse_args() + main(args.folder) - if (len(sys.argv) == 2): - checkpoint_folder = sys.argv[1] - main(checkpoint_folder) - else: - main() diff --git a/neat/neat_train.py b/cocktail-mash-ai/neat_train.py old mode 100644 new mode 100755 similarity index 90% rename from neat/neat_train.py rename to cocktail-mash-ai/neat_train.py index 1857af4..d0b3e70 --- a/neat/neat_train.py +++ b/cocktail-mash-ai/neat_train.py @@ -1,8 +1,10 @@ +#!/usr/bin/python3 """ Implementation of Goda Game """ from __future__ import print_function +import argparse import os import asyncio import neat @@ -14,9 +16,10 @@ import json #import from local files -from neat_goda_emulator import playGameEmulator -from neat_goda_play import runSingleGame import visualize +#from .visualize import * +from neat_game_emulator import playGameEmulator +from neat_play import runSingleGame #changeable parameters numberOfEvolutions = 0 @@ -60,7 +63,7 @@ def addRatioColors(): for ratioA in ratio: for ratioB in ratio: for ratioC in ratio: - if (ratioA is not ratioB and ratioB is not ratioC and ratioA is not ratioC): + if (ratioA is not ratioB is not ratioC): trainRecipes.append(generateRatioColor(ratioA, ratioB, ratioC)) def generateRatioColor(ratioC, ratioM, ratioY): @@ -108,7 +111,7 @@ def eval_genomes(genomes, config): runGameWithGenome(bestGenome, config, fillSteps) return -#finds the best genome grom a array of genomes +#finds the best genome from a array of genomes def findBestGenome(genomes): # find fittest genome bestGenomeFitness = 0 @@ -121,7 +124,7 @@ def findBestGenome(genomes): #opens a browser and starts game with specified genomes model def runGameWithGenome(genome, config, fillSteps): - print("Run game with fitness of"+str(genome.fitness)) + print("Run game with fitness of "+str(genome.fitness)) loop = asyncio.get_event_loop() loop.run_until_complete(runSingleGame(genome, config, fillSteps)) @@ -184,8 +187,8 @@ def main(config_name = "config"): viewNetwork, viewSpeciesGraph,viewFitnessGraph # load config file data - assert (os.path.isfile('neat_goda_config.json')), "neat_goda_config.json file is missing" - with open('neat_goda_config.json') as config_file: + assert (os.path.isfile('neat_config.json')), "neat_config.json file is missing" + with open('neat_config.json') as config_file: config_data = json.load(config_file) numberOfEvolutions = config_data['numberOfEvolutions'] @@ -219,7 +222,7 @@ def main(config_name = "config"): # move neat config to savefolder savefolderPath = "history/" + savefolderName shutil.copy('config', savefolderPath + "/" + config_name) - shutil.copy('neat_goda_config.json', savefolderPath + "/" + 'neat_goda_config.json') + shutil.copy('neat_config.json', savefolderPath + "/" + 'neat_config.json') config_path = os.path.join(local_dir, config_name) os.chdir(savefolderPath) @@ -235,8 +238,9 @@ def main(config_name = "config"): if __name__ == '__main__': - if (len(sys.argv) == 2): - config_name = sys.argv[1] - main(config_name) - else: - main('config') + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--config', + help="Specifiy a alternative configuration", default='config') + args = parser.parse_args() + main(args.config) + diff --git a/neat/requirements.txt b/cocktail-mash-ai/requirements.txt similarity index 99% rename from neat/requirements.txt rename to cocktail-mash-ai/requirements.txt index 7652856..9c9d0b9 100644 --- a/neat/requirements.txt +++ b/cocktail-mash-ai/requirements.txt @@ -1,6 +1,5 @@ #### Run the following command to install all requirements ##### #### python3 -m pip install -r requirements.txt ##### - matplotlib graphviz neat-python == 0.92 diff --git a/neat/visualize.py b/cocktail-mash-ai/visualize.py similarity index 99% rename from neat/visualize.py rename to cocktail-mash-ai/visualize.py index 27b4a4a..eaea3e8 100644 --- a/neat/visualize.py +++ b/cocktail-mash-ai/visualize.py @@ -2,7 +2,6 @@ import copy import warnings - import graphviz import matplotlib.pyplot as plt import numpy as np