From c149452c8a32cd407e101b0b2c598ef14278b803 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 27 Jun 2021 21:37:56 -0400 Subject: [PATCH] First pass at expectimax implementation --- README.md | 4 +- include/Expectimax.hpp | 14 ++++ src/Expectimax.cpp | 151 +++++++++++++++++++++++++++++++++++++++++ src/Main.cpp | 9 ++- src/Minimax.cpp | 4 -- 5 files changed, 173 insertions(+), 9 deletions(-) create mode 100644 include/Expectimax.hpp create mode 100644 src/Expectimax.cpp diff --git a/README.md b/README.md index 778ad64..c202316 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The original project (as well as this one) was created with the goal of implemen algorithms to achieve as high of a score as possible in the game 2048. Game.cpp contains the code for the game logic, Heuristics.cpp contains several heuristic metrics/functions to evaluate game state quality, and algorithm implementations are -split between MonteCarlo.cpp and Minimax.cpp. +split between MonteCarlo.cpp, Minimax.cpp, and Expectimax.cpp. If unfamiliar with the game rules, check out [2048](https://play2048.co/). @@ -34,7 +34,7 @@ AISolver < flag_1 > < flag_1_val > < flag_2 > < flag_2_val > ... ``` Flag list: -* -a: Integer/String value; Algorithm to run. 0 = MonteCarlo, 1 = Minimax +* -a: Integer/String value; Algorithm to run. 0 = MonteCarlo, 1 = Minimax, 2 = Expectimax * -n: Integer value; # times to run the algorithm. Stats displayed at program completion * -r: Integer value; # runs MonteCarlo completes for each move. Higher=better but slower. Recommend 10-100 * -d: Integer value; # depth level for Minimax / Expectimax diff --git a/include/Expectimax.hpp b/include/Expectimax.hpp new file mode 100644 index 0000000..3187c3a --- /dev/null +++ b/include/Expectimax.hpp @@ -0,0 +1,14 @@ +#ifndef EXPECTIMAX_H +#define EXPECTIMAX_H + +#include +#include +#include "Game.hpp" +#include "Heuristics.hpp" + +float expectimaxScore(int depth, Game game, bool is_max); +std::pair expectimaxSearch(int depth, int display_level, Game game); +int expectimaxSolve(int n, int depth, int display_level, + std::vector *scores, std::vector *highest_tiles); + +#endif diff --git a/src/Expectimax.cpp b/src/Expectimax.cpp new file mode 100644 index 0000000..ea7c793 --- /dev/null +++ b/src/Expectimax.cpp @@ -0,0 +1,151 @@ +#include "Expectimax.hpp" + +float expectimaxScore(int depth, Game game, bool is_max) { + std::map possibilities = game.computePossibilities(); + if (possibilities.size() == 0) { + return 0.0; + } + else if (is_max) { + if (depth == 0) { + return get_h_score(game); + } + else { + std::map scores; + for (auto const& entry : possibilities) { + int move = entry.first; // Direction of move + weightedmoves weighted_subset = entry.second; // Vector of (prob, game) + int len = (int) weighted_subset.size(); + if (len > 0) { + scores[move] = 0.0; + for (int j = 0; j < len; ++j) { + scores[move] += get_h_score(weighted_subset[j].second) * weighted_subset[j].first; + } + } + } + + float max_score = -std::numeric_limits::max(); + int max_choice = UP; + for (auto const& score : scores) { // Determine best move choice based on tabulated scores + if (score.second > max_score) { + max_score = score.second; + max_choice = score.first; + } + } + + switch (max_choice) { + case UP: + game.up(false); + break; + case DOWN: + game.down(false); + break; + case LEFT: + game.left(false); + break; + case RIGHT: + game.right(false); + } + + return expectimaxScore(depth - 1, game, false); + } + } + else { + std::map scores; + for (auto const& entry : possibilities) { + int move = entry.first; // Direction of move + weightedmoves weighted_subset = entry.second; // Vector of (prob, game) + int len = (int) weighted_subset.size(); + if (len > 0) { + scores[move] = 0.0; + for (int j = 0; j < len; ++j) { + scores[move] += expectimaxScore(depth, weighted_subset[j].second, true) * weighted_subset[j].first; + } + } + } + float sum = 0.0; + for (auto const& score : scores) { + sum += score.second; + } + return sum / scores.size(); + } +} + +std::pair expectimaxSearch(int depth, int display_level, Game game) { + std::cout << "Attempting to solve a new game with Expectimax... " << std::flush; + while (game.canContinue()) { + if (display_level >= 2) { + std::cout << std::endl << game; + } + + std::map possibilities = game.computePossibilities(); + std::map scores; + + for (auto const& entry : possibilities) { + int move = entry.first; // Direction of move + weightedmoves weighted_subset = entry.second; // Vector of (prob, game) + int len = (int) weighted_subset.size(); + if (len > 0) { + scores[move] = 0.0; + for (int j = 0; j < len; ++j) { + scores[move] += expectimaxScore(depth, weighted_subset[j].second, true) * weighted_subset[j].first; + } + } + } + if (display_level >= 3) { + std::cout << "Heuristic score: " << get_h_score(game) << std::endl << "Move scores: "; + for (auto const& score : scores) { + std::cout << score.first << ": " << score.second << ", "; + } + std::cout << std::endl; + } + + float max_score = -std::numeric_limits::max(); + int max_choice = UP; + for (auto const& score : scores) { // Determine best move choice based on tabulated scores + if (score.second > max_score) { + max_score = score.second; + max_choice = score.first; + } + } + + switch (max_choice) { + case UP: + game.up(false); + break; + case DOWN: + game.down(false); + break; + case LEFT: + game.left(false); + break; + case RIGHT: + game.right(false); + } + } + if (display_level <= 1) { + std::cout << "Done!" << (display_level == 0 ? "\n" : ""); + } + if (display_level >= 1) { + std::cout << std::endl << game << std::endl; + } + return std::pair(game.getHighestTile(), game.score); +} + +/** + * Creates and completes n-many games using the Expectimax simulation function. + * Tabulates data from game each in order to display results at completion. + */ +int expectimaxSolve(int n, int depth, int display_level, + std::vector *scores, std::vector *highest_tiles) { + + int successes = 0; + for (int i = 0; i < n; ++i) { + std::pair result = expectimaxSearch(depth, display_level, Game()); + if (result.first >= WIN) { + successes++; + } + (*highest_tiles).push_back(result.first); + (*scores).push_back(result.second); + } + return successes; +} \ No newline at end of file diff --git a/src/Main.cpp b/src/Main.cpp index e43430a..072161e 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -4,6 +4,7 @@ #include "Game.hpp" #include "MonteCarlo.hpp" #include "Minimax.hpp" +#include "Expectimax.hpp" using namespace std::chrono; @@ -50,8 +51,8 @@ class CmdParser { }; int main(int argc, char** argv) { - enum ALGORITHMS {MONTECARLO = 0, MINIMAX = 1}; - std::map alg_map = {{"montecarlo", MONTECARLO}, {"minimax", MINIMAX}}; + enum ALGORITHMS {MONTECARLO, MINIMAX, EXPECTIMAX}; + std::map alg_map = {{"montecarlo", MONTECARLO}, {"minimax", MINIMAX}, {"expectimax", EXPECTIMAX}}; int alg_key = MINIMAX; int num_games = 1; int num_runs = 25; @@ -65,7 +66,7 @@ int main(int argc, char** argv) { std::string msg = "Usage:\ \n AISolver ...\ \n Flag list:\ - \n -a: Integer/String value; Algorithm to run. 0=montecarlo, 1=minimax\ + \n -a: Integer/String value; Algorithm to run. 0=montecarlo, 1=minimax, 2=expectimax\ \n -n: Integer value; # times to run the algorithm. Stats displayed at program completion\ \n -r: Integer value; # runs MonteCarlo completes for each move. Higher=better but slower. Recommend 10-100\ \n -d: Integer value; # depth level for Minimax / Expectimax\ @@ -121,6 +122,8 @@ int main(int argc, char** argv) { case MINIMAX: successes = minimaxSolve(num_games, depth, print_level, &scores, &highest_tiles); break; + case EXPECTIMAX: + successes = expectimaxSolve(num_games, depth, print_level, &scores, &highest_tiles); } high_resolution_clock::time_point t2 = high_resolution_clock::now(); duration time_span = duration_cast>(t2 - t1); diff --git a/src/Minimax.cpp b/src/Minimax.cpp index 096464d..6dc03f1 100644 --- a/src/Minimax.cpp +++ b/src/Minimax.cpp @@ -108,10 +108,6 @@ std::pair minimaxSearch(int depth, int display_level, Game game) { case RIGHT: game.right(false); } - - // if (display_level >= 2) { - // std::cout << std::endl; - // } } if (display_level <= 1) { std::cout << "Done!" << (display_level == 0 ? "\n" : "");