Homework 3: Let’s play Sokoban!
CIS 194: Homework 3
Due Tuesday, September 20
The general remarks about style and submittion from the first week still apply.
The goal of this week is simply: Implement Sokoban!
In order to guide you through the process, and also to make partial grading possible, we will break it down into smaller steps.
You can start from this code. It already includes type signatures for some of the functions described below. Make sure you adjust everything defined to be undefined
or marked as FIXME
. Please use your own code from previous homeworks where appropriate (i.e. your custom maze, tiles and player).
Step 1: The state
Define a data type State
that capture the state of the game. It needs to store these bits of information:
- The current position of the player.
- The direction the player is facing.
- The current positions of all boxes, as a
List
.
Step 2: The initial state
Define a value initialState :: State
for the initial state:
- Manually define a sensible position for the player (i.e. on some
Ground
tile). - Use an arbitrary position.
- Find where the boxes are.
The latter is a bit tricky: The information is there (in the definition of maze
), but not very accessible. Do not just write down the list of coordinates by hand! Instead, define a value initialBoxes :: List Coord
that is calcuated from looking at each coordinate (going from -10 to 10, as usual), and adding that coordinate to the list if there is a box.
There are two ways of doing that. Pick one (but try to understand both):
Define a function
appendList :: List a -> List a -> List a
which appends two lists. Then you can implement
initialBoxes
similar topictureOfMaze
, usingappendList
instead of(&)
.This is called folding
appendList
over the set of coordinate.Alternatively, in your recursions that traverse the coordinate space, pass a list down as a parameter. Initially, this list is
Empty
. If the current coordinate is not aBox
, you simply pass it on. If the current coordinate is a box, you add anEntry
to the list, and pass the extended list on.The helper function could possibly have type
go :: Integer -> Integer -> List Coord -> List Coord
Such a parameter is called an accumulating parameter.
You can test your initialBoxes
value using
main = drawingOf (pictureOfBoxes initialBoxes)
Step 3: Many mazes
The maze
as given has boxes in the initial position. That is no good in the long run. Therefore, you need to define two more mazes:
Define
noBoxMaze :: Coord -> Maze
which behaves like
maze
with the exception that wheremaze
would return aBox
, this returnsGround
.Use this in
pictureOfMaze
instead ofmaze
. You can test this usingmain = drawingOf pictureOfMaze
Define
mazeWithBoxes :: List Coord -> (Coord -> Maze)
which behaves like
noBoxMaze
for every coordinate that is not in the list, but returnsBox
if queried for a coordinate that is in the given list of coordinates.It will be useful (also below) to define a function
eqCoord :: Coord -> Coord -> Bool
that checks if two coordinates are the same.
Note that
mazeWithBoxes
, partially applied to one argument (the list with the curren positions of the boxes), has the same type asmaze
.
Step 4: Draw the state
Implement
draw :: State -> Picture
The picture consists of three elements:
- The player, at the current position (use
player
andatCoord
). - The boxes in their current positions (use
pictureOfBoxes
). - The
pictureOfMaze
(which usesnoBoxMaze
internally).
Step 5: Handling event
Implement
handleEvent :: Event -> State -> State
React to the arrow keys only. such an event can either succeed or fail.
It succeeds if the tile moved to is either Ground
or Storage
or Box
. If it is Box
, the next tile in that direction has to be Ground
or Storage
. It fails otherwise.
If the move succeeds, update the state with the new position of the player, the direction he walked to, and the updated position of the boxes.
Hint (you can do it differently if you want): To update the position of the boxes, define a function
moveFromTo :: Coord -> Coord -> (Coord -> Coord)
that takes a position to move from, a position to move to, and the coordinate to adjust. If the first parameter matches the third, return the second, otherwise the third. You can use this function with mapList
to update the whole list of coordinates. This conveniently does the right thing (i.e. nothing) if no box is pushed, and does the right thing with boxes that are not pushed. Do not worry about efficiency here.
If the move fails, update only the direction the player.
Step 6: Putting it all together
Define an Interaction
based on the functions you defined in the prevoius steps. Wrap it in resetable
and withStartScreen
, so that pressing the escape key returns to the start screen.
It should now behave roughly like this:
Step 7: Winning
One bit is missing: If the game has been won, you need to say so, and futher interaction should stop.
Define a function
isWon :: State -> Bool
which is True
if the game is won. It is won if every box is on a Storage
tile. To that end, define two helper functions:
haskell isOnStorage :: Coord -> Bool
that returns true if the given coordinate is aStorage
field.haskell allList :: List Bool -> Bool
which returns true if all entries in the list areTrue
, andFalse
otherwise. If given an empty list it should also returnTrue
.
You can implement isWon
using these two functions and mapList
.
Use isWon
in two places:
- In
draw
, if the game is won, writeYou won!
or something similar across the drawn picture of the game. - In
handleEvent
, if the game is won, ignore all events (i.e. return the state unaltered)
The final game should now behave like this: