Skip to content

Instantly share code, notes, and snippets.

@sebnozzi
Last active December 22, 2015 20:39
Show Gist options
  • Save sebnozzi/6527481 to your computer and use it in GitHub Desktop.
Save sebnozzi/6527481 to your computer and use it in GitHub Desktop.
A TicTacToe implementation. Result of a TDD practice session (solo).
package com.sebnozzi.katas.tictactoe
object TicTacToe {
sealed class Player
case object O extends Player
case object X extends Player
case class Board(state: State = State()) {
def makeMove(col: Int, row: Int, player: Player) = {
Board(state.updated(col, row, player))
}
def winningPlayer(): Option[Player] = {
val potentialCandidates: Seq[Option[Player]] =
Seq(
state.playerFillingRow(1),
state.playerFillingRow(2),
state.playerFillingRow(3),
state.playerFillingCol(1),
state.playerFillingCol(2),
state.playerFillingCol(3),
state.playerFillingTopLeftBottomRightDiagonal,
state.playerFillingTopRightBottomLeftDiagonal)
val candidates = potentialCandidates.flatten
candidates.headOption
}
}
object State {
val emptyCells = Seq[Option[Player]]().padTo(9, None)
}
case class State(cells: Seq[Option[Player]] = State.emptyCells) {
def isEmpty = cells.forall(player => player == None)
def playerAt(col: Int, row: Int): Option[Player] = cells(indexFor(col, row))
def indexFor(col: Int, row: Int) = (row - 1) * 3 + (col - 1)
def updated(col: Int, row: Int, player: Player) = {
val newState = cells.updated(indexFor(col, row), Some(player))
State(newState)
}
def playerFillingRow(row: Int): Option[Player] = {
val positions = Seq((1, row), (2, row), (3, row))
playerFillingPositions(positions)
}
def playerFillingCol(col: Int): Option[Player] = {
val positions = Seq((col, 1), (col, 2), (col, 3))
playerFillingPositions(positions)
}
def playerFillingTopLeftBottomRightDiagonal: Option[Player] = {
val positions = Seq((1, 1), (2, 2), (3, 3))
playerFillingPositions(positions)
}
def playerFillingTopRightBottomLeftDiagonal: Option[Player] = {
val positions = Seq((3, 1), (2, 2), (1, 3))
playerFillingPositions(positions)
}
private def playerFillingPositions(positions: Seq[(Int, Int)]): Option[Player] = {
val players = positions.map { case (col, row) => playerAt(col, row) }
players.distinct match {
case Some(player) :: Nil => Some(player)
case _ => None
}
}
}
package com.sebnozzi.katas.tictactoe
import org.scalatest.FunSuite
import TicTacToe._
class BoardSuite extends FunSuite with BoardSuiteHelpers {
test("it should be possible to make move, getting a new board") {
val newBoard = emptyBoard.makeMove(col = 1, row = 1, player = X)
assert(newBoard.getClass === classOf[com.sebnozzi.tictactoe.TicTacToe.Board], "Should be a Board")
}
test("making two different moves should result in different boards") {
val alternative1 = emptyBoard.makeMove(col = 1, row = 1, player = X)
val alternative2 = emptyBoard.makeMove(col = 2, row = 2, player = X)
assert(alternative1 != alternative2)
}
test("initially, nobody won") {
val winningPlayer = emptyBoard.winningPlayer()
assert(winningPlayer === None)
}
test("after one move, still nobody wins") {
val newBoard = emptyBoard.makeMove(1, 1, player = X)
val winningPlayer = newBoard.winningPlayer()
assert(winningPlayer === None)
}
test("after putting 3 X in a row, player X wins") {
val newBoard = boardWithRow_1_filled
val winningPlayer = newBoard.winningPlayer()
assert(winningPlayer === Some(X))
}
test("after putting 3 O in a column, player O wins") {
val newBoard = boardWithCol_2_filled
val winningPlayer = newBoard.winningPlayer()
assert(winningPlayer === Some(O))
}
test("after putting 3 X in the TL-BR diagonal, player X wins") {
val newBoard = boardWithTopLeftBottomRightDiagonalFilled
val winningPlayer = newBoard.winningPlayer()
assert(winningPlayer === Some(X))
}
test("after putting 3 X in the TR-BL diagonal, player O wins") {
val newBoard = boardWithTopRightBottomLeftDiagonalFilled
val winningPlayer = newBoard.winningPlayer()
assert(winningPlayer === Some(O))
}
test("after filling rows, player wins") {
assert(boardWithRowFilled(1, X).winningPlayer === Some(X))
assert(boardWithRowFilled(2, X).winningPlayer === Some(X))
assert(boardWithRowFilled(3, X).winningPlayer === Some(X))
}
test("after filling columns, player wins") {
assert(boardWithColFilled(1, X).winningPlayer === Some(X))
assert(boardWithColFilled(2, X).winningPlayer === Some(X))
assert(boardWithColFilled(3, X).winningPlayer === Some(X))
}
}
class StateSuite extends FunSuite with StateFillers {
test("initial state should be empty") {
assert(emptyState.isEmpty)
}
test("an updated state should not be empty") {
val newState = emptyState.updated(1, 1, X)
assert(newState.isEmpty === false)
}
test("it should be possible to determine the player at one position") {
val newState = emptyState.updated(col = 1, row = 1, player = X)
val player: Option[Player] = newState.playerAt(1, 1)
assert(player === Some(X))
}
test("it should be possible to translate position to index") {
assert(emptyState.indexFor(col = 1, row = 1) === 0)
assert(emptyState.indexFor(col = 2, row = 2) === 4)
}
test("it should be possible to determine the player at another position") {
val newState = emptyState.updated(col = 2, row = 2, player = O)
val player: Option[Player] = newState.playerAt(2, 2)
assert(player === Some(O))
}
test("initially, no player fills row nr 1") {
assert(emptyState.playerFillingRow(1) === None)
}
test("it should be possible to ask if a row is filled") {
val newState = stateWithRowFilled(1, X)
assert(newState.playerFillingRow(1) === Some(X))
}
test("initially, no player fills col nr 2") {
assert(emptyState.playerFillingCol(2) === None)
}
test("it should be possible to ask if a column is filled") {
val newState = stateWithColFilled(2, O)
assert(newState.playerFillingCol(2) === Some(O))
}
}
trait BoardSuiteHelpers extends StateFillers {
val emptyBoard = Board()
def boardWithRowFilled(row: Int, player: Player) = Board(stateWithRowFilled(row, player))
def boardWithColFilled(col: Int, player: Player) = Board(stateWithColFilled(col, player))
val boardWithRow_1_filled = boardWithRowFilled(1, X)
val boardWithCol_2_filled = boardWithColFilled(2, O)
val boardWithTopLeftBottomRightDiagonalFilled = emptyBoard
.makeMove(1, 1, player = X)
.makeMove(2, 2, player = X)
.makeMove(3, 3, player = X)
val boardWithTopRightBottomLeftDiagonalFilled = emptyBoard
.makeMove(3, 1, player = O)
.makeMove(2, 2, player = O)
.makeMove(1, 3, player = O)
}
trait StateFillers {
val emptyState = State()
def stateWithRowFilled(row: Int, player: Player) = State()
.updated(1, row, player)
.updated(2, row, player)
.updated(3, row, player)
def stateWithColFilled(col: Int, player: Player) = State()
.updated(col, 1, player)
.updated(col, 2, player)
.updated(col, 3, player)
}
@sebnozzi
Copy link
Author

Initially everything was part of Board. At the end it was obvious that methods like indexFor, playerAt and playerFillingRow were implementation details. But in the process I had written tests for them.

So I was faced with the question: do I make them private and delete the tests? Or leave them there polluting the class interface?

That was the point when it was clear that they belonged to a separate class (State), with another responsibility. The wonders of refactoring! The only thing I would have wanted to know is whether it was correct to extract them all to a new class in one big step or not...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment