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
Groundtile). - 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 awhich appends two lists. Then you can implement
initialBoxessimilar topictureOfMaze, usingappendListinstead of(&).This is called folding
appendListover 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 anEntryto the list, and pass the extended list on.The helper function could possibly have type
go :: Integer -> Integer -> List Coord -> List CoordSuch 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 -> Mazewhich behaves like
mazewith the exception that wheremazewould return aBox, this returnsGround.Use this in
pictureOfMazeinstead ofmaze. You can test this usingmain = drawingOf pictureOfMazeDefine
mazeWithBoxes :: List Coord -> (Coord -> Maze)which behaves like
noBoxMazefor every coordinate that is not in the list, but returnsBoxif 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 -> Boolthat 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 -> PictureThe picture consists of three elements:
- The player, at the current position (use
playerandatCoord). - The boxes in their current positions (use
pictureOfBoxes). - The
pictureOfMaze(which usesnoBoxMazeinternally).
Step 5: Handling event
Implement
handleEvent :: Event -> State -> StateReact 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 -> Boolwhich 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 -> Boolthat returns true if the given coordinate is aStoragefield.haskell allList :: List Bool -> Boolwhich returns true if all entries in the list areTrue, andFalseotherwise. 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: