Skip to content

Instantly share code, notes, and snippets.

@idarlington
Created September 14, 2021 14:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save idarlington/7394c66c94f9e5d6f14635b354fa2cea to your computer and use it in GitHub Desktop.
Save idarlington/7394c66c94f9e5d6f14635b354fa2cea to your computer and use it in GitHub Desktop.
TicTacToe in Scala
package tictactoe
import scala.annotation.tailrec
// model
trait Square
case object X extends Square {
override def toString: String = "X"
}
case object O extends Square {
override def toString: String = "O"
}
case object Empty extends Square {
override def toString: String = "."
}
case class Row(col1: Square, col2: Square, col3: Square) {
def toMap: Map[String, Square] = Map("A" -> col1, "B" -> col2, "C" -> col3)
}
case class Board(row1: Row, row2: Row, row3: Row) {
def toMap: Map[String, Row] = Map("1" -> row1, "2" -> row2, "3" -> row3)
}
// Game service
object GameService {
@tailrec
def receiveSquareInput(): Square = {
scala.io.StdIn.readLine().trim.toLowerCase.strip() match {
case "x" => X
case "o" => O
case _ =>
println(GameText.invalidInput)
receiveSquareInput()
}
}
@tailrec
def receiveCoordinateInput(availableMoves: Iterable[String]): String = {
val coordinate = scala.io.StdIn.readLine().trim.toUpperCase.strip()
availableMoves.find { move =>
move == coordinate
} match {
case Some(value) =>
value
case None =>
println(GameText.invalidInput)
receiveCoordinateInput(availableMoves)
}
}
def showNextMoves(square: Square, board: Board): Iterable[String] = {
val availableMoves: Iterable[String] = for {
(rowKey, row) <- board.toMap
(colKey, square) <- row.toMap if square == Empty
} yield s"$colKey$rowKey"
val formattedAvailableMoves: String = availableMoves.toSeq.sorted.foldLeft("") {
case (moves, coordinate) => s"$moves $coordinate"
}
println(GameText.showNextMove(square, formattedAvailableMoves))
availableMoves
}
def switch(square: Square): Square = {
square match {
case X => O
case O => X
case Empty => Empty
}
}
def choosePlayer(): Square = {
println(GameText.choosePlayer)
receiveSquareInput()
}
def updateBoard(input: Square, coordinate: String, board: Board): Board = {
coordinate match {
case "A1" if board.row1.col1 == Empty =>
(board.copy(row1 = board.row1.copy(col1 = input)))
case "A2" if board.row2.col1 == Empty =>
(board.copy(row2 = board.row2.copy(col1 = input)))
case "A3" if board.row3.col1 == Empty =>
(board.copy(row3 = board.row3.copy(col1 = input)))
case "B1" if board.row1.col2 == Empty =>
(board.copy(row1 = board.row1.copy(col2 = input)))
case "B2" if board.row2.col2 == Empty =>
(board.copy(row2 = board.row2.copy(col2 = input)))
case "B3" if board.row3.col2 == Empty =>
(board.copy(row3 = board.row3.copy(col2 = input)))
case "C1" if board.row1.col3 == Empty =>
(board.copy(row1 = board.row1.copy(col3 = input)))
case "C2" if board.row2.col3 == Empty =>
(board.copy(row2 = board.row2.copy(col3 = input)))
case "C3" if board.row3.col3 == Empty =>
(board.copy(row3 = board.row3.copy(col3 = input)))
}
}
def collateSquareCoordinates(square: Square, board: Board): Iterable[String] = {
for {
(rowKey, row) <- board.toMap
(colKey, existingSquare) <- row.toMap if square == existingSquare
} yield s"$colKey$rowKey"
}
def checkColumnWinner(square: Square, board: Board): Option[Square] = {
val columnWinnerMatches: Seq[Seq[String]] =
Seq(Seq("A1", "A2", "A3"), Seq("B1", "B2", "B3"), Seq("C1", "C2", "C3"))
val existingSquareCoordinate = collateSquareCoordinates(square, board)
columnWinnerMatches
.map { win =>
win.forall { coordinate =>
existingSquareCoordinate.exists(_ == coordinate)
}
}
.collectFirst {
case matchAll if matchAll => square
}
}
def checkRowWinner(board: Board): Option[Square] = {
board.toMap
.find {
case (_, row) =>
row match {
case _ if row == Row(O, O, O) => true
case _ if row == Row(X, X, X) => true
case _ => false
}
}
.map {
case (_, row) =>
row.col1
}
}
def checkDiagonalWinner(square: Square, board: Board): Option[Square] = {
val existingSquareCoordinate = collateSquareCoordinates(square, board)
val diagonalWinnerMatches = Seq(Seq("A1", "B2", "C3"), Seq("C1", "B2", "A3"))
diagonalWinnerMatches
.map { win =>
win.forall { coordinate =>
existingSquareCoordinate.exists(_ == coordinate)
}
}
.collectFirst {
case matchAll if matchAll => square
}
}
def checkWinner(square: Square, board: Board): Option[Square] = {
checkRowWinner(board)
.orElse(checkColumnWinner(square, board))
.orElse(checkDiagonalWinner(square, board))
}
def checkFull(board: Board): Boolean = {
board.toMap.forall {
case (_, row) =>
row.toMap.forall {
case (_, square) =>
square != Empty
}
}
}
@tailrec
def gameLoop(player: Square, board: Board): Unit = {
GameText.displayBoard(board)
val availableMoves: Iterable[String] = showNextMoves(player, board)
val coordinate: String = receiveCoordinateInput(availableMoves)
val updatedBoard: Board = updateBoard(player, coordinate, board)
checkWinner(player, updatedBoard) match {
case Some(square) =>
println(GameText.win(square))
System.exit(0)
case None =>
if (checkFull(updatedBoard)) {
println(GameText.draw)
System.exit(0)
} else {
val otherPlayer = switch(player)
gameLoop(otherPlayer, updatedBoard)
}
}
}
def startGame(board: Board): Unit = {
val square = choosePlayer()
gameLoop(square, board)
}
}
// Game text
object GameText {
val invalidInput: String = "Invalid choice, try again"
val choosePlayer: String = "Please choose a player: X or O"
val draw: String = "It is a draw!!"
def displayBoard(board: Board): String = {
val boardDisplay =
s"""
| A B C
|1 ${board.row1.col1} ${board.row1.col2} ${board.row1.col3}
|2 ${board.row2.col1} ${board.row2.col2} ${board.row2.col3}
|3 ${board.row3.col1} ${board.row3.col2} ${board.row3.col3}
|""".stripMargin
println(boardDisplay)
boardDisplay
}
def win(square: Square): String = s"Player $square wins the game!!"
def showNextMove(square: Square, formattedMoves: String): String =
s"""
| Your next move with $square:
| $formattedMoves
|""".stripMargin
}
// Game App
object TicTacToe extends App {
val board: Board =
Board(Row(Empty, Empty, Empty), Row(Empty, Empty, Empty), Row(Empty, Empty, Empty))
GameService.startGame(board)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment