Tic Tac Toe

Due: Monday, Nov 3 11:59PM

Test cases due: Friday, Oct 31, 23:59

For this assignment, you will write a program that given a tic-tac-toe board, determines which player will win, or if the game will be a draw.

You’re going to make significant use of Scala collections and learn the the Minimax algorithm, which is a form of backtracking search.

Preliminaries

You should create a series of directories that look like this:

 ./tictactoe
 |-- build.sbt
 `-- project
     `-- plugins.sbt
 `-- src
     |-- main
     |   `-- scala
     |       `-- your solution goes here
     `-- test
        `|-- scala
             `-- your tests goes here

Your build.sbt file must have exactly these lines:

name := "tictactoe"

scalaVersion := "2.11.2"

libraryDependencies += "edu.umass.cs" %% "cmpsci220" % "1.5"

libraryDependencies += "org.scalatest" %% "scalatest" % "2.2.1" % "test"

parallelExecution in Test := false

fork in Test := true

The plugins.sbt file must have exactly this line:

addSbtPlugin("edu.umass.cs" % "cmpsci220" % "2.2")

Representing a Tic Tac Toe Board

We assume you know how to play Tic Tac Toe. This section talks about the representation of tic-tac-toe boards that you will use. All the types mentioned below are in the cmpsci220.hw.tictactoe package.

The sealed Player trait has two constructors, O and X, that represent the two players.

We are going to treat game boards as a 3x3 matrix, where (0,0) is the coordinate of the top-left corner and (2,2) is the coordinate of the bottom-right corner:

       |        |
 (0,0) | (1, 0) | (2, 0)
       |        |
-------+--------+--------
       |        |
 (0,1) | (1, 1) | (2, 1)
       |        |
-------+--------+--------
       |        |
 (0,2) | (1, 2) | (2, 2)
       |        |

We’ll represent boards as values of type Matrix[Option[Player]]. (Matrix is defined in the cmpsci220.hw.tictactoe packet.)

Here are some examples:

 | |
-+-+-
 | |
-+-+-
 | |
val emptyBoard = Matrix[Option[Player]](3, None)
X| |
-+-+-
 | |
-+-+-
 | |O
val ex1 = emptyBoard.set(0, 0, Some(X)).set(2, 2, Some(O))
X| |
-+-+-
 | |
-+-+-
X| |O
val ex2 = ex1.set(0, 2, Some(X))

The Minimax Algorithm

*Minimax is an algorithm to to determine who will win (or draw) a two-player game, if both players are playing perfectly. To do so, Minimax searches all possible game-states that are reachable from a given inital state. Here is an outline of a recursive implementation of Minimax:

def minimax(game: Game): Some[Player] = {

  If it is Xs turn:

    1. If X has won the game, return Some(X).
    2. If the game is a draw, return None. (If all squares are filled
       and nobody has won, then the game is a draw. However, you are
       free to detect a draw earlier, if you wish.)
    3. Recursively apply minimax to all the successor states of game
       - If any recursive call produces X, return Some(X)
       - Or, if any recursive call produces None, return None
       - Or, return Some(O)

  The case for Os turn is similar.

}

You can find several other descriptions of Minimax on the Web. But, this is the last step of the assignment. Follow the programming directions below and implement (and test) everything leading up to Minimax. Implementing Minimax itself will be staightforward.

Programming Task

Your task is to implement the MinimaxLike trait, which has two functions: one initializes the game state and running the Minimax algorithm. To do so, you’ll have to design a class to represent the state of the game, using the GameLike trait.

Use this template for your code:

class Game(/* add fields here */) extends GameLike[Game] {

  def isFinished(): Boolean = {
    throw new UnsupportedOperationException("not implemented")
  }

  /* Assume that isFinished} is true */
  def getWinner(): Option[Player] = {
    throw new UnsupportedOperationException("not implemented")
  }

  def nextBoards(): List[Game] = {
    throw new UnsupportedOperationException("not implemented")
  }
}

object Solution extends MinimaxLike {

  type T = Game // T is an "abstract type member" of MinimaxLike

  def createGame(turn: Player, board: Matrix[Option[Player]]): Game = {
    require(board.dim == 3)
    throw new UnsupportedOperationException("not implemented")
  }

  def minimax(board: Game): Option[Player] = {
    throw new UnsupportedOperationException("not implemented")
  }
}

I recommend proceeding in the following way:

  1. Add fields to the Game class to represent the state of the game and fill in the body of the createGame function. createGame takes two arguments: turn indicates the current player’s turn and board describes the state of the board. The board may be in an arbitrary, even illegal state. For example, the board may have seven Xs. Similarly, the turn could be either X or O.

  2. Implement the isFinished method. Human players often end a game early, when the outcome is inevitable. However, you may find it easier to write a program that plays until every single square is filled. Remember that if you determine that the game isFinished, you need to be able to determine the winner too (using getWinner).

  3. Implement the getWinner method. The Matrix class has several methods that will help. There is no need for you to use direct recursion.

  4. Implement the nextBoards method, which returns a list of boards that represent all the moves the next player could make.

    For example, if the current board looks like this:

    X|X|
    -+-+-
     |O|
    -+-+-
    X|O|O
    

    Then, it is O’s turn, and these are the three possible next boards:

    X|X|     X|X|O    X|X|
    -+-+-    -+-+-    -+-+-
     |O|O     |O|     O|O|
    -+-+-    -+-+-    -+-+-
    X|O|O    X|O|O    X|O|O
    

As you implement each successive step, you may need to revisit design decisisions you made earlier.

Check Your Work

Here is a trivial test suite that simply checks to see if you’ve defined the Solution object with the right type:

class TrivialTestSuite extends org.scalatest.FunSuite {

  test("The solution object must be defined") {
    val obj : cmpsci220.hw.tictactoe.MinimaxLike = Solution
  }
}

You should place this test suite in src/test/scala/TrivialTestSuite.scala. If this test suite does not run as-is, you risk getting a zero.

Testing and Review

Starting with this assignment, we are going to grade your test cases and your comprehension of others’ test cases. This is worth 5% of your grade and is due on Friday, Oct 31 23:59.

Create a separate test suite called GradedTestSuite.scala. In it, place 5–10 interesting test cases for the isFinished and/or the getWinner methods. For example, here is a test that checks that an empty board is not finished.

class GradedTestSuite extends org.scalatest.FunSuite {

  val emptyBoard = Matrix[Option[Player]](3, None)

  test("Empty board should not be isFinished()") {
    assert(!Solution.createGame(X, emptyBoard).isFinished())
  }

}

Write 5–10 tests that try to cover the space of interesting inputs to these functions. Once you submit, you will be given upto three other students’ tests to review, which you must complete by Monday, Nov 3, 5PM. This ensures that everyone gets feedback in time for the final assignment submission.

How to submit GradedTestSuite.scala

We will use Captain Teach to manage the submission and review process for tests. You can login to Captain Teach using your @umass.edu e-mail address. To ensure you are logged into this account, first visit:

https://apps.umass.edu/

Once you have authenticated your account visit:

https://www.captain-teach.org/umass-cmpsci220/assignments/

If asked, select your @umass.edu account as the one you would like to use while accessing Captain Teach. From the assignments screen, select the the tic-tac-toe assignment. This will bring you to a screen that shows what you need to do next, the work you’ve submitted, reviews you have pending, reviews you’ve completed, and feedback that others have provided for you.

Select the publish a submission under the Next Required Action header. This will bring you to a screen where you can upload GradedTestSuite.scala. Upload that file.

After uploading, you will be brought to a page that lets you preview your submission before publishing it for review. Review your submission and when you are ready to proceed click Publish Submission at the bottom of the page.

Right after submitting, the page should say that the submission was successful, and give you a Continue link. This will bring you back to the dashboard where you will now have up to three pending reviews you must complete. You can complete the reviews in any order (and view them all at the same time), but you must complete all three reviews. Changes to the reviews are automatically saved so you may leave them at any point without submitting and come back to complete your work later. Once you click submit, you may no longer make changes to your review.

When you receive reviews from your classmates, you will also be given the ability to submit feedback on the review. This feedback is only for the course staff and has no effect on your grade. We’re interested in hearing if the review was particularly helpful or unhelpful, or if you think it was wrong. Also, if you feel the review you received is in any way inappropriate, you can flag it to bring the problem to our attention.

Note: You will only be reviewing test cases. This means you should not submit any implementation details for review.

Submit Your Work

Use the submit command within sbt to create submission.tar.gz. Upload this file to Moodle.

Comic