Last active
December 22, 2015 20:39
-
-
Save sebnozzi/6527481 to your computer and use it in GitHub Desktop.
A TicTacToe implementation. Result of a TDD practice session (solo).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Initially everything was part of
Board
. At the end it was obvious that methods likeindexFor
,playerAt
andplayerFillingRow
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...