Kayla Curtis, Katharine Cavers, Justin Oh
What is the problem?
We will be implementing the popular board game Battleship, in which 2 players secretly position their ships on a board and then attempt to hit their opponent's ships by stating coordinates. This will be a one player version, with the opponent being an AI.
We will be investigating whether Haskell lends itself to creating games like Battleship, and implementing an AI.
What is the something extra?
We introduced a visual component by printing the board in a grid format using the tabl library. This brings the game closer to the traditional Battleship board game, unlike many programs which are purely text-based. The AI will also make more sophisticated guesses than simply attempting to hit a random square-- for example, after a hit, it will try to hit the four squares around the hit.
We also used Cabal as a build system.
What did we learn from doing this?
Completing this project helped to solidify the features and design of the functional programming paradigm. Coming from mainly imperative programming backgrounds, the idea of not have mutable variables and state at first felt very foreign. For instance, the initial setup of the board seemed a really daunting task - how could we do this without being able to modify the board itself? As soon as we were able to get into the correct way of thinking about functional programming, and using its features instead of trying to force it to be imperative, tasks like this became a lot simpler. For instance, reconstructing the board with the new ship placed was actually made quite easy with the use of Haskell's list comprehensions. We became used to taking in some state, making some changes by creating a new state based off of the original, and then passing that new state to the next function. Essentially, the idea of threading the state of a game board through our program, instead of having some global, mutable state.
From working on this project, we found that Haskell was quite suitable to the task of implementing the game Battleships. Some reasons for this:
- list comprehensions made generating a new state of the game board after some action (e.g. placing a ship, hitting a target) very straightforward
- Haskell's higher-order functions (e.g. foldr, filter) made the code easy to write and very consise
- For instance, to be able to check the end condition of a game in imperative programming, we might have had some variable that we updated each time a ship was hit. However, using functional programming made us think of a different solution that was very simple; using filter on each row to keep only those elements representing a hit square, and then using foldr to sum the lengths of all the lists of rows to see if the expected number of hit squares were present on the board.
- In a similar way to above, using filter and foldr to quickly check whether the most recent action sunk a ship
- The game proceeds as a sequence of repeatable steps, and using IO functions made implementing the flow of the game quite simple.
- Haskell lent itself to writing code that was robust. By separating the game play into a few different functions, we were able to easily handle cases where users gave invalid input, by recursively calling the current function with the same state. For example, if a user gives an invalid coordinate as part of setting up their board or selecting a target, or selects a target they have already hit in a previous turn, we just call the current function again with the same board array that was given originally as input.
Many of these items could also apply to non-functional programming languages. However another advantage we found using Haskell was that the code we wrote was very expressive, and for the most part bug-free. It was easy to test individual functions once they were written by simply interacting with the specific function through ghci, whereas using an object-oriented, imperative language would have most likely required a lot of setup of state in order to reach a testable state for certain methods.