This post describes fx-tictactoe, a tictactoe game written in Scala with JavaFX.
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.
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.
![]() |
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.
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.
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 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