15-212-X : Homework Assignment 5

Due Wed Nov 12, 2:00pm (electronically)

Maximum Points: 100 (+30 extra credit)


Please include needed auxiliary declarations from the ML files in /afs/andrew/cmu.edu/scs/cs/15-212-X/assignments/ass5/sokoban.sml in your solution file, so it compiles relying only on the libraries stream.sml and mstream-io.sml. Points will be deducted if your file does not compile in an environment with the standard basis and those two files loaded.

In this homework, you may add operations to the given signatures, widening the interface to your abstract types if this turns out to be convenient or helps to simplify your code. However, you may not change the types specified in the assignment, since we will be relying on those to run your programs.

In this assignment, we write a program to allow us to play the one-person game Sokoban. To get a feel for the game, you can play an X version (written in C) in /afs/andrew/scs/cs/15-212-X/bin/xsokoban.

picture of level 1

Sokoban is a puzzle game where a player (red, in the picture) moves in a crowded warehouse, trying to push the boxes (or treasures, gold in the picture) into the goal squares (gray, in the picture). A treasure is heavy, so player can only push one at a time and cannot pull it at all.

This is more difficult than it might seem at first. For example, one has to make sure to have a possibility to get behind every treasure to push it.

Our overall goal is to implement a structure which exports operations to read a puzzle (called level) from a file, print the current situation, and carry out legal moves as given by a player. This is specified in the following signature.

signature SOKOBAN =
  exception Error of string

  type level                            (* abstract *)
  val init : string -> level            (* init (filename), may raise Error *)
  val print : level -> unit

  datatype direction = Up | Down | Left | Right

  exception Illegal of string           (* raised for Illegal moves *)
  val move : level -> direction -> unit (* may raise Illegal(msg) *)
  val moveTo : level -> int * int -> unit (* may raise Illegal(msg) *)
end;  (* signature SOKOBAN *)

Problem 1: Two-Dimensional Arrays (30 points)

ML does not have two-dimensional or higher-dimensional arrays in the standard library. In this problem we build arrays over an arbitrary index type, assuming that we have a function which is a bijection between the elements of the index type and natural numbers between 0 and some upper bound. We then use this to implement two-dimensional arrays.

Question 1.1 (15 points)

Given is the following signature IDX_ARRAY

signature IDX_ARRAY =
  type ('b, 'a) array

  val array : ('b -> int) -> 'b * 'a -> ('b, 'a) array
  val sub : ('b, 'a) array * 'b -> 'a
  val update : ('b, 'a) array * 'b * 'a -> unit
with the following specifications.
  1. ('b, 'a) array is an array indexed by values of type 'b and storing values of type 'a.
  2. array f (max,init) creates a new array indexed by values of type 'b. We assume that f maps legal values of type 'b to the interval 0,...,f(max) (both sides inclusive).
  3. sub (a,x) retrieves the element indexed by x in array a. This may raise the exception Subscript.
  4. update (a, x, y) updates the cell indexed by x in array a with value y. This may raise the exception Subscript

Using the operations and types in the pervasive structure Array, implement a structure IdxArray :> IDX_ARRAY.

Question 1.2 (15 Points)

Now, use the structure IdxArray to implement a structure Array2 :> ARRAY2, where

signature ARRAY2 =
  type 'a array
  val array : (int * int) * 'a -> 'a array
  val sub : 'a array * (int * int) -> 'a
  val update : 'a array * (int * int) * 'a -> unit

Each of the operations should be obvious. The first argument to array is a pair consisting of the number of columns and the number of rows. When the array is accessed, the minimal index should be (0,0).

Problem 2: Basic Playing of Sokoban (70 points)

Given the structure Array2 from Problem 1 and the libraries stream.sml and mstream-io.sml, we now begin the implementation of the game. Signatures for inclusion in your solution can be found in sokoban.sml
structure Sokoban :> SOKOBAN =
  exception Error of string

  datatype Background =
      Floor                    (* plain floor space *)
    | Goal                     (* goal space for treasure *)

  datatype Square =
      Wall                     (* wall, #"#" *)
    | Empty of Background      (* empty space, either #" " for plain floor *)
                               (* or #"." for goal space *)
    | Player of Background     (* player, either #"@" on plain floor *)
                               (* or #"+" on goal space *)
    | Treasure of Background   (* treasure, either #"$" for plain floor *)
                               (* or #"*" on goal space *)
    | Void                     (* outside playing area *)

  type level = Square Array2.array      (* current board *)
               * (int * int) option ref (* current player position *)
               * int ref                (* current number of unfilled goals *)

We represent a situation by a triple consisting of a two-dimensional array with 32 columns and 19 rows, where 31 by 18 is the maximally allowable size for a level (the additional row and column simplify bounds checking). We also record separately the current player position (so we don't have to search for it) and the number of unfilled goal spaces (so we can detect success quickly). Note these two important invariants which must be established and maintained by your implementation!

The data file defining a level contains the characters defining the initial situation, according to their printed representation given in the comments above. For example, the situation on the right would be represented by the file contents on the left.

picture of level 1

    #   #
    #$  #
  ###  $##
  #  $ $ #
### # ## #   ######
#   # ## #####  ..#
# $  $          ..#
##### ### #@##  ..#
    #     #########

Here the initial place of the player is (11, 8), since we count starting with (0, 0) at the upper left-hand corner to simplify parsing and printing. The initial number of goal spaces not filled by treasure is 6. This situation can be found in the file level1.txt

Question 2.1 (20 points)

Using MStreamIO.readFile to provide you with a stream of characters, write the function init : string -> level, whose argument should be interpreted as a filename. This function may raise an error if the file format is incorrect, or exceeds the size bounds However, you do not need to check other conditions one usually assumes. For example, you do not need to check if there is only one player, or if there are walls, or if the wall has holes, or if there are enough treasures to fill all goal spaces, etc.

Question 2.2 (20 points)

Give an implementation of move : level -> direction -> unit. Your function must raise the exception Illegal(msg) with a brief message msg when a given move is illegal. Moving means either for player to enter an adjacent empty square, or pushing a treasure if the square behind it is empty.

For your reference, the print function is already provided in the file sokoban.sml. It prints one additional space after each character to make the board appearance less distorted.

Question 2.3 (30 points)

Implement the function moveTo : level -> int * int -> unit, where moveTo level (x,y) should move the player to the square (x, y), counting (as specified above) from 0, starting in the upper left-hand corner.

This operation must raise an exception Illegal (msg) if the destination square is not reachable by player from his current position without moving a treasure.

Hint: Use either depth-first or breadth-first search where you keep track of all visited squares to avoid re-doing work.

Problem 3: Interactive Playing (30 points extra credit)

Now we implement a very simple-minded top-level to test the implementation above. For a more serious implementation, we would like to use the X/Motif interface of MLWorks. The interactive top-level prompts with

and then reads input from the terminal. It does not act on the input until it encounters a newline character. You should use MStreamIO.readTerminal, which will automatically have this behavior.

The resulting character stream should be interpreted as follows:

  j  --- move left
  k  --- move down
  l  --- move right
  i  --- move up
 \n  --- (newline) print board and re-prompt
  q  --- quit play and return to ML
All other characters should be ignored with a warning message. Your implementation should be robust, that is, print a message for an attempted illegal move, but not return control to the ML top level, so that the game player can try another move.

Question 3.1 (15 points extra credit)

Given the signature
signature SOKOTOP =
  val play : string -> unit
implement a functor
functor SokoTop (structure Sokoban : SOKOBAN) :> SOKOTOP
according to the specifications above. The function play takes the name of a file with the initial situation as an argument.

Question 3.2 (15 points extra credit)

Enrich the set of commands available in the SokoTop.play function to include Sokoban.moveTo. We address a destination square by a four-digit sequence of characters xxyy where xx describes column and yy describes the row of the destination square, counting from 0 starting in the upper left-hand corner. The starting position (11,8) of player in the example above would be addressed by typing 1108.

Handin instructions