In this assignment, you will create an adversarial search agent to play the 2048-puzzle game
I. 2048 As A Two-Player Game
2048 is played on a 4×4 grid with numbered tiles which can slide up, down, left, or right. This game can be modeled as a two player game, in which the computer AI generates a 2- or 4-tile placed randomly on the board, and the player then selects a direction to move the tiles. Note that the tiles move until they either (1) collide with another tile, or (2) collide with the edge of the grid. If two tiles of the same number collide in a move, they merge into a single tile valued at the sum of the two originals. The resulting tile cannot merge with another tile again in the same move.
Usually, each role in a two-player games has a similar set of moves to choose from, and similar objectives (e.g. chess). In 2048 however, the player roles are inherently a symmetric, as the Computer AI places tiles and the Player moves them. Adversarial search can still be applied! Using your previous experience with objects, states, nodes, functions, and implicit or explicit search trees, along with our skeleton code, focus on optimizing your player algorithm to solve 2048 as efficiently and consistently as possible.
II. Choosing A Search Algorithm: Expectiminimax
Review the lecture on adversarial search. Is 2048 a zero-sum game? What are the minimax and expectiminimax principles?
The tile-generating Computer AI of 2048 is not particularly adversarial as it spawns tiles irrespective of whether a spawn is the most adversarial to the user’s progress, with a 90% probability of a 2 and 10% for a 4 (from GameManager_3.py). However, our Player AI will play as if the computer is adversarial since this proves more effective in beating the game. We will specifically use the expectiminimax algorithm.
With expectiminimax, your game playing strategy assumes the Computer AI chooses a tile to place in a way that minimizes the Player's outcome. Note whether or not the Computer AI is optimally adversarial is a question to consider. As a general principle, how far the opponent's behavior deviates from the player’s assumption certainly affects how well the AI performs. However you will see that this strategy works well in this game.
.
Expectiminimax is a natural extension of the minimax algorithm, so think about how to implement minimax first. As we saw in the simple case of tic-tac-toe, it is useful to employ the minimax algorithm assuming the opponent is a perfect "minimizing" agent. In practice, an algorithm with the perfect opponent assumption deviates from reality when playing a s ub-par opponent making silly moves, but still leads to the desired outcome of never losing. If the deviation goes the other way, however, (a "maximax" opponent in which the opponent wants us to win), winning is obviously not guaranteed.
III. Using The Skeleton Code
The skeleton code includes the following files. Note that you will only be working in o ne of them, and the rest are read-only:
● Read-only: GameManager_3.py. This is the driver program that loads your Computer AI and Player AI and begins a game where they compete with each other. See below on how to execute this program.
● Read-only: Grid_3.py. This module defines the Grid object, along with some useful operations: move(), getAvailableCells(), insertTile(), and clone(), which you may use in your code. These are by no means the most efficient methods available, so if you wish to strive for better performance, feel free to ignore these and write your own helper methods in a separate file.
● Read-only: BaseAI_3.py. This is the base class for any AI component. All AIs inherit from this module, and implement the getMove() function, which takes a Grid object as parameter and returns a move (there are different "moves" for different AIs).
● Read-only: ComputerAI_3.py. This inherits from BaseAI. The g etMove() function returns a computer action that is a tuple (x, y) indicating the place you want to place a tile.
● Writable: PlayerAI_3.py. You will create this file. The PlayerAI class should inherit from BaseAI. The getMove() function to implement must return a number that indicates the player’s action. In particular, 0 stands for "Up", 1 stands for "Down", 2 stands for "Left", and 3 stands for "Right". This is where your player-optimizing logic lives and is executed. Feel free to create submodules for this file to use, and include any submodules in your submission.
● Read-only: BaseDisplayer_3.py and Displayer_3.py. These print the grid.
.
To test your code, execute the game manager like so: $ python GameManager_3.py
The progress of the game will be displayed on your terminal screen with one snapshot printed after each move that the Computer AI or Player AI makes. Your Player AI is allowed 0.2 seconds to come up with each move. The process continues until the game is over; that is, until no further legal moves can be made. At the end of the game, the maximum tile value on the board is printed.
● Employ the expectiminimax algorithm. This is a requirement. There are many viable strategies to beat the 2048-puzzle game, but in this assignment we will be using he expectiminimax algorithm. Note that 90% of tiles placed by the computer are 2’s, while the remaining 1 0% are 4’s. It may be helpful to first implement regular minimax.
● Implement alpha-beta pruning. This is a requirement. This should speed up the search process by eliminating irrelevant branches. In this case, is there anything we can do about move ordering?
● Use heuristic functions. What is the maximum height of the game tree? Unlike elementary games like tic-tac-toe, in this game it is highly impracticable to search the entire depth of the theoretical game tree. To be able to cut off your search at any point, you must employ h euristic functions to allow you to assign approximate values to nodes in the tree. Remember, the time limit allowed for each move is 0.2 seconds, so you must implement a systematic way to cut off your search before time runs out.
● Assign h euristic weights. You will likely want to include more than one heuristic function. In that case, you will need to assign weights associated with each individual heuristic. Deciding on an appropriate set of weights will take careful reasoning, along with careful experimentation. If you feel adventurous, you can also simply write an optimization meta-algorithm to iterate over the space of weight vectors, until you arrive at results that you are happy enough with.