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.
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 = sig 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 *)
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.
Given is the following signature IDX_ARRAY
signature IDX_ARRAY = sig 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 end;with the following specifications.
('b, 'a) array
is an array indexed by values of type
'b
and storing values of type 'a
. 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). sub (a,x)
retrieves the element indexed
by x in array a. This may raise the exception
Subscript
. 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
.
Now, use the structure IdxArray
to implement a structure
Array2 :> ARRAY2
, where
signature ARRAY2 = sig 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 end;
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)
.
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 = struct 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.
##### # # #$ # ### $## # $ $ # ### # ## # ###### # # ## ##### ..# # $ $ ..# ##### ### #@## ..# # ######### #######
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
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.
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.
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.
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 MLAll 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.
signature SOKOTOP = sig val play : string -> unit end;implement a functor
functor SokoTop (structure Sokoban : SOKOBAN) :> SOKOTOPaccording to the specifications above. The function
play
takes
the name of a file with the initial situation as an argument.
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.
/afs/andrew/scs/cs/15-212-X/studentdir/<your andrew id>/ass5/