diff --git a/Tetris AI/src/AI.cpp b/Tetris AI/src/AI.cpp index 3b4cc424..bc3e9348 100644 --- a/Tetris AI/src/AI.cpp +++ b/Tetris AI/src/AI.cpp @@ -3,6 +3,7 @@ // Tetris AI // // Created by Roberto Ariosa Hernández on 09/01/2018. +// Copyright © 2018 Mr.Robb. All rights reserved. // #include "AI.hpp" diff --git a/Tetris AI/src/AI.hpp b/Tetris AI/src/AI.hpp index 46d3e3a2..2c34bb50 100644 --- a/Tetris AI/src/AI.hpp +++ b/Tetris AI/src/AI.hpp @@ -3,6 +3,7 @@ // Tetris AI // // Created by Roberto Ariosa Hernández on 09/01/2018. +// Copyright © 2018 Mr.Robb. All rights reserved. // #ifndef AI_hpp @@ -26,10 +27,10 @@ struct Shape { }; class AI { - float aggregate_height = -3.78; - float complete_lines = 1.6; - float holes = -2.31; - float bumpiness = -0.59; + float aggregate_height = 0.132802; + float complete_lines = 0.132802; + float holes = -0.344799; + float bumpiness = -0.0387222; vector< vector > grid; map > > shapes; diff --git a/Tetris AI/src/DNA.hpp b/Tetris AI/src/DNA.hpp index e2786830..208befa6 100644 --- a/Tetris AI/src/DNA.hpp +++ b/Tetris AI/src/DNA.hpp @@ -40,6 +40,7 @@ class DNA { @brief Access the genes directly. @pre i must be between 0 and lenght-1. @param i Position of the gene. + @return The gene at position i. */ float& operator[](int i); @@ -60,6 +61,7 @@ class DNA { /** @brief Get the fitness of the individual. @pre Fitness function has been already executed. + @return The fitness of the individual. @see DNA::fitness */ float getFitness(); @@ -68,6 +70,7 @@ class DNA { @brief Creates a child with mixed genes via crossover. @f$ itself + partner = child @f$ @param partner It will provide some genetic information. + @return The child. */ DNA crossover(DNA &partner); diff --git a/Tetris AI/src/Population.cpp b/Tetris AI/src/Population.cpp index 0d43428a..257d9c35 100644 --- a/Tetris AI/src/Population.cpp +++ b/Tetris AI/src/Population.cpp @@ -8,104 +8,119 @@ #include "Population.hpp" -Population::Population() { - -} +Population::Population() {} -// construct -Population::Population(int length, float mutationRate, int N_ind, int generation) { +Population::Population(int N_genes, int N_ind, float mutationRate, unsigned int generation) +{ this->mutationRate = mutationRate; this->generation = generation; pool = vector (); population = vector (N_ind, DNA()); - for (int i = 0; i < N_ind; i++) { - population[i] = DNA(length); - } + + for (int i = 0; i < N_ind; i++) + population[i] = DNA(N_genes); } -DNA Population::operator[](int i) { +DNA& Population::operator[](int i) +{ return this->population[i]; } -// call fitness for every specimen -void Population::calculateFitness(vector scores) { +void Population::calculateFitness(vector scores) +{ int max_score = *max_element(scores.begin(), scores.end()); - for (int i = 0; i < population.size(); i++) { + + // Call fitness() for every Individual + for (int i = 0; i < population.size(); i++) population[i].fitness(scores[i], max_score); - } } -// reproduce -void Population::naturalSelection() { - int i = getBest(); - float maxFitness = population[i].getFitness(); - - for (int i = 0; i < population.size(); i++) { - float relative_fitness = population[i].getFitness() / maxFitness; - int percentage = int(relative_fitness * 100); - for (int j = 0; j < percentage; j++) { - pool.push_back(population[i]); - } - } -} - -// create new generation -Population Population::generate(int length) { - Population p (length, mutationRate, int(population.size()), generation + 1); - if (pool.size() > 0) { +Population Population::generate() +{ + // Fill the temporary reproduction pool + naturalSelection(); + + // Create container of the next generation + int N_ind = int(population.size()); + int N_genes = int(population[0].getValues().size()); + Population p (N_genes, N_ind, mutationRate, generation + 1); + + // Fill pool + if (pool.size() > 0) + { for (int i = 0; i < population.size(); i++) { int a = int(rand() % pool.size()); int b = int(rand() % pool.size()); DNA dna_a = this->pool[a]; DNA dna_b = this->pool[b]; DNA child = dna_a.crossover(dna_b); - child.mutate(mutationRate, generation+1); + child.mutate(mutationRate); p.population[i] = child; } + + // Free pool pool = vector(); } + return p; } -// most fit -int Population::getBest() { +int Population::getBest() +{ float record = 0.0; int index = 0; - for (int i = 0; i < population.size(); i++) { - if (population[i].getFitness() > record) { + + for (int i = 0; i < population.size(); i++) + { + float current = population[i].getFitness(); + + if (current > record) { index = i; - - record = population[i].getFitness(); + record = current; } } + return index; } -// finished -bool Population::isFinished() { - finished = (population[getBest()].getFitness() == perfectScore); - return finished; -} - -int Population::getGenerations() { +int Population::getGenerations() +{ return generation; } -float Population::getAverage() { +float Population::getAverage() +{ float total = 0.0; - for (int i = 0; i < population.size(); i++) { + + for (int i = 0; i < population.size(); i++) total += population[i].getFitness(); - } + return total / (float) population.size(); } -vector< vector > Population::allValues() { - vector< vector > all (population.size(), vector (4)); - for (int i = 0; i < population.size(); i++) { - for (int j = 0; j < 4; j++) { +vector< vector > Population::allValues() +{ + int N_ind = int(population.size()); + int N_genes = int(population[0].getValues().size()); + vector< vector > all (N_ind, vector (N_genes)); + + for (int i = 0; i < N_ind; i++) { + for (int j = 0; j < N_genes; j++) { all[i][j] = population[i][j]; } } + return all; } +void Population::naturalSelection() +{ + for (int i = 0; i < population.size(); i++) + { + float fitness = population[i].getFitness(); + int percentage = int(fitness * 100); + + for (int j = 0; j < percentage; j++) + pool.push_back(population[i]); + } +} diff --git a/Tetris AI/src/Population.hpp b/Tetris AI/src/Population.hpp index c2e37053..7872e821 100644 --- a/Tetris AI/src/Population.hpp +++ b/Tetris AI/src/Population.hpp @@ -17,43 +17,87 @@ using namespace std; class Population { -private: - float mutationRate; - vector population; - vector pool; - string target; - int generation; - bool finished; - int perfectScore; public: + /** + @brief Creates an empty Population. + */ Population(); - // construct - Population(int length, float mutationRate, int N_ind, int generation); + /** + @brief Creates a Population with N_ind individuals with random genes. + @param N_genes Number of genes of each individual. + @param N_ind Number of individuals. + @param mutationRate Probability of mutating a gene, between 0 and 1. + @param generation Number of the generation. + */ + Population(int N_genes, int N_ind, float mutationRate, unsigned int generation); - DNA operator[](int i); + /** + @brief Access the individual directly. + @pre i must be between 0 and N_ind-1. + @param i Position of the Individual. + @return The individual at position i. + */ + DNA& operator[](int i); - // call fitness for every specimen + /** + @brief Calculates the fitness of the population. + @param scores Vector containing the scores of all of the individuals. + */ void calculateFitness(vector scores); - // reproduce - void naturalSelection(); - - // create new generation - Population generate(int length); + /** + @brief Generates the next generation. + @pre calculateFitness have already been executed. + @pre Population has at least one individual. + @return The next generation. + */ + Population generate(); - // most fit + /** + @brief Get position of the fittest individual. In case of having more than one, the first one is returned. + @return The position of the fittest. + */ int getBest(); - // finished - bool isFinished(); - + /** + @brief Get the number of the generation. + @return The number of the generation. + */ int getGenerations(); + /** + @brief Get the average fitness of the population. + @return The average fitness of the population, between 0 and 1. + */ float getAverage(); + /** + @brief Get all of the genes of the individuals. + @return Vector of vector of genes. Each vector represents an individual. Each value represents a gene. + */ vector< vector > allValues(); + +private: + + /// @brief Probability of mutation between 0 and 1. + float mutationRate; + + /// @brief Contains all of the individuals. + vector population; + + /// @brief Creates a temporary reproduction pool. + vector pool; + + /// @brief Number of the generation. + unsigned int generation; + + /** + @brief Fills the reproduction pool based on the fitness of every individual. The fittest have more probabilities of being selected out of the reproduction pool. + */ + void naturalSelection(); + }; #endif /* Population_hpp */ diff --git a/Tetris AI/src/Tetris.cpp b/Tetris AI/src/Tetris.cpp index 93ad3855..e6ec5194 100644 --- a/Tetris AI/src/Tetris.cpp +++ b/Tetris AI/src/Tetris.cpp @@ -3,6 +3,7 @@ // Tetris AI // // Created by Roberto Ariosa Hernández on 06/01/2018. +// Copyright © 2018 Mr.Robb. All rights reserved. // #include "Tetris.hpp" @@ -74,6 +75,7 @@ Tetris::Tetris(int w1, int w2, int h1, int h2, bool withAI, DNA dna, queuewithAI = withAI; + this->training = training; this->bot = AI(shapes); if (training) { bot.setDNA(dna[0], dna[1], dna[2], dna[3]); @@ -91,16 +93,17 @@ Tetris::Tetris(int w1, int w2, int h1, int h2, bool withAI, DNA dna, queuepieces = pieces; + if (not pieces.empty()) { + current.shape = Blocks(int(this->pieces.front() % 7) * 4); + this->pieces.pop(); + next.shape = Blocks(int(this->pieces.front() % 7) * 4); + this->pieces.pop(); + } + // Create AI if (withAI) { - this->pieces = pieces; this->bot.setGrid(grid); - if (not pieces.empty()) { - current.shape = Blocks(int(this->pieces.front() % 7) * 4); - this->pieces.pop(); - next.shape = Blocks(int(this->pieces.front() % 7) * 4); - this->pieces.pop(); - } vector aux (2); aux[0] = current; aux[1] = next; @@ -185,13 +188,14 @@ void Tetris::update() { next.y = 0; next.x = 4; + if (not pieces.empty()) { + next.shape = Blocks(int(this->pieces.front() % 7) * 4); + this->pieces.pop(); + } + // Create AI if (withAI) { bot.setGrid(grid); - if (not pieces.empty()) { - next.shape = Blocks(int(this->pieces.front() % 7) * 4); - this->pieces.pop(); - } vector aux (2); aux[0] = current; aux[1] = next; @@ -202,10 +206,10 @@ void Tetris::update() { for (int i = 0; i < (the_one.shape % 4); i++) rotate(); - while (the_one.x > current.x and j++ < 1000) { + while (the_one.x > current.x and j++ < 100) { moveRight(); } - while (the_one.x < current.x and j++ < 1000) { + while (the_one.x < current.x and j++ < 100) { moveLeft(); } } @@ -274,12 +278,19 @@ void Tetris::reset() { int Tetris::drawScore(ofTrueTypeFont &myFont) { string s = to_string(score); myFont.drawString(s, x - 7 * blockSize, y + blockSize); + + ofTrueTypeFont myFont2; + myFont2.load("data/myfont.otf", blockSize / 1.25); + if (withAI) + myFont2.drawString("AI", x + (width - 0.75) * blockSize / 2, y - 2 * blockSize); + else + myFont2.drawString("YOU", x + (width - 2) * blockSize / 2, y - 2 * blockSize); return this->score; } bool Tetris::draw(ofTrueTypeFont &myFont) { // Game over - if (pieces.empty()) return true; + if (pieces.empty() and training) return true; for (int j = 0; j < width; j++) { if (grid[1][j]) { return true; diff --git a/Tetris AI/src/Tetris.hpp b/Tetris AI/src/Tetris.hpp index 0c5d18c5..d5274ad3 100644 --- a/Tetris AI/src/Tetris.hpp +++ b/Tetris AI/src/Tetris.hpp @@ -3,6 +3,7 @@ // Tetris AI // // Created by Roberto Ariosa Hernández on 06/01/2018. +// Copyright © 2018 Mr.Robb. All rights reserved. // #ifndef Tetris_hpp @@ -44,6 +45,7 @@ class Tetris { int x; int y; bool withAI = true; + bool training = false; AI bot; queue pieces; diff --git a/Tetris AI/src/ofApp.cpp b/Tetris AI/src/ofApp.cpp index 39c3489e..5d9938fb 100644 --- a/Tetris AI/src/ofApp.cpp +++ b/Tetris AI/src/ofApp.cpp @@ -1,55 +1,110 @@ #include "ofApp.h" #include "DNA.hpp" +void evolve(Population& population, const vector& scores) +{ + population.calculateFitness(scores); + int i = population.getBest(); + vector best = population[i].getValues(); + + // Print stats + cerr << population.getAverage() << " "; + cerr << population.getGenerations() << " -> "; + cerr << "Best: "; + cerr << scores[i]; + for (auto& value : best) { + cerr << ' ' << value; + } + cerr << endl; + + // Set new generation + Population aux = population.generate(); + population = move(aux); +} + +void init_game_with_player(vector& games, queue& pieces, int rowSize, int cols, int n, int ai, bool training) +{ + int w = ofGetWidth()/2; + int h = ofGetHeight(); + for (int i = 0; i < 100000; i++) { + pieces.push(rand()); + } + for (int i = 0; i < n-1; i++) { + int w1 = w/rowSize * (i % rowSize); + int w2 = w/rowSize * ((i % rowSize) + 1); + int h1 = h/cols * int(i/rowSize); + int h2 = h/cols * int(i/rowSize) + h/cols; + games[i] = Tetris (w1, w2, h1, h2, (i < ai), NULL, pieces, training); + } + int i = n - 1; + games[i] = Tetris (w, w*2, 0, h, (i < ai), NULL, pieces, training); +} + +void init_game_without_player(vector& games, Population& population, queue& pieces, int rowSize, int cols, int n, int ai, bool training) +{ + int w = ofGetWidth(); + int h = ofGetHeight(); + int generation = population.getGenerations(); + for (int i = 0; i < 100 * (generation + 1); i++) { + pieces.push(rand()); + } + for (int i = 0; i < n; i++) { + int w1 = w/rowSize * (i % rowSize); + int w2 = w/rowSize * ((i % rowSize) + 1); + int h1 = h/cols * int(i/rowSize); + int h2 = h/cols * int(i/rowSize) + h/cols; + games[i] = Tetris (w1, w2, h1, h2, (i < ai), population[i], pieces, training); + } +} + +void draw_instructions() +{ + ofTrueTypeFont myFont; + int x = 20; + int y = ofGetHeight(); + int line = 50; + myFont.load("data/myfont.otf", 22); + ofSetColor(255, 255, 255); + myFont.drawString("left arrow: move left", x, y - line * 1); + myFont.drawString("right arrow: move right", x, y - line * 2); + ofSetHexColor(ofHexToInt("36E0FF")); + myFont.drawString("down arrow: +speed", x, y - line * 3); + ofSetHexColor(ofHexToInt("53D504")); + myFont.drawString("up arrow: rotate", x, y - line * 4); + ofSetHexColor(ofHexToInt("F8931D")); + myFont.drawString("M: max speed", x, y - line * 5); + ofSetHexColor(ofHexToInt("FEE356")); + myFont.drawString("N: normal speed", x, y - line * 6); + ofSetHexColor(ofHexToInt("F92338")); + myFont.drawString("space: pause / reset", x, y - line * 7); + ofSetHexColor(ofHexToInt("C973FF")); + myFont.drawString("enter: reset while playing", x, y - line * 8); + ofSetColor(150, 150, 150); + myFont.drawString("H: show/hide instructions", x, y - line * 9); +} + //-------------------------------------------------------------- void ofApp::setup(){ // Config - games = vector (n); - scores = vector (n, 0); - gameOvers = vector (n, false); ofSetVerticalSync(false); ofSetFrameRate(60); ofSetBackgroundColorHex(ofHexToInt("0D1B1E")); ofDisableDataPath(); - myFont.load("data/myfont.otf", min(ofGetWidth(), ofGetHeight())/ (rowSize * 20)); + // Initialize + myFont.load("data/myfont.otf", min(ofGetWidth(), ofGetHeight()) / (rowSize * 20)); + games = vector (n); + scores = vector (n, 0); + gameOvers = vector (n, false); queue pieces; - population = Population(4, 0.025, ai, 0); - for (auto dna : population.allValues()) { - for (auto value : dna) { - cerr << value << ' '; - } - cerr << endl; - } + population = Population(4, ai, 0.025, 0); - // Initialize + // Init Game if (ai == n) { - int w = ofGetWidth(); - int h = ofGetHeight(); - int generation = population.getGenerations(); - for (int i = 0; i < 100 * (generation + 1); i++) { - pieces.push(rand()); - } - for (int i = 0; i < n; i++) { - int w1 = w/rowSize * (i % rowSize); - int w2 = w/rowSize * ((i % rowSize) + 1); - int h1 = h/cols * int(i/rowSize); - int h2 = h/cols * int(i/rowSize) + h/cols; - games[i] = Tetris (w1, w2, h1, h2, (i < ai), population[i], pieces, training); - } + init_game_without_player(games, population, pieces, rowSize, cols, n, ai, training); } else { - int w = ofGetWidth()/2; - int h = ofGetHeight(); - for (int i = 0; i < n-1; i++) { - int w1 = w/rowSize * (i % rowSize); - int w2 = w/rowSize * ((i % rowSize) + 1); - int h1 = h/cols * int(i/rowSize); - int h2 = h/cols * int(i/rowSize) + h/cols; - games[i] = Tetris (w1, w2, h1, h2, (i < ai), population[i], pieces, training); - } - int i = n - 1; - games[i] = Tetris (w, w*2, 0, h, (i < ai), NULL, pieces, training); + init_game_with_player(games, pieces, rowSize, cols, n, ai, training); } } @@ -66,51 +121,28 @@ void ofApp::update(){ //-------------------------------------------------------------- void ofApp::draw(){ - if (isPaused) { + if (isPaused) + { + ofSetColor(255, 255, 255); myFont.drawString("PAUSED", ofGetWidth()/2 - 85, ofGetHeight()/2); } else { - if (dead == n and dead == ai) { + // All are dead + if (dead == n and dead == ai and training) { // Evolve - population.calculateFitness(scores); - int i = population.getBest(); - vector best = population[i].getValues(); - cerr << population.getAverage() << " "; - cerr << population.getGenerations() << " -> "; - cerr << "Best: "; - cerr << scores[i]; - for (auto& value : best) { - cerr << ' ' << value; - } - cerr << endl; - population.naturalSelection(); - Population aux = population.generate(4); - population = aux; - int w = ofGetWidth(); - int h = ofGetHeight(); - int generation = population.getGenerations(); + evolve(population, scores); + + // Draw new game queue pieces; - for (int i = 0; i < 100 * (generation + 1); i++) { - pieces.push(rand()); - } - for (int i = 0; i < n; i++) { - gameOvers[i] = not gameOvers[i]; - games[i].reset(); - int w1 = w/rowSize * (i % rowSize); - int w2 = w/rowSize * ((i % rowSize) + 1); - int h1 = h/cols * int(i/rowSize); - int h2 = h/cols * int(i/rowSize) + h/cols; - games[i] = Tetris (w1, w2, h1, h2, (i < ai), population[i], pieces, training); - } + gameOvers = vector (n, false); dead = 0; + init_game_without_player(games, population, pieces, rowSize, cols, n, ai, training); } + for (int i = 0; i < n; i++) { ofSetColor(255, 255, 255); if (gameOvers[i]) { games[i].gameOver(myFont); - // cerr << "Player: " << score << endl; - // gameOver = false; - // games[0].reset(); } else { scores[i] = games[i].drawScore(myFont); @@ -120,15 +152,19 @@ void ofApp::draw(){ } } } + + if (instructions) + draw_instructions(); } } //-------------------------------------------------------------- void ofApp::keyPressed(int key){ + queue pieces; switch (key) { case OF_KEY_DOWN: if (not isPaused and not gameOvers[n-1]) { - speed = 3.0; + speed = 5.0; } break; @@ -154,8 +190,12 @@ void ofApp::keyPressed(int key){ break; case OF_KEY_RETURN: - if (not isPaused and not gameOvers[n-1]) { - games[n-1].reset(); + if (not isPaused) { + if (n > 2 and (gameOvers[0] or gameOvers[1])) { + gameOvers[0] = false; + gameOvers[1] = false; + } + init_game_with_player(games, pieces, rowSize, cols, n, ai, training); } break; @@ -175,8 +215,12 @@ void ofApp::keyPressed(int key){ break; case 'n': - speed = 1.0; + speed = 2.0; ofSetFrameRate(speed * 60); + + case 'h': + instructions = !instructions; + default: break; } @@ -186,7 +230,7 @@ void ofApp::keyPressed(int key){ void ofApp::keyReleased(int key){ switch (key) { case OF_KEY_DOWN: - speed = 1.0; + speed = 1.25; ofSetFrameRate(speed * 60); break; @@ -233,7 +277,9 @@ void ofApp::mouseExited(int x, int y){ void ofApp::windowResized(int w, int h){ // Initialize for (int i = 0; i < n; i++) { + if (n != ai) rowSize++; games[i].realloc(w/rowSize * (i%rowSize), w/rowSize * ((i%rowSize) + 1), h/cols * int(i/rowSize), h/cols * int(i/rowSize) + h/cols); + if (n != ai) rowSize--; } myFont.load("data/myfont.otf", min(w, h)/ (rowSize * 20)); } diff --git a/Tetris AI/src/ofApp.h b/Tetris AI/src/ofApp.h index 428ab141..b90b8d3d 100644 --- a/Tetris AI/src/ofApp.h +++ b/Tetris AI/src/ofApp.h @@ -23,21 +23,22 @@ class ofApp : public ofBaseApp{ void dragEvent(ofDragInfo dragInfo); void gotMessage(ofMessage msg); - float speed = 1.0; + float speed = 1.25; int timeFrame = 0; vector games; float counter = 0; bool finishRotation = true; + bool instructions = true; bool isPaused = false; vector gameOvers; bool withAI = true; vector scores; - int n = 49; - int ai = 49; - int rowSize = 7; + int n = 2; + int ai = 1; + int rowSize = 1; int cols = ai / (rowSize+1) + 1; int dead = 0; - bool training = true; + bool training = false; ofTrueTypeFont myFont; Population population; };