Sunday, January 3, 2016

fx-tictactoe - A TicTacToe App using JavaFX and Scala

This post describes fx-tictactoe, a tictactoe game written in Scala with JavaFX.

Screenshot of the TicTacToe application

I was inspired to write this application because I recently watched an old 80's film with Matthew Broderick called "War games". Moreover, I used this topic as an assignment for an introductory JavaFX / Scala course I held in autumn 2015.

To get a little bit into the mood for playing TicTacToe, you could watch one scene of this film on youtube, where the evil master program learns how to play the game.

TicTacToe is one of the games which is simple enough such that you can pre-calculate all possible game states. This "game tree" approach allows you then to choose one of the remaining moves which leads to a win.

To make the application a bit more interesting, it also shows some css wrangling which could be used for your experiments as well.

Here is a snippet of the source code which shows the relevant part of the solving algorithm.

package net.ladstatt.fx.ttt
import scala.util.Random
/**
* A naive way to implement a brute force strategy for tic tac toe
*/
object BruteForceTicTacToeStrategy extends TicTacToeStrategy {
val newGame: TicTacToe = TicTacToe()
/**
* for a given list of moves, this function reduces the moves and the
* starting state (the tic tac toe) to a resulting tic tac toe. If the
* game is already over, the remaining moves are ignored.
*
* @param t
* @param moves
* @return
*/
def play(t: TicTacToe, moves: Seq[TMove]): TicTacToe = {
moves.foldLeft(t) {
case (game, move) =>
if (!game.isOver) {
game.turn(move)
} else game
}
}
// calculate all tic tac toe games possible
lazy val allGames: Map[Seq[TMove], TicTacToe] = {
TicTacToe.allMoves.toSeq.permutations.map {
case moves =>
val t = play(newGame, moves)
(t.movesSoFar, t)
}.toMap
}
// separate draw games from games where a winner exists
lazy val (gamesWithWinner: Map[Seq[TMove], TicTacToe], gamesWithDraw: Map[Seq[TMove], TicTacToe]) =
allGames.partition {
case (_, game) => game.winner.isDefined
}
// partition all games where a winner exists
lazy val (gamesA, gamesB) = gamesWithWinner.partition {
case (_, game) => game.winner.get._1 == PlayerA
}
/**
* returns the next turn for a given tic tac toe game, or none if the tic tac toe
* game is already over, meaning somebody won or there is a draw
*
* @param game
* @return
*/
override def calcNextTurn(game: TicTacToe): Option[TMove] = {
if (game.isOver) None
else {
val plr = game.nextPlayer
// only interested in the win for the player
val winningGames: Map[Seq[TMove], TicTacToe] =
if (plr == PlayerA) gamesA else gamesB
// we try to win, so lets search if there exists a path to a winning game
val potentialWinningMoves: Seq[Seq[TMove]] =
winningGames.filter {
case (moves, _) => moves.startsWith(game.movesSoFar)
}.keys.toSeq
if (potentialWinningMoves.nonEmpty) {
// println(plr + ": " + potentialWinningMoves.size + " ways to to win the game.")
Some(determineMove(game, potentialWinningMoves))
} else {
// check if we can reach a draw
val potentialDraws =
gamesWithDraw.filter {
case (moves, _) => moves.startsWith(game.movesSoFar)
}.keySet.toSeq
if (potentialDraws.nonEmpty) {
//println(plr + ": " + potentialDraws.size + " ways to a draw left.")
Some(determineMove(game, potentialDraws))
} else {
// no winning path nor draw could be found, we'll loose
// so just take any random move
//println(plr + ": I take a random move since it is already clear I loose.")
Some(game.remainingMoves.toSeq(Random.nextInt(game.remainingMoves.size)))
}
}
}
}
/**
* uses some heuristics to choose the best move.
*
* @param game
* @param potentialMoves
* @return
*/
def determineMove(game: TicTacToe, potentialMoves: Seq[Seq[TMove]]): TMove = {
// check if we could win with the next move
val winningMove = game.lookAhead(PlayerB)
if (winningMove.isDefined) {
winningMove.get
} else {
// check if there is already an obvious threat from the opponent to win the game
// if there is, we'll take the move
val winningMoveForOpponent = game.lookAhead(PlayerA)
if (winningMoveForOpponent.isDefined) {
winningMoveForOpponent.get
} else {
// prefer the middle center f
if (potentialMoves.exists {
case moves => moves.drop(game.movesSoFar.length).head == MiddleCenter
}) {
MiddleCenter
} else {
// we take the shortest path to win
val possibilities = potentialMoves.sortWith((a, b) => a.size < b.size)
val aPathToWin = possibilities.head
aPathToWin.drop(game.movesSoFar.length).head
}
}
}
}
}

Have a look at the self contained source code for the whole app on my github site.

Ps: For some readers the most interesting part maybe is the background photo, which shows a frozen lake on one of my last mountain hikes at about 2700 meters above sea level in the Austrian alps.




No comments:

Post a Comment