CS 1101 Computer Science I
Spring 2016

Computer Science Department
The Morrissey College of Arts and Sciences
Boston College

About Staff Textbook Grading Schedule Resources
Notes Labs Piazza Canvas GitHub Problem Sets
Manual StdLib Pervasives UniLib OCaml.org
Problem Set 9: Flow and Friends

Assigned: Tuesday December 1, 2015
Due: Friday December 11, 2015
Points: 12 Points up to 16 Points

Thanks to Prof. Bill Ames for suggesting this problem.

This is a pair project. You can work with one partner if you would like to. If you would like an assist in finding a partner you can ask a course staffer or you can use the Piazza partner finding tool.

The game of Flow involves working with a 2D grid of cells. Some cells are initially empty while others contain colored circles.

       

The object of the game is for the player to connect all of the like-colored circles leaving no empty cells. The connections formed between two like-colored circles are made up of sequences of "line pieces" of the matching color.

This problem set involves writing an OCaml program that solves a given flow grid.

This problem set has plenty of room for extra credit. See below.

Overview

This problem is an example of a search problem: your progam will implement an algorithm that searches for an ordering of pieces in the open cells that solves the problem.

Pieces

There are 7 piece shapes:

                       

The images of the pieces that you see above are just gif/png images. These images are provided and managed by the harness code. In solving this problem, your code can manipulate pieces using the functions provided in the harness code. These are described below.

Of the 7 pieces, only the first 6 are playable by the program --- the circular end-pieces are fixed at the outset.

Since pieces come in 9 different colors, there are a total of 7 x 9 = 63 different pieces and 6 x 9 = 54 different playable pieces. To get an idea of the stupendous size of the search space, since there are 6 x 6 = 36 cells, there are 6336 possible configurations of pieces on the board. This number is reasonbly close to (in fact, larger than) 6432 = (26)32 = 2192. Now that's clearly a big number but it's hard to get intuition about it so here's what it is:

2192 = 6277101735386680763835789423207666416102355444464034512896

So any program that is going to sort through all sequences of 36 items, each of which can hold 63 values, well, it's going to take a while.

Fortunately, there are many factors working in our favor that cut down the search space to something actually manageable. First, there are only 54 playable pieces and there will be somewhat fewer than 36 slots --- some of them will be occupied by circles. This may reduce it to something on the order of 5430, still intractable. But there are significant physical and color constraints that greatly restrict the search space. For example, lets say there is an orange corner in the upper left cell:

Then only 3 of the 63 pieces would fit in the cell to the right of it:

       

Just this one constraint eliminates 60 * 6334 cases.

And moreover, our Flow program isn't trying to inspect all solutions, it should quit when it finds the first sequence of pieces that solves the problem. With these constraints, our search space will be down to a manageable few 10s of thousands of configurations to be tried.

Backtracking

The Flow problem can be solved using a backtracking algorithm. The idea is to work our way through the board, visiting empty cells from left to right, top to bottom. For each empty cell, we'll try each piece to see if it is compatible with the ones already placed. If it is, it will be placed on the board and the placement will be recorded on a stack recording the play. In particular, the stack should record the following information for each of the decision points:

  1. The (row, col) of the piece played,

  2. The pieces that have not yet been tried at (row, col).
The harness code defines the following two types to support this:

  type decision = {row:int; col:int; pieces:Piece.t list}
  type stack_t  = decision Stack.t

The application of the algorithm here is not unlike the one that we discussed in solving the 8 Queens problem. (Code solving the 8 Queens problem will be posted on the Notes page.) Like the 8 Queens problem, we're concerned with placing pieces in a 2D grid. In that problem there was only one piece, a queen, and we needed only one queen per row. In this problem there are many pieces and every cell needs to be filled.

Getting Started

Download the harness code and unpack it. Executing an ls in a unix shell reveals the following files:

    Makefile*       board.mli       images/         piece.mli       util/
    board.ml        flow.ml         piece.ml        solved*

  • The file flow.ml is a harness for the main part of the program, it implements the control and interacts with the World.big_bang function to update and display the board and to determine when to quit.

  • The pair of files piece.mli and piece.ml are (resp.) a module signature and a module structure implementating an OCaml module named Piece. A module signature is a specification of what is required in an implemention of the module. Together, the signature and structure define a new type -- an abstract data type (ADT) for pieces.

    The OCaml compiler will check to ensure that implementation file (e.g., piece.ml) contains everything specified in its companion interface file (e.g., piece.mli).

  • The pair of files board.mli and board.ml are (resp.) the signature (or specification) and structure (or implementation) of a Board module, an ADT for the board.

  • The file solved is an executable of a solution. You can run it by type >./solved in the unix shell and then clicking on the window to start it.

  • The folders images/ and util/ contain support code that you won't need to modify.

Your modifications will be restricted to the implementation files: piece.ml, board.ml and flow.ml. In particular, your tasks include:

  1. To finish the implementation of the Piece ADT in file piece.ml. All of the definitions specified in piece.mli are provided in the harness file piece.ml except for those marked with (* ** *) below.

    
      type t
    
      val empty : t
      val wall  : t
    
      val isEmpty : t -> bool
      (* 
       * The call (isEmpty piece) returns true if the piece is empty.
       *)
    
      val isWall : t -> bool
      (* 
      * The call (isWall piece) returns true if the piece is a wall.
      *)
    
      val toInt : t -> int
      val pieceName : t -> string
    
      val isEndPiece : t -> bool
      val getEndPiece : int -> t
      
      val makePieces : unit -> t list
      val getPlayablePieces : t list -> t list
    
      val goesUp    : t -> bool
      (* 
      * The call (goesUp piece) returns true if the piece goes upward.
      * See the code.
      *)
      val goesDown  : t -> bool                         (* ** *)
      val goesLeft  : t -> bool                         (* ** *)
      val goesRight : t -> bool                         (* ** *)
    
      val isAllowedAbove : t -> t -> bool
      (* 
      * The call (isAllowedAbove piece1 piece2) returns true if piece1
      * is allowed above piece2.
      *)
      val isAllowedBelow : t -> t -> bool               (* ** *)
      val isAllowedLeft  : t -> t -> bool               (* ** *)
      val isAllowedRight : t -> t -> bool               (* ** *)
      
    
    NB: you can test the implementation of the Piece module independently of the other parts by compiling it from a unix command shell as in

    
        > ocamlc piece.mli piece.ml
    
    

  2. To implement the Board ADT in the file board.ml. The required definitions are specified in the signature file board.mli:

    
      type t
    
      val get : t -> int -> int -> Piece.t
      (* 
      * The call (get board row col) returns the piece at (row, col).
      *)
    			       
      val put : Piece.t -> t -> int -> int -> unit
      (* 
      * The call (put piece board row col) puts piece at (row, col).
      *)
    
      val make : (t -> t) -> int -> t
      (* 
      * The call (make sample n) makes a new board of size nxn initializing
      * all cells to Piece.empty and placing walls on all sides. Then the
      * sample function is applied to install endpoints.
      *)
      
      val print : t -> unit				  
      (* 
      * The call (print board) prints the board to stdout.
      *)
    
    

    Note that none of the definitions specified in the signature file board.mli are provided in the harness file board.ml, you'll have to implement the whole thing. The key step in this subtask is to define a type t to represent the board. I suggest that you use a 2D array of Piece.t, i.e., the definition of the type t in your board.ml file might be:

    
        type t = (Piece.t array) array
        ...
    
    
    Then the get, put, make and print functions can be implemented.

    NB: you can test the implementation of the Board module independently of the flow.ml code by compiling it from the unix command shell as in

    
        > ocamlc piece.mli piece.ml board.mli board.ml
    
    

  3. To implement several of the control functions in the main program flow.ml. The flow.ml file defines several new types to support the control code:
    
      type decision = {row:int; col:int; pieces:Piece.t list}
      type stack_t = decision Stack.t
    
      type state_t = Start | Go
      type world_t  = {board:Board.t; stack:stack_t; solved:bool; state:state_t}
    
      
    The top-level task is to implement the function

    val move : world_t -> Piece.t list -> world_t World.t

    Given the call (move world pieces), the move function attempts to find the next open cell to make a play. If there are no open cells left, then the game is solved and the move function can immediately return a new version of world with the solved field having the value true.

    If there is an open cell is at (row, col), then the move function should try each playable piece in turn to see if they are compatible with the pieces already placed on the board (i.e., on world.board).

    If such a piece is found, use the Board.put function to place the piece on the board at (row, col), record the row, column and remaining pieces on the stack (i.e., on world.stack) so that this decision can be revisted later, if needed, and then return the resulting world.

    If no compatible piece is found, then we're going to have to use the information stored in the stack to backtrack to the most recent decision point. If the stack is empty, there are no previous decision points -- the game is unsolvable. Otherwise pop the previous decision {row; col; pieces} from the stack, place Piece.empty in the board at (row, col) and then recursively call move to reconsider the decision made at cell (row, col).

    In implementing the move function, we'll depend on several other functions. Implement:

    • val findFirstOpenSpot : Board.t -> (int * int) option

      The call (findFirstOpenSpot board) starts from row = 1, col = 1, and scans the board from left to right, top to bottom until the first cell containing open is found. If the first occurrence of open was at indices (row, col), then Some(row, col) is returned. if no open spot is found then findFirstOpenSpot returns None. Hint: use the provided next function to index through the indices.

    • val tryAllPieces : Piece.t list -> int -> int -> (Piece.t * Piece.t list) option

      Given the call (tryAllPieces pieces row col), the tryAllPieces function should index through the provided pieces, trying each in turn on the board. By "trying each", we mean to use the Board.put function to (tentatively) place the piece on the board and then check the compatability. (More on compatibility below.) If the piece checks out, it can be left on the board and then tryAllPieces should return Some(piece, pieces) where piece is the compatible piece and pieces is the list of remaining ones that haven't (yet) been tried.

      Note that the type given for tryAllPieces above assumes that it is defined within the scope of a variable providing access to the board. If you'd prefer to define this function at the top level, you'll have to provide the board as an additional parameter.

    Returning to the question of whether or not the tentative placement of a piece is OK, the harness code comes with a function that performs the check:

    
        (* isOK : Board.t -> int -> int -> bool
         *
         * The call (isOK board row col) returns true if the current cell
         * checks out and all of the end pieces on the board are OK.
         *)
        let isOK board row col = (checkCell board row col) && (endPieceEntriesOK board)
        
    

    A piece is OK if it checks out with respect to its immediate neighbors and if it hasn't interfered with any of the end pieces on the board. These properties are checked by the checkCell and endPieceEntriesOK functions. Implement these functions.

    • val checkCell : Board.t -> row -> col -> bool

      Given the call (checkCell board row col), is the piece at (row, col), i.e., is (Board.get board row col), compatible with all of the neighbors? Well, it suffices to show that the piece above it is Piece.isAllowedAbove, the piece below it is Piece.isAllowedBelow and so forth.

    • val endPieceEntriesOK : Board.t -> bool

      The call (endPieceEntriesOK board) determines whether or not all of the end pieces in board are compatible with the cells around them.

    • val endPieceEntryOK : Board.t -> row -> col -> bool

      The call (endPieceEntryOK board row col) determines whether or not the end piece at (row, col) is compatible with the cells around it. It suffices to show that it has zero or one piece entering it from neighboring cells. If it has zero entering pieces it must otherwise have some empty neighbors.

    Our Algorithm is Broken!

    The algorithm that we are using has a logical error in it that allows for "solutions" like the following:

    We don't expect you to solve this problem.

    Extra Credit

    Here are a few ideas.
    1. (1 Point) Count the number of pieces tried and print the result when the board is solved.

    2. (2 Points) Our simple solution tries playable pieces of colors that don't match the colors of circles on the board. They cannot possibly work. Implement the code so that it doesn't try pieces that cannot appear in a solution.

    3. (LOTS of Points) Solve the problem cited above relating to lines that do not end at circles.

    4. (Middling Points) Develop an algorithm that solves the board more efficiently that the naive backtracking algorithm.

Created on 01-19-2016 23:09.