Authors: Allison, Burcu
What is the problem?
We will implement a version of the single-player game 2048 in Haskell. The game involves a grid of tiles which the player slides the tiles to combine numbers. The numbers combine only if they are the same number. After each move, a new tile appears randomly on the board. The score increases each time numbers are combined, and the player wins by reaching a 2048 tile. The game ends if the grid is filled, and there are no more numbers to combine.
What is the something extra?
For something extra, we will allow players to save their scores and load usernames and scores from a file to display a leaderboard. We will also implement challenge levels that add more complex functionality to the game.
What did we learn from doing this?
Functional programming was somewhat suitable for our task. It worked well for many aspects of the project, but it brought challenges to parts such as the user interface.
We found pattern matching to be quite useful. By having pattern matching, we were able to seperate the functionalities of a single function and were able to look into them on a case by case basis. This helped us to focus on only one task of that function at a time, and made it easier to think of a solution, and test it out.
By defining data structures we were able to hold the information we needed easier, and with the help of pattern matching it became simpler to directly edit their elements. Moreover, we found the defined type being able to have more than one constructor to be very useful. With that we were able to represent each result that has been returned from the played move clearly by having two separate constructor with different elements. This not only enhanced our understanding of pattern matching and data structures, but also made us get more comfortable with working with lists, and list functions as the board is a 2d array.
For this project we also learned how to read from a file and write to it in Haskell. Moreover, to read from the file, we needed to parse its content to a suitable type to be able to load it in the dictionary we had, and we found this to be quite an interesting process. However, reading and writing in Haskell was also one of the challenges we faced. We came across an error when we tried to read from a file and write back to the same file before using the contents. This made us become more aware of Haskell's lazy evaluation as we found out that the error was caused by it. To fix the error, we added functionality that uses the contents of the file immediately after reading it, so it would be safe to write anytime later.
Another drawback was that our program relies heavily on random numbers, which made it difficult to keep the number of IO components to a minimum. For example, initializing the game board in the “play2048” file required IO to make the values of the initialized tiles random. We also needed to create a random integer in the game file as well, as we realized it was more interesting to have a game place the new tile randomly rather than having it place the new tile at the first available spot it sees. However, integrating this into the back-end of the game was even more challenging, as it didn’t have any connection to IO. We have found a package called “System.IO.Unsafe” which helped us to work around this issue, but we still think that this is not the best solution to have. However, this process helped us to have a better understanding of how IO worked, and how and why random is related to it.
The biggest disadvantage of functional programming for this task was in creating a user interface. We originally planned on having a graphical user interface for our game. However, after looking into our options for frameworks and the difficulties students had in the past with this aspect of the project, we found that Haskell is not well-suited for GUI creation compared to popular object-oriented languages. We instead decided on a command-line interface, which worked well in Haskell, but was less user-friendly than a GUI.