Created
September 14, 2021 14:04
-
-
Save idarlington/7394c66c94f9e5d6f14635b354fa2cea to your computer and use it in GitHub Desktop.
TicTacToe in Scala
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 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