├── .gitignore ├── README.md ├── build.sbt ├── core └── src │ ├── main │ ├── resources │ │ └── 2322324186_ca41fba641_o.jpg │ └── scala │ │ └── com │ │ └── sebnozzi │ │ └── slidingpuzzle │ │ ├── model │ │ ├── Puzzle.scala │ │ ├── PuzzleTile.scala │ │ ├── structs │ │ │ ├── GridSize.scala │ │ │ ├── Position.scala │ │ │ └── Rect.scala │ │ └── tile │ │ │ └── traits │ │ │ ├── AdjacentTilesAware.scala │ │ │ ├── PositionAware.scala │ │ │ └── VisibilityAware.scala │ │ └── ui │ │ ├── AppController.scala │ │ ├── AppView.scala │ │ ├── PuzzleView.scala │ │ ├── TileView.scala │ │ └── keys │ │ └── ArrowKey.scala │ └── test │ ├── resources │ └── size-change.feature │ └── scala │ └── com │ └── sebnozzi │ └── slidingpuzzle │ ├── model │ ├── PuzzleSuite.scala │ ├── PuzzleTileSuite.scala │ ├── structs │ │ ├── PositionSuite.scala │ │ └── RectSuite.scala │ └── tile │ │ └── traits │ │ ├── AdjacentTilesAwareSuite.scala │ │ ├── PositionAwareSuite.scala │ │ └── VisibilityAwareSuite.scala │ └── ui │ ├── AppControllerSuite.scala │ ├── AppViewSuite.scala │ ├── PuzzleViewSuite.scala │ ├── TestTileView.scala │ └── TileViewSuite.scala ├── images ├── original_small.jpg ├── screenshot-browser.jpeg └── screenshot-javafx.jpg ├── javafx └── src │ ├── main │ ├── resources │ │ └── 2322324186_ca41fba641_o.jpg │ └── scala │ │ └── com │ │ └── sebnozzi │ │ └── slidingpuzzle │ │ ├── SlidingPuzzleMain.scala │ │ └── ui │ │ └── javafx │ │ ├── ControlPanel.scala │ │ ├── JFXAppController.scala │ │ ├── JFXAppView.scala │ │ ├── JFXPuzzleView.scala │ │ ├── JFXTileView.scala │ │ └── utils │ │ └── ImageSlicer.scala │ └── test │ ├── resources │ └── size-change.feature │ └── scala │ └── com │ └── sebnozzi │ └── slidingpuzzle │ └── ui │ └── javafx │ └── utils │ └── ImageSlicerSuite.scala ├── project ├── build.properties └── plugins.sbt └── scalajs ├── index-dev.html ├── index.html ├── resources ├── css │ └── styles.css ├── img │ ├── 2322324186_ca41fba641_o.jpg │ └── scala-js-logo-16.png └── js │ ├── jquery-1.10.2.min.js │ └── jquery-1.10.2.min.map └── src └── main └── scala └── com └── sebnozzi └── slidingpuzzle ├── SlidingPuzzleMain.scala └── ui ├── HtmlBuilder.scala ├── JqToolbar.scala ├── JsAppController.scala ├── JsAppView.scala ├── JsPuzzleView.scala ├── JsTileView.scala └── UIBuilder.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .cache 3 | .classpath 4 | .project 5 | .settings/ 6 | *~ 7 | .DS_Store 8 | /*.sc 9 | logs 10 | project/project 11 | project/target 12 | target 13 | tmp 14 | .history 15 | dist 16 | /.idea 17 | /*.iml 18 | /out 19 | /.idea_modules 20 | /.classpath 21 | /.project 22 | /RUNNING_PID 23 | /.settings 24 | .scala_dependencies 25 | .target/ 26 | /.cache 27 | bin/ 28 | /.worksheet 29 | lib/jfxrt.jar 30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sliding Puzzle 2 | 3 | [Sliding Puzzle](http://en.wikipedia.org/wiki/Sliding_puzzle) implemented in [**Scala**](http://www.scala-lang.org/). 4 | 5 | See the online demo here: [http://www.sebnozzi.com/demos/sliding-puzzle/](http://www.sebnozzi.com/demos/sliding-puzzle/) 6 | 7 | ## Multi-platform 8 | 9 | Thanks to [Scala.js](http://www.scala-js.org/), it compiles both to: 10 | 11 | 1. Desktop JVM application 12 | 2. Browser JavaScript single-page application 13 | 14 | As much as possible, I've tried to have UI and platform-agnostic code. This lead to a better design which made the different implementations not only possible but straightforward. 15 | 16 | ### Desktop Version 17 | 18 | The native JVM version uses [JavaFX](http://en.wikipedia.org/wiki/JavaFX) for its UI, and can be seen here: 19 | 20 |  21 | 22 | ### Browser Version 23 | 24 | Here's a screenshot of the [Scala.js](http://www.scala-js.org/) based version, running on the browser: 25 | 26 |  27 | 28 | ## Building 29 | 30 | ### JDK 31 | 32 | Tested on JDK version 11 and 14. 33 | 34 | #### Running 35 | 36 | Run using SBT with: 37 | 38 | ``` 39 | sbt javafx/run 40 | ``` 41 | 42 | ### Scala.js Version 43 | 44 | #### Unoptimized / Development 45 | 46 | Development, or "fast-optimized", mode compiles code very quick. The generated file is still small enough (600 Kb)to load quickly in the browser (while developing), but not as small as in the "full-optimized" mode (131 Kb). 47 | 48 | Package "fast-optimized" code with SBT with: 49 | 50 | ``` 51 | sbt scalajs/fastOptJS 52 | ``` 53 | 54 | This will generate "fast-optimized" JavaScript files. 55 | 56 | To load this "development" version of the puzzle, open this file in your browser: 57 | 58 | ``` 59 | scalajs/index-dev.html 60 | ``` 61 | 62 | #### Full-Optimized / Production 63 | 64 | Optimized code takes longer to compile / optimize, but the generated code is much smaller (around 130 Kb) and loads very quickly in the browser. 65 | 66 | Generate fully-optimized code with SBT with: 67 | 68 | ``` 69 | sbt scalajs/fullOptJS 70 | ``` 71 | 72 | This will generate one optimized JavaScript file using [Google's closure compiler](https://developers.google.com/closure/compiler/). 73 | 74 | To load the optimized version of the puzzle, open this file in your browser: 75 | 76 | ``` 77 | scalajs/index.html 78 | ``` 79 | 80 | ## Credits 81 | 82 | ### Scala.js 83 | 84 | Scala.js-related acknowledgements: 85 | 86 | * Thanks to Sébastien Doeraene and the EPFL for Scala.js! 87 | * Thanks to all Scala.js [contributors](http://www.scala-js.org/contribute/) 88 | * Thanks to all maintainers of the Scala.js [jQuery wrappers](https://github.com/scala-js/scala-js-jquery) 89 | * The multi-platform SBT setup is "inspired" by [this project](https://github.com/sjrd/funlabyrinthe-scala), also by Sébastien Doeraene 90 | 91 | ### About the picture 92 | 93 | The musicians you see on the picture are part of a Jazz "[Big Band](http://en.wikipedia.org/wiki/Big_band)". 94 | 95 |  96 | 97 | > Tini Thomsen and Matthias Konrad 98 | > with the Thomsen Group @ Birdland, Hamburg 99 | 100 | I wanted to use this picture for this puzzle because it portraits two of my favourite jazz-instruments: 101 | 102 | * **baritone sax** (left) 103 | * **trombone** (right) 104 | 105 | Original [picture](http://www.flickr.com/photos/mawel/2322324186/) taken by [Marc Wellekötter](http://www.flickr.com/photos/mawel/) 106 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | 2 | val slidingPuzzleScalaVersion = "2.13.2" 3 | 4 | lazy val defaultSettings = Seq( 5 | scalaVersion := slidingPuzzleScalaVersion, 6 | scalacOptions ++= Seq( 7 | "-deprecation", slidingPuzzleScalaVersion, 8 | "-unchecked", 9 | "-feature", 10 | "-encoding", "utf8" 11 | ), 12 | version := "0.2-SNAPSHOT" 13 | ) 14 | 15 | val scalaTest = "org.scalatest" %% "scalatest" % "3.1.1" % "test" 16 | 17 | lazy val root = (project in file(".")).settings( 18 | defaultSettings: _* 19 | ).settings( 20 | name := "SlidingPuzzle" 21 | ).aggregate( 22 | core, javafx 23 | ) 24 | 25 | lazy val core = (project in file("core")).settings( 26 | defaultSettings: _* 27 | ).settings( 28 | name := "SlidingPuzzle Core", 29 | libraryDependencies += scalaTest 30 | ) 31 | 32 | // === JavaFX === 33 | 34 | lazy val javafxSettings = Seq( 35 | fork := true 36 | ) 37 | 38 | lazy val javafx = project.in(file("javafx")).settings( 39 | defaultSettings ++ scalaFxSettings: _* 40 | ).settings( 41 | name := "SlidingPuzzle JavaFX", 42 | libraryDependencies += scalaTest 43 | ).dependsOn(core) 44 | 45 | lazy val scalaFxSettings = { 46 | // Add dependency on JavaFX libraries, OS dependent 47 | lazy val javaFXModules = Seq("base", "controls", "fxml", "graphics", "media", "swing", "web") 48 | 49 | // Determine OS version of JavaFX binaries 50 | lazy val osName = System.getProperty("os.name") match { 51 | case n if n.startsWith("Linux") => "linux" 52 | case n if n.startsWith("Mac") => "mac" 53 | case n if n.startsWith("Windows") => "win" 54 | case _ => throw new Exception("Unknown platform!") 55 | } 56 | 57 | javafxSettings ++ Seq( 58 | libraryDependencies ++= javaFXModules.map( m => 59 | "org.openjfx" % s"javafx-$m" % "14.0.1" classifier osName 60 | ), 61 | libraryDependencies += "org.scalafx" %% "scalafx" % "14-R19" 62 | ) 63 | } 64 | 65 | // === Scala JS === 66 | 67 | lazy val coreJs = project.settings( 68 | defaultSettings: _* 69 | ).settings( 70 | name := "SlidingPuzzle CoreJS", 71 | sourceDirectory := (sourceDirectory in core).value, 72 | libraryDependencies += scalaTest 73 | ).enablePlugins(ScalaJSPlugin) 74 | .dependsOn(core) 75 | 76 | lazy val scalajs = project.in(file("scalajs")).settings( 77 | defaultSettings: _* 78 | ).settings( 79 | name := "SlidingPuzzle ScalaJS", 80 | libraryDependencies ++= Seq( 81 | "be.doeraene" %%% "scalajs-jquery" % "1.0.0", 82 | "org.scala-js" %%% "scalajs-dom" % "1.0.0") 83 | ).enablePlugins(ScalaJSPlugin) 84 | .dependsOn(coreJs) 85 | 86 | -------------------------------------------------------------------------------- /core/src/main/resources/2322324186_ca41fba641_o.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebnozzi/sliding-puzzle/6dadde44d909ea2ae78740393edd50a299739657/core/src/main/resources/2322324186_ca41fba641_o.jpg -------------------------------------------------------------------------------- /core/src/main/scala/com/sebnozzi/slidingpuzzle/model/Puzzle.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model 2 | 3 | import com.sebnozzi.slidingpuzzle.model.structs.{GridSize, Position, Rect} 4 | 5 | class Puzzle(gridSize: GridSize) { 6 | 7 | private var _movesDone = 0 8 | private var _solvedCallback: Option[() => Unit] = None 9 | private var _movesCountCallback: Option[() => Unit] = None 10 | private var _hiddenTile: Option[PuzzleTile] = None 11 | 12 | val columns: Int = gridSize.columns 13 | val rows: Int = gridSize.rows 14 | 15 | val tiles: List[PuzzleTile] = { 16 | (for ( 17 | rowNr <- 1 to rows; 18 | colNr <- 1 to columns 19 | ) yield new PuzzleTile(puzzle = this, initialPosition = Position(colNr, rowNr))).toList 20 | } 21 | 22 | val positionsRect: Rect = Rect(Position(1, 1), Position(columns, rows)) 23 | 24 | private def makeRandomMove(times: Int = 1): Unit = { 25 | if (hasHiddenTile) { 26 | for (_ <- 1 to times) { 27 | val tileToMove = hiddenTile.randomAdjacentTile 28 | tileToMove.moveToEmptySlot(shuffling = true) 29 | } 30 | } 31 | } 32 | 33 | def shuffle(): Unit = { 34 | makeRandomMove(times = 300) 35 | movesDone = 0 36 | } 37 | 38 | def movesDone: Int = _movesDone 39 | 40 | private def movesDone_=(newCount: Int): Unit = { 41 | _movesDone = newCount 42 | if (_movesCountCallback.isDefined) 43 | _movesCountCallback.get() 44 | } 45 | 46 | def canMoveToEmptySlot(tile: PuzzleTile): Boolean = { 47 | if (hasHiddenTile) { 48 | tile.isAdjacentTo(hiddenTile) 49 | } else { 50 | false 51 | } 52 | } 53 | 54 | def moveToEmptySlot(tile: PuzzleTile, shuffling: Boolean = false): Boolean = { 55 | if (canMoveToEmptySlot(tile)) { 56 | tile.swapPositionWith(hiddenTile, shuffling) 57 | if (!shuffling) 58 | didMoveToEmptySlot(tile) 59 | true 60 | } else { 61 | false 62 | } 63 | } 64 | 65 | def didMoveToEmptySlot(tile: PuzzleTile): Unit = { 66 | movesDone = movesDone + 1 67 | if (this.isSolved && _solvedCallback.isDefined) 68 | _solvedCallback.get() 69 | } 70 | 71 | def onSolved(callback: => Unit): Unit = { 72 | _solvedCallback = Some(() => callback) 73 | } 74 | 75 | def onMovesCountChange(callback: => Unit): Unit = { 76 | _movesCountCallback = Some(() => callback) 77 | } 78 | 79 | def reset(): Unit = { 80 | tiles.foreach(_.moveToInitialPosition()) 81 | movesDone = 0 82 | clearHiddenTile() 83 | } 84 | 85 | def isSolved: Boolean = { 86 | tiles.forall { tile => tile.isAtInitialPosition } 87 | } 88 | 89 | def tileAt(position: Position): PuzzleTile = { 90 | tiles.find(tile => tile.currentPosition == position).get 91 | } 92 | 93 | def tileAt(col: Int, row: Int): PuzzleTile = tileAt(Position(col, row)) 94 | 95 | def setHiddenTile(newTile: PuzzleTile): Unit = { 96 | _hiddenTile match { 97 | case Some(currentTile) if newTile != currentTile => 98 | currentTile.visibilityChanged(toVisible = true) 99 | _hiddenTile = Some(newTile) 100 | newTile.visibilityChanged(toVisible = false) 101 | case None => 102 | _hiddenTile = Some(newTile) 103 | newTile.visibilityChanged(toVisible = false) 104 | case _ => // Do nothing 105 | } 106 | } 107 | 108 | def setHiddenTileAt(col: Int, row: Int): Unit = setHiddenTileAt(Position(col, row)) 109 | 110 | def setHiddenTileAt(position: Position): Unit = { 111 | val newTile = tileAt(position) 112 | setHiddenTile(newTile) 113 | } 114 | 115 | def clearHiddenTile(): Unit = { 116 | _hiddenTile.foreach(_.visibilityChanged(toVisible = true)) 117 | _hiddenTile = None 118 | } 119 | 120 | def hasHiddenTile: Boolean = _hiddenTile.isDefined 121 | 122 | def hiddenTile: PuzzleTile = _hiddenTile.getOrElse(throw new java.util.NoSuchElementException("Does not have a hidden tile")) 123 | 124 | } 125 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sebnozzi/slidingpuzzle/model/PuzzleTile.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model 2 | 3 | import com.sebnozzi.slidingpuzzle.model.structs.{Position, Rect} 4 | import com.sebnozzi.slidingpuzzle.model.tile.traits._ 5 | 6 | class PuzzleTile(val puzzle: Puzzle, val initialPosition: Position) 7 | extends PositionAware with AdjacentTilesAware with VisibilityAware { 8 | 9 | protected def positionsRect: Rect = puzzle.positionsRect 10 | protected def tileAt(pos: Position): PuzzleTile = puzzle.tileAt(pos) 11 | 12 | def makeHidden(): Unit = { 13 | puzzle.setHiddenTileAt(PuzzleTile.this.currentPosition) 14 | } 15 | 16 | def makeVisible(): Unit = { 17 | puzzle.clearHiddenTile() 18 | } 19 | 20 | def canMoveToEmptySlot: Boolean = { 21 | puzzle.canMoveToEmptySlot(this) 22 | } 23 | 24 | def moveToEmptySlot(shuffling: Boolean = false): Boolean = { 25 | puzzle.moveToEmptySlot(this, shuffling) 26 | } 27 | 28 | override def toString: String = { 29 | val positionStr = "ini(%d, %d)|cur(%d, %d)".format( 30 | initialPosition.col, 31 | initialPosition.row, 32 | currentPosition.col, 33 | currentPosition.row) 34 | s"Tile($positionStr)" 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /core/src/main/scala/com/sebnozzi/slidingpuzzle/model/structs/GridSize.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model.structs 2 | 3 | case class GridSize(columns:Int, rows:Int) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sebnozzi/slidingpuzzle/model/structs/Position.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model.structs 2 | 3 | case class Position(col: Int, row: Int) { 4 | 5 | lazy val above: Position = Position(col, row - 1) 6 | lazy val below: Position = Position(col, row + 1) 7 | lazy val left: Position = Position(col - 1, row) 8 | lazy val right: Position = Position(col + 1, row) 9 | 10 | def adjacentPositionsWithin(rect: Rect): Seq[Position] = 11 | Seq(above, left, right, below) 12 | .filter( pos => rect.contains(pos) ) 13 | 14 | def aboveWithin(rect: Rect): Option[Position] = 15 | above.optionallyWithin(rect) 16 | 17 | def belowWithin(rect: Rect): Option[Position] = 18 | below.optionallyWithin(rect) 19 | 20 | def leftWithin(rect: Rect): Option[Position] = 21 | left.optionallyWithin(rect) 22 | 23 | def rightWithin(rect: Rect): Option[Position] = 24 | right.optionallyWithin(rect) 25 | 26 | private def optionallyWithin(rect: Rect): Option[Position] = { 27 | if (rect.contains(this)) 28 | Some(this) 29 | else 30 | None 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sebnozzi/slidingpuzzle/model/structs/Rect.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model.structs 2 | 3 | case class Rect(topLeft: Position, bottomRight: Position) { 4 | 5 | def contains(pos: Position): Boolean = 6 | (pos.col >= topLeft.col && pos.col <= bottomRight.col) && 7 | (pos.row >= topLeft.row && pos.row <= bottomRight.row) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sebnozzi/slidingpuzzle/model/tile/traits/AdjacentTilesAware.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model.tile.traits 2 | 3 | import com.sebnozzi.slidingpuzzle.model.PuzzleTile 4 | import com.sebnozzi.slidingpuzzle.model.structs.{Position, Rect} 5 | 6 | import scala.util.Random 7 | 8 | trait AdjacentTilesAware { self:PositionAware => 9 | 10 | protected def positionsRect: Rect 11 | protected def tileAt(pos: Position): PuzzleTile 12 | 13 | def isAdjacentTo(other: PuzzleTile): Boolean = adjacentTiles.contains(other) 14 | 15 | def adjacentTiles: List[PuzzleTile] = List( 16 | tileAbove, 17 | tileLeft, 18 | tileRight, 19 | tileBelow).flatten 20 | 21 | def tileAbove: Option[PuzzleTile] = 22 | currentPosition.aboveWithin(positionsRect).map(pos => tileAt(pos)) 23 | 24 | def tileBelow: Option[PuzzleTile] = 25 | currentPosition.belowWithin(positionsRect).map(pos => tileAt(pos)) 26 | 27 | def tileLeft: Option[PuzzleTile] = 28 | currentPosition.leftWithin(positionsRect).map(pos => tileAt(pos)) 29 | 30 | def tileRight: Option[PuzzleTile] = 31 | currentPosition.rightWithin(positionsRect).map(pos => tileAt(pos)) 32 | 33 | def randomAdjacentTile: PuzzleTile = { 34 | val tiles = adjacentTiles 35 | Random.shuffle(tiles).head 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sebnozzi/slidingpuzzle/model/tile/traits/PositionAware.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model.tile.traits 2 | 3 | import com.sebnozzi.slidingpuzzle.model.structs.Position 4 | 5 | trait PositionAware { 6 | 7 | val initialPosition: Position 8 | 9 | private var _currentPosition = initialPosition 10 | private var _positionChangeCallback: Option[Boolean => Unit] = None 11 | 12 | def currentPosition: Position = _currentPosition 13 | 14 | def isAtInitialPosition: Boolean = { 15 | currentPosition == initialPosition 16 | } 17 | 18 | def currentPosition_=(newPosition: Position, shuffling: Boolean = false): Unit = { 19 | _currentPosition = newPosition 20 | _positionChangeCallback.foreach(callback => callback(shuffling)) 21 | } 22 | 23 | def moveToInitialPosition(): Unit = { 24 | currentPosition = initialPosition 25 | } 26 | 27 | def onPositionChange(callback: Boolean => Unit): Unit = { 28 | _positionChangeCallback = Some(callback) 29 | } 30 | 31 | def swapPositionWith(other: PositionAware, shuffling: Boolean = false): Unit = { 32 | val previousPosition = currentPosition 33 | currentPosition_=(other.currentPosition, shuffling) 34 | other.currentPosition_=(previousPosition, shuffling) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sebnozzi/slidingpuzzle/model/tile/traits/VisibilityAware.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model.tile.traits 2 | 3 | trait VisibilityAware { 4 | 5 | private var _visibilityCallback: Option[Boolean => Unit] = None 6 | 7 | def onVisibilityChange(callback: Boolean => Unit): Unit = { 8 | _visibilityCallback = Some(callback) 9 | } 10 | 11 | def visibilityChanged(toVisible: Boolean): Unit = { 12 | _visibilityCallback foreach (callback => callback(toVisible)) 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /core/src/main/scala/com/sebnozzi/slidingpuzzle/ui/AppController.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui 2 | 3 | import com.sebnozzi.slidingpuzzle.model.{Puzzle, PuzzleTile} 4 | import com.sebnozzi.slidingpuzzle.model.structs.GridSize 5 | import com.sebnozzi.slidingpuzzle.ui.keys.ArrowKey 6 | 7 | abstract class AppController() { 8 | 9 | private var puzzle: Puzzle = _ 10 | 11 | private var appView: AppView = _ 12 | private var puzzleView: PuzzleView = _ 13 | 14 | private def hiddenTile = puzzle.tiles.last 15 | 16 | def createAppView(): AppView 17 | 18 | def createPuzzleView(gridSize: GridSize): PuzzleView 19 | 20 | /** 21 | * After creating the controller, you should call 22 | * this method to get the application running. 23 | */ 24 | def start(): Unit = { 25 | val initialGridSize = GridSize(3, 3) 26 | appView = createAppView() 27 | setupAppView(appView, initialGridSize) 28 | setupGame(initialGridSize) 29 | appView.show() 30 | } 31 | 32 | private def setupAppView(appView: AppView, gridSize: GridSize): AppView = { 33 | appView.selectGridSize(gridSize) 34 | 35 | appView.onArrowKeyPressed { arrowKey => 36 | arrowKeyPressed(arrowKey) 37 | } 38 | 39 | appView.onShuffleClicked { 40 | puzzle.setHiddenTile(hiddenTile) 41 | puzzle.shuffle() 42 | puzzleView.requestFocus() 43 | } 44 | 45 | appView.onResetClicked { 46 | puzzle.reset() 47 | puzzleView.requestFocus() 48 | } 49 | 50 | appView.onNewSizeSelected { newSize => 51 | setupGame(newSize) 52 | } 53 | 54 | appView 55 | } 56 | 57 | private def setupGame(gridSize: GridSize): Unit = { 58 | puzzle = new Puzzle(gridSize) 59 | 60 | puzzleView = createPuzzleView(gridSize) 61 | 62 | appView.setPuzzleView(puzzleView) 63 | 64 | puzzle.tiles.zip(puzzleView.tileViews).foreach { 65 | case (modelTile: PuzzleTile, uiTile: TileView) => 66 | bindUiAndModelTiles(uiTile, modelTile) 67 | } 68 | 69 | puzzle.onMovesCountChange { 70 | updateMovesCount() 71 | } 72 | 73 | puzzle.onSolved { 74 | puzzle.clearHiddenTile() 75 | } 76 | 77 | updateMovesCount() 78 | } 79 | 80 | private def updateMovesCount(): Unit = { 81 | appView.setMovesCount(puzzle.movesDone) 82 | } 83 | 84 | private def bindUiAndModelTiles(tileView: TileView, modelTile: PuzzleTile): Unit = { 85 | modelTile.onPositionChange { shuffling: Boolean => 86 | tileView.moveTileTo(modelTile.currentPosition, animate = !shuffling) 87 | } 88 | modelTile.onVisibilityChange { toVisible => 89 | if (toVisible) { 90 | tileView.makeVisible(animate = modelTile.puzzle.isSolved) 91 | } else { 92 | tileView.makeHidden() 93 | } 94 | } 95 | tileView.onMousePressed { 96 | modelTile.moveToEmptySlot() 97 | } 98 | } 99 | 100 | private def arrowKeyPressed(arrowKey: ArrowKey): Unit = { 101 | import com.sebnozzi.slidingpuzzle.ui.keys.{Down, Left, Right, Up} 102 | val optTileToMove: Option[PuzzleTile] = arrowKey match { 103 | case Up => hiddenTile.tileBelow 104 | case Down => hiddenTile.tileAbove 105 | case Left => hiddenTile.tileRight 106 | case Right => hiddenTile.tileLeft 107 | } 108 | optTileToMove foreach { 109 | _.moveToEmptySlot() 110 | } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sebnozzi/slidingpuzzle/ui/AppView.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui 2 | 3 | import com.sebnozzi.slidingpuzzle.model.structs.GridSize 4 | import com.sebnozzi.slidingpuzzle.ui.keys.ArrowKey 5 | 6 | trait AppView { 7 | 8 | private var shuffleCallback: Option[() => Unit] = None 9 | private var resetCallback: Option[() => Unit] = None 10 | private var newSizeCallback: Option[GridSize => Unit] = None 11 | private var arrowKeyCallback: Option[ArrowKey => Unit] = None 12 | 13 | /** 14 | * Implement. 15 | * Called when setting a new puzzle-view 16 | */ 17 | def setPuzzleView(puzzleView: PuzzleView): Unit 18 | 19 | /** 20 | * Implement. 21 | * Called when the moves count has to be updated 22 | */ 23 | def setMovesCount(newCount: Int): Unit 24 | 25 | /** 26 | * Implement. 27 | * Called when a new grid-size was selected 28 | */ 29 | def selectGridSize(newSize: GridSize): Unit 30 | 31 | /** 32 | * Implement. 33 | * Called when this view should be shown 34 | */ 35 | def show(): Unit 36 | 37 | /** 38 | * Your code should call this 39 | * if "shuffle" clicked on the UI 40 | */ 41 | def shuffleClicked(): Unit = { 42 | shuffleCallback foreach (callback => callback()) 43 | } 44 | 45 | /** 46 | * Your code should call this 47 | * if "reset" clicked on the UI 48 | */ 49 | def resetClicked(): Unit = { 50 | resetCallback foreach (callback => callback()) 51 | } 52 | 53 | /** 54 | * Your code should call this 55 | * if a new size was selected on the UI 56 | */ 57 | def newSizeSelected(newSize: GridSize): Unit = { 58 | newSizeCallback foreach (callback => callback(newSize)) 59 | } 60 | 61 | /** 62 | * Your code should call this 63 | * if a key was pressed on the UI 64 | */ 65 | def arrowKeyPressed(arrowKey: ArrowKey): Unit = { 66 | arrowKeyCallback foreach (callback => callback(arrowKey)) 67 | } 68 | 69 | /** 70 | * Used to set callback (don't modify / call) 71 | */ 72 | def onShuffleClicked(callback: => Unit): Unit = { 73 | shuffleCallback = Some(() => callback) 74 | } 75 | 76 | /** 77 | * Used to set callback (don't modify / call) 78 | */ 79 | def onResetClicked(callback: => Unit): Unit = { 80 | resetCallback = Some(() => callback) 81 | } 82 | 83 | /** 84 | * Used to set callback (don't modify / call) 85 | */ 86 | def onNewSizeSelected(callback: (GridSize) => Unit): Unit = { 87 | newSizeCallback = Some(callback) 88 | } 89 | 90 | /** 91 | * Used to set callback (don't modify / call) 92 | */ 93 | def onArrowKeyPressed(callback: (ArrowKey) => Unit): Unit = { 94 | arrowKeyCallback = Some(callback) 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /core/src/main/scala/com/sebnozzi/slidingpuzzle/ui/PuzzleView.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui 2 | 3 | trait PuzzleView { 4 | def requestFocus(): Unit = {} 5 | def tileViews: Seq[TileView] 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sebnozzi/slidingpuzzle/ui/TileView.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui 2 | 3 | import com.sebnozzi.slidingpuzzle.model.structs.Position 4 | 5 | trait TileView { 6 | 7 | private var onTilePressedCallback: Option[() => Unit] = None 8 | 9 | def mousePressed(): Unit = { 10 | onTilePressedCallback foreach (callback => callback()) 11 | } 12 | 13 | def onMousePressed(callback: => Unit): Unit = { 14 | onTilePressedCallback = Some(() => callback) 15 | } 16 | 17 | def makeVisible(animate: Boolean): Unit 18 | 19 | def makeHidden(): Unit 20 | 21 | def moveTileTo(pos: Position, animate: Boolean = false): Unit 22 | 23 | } -------------------------------------------------------------------------------- /core/src/main/scala/com/sebnozzi/slidingpuzzle/ui/keys/ArrowKey.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui.keys 2 | 3 | sealed abstract class ArrowKey 4 | 5 | case object Up extends ArrowKey 6 | case object Down extends ArrowKey 7 | case object Left extends ArrowKey 8 | case object Right extends ArrowKey 9 | -------------------------------------------------------------------------------- /core/src/test/resources/size-change.feature: -------------------------------------------------------------------------------- 1 | 2 | Feature: Changing game size 3 | 4 | The user might want to change game size, when this happens a new 5 | game should be presented in solved state. 6 | 7 | It's up to the user to shuffle and start playing. 8 | 9 | Background: 10 | Given I am presented with a game in size "3x3" 11 | 12 | Scenario: Initial moves-count 13 | Then the moves counter should be at 0 14 | 15 | Scenario: Changing size to "4x3" 16 | When I change the size to "4x3" 17 | Then I should see a game with 12 tiles 18 | And the game should be in solved state 19 | And the moves counter should be at 0 20 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sebnozzi/slidingpuzzle/model/PuzzleSuite.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model 2 | 3 | import com.sebnozzi.slidingpuzzle.model.structs.{GridSize, Position, Rect} 4 | import org.scalatest.BeforeAndAfter 5 | import org.scalatest.funsuite.AnyFunSuite 6 | 7 | import scala.language.implicitConversions 8 | 9 | class PuzzleSuite extends AnyFunSuite with BeforeAndAfter { 10 | 11 | implicit def tupleToPosition(tuple: (Int, Int)): Position = Position(tuple._1, tuple._2) 12 | 13 | var puzzle4x3: Puzzle = _ 14 | var puzzle2x2: Puzzle = _ 15 | var puzzle3x3: Puzzle = _ 16 | 17 | before { 18 | puzzle4x3 = new Puzzle(GridSize(columns = 4, rows = 3)) 19 | puzzle2x2 = new Puzzle(GridSize(columns = 2, rows = 2)) 20 | puzzle3x3 = new Puzzle(GridSize(columns = 3, rows = 3)) 21 | } 22 | 23 | test("it is possible to define a hidden tile") { 24 | val tile = puzzle4x3.tileAt(4, 3) 25 | puzzle4x3.setHiddenTileAt(4, 3) 26 | assert(puzzle4x3.hiddenTile === tile) 27 | } 28 | 29 | test("moving to empty slot") { 30 | val tile1 = puzzle4x3.tileAt(4, 2) 31 | val tile2 = puzzle4x3.tileAt(4, 3) 32 | tile2.makeHidden() 33 | tile1.moveToEmptySlot() 34 | assert(tile1.currentPosition === Position(4, 3)) 35 | assert(tile2.currentPosition === Position(4, 2)) 36 | assert(puzzle4x3.tileAt(4, 2) === tile2) 37 | assert(puzzle4x3.tileAt(4, 3) === tile1) 38 | } 39 | 40 | test("moving to empty slot when not adjacent to it should do nothing") { 41 | val tile1 = puzzle4x3.tileAt(1, 1) 42 | val tile2 = puzzle4x3.tileAt(4, 3) 43 | puzzle4x3.setHiddenTileAt(4, 3) 44 | val result = tile1.moveToEmptySlot() 45 | assert(result === false) 46 | assert(tile1.currentPosition === Position(1, 1)) 47 | assert(tile2.currentPosition === Position(4, 3)) 48 | } 49 | 50 | test("asking if a puzzle has a hidden tile") { 51 | assert(puzzle4x3.hasHiddenTile === false) 52 | puzzle4x3.setHiddenTileAt(4, 3) 53 | assert(puzzle4x3.hasHiddenTile === true) 54 | } 55 | 56 | test("initially, puzzle is in a solved state") { 57 | assert(puzzle4x3.isSolved) 58 | } 59 | 60 | test("as soon as one move is made, the puzzle is not in solved state") { 61 | val tile1 = puzzle4x3.tileAt(4, 2) 62 | val tile2 = puzzle4x3.tileAt(4, 3) 63 | tile2.makeHidden() 64 | tile1.moveToEmptySlot() 65 | assert(puzzle4x3.isSolved === false) 66 | } 67 | 68 | test("puzzle solved after putting back one moved tile") { 69 | val tile = puzzle4x3.tileAt(4, 2) 70 | val hiddenTile = puzzle4x3.tileAt(4, 3) 71 | hiddenTile.makeHidden() 72 | val firstMoveDone = tile.moveToEmptySlot() 73 | val secondMoveDone = tile.moveToEmptySlot() 74 | assert(firstMoveDone) 75 | assert(secondMoveDone) 76 | assert(puzzle4x3.isSolved) 77 | } 78 | 79 | test("shuffling tiles should result in unsolved state") { 80 | puzzle4x3.tileAt(4, 3).makeHidden() 81 | puzzle4x3.shuffle() 82 | assume(puzzle4x3.isSolved === false) 83 | } 84 | 85 | test("resetting the puzzle should leave it in solved state") { 86 | puzzle4x3.tileAt(4, 3).makeHidden() 87 | puzzle4x3.shuffle() 88 | puzzle4x3.reset() 89 | assert(puzzle4x3.isSolved) 90 | } 91 | 92 | test("callback when puzzle solved") { 93 | var called = false 94 | val tile = puzzle4x3.tileAt(4, 2) 95 | puzzle4x3.tileAt(4, 3).makeHidden() 96 | tile.moveToEmptySlot() 97 | puzzle4x3.onSolved { 98 | called = true 99 | } 100 | tile.moveToEmptySlot() // make the winning move 101 | assert(called) 102 | } 103 | 104 | test("initially, moves are at 0") { 105 | assert(puzzle4x3.movesDone === 0) 106 | } 107 | 108 | test("after one move is made, the counter should reflect it") { 109 | val tile = puzzle4x3.tileAt(4, 2) 110 | puzzle4x3.tileAt(4, 3).makeHidden() 111 | tile.moveToEmptySlot() 112 | assert(puzzle4x3.movesDone === 1) 113 | } 114 | 115 | test("shuffling resets the amount of moves") { 116 | val tile = puzzle4x3.tileAt(4, 2) 117 | puzzle4x3.tileAt(4, 3).makeHidden() 118 | tile.moveToEmptySlot() 119 | puzzle4x3.shuffle() 120 | assert(puzzle4x3.movesDone === 0) 121 | } 122 | 123 | test("callback when move-count changes") { 124 | var calls = 0 125 | val tile = puzzle4x3.tileAt(4, 2) 126 | puzzle4x3.tileAt(4, 3).makeHidden() 127 | puzzle4x3.onMovesCountChange { 128 | calls += 1 129 | } 130 | tile.moveToEmptySlot() 131 | puzzle4x3.reset() 132 | assert(calls === 2) 133 | } 134 | 135 | test("moves go back to 0 after reset") { 136 | val tile = puzzle4x3.tileAt(4, 2) 137 | puzzle4x3.tileAt(4, 3).makeHidden() 138 | tile.moveToEmptySlot() 139 | puzzle4x3.reset() 140 | assert(puzzle4x3.movesDone === 0) 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sebnozzi/slidingpuzzle/model/PuzzleTileSuite.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model 2 | 3 | import com.sebnozzi.slidingpuzzle.model.structs.GridSize 4 | import org.scalatest.funsuite.AnyFunSuite 5 | import org.scalatest.BeforeAndAfter 6 | 7 | class PuzzleTileSuite extends AnyFunSuite with BeforeAndAfter { 8 | 9 | var puzzle4x3: Puzzle = _ 10 | 11 | before { 12 | puzzle4x3 = new Puzzle(GridSize(columns = 4, rows = 3)) 13 | } 14 | 15 | test("a puzzle-tile knows its puzzle") { 16 | puzzle4x3.tiles.foreach { tile => assert(tile.puzzle === puzzle4x3) } 17 | } 18 | 19 | test("moving to empty slot, when no tile is hidden, should be false") { 20 | val tile1 = puzzle4x3.tileAt(3, 3) 21 | val tile2 = puzzle4x3.tileAt(4, 3) 22 | assert(tile1.canMoveToEmptySlot === false) 23 | } 24 | 25 | test("making a tile hidden") { 26 | val tile1 = puzzle4x3.tileAt(1, 1) 27 | tile1.makeHidden() 28 | assert(puzzle4x3.hiddenTile === tile1) 29 | } 30 | 31 | test("unhiding the tile") { 32 | assert(puzzle4x3.hasHiddenTile === false) 33 | val tile = puzzle4x3.tileAt(1, 1) 34 | tile.makeHidden() 35 | assert(puzzle4x3.hasHiddenTile) 36 | tile.makeVisible() 37 | assert(puzzle4x3.hasHiddenTile === false) 38 | } 39 | 40 | test("a tile non-adjacent to the hidden one can not be moved") { 41 | val tile1 = puzzle4x3.tileAt(1, 1) 42 | val tile2 = puzzle4x3.tileAt(4, 3) 43 | puzzle4x3.setHiddenTileAt(4, 3) 44 | assert(tile1.canMoveToEmptySlot === false) 45 | } 46 | 47 | test("tile notifies puzzle on every move") { 48 | var called = false 49 | val puzzle = new Puzzle(GridSize(columns = 4, rows = 3)) { 50 | override def didMoveToEmptySlot(tile: PuzzleTile): Unit = { 51 | super.didMoveToEmptySlot(tile) 52 | called = true 53 | } 54 | } 55 | puzzle.tileAt(4, 3).makeHidden() 56 | puzzle.tileAt(4, 2).moveToEmptySlot() 57 | assert(called) 58 | } 59 | 60 | test("tile notifies puzzle on every move to empty slot") { 61 | var calls = 0 62 | val puzzle = new Puzzle(GridSize(columns = 4, rows = 3)) { 63 | override def didMoveToEmptySlot(tile: PuzzleTile): Unit = { 64 | super.didMoveToEmptySlot(tile) 65 | calls += 1 66 | } 67 | } 68 | puzzle.tileAt(4, 3).makeHidden() 69 | puzzle.tileAt(4, 2).moveToEmptySlot() 70 | puzzle.reset() 71 | assert(calls === 1) 72 | } 73 | 74 | test("setting another tile as hidden unsets the previous one") { 75 | puzzle4x3.tileAt(4, 2).makeHidden() 76 | puzzle4x3.tileAt(4, 3).makeHidden() 77 | assert(puzzle4x3.hiddenTile === puzzle4x3.tileAt(4, 3)) 78 | assert(puzzle4x3.hiddenTile != puzzle4x3.tileAt(4, 2)) 79 | } 80 | 81 | test("tile is informed on visibility change (only real changes)") { 82 | var calls = 0 83 | val tile = puzzle4x3.tileAt(4, 2) 84 | tile.onVisibilityChange { toVisible => 85 | calls += 1 86 | } 87 | tile.makeHidden() 88 | tile.makeHidden() 89 | tile.makeVisible() 90 | tile.makeVisible() 91 | 92 | assert(calls === 2) 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sebnozzi/slidingpuzzle/model/structs/PositionSuite.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model.structs 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | 6 | class PositionSuite extends AnyFunSuite { 7 | 8 | test("a Position consists of column and row") { 9 | val pos1 = Position(col = 1, row = 1) 10 | val pos2 = Position(col = 2, row = 3) 11 | assert(pos1.col === 1) 12 | assert(pos1.row === 1) 13 | assert(pos2.col === 2) 14 | assert(pos2.row === 3) 15 | } 16 | 17 | test("adjacent positions of 1,1 (top left corner)") { 18 | val pos = Position(col = 1, row = 1) 19 | val rect = Rect(Position(1, 1), Position(4, 3)) 20 | assert(pos.adjacentPositionsWithin(rect) === List(Position(2, 1), Position(1, 2))) 21 | } 22 | 23 | test("adjacent positions of 4,3 (bottom right corner)") { 24 | val pos = Position(col = 4, row = 3) 25 | val rect = Rect(Position(1, 1), Position(4, 3)) 26 | assert(pos.adjacentPositionsWithin(rect) === List(Position(4, 2), Position(3, 3))) 27 | } 28 | 29 | test("named adjacent positions of 1,1") { 30 | val pos = Position(col = 1, row = 1) 31 | val rect = Rect(Position(1, 1), Position(4, 3)) 32 | assert(pos.aboveWithin(rect) === None) 33 | assert(pos.belowWithin(rect) === Some(Position(1, 2))) 34 | assert(pos.leftWithin(rect) === None) 35 | assert(pos.rightWithin(rect) === Some(Position(2, 1))) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sebnozzi/slidingpuzzle/model/structs/RectSuite.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model.structs 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | 6 | class RectSuite extends AnyFunSuite { 7 | 8 | test("if within rect") { 9 | val pos1 = Position(col = 1, row = 1) 10 | val pos2 = Position(col = 2, row = 2) 11 | val pos3 = Position(col = 2, row = 3) 12 | val pos4 = Position(col = 4, row = 3) 13 | val rect = Rect(topLeft = Position(1, 1), bottomRight = Position(2, 2)) 14 | 15 | assert(rect.contains(pos1)) 16 | assert(rect.contains(pos2)) 17 | assert(!rect.contains(pos3)) 18 | assert(!rect.contains(pos4)) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sebnozzi/slidingpuzzle/model/tile/traits/AdjacentTilesAwareSuite.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model.tile.traits 2 | 3 | import com.sebnozzi.slidingpuzzle.model.Puzzle 4 | import com.sebnozzi.slidingpuzzle.model.structs.{GridSize, Position} 5 | import org.scalatest.funsuite.AnyFunSuite 6 | import org.scalatest.BeforeAndAfter 7 | 8 | class AdjacentTilesAwareSuite extends AnyFunSuite with BeforeAndAfter { 9 | 10 | var puzzle4x3: Puzzle = _ 11 | var puzzle2x2: Puzzle = _ 12 | var puzzle3x3: Puzzle = _ 13 | 14 | before { 15 | puzzle4x3 = new Puzzle(GridSize(columns = 4, rows = 3)) 16 | puzzle2x2 = new Puzzle(GridSize(columns = 2, rows = 2)) 17 | puzzle3x3 = new Puzzle(GridSize(columns = 3, rows = 3)) 18 | } 19 | 20 | test("a tile knows its adjacent tiles") { 21 | val topLeft = puzzle2x2.tileAt(1, 1) 22 | val topRight = puzzle2x2.tileAt(2, 1) 23 | val bottomLeft = puzzle2x2.tileAt(1, 2) 24 | val bottomRight = puzzle2x2.tileAt(2, 2) 25 | assert(topLeft.adjacentTiles === List(topRight, bottomLeft)) 26 | assert(topRight.adjacentTiles === List(topLeft, bottomRight)) 27 | } 28 | 29 | test("a tile knows directional adjacent tiles") { 30 | val tile = puzzle3x3.tileAt(1, 2) 31 | assert(tile.tileAbove.get.initialPosition === Position(1, 1)) 32 | assert(tile.tileRight.get.initialPosition === Position(2, 2)) 33 | assert(tile.tileBelow.get.initialPosition === Position(1, 3)) 34 | assert(tile.tileLeft === None) 35 | } 36 | 37 | test("asking for a random adjacent tile") { 38 | val tile = puzzle4x3.tileAt(2, 2) 39 | val possibleTiles = List( 40 | puzzle4x3.tileAt(3, 2), 41 | puzzle4x3.tileAt(1, 2), 42 | puzzle4x3.tileAt(2, 1), 43 | puzzle4x3.tileAt(2, 3)) 44 | (1 to 100).foreach { _ => assert(possibleTiles.contains(tile.randomAdjacentTile)) } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sebnozzi/slidingpuzzle/model/tile/traits/PositionAwareSuite.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model.tile.traits 2 | 3 | import com.sebnozzi.slidingpuzzle.model.structs.Position 4 | import org.scalatest.BeforeAndAfter 5 | import org.scalatest.funsuite.AnyFunSuite 6 | 7 | class PositionAwareSuite extends AnyFunSuite with BeforeAndAfter { 8 | 9 | class TestTile(val initialPosition: Position) extends PositionAware {} 10 | 11 | var tile1: TestTile = _ 12 | var tile2: TestTile = _ 13 | 14 | before { 15 | tile1 = new TestTile(Position(1, 1)) 16 | tile2 = new TestTile(Position(4, 3)) 17 | } 18 | 19 | test("a tile has an original position") { 20 | assert(tile1.initialPosition === Position(1, 1)) 21 | assert(tile2.initialPosition === Position(4, 3)) 22 | } 23 | 24 | test("a tile's current position is initially the initial position") { 25 | assert(tile1.currentPosition === tile1.initialPosition) 26 | } 27 | 28 | test("changing a tile's current position") { 29 | tile1.currentPosition = Position(2, 2) 30 | assert(tile1.currentPosition != tile1.initialPosition) 31 | } 32 | 33 | test("swapping positions between two tiles") { 34 | tile1.swapPositionWith(tile2) 35 | assert(tile1.currentPosition === tile2.initialPosition) 36 | assert(tile2.currentPosition === tile1.initialPosition) 37 | } 38 | 39 | def testShufflingState(expectedState: Boolean): Unit = { 40 | var wasShuffling1 = false 41 | var wasShuffling2 = false 42 | tile1.onPositionChange(shuffling => { wasShuffling1 = shuffling }) 43 | tile2.onPositionChange(shuffling => { wasShuffling2 = shuffling }) 44 | tile1.swapPositionWith(tile2, shuffling = expectedState) 45 | assert(wasShuffling1 === expectedState) 46 | assert(wasShuffling2 === expectedState) 47 | } 48 | test("should passe shuffling state (true) to callback") { 49 | testShufflingState(expectedState=true) 50 | } 51 | test("should passe shuffling state (false) to callback") { 52 | testShufflingState(expectedState=false) 53 | } 54 | 55 | test("after swapping once, the tile should not be at its initial position") { 56 | assert(tile1.isAtInitialPosition) 57 | tile1.swapPositionWith(tile2) 58 | assert(tile1.isAtInitialPosition === false) 59 | } 60 | 61 | test("ask tile to go back to initial position") { 62 | tile1.swapPositionWith(tile2) 63 | tile1.moveToInitialPosition() 64 | tile2.moveToInitialPosition() 65 | 66 | assert(tile1.isAtInitialPosition) 67 | assert(tile2.isAtInitialPosition) 68 | } 69 | 70 | test("callback called when tile swapped") { 71 | var tileMoved = false 72 | tile1.onPositionChange { shuffling: Boolean => 73 | tileMoved = true 74 | } 75 | tile1.swapPositionWith(tile2) 76 | assert(tileMoved) 77 | } 78 | 79 | test("callback called when tile moved to initial position") { 80 | var tileMoved = false 81 | tile1.swapPositionWith(tile2) 82 | assert(!tileMoved, "callback should not have been called yet") 83 | tile1.onPositionChange { shuffling: Boolean => 84 | tileMoved = true 85 | } 86 | tile1.moveToInitialPosition() 87 | assert(tileMoved, "callback should have been called after moving to initial position") 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sebnozzi/slidingpuzzle/model/tile/traits/VisibilityAwareSuite.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.model.tile.traits 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | 6 | class VisibilityAwareSuite extends AnyFunSuite { 7 | 8 | test("callback when visibility changes") { 9 | var calls = 0 10 | val tile = new VisibilityAware() {} 11 | tile.onVisibilityChange { toVisible => 12 | calls += 1 13 | } 14 | tile.visibilityChanged(toVisible=true) 15 | tile.visibilityChanged(toVisible=false) 16 | assert(calls === 2) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /core/src/test/scala/com/sebnozzi/slidingpuzzle/ui/AppControllerSuite.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui 2 | 3 | import com.sebnozzi.slidingpuzzle.model.structs.GridSize 4 | import org.scalatest.funsuite.AnyFunSuite 5 | 6 | class AppControllerSuite extends AnyFunSuite { 7 | 8 | def mockAppView: AppView = new AppView() { 9 | override def setPuzzleView(puzzleView: PuzzleView): Unit = {} 10 | override def setMovesCount(newCount: Int): Unit = {} 11 | override def selectGridSize(newSize: GridSize): Unit = {} 12 | override def show(): Unit = {} 13 | } 14 | 15 | def mockPuzzleView: PuzzleView = new PuzzleView { 16 | override def requestFocus(): Unit = {} 17 | override val tileViews: List[TileView] = List[TileView]() 18 | } 19 | 20 | test("AppView is created") { 21 | var called = false 22 | val appController = new AppController() { 23 | override def createAppView(): AppView = { 24 | called = true 25 | mockAppView 26 | } 27 | override def createPuzzleView(gridSize: GridSize): PuzzleView = { 28 | mockPuzzleView 29 | } 30 | } 31 | appController.start() 32 | assert(called, "Should have created app-view") 33 | } 34 | 35 | test("PuzzleView is created") { 36 | var called = false 37 | val appController = new AppController() { 38 | override def createAppView(): AppView = { 39 | mockAppView 40 | } 41 | override def createPuzzleView(gridSize: GridSize): PuzzleView = { 42 | called = true 43 | mockPuzzleView 44 | } 45 | } 46 | appController.start() 47 | assert(called, "Should have created puzzle-view") 48 | } 49 | 50 | test("new puzzle is created after size change, hooks work") { 51 | pending 52 | } 53 | 54 | test("hidden tile becomes visible after puzzle won") { 55 | pending 56 | } 57 | 58 | test("shuffling works") { 59 | pending 60 | } 61 | 62 | test("shuffling does not move tiles with animation") { 63 | pending 64 | } 65 | 66 | test("reset works") { 67 | pending 68 | } 69 | 70 | test("update on move-count works") { 71 | pending 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sebnozzi/slidingpuzzle/ui/AppViewSuite.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui 2 | 3 | import com.sebnozzi.slidingpuzzle.model.structs.GridSize 4 | import com.sebnozzi.slidingpuzzle.ui.keys.{ArrowKey, Down, Left, Right, Up} 5 | import org.scalatest.funsuite.AnyFunSuite 6 | import org.scalatest.BeforeAndAfter 7 | 8 | class AppViewSuite extends AnyFunSuite with BeforeAndAfter { 9 | 10 | class TestAppView extends AppView { 11 | var hasPuzzleView = false 12 | def setPuzzleView(puzzleView: PuzzleView): Unit = { 13 | hasPuzzleView = true 14 | } 15 | def selectGridSize(gridSize: GridSize): Unit = {} 16 | def setMovesCount(newCount: Int): Unit = {} 17 | def show(): Unit = {} 18 | } 19 | 20 | var appView: TestAppView = _ 21 | 22 | before { 23 | appView = new TestAppView() 24 | } 25 | 26 | test("handles shuffle click") { 27 | var called = false 28 | appView.onShuffleClicked { 29 | called = true 30 | } 31 | appView.shuffleClicked() 32 | assert(called, "was not called") 33 | } 34 | 35 | test("handles shuffle click when no callback") { 36 | appView.shuffleClicked() 37 | } 38 | 39 | test("handles reset click") { 40 | var called = false 41 | appView.onResetClicked { 42 | called = true 43 | } 44 | appView.resetClicked() 45 | assert(called, "was not called") 46 | } 47 | 48 | test("handles reset click when no callback") { 49 | appView.resetClicked() 50 | } 51 | 52 | test("handles size change") { 53 | var called = false 54 | val newSize = GridSize(columns = 4, rows = 3) 55 | appView.onNewSizeSelected { newSizeSelected => 56 | assert(newSizeSelected === newSize) 57 | called = true 58 | } 59 | appView.newSizeSelected(newSize) 60 | assert(called, "was not called") 61 | } 62 | 63 | test("handles size change when no callback") { 64 | val newSize = GridSize(columns = 4, rows = 3) 65 | appView.newSizeSelected(newSize) 66 | } 67 | 68 | def assertKeyPressed(key: ArrowKey): Unit = { 69 | var pressed = false 70 | appView.onArrowKeyPressed { arrowKey => 71 | if (arrowKey == key) 72 | pressed = true 73 | } 74 | appView.arrowKeyPressed(key) 75 | assert(pressed, "key was not handled: " + key) 76 | } 77 | 78 | test("handles arrow keys, with no callback") { 79 | appView.arrowKeyPressed(Up) 80 | } 81 | 82 | test("handles arrow keys") { 83 | assertKeyPressed(Up) 84 | assertKeyPressed(Down) 85 | assertKeyPressed(Left) 86 | assertKeyPressed(Right) 87 | } 88 | 89 | test("can set the moves-count") { 90 | appView.setMovesCount(2) 91 | } 92 | 93 | test("can set new grid size") { 94 | appView.selectGridSize(GridSize(4, 3)) 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sebnozzi/slidingpuzzle/ui/PuzzleViewSuite.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui 2 | 3 | import org.scalatest.BeforeAndAfter 4 | import org.scalatest.funsuite.AnyFunSuite 5 | 6 | class PuzzleViewSuite extends AnyFunSuite with BeforeAndAfter { 7 | 8 | var view: PuzzleView = _ 9 | 10 | before { 11 | view = new PuzzleView() { 12 | val tileViews: List[TileView] = { 13 | List(new TestTileView, new TestTileView, new TestTileView) 14 | } 15 | // 16 | } 17 | } 18 | 19 | test("requests focus") { 20 | view.requestFocus() 21 | } 22 | 23 | test("getting tile views") { 24 | val tiles: Seq[TileView] = view.tileViews 25 | } 26 | 27 | ignore("amount of tiles") { 28 | val tiles: Seq[TileView] = view.tileViews 29 | assert(tiles.size === 9) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sebnozzi/slidingpuzzle/ui/TestTileView.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui 2 | 3 | import com.sebnozzi.slidingpuzzle.model.structs.Position 4 | 5 | class TestTileView extends TileView { 6 | 7 | def makeVisible(animate: Boolean): Unit = {} 8 | def makeHidden(): Unit = {} 9 | def moveTileTo(pos: Position, animate: Boolean = false): Unit = {} 10 | 11 | } -------------------------------------------------------------------------------- /core/src/test/scala/com/sebnozzi/slidingpuzzle/ui/TileViewSuite.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui 2 | 3 | import com.sebnozzi.slidingpuzzle.model.structs.Position 4 | import org.scalatest.funsuite.AnyFunSuite 5 | 6 | class TileViewSuite extends AnyFunSuite { 7 | 8 | val view: TileView = new TileView() { 9 | def makeVisible(animate: Boolean): Unit = {} 10 | def makeHidden(): Unit = {} 11 | def moveTileTo(pos: Position, animate: Boolean = false): Unit = {} 12 | } 13 | 14 | test("on mouse pressed callback") { 15 | var called = false 16 | view.onMousePressed { 17 | called = true 18 | } 19 | view.mousePressed() 20 | assert(called) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /images/original_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebnozzi/sliding-puzzle/6dadde44d909ea2ae78740393edd50a299739657/images/original_small.jpg -------------------------------------------------------------------------------- /images/screenshot-browser.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebnozzi/sliding-puzzle/6dadde44d909ea2ae78740393edd50a299739657/images/screenshot-browser.jpeg -------------------------------------------------------------------------------- /images/screenshot-javafx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebnozzi/sliding-puzzle/6dadde44d909ea2ae78740393edd50a299739657/images/screenshot-javafx.jpg -------------------------------------------------------------------------------- /javafx/src/main/resources/2322324186_ca41fba641_o.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebnozzi/sliding-puzzle/6dadde44d909ea2ae78740393edd50a299739657/javafx/src/main/resources/2322324186_ca41fba641_o.jpg -------------------------------------------------------------------------------- /javafx/src/main/scala/com/sebnozzi/slidingpuzzle/SlidingPuzzleMain.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle 2 | 3 | import _root_.javafx.application.Application 4 | import _root_.javafx.stage.Stage 5 | 6 | import com.sebnozzi.slidingpuzzle.ui.javafx.JFXAppController 7 | 8 | object SlidingPuzzleMain extends App { 9 | import Application._ 10 | launch(classOf[SlidingPuzzleJFXApp], args:_*) 11 | } 12 | 13 | class SlidingPuzzleJFXApp extends Application { 14 | override def start(mainWindow: Stage): Unit = { 15 | val appController = new JFXAppController(mainWindow) 16 | appController.start() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /javafx/src/main/scala/com/sebnozzi/slidingpuzzle/ui/javafx/ControlPanel.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui.javafx 2 | 3 | import javafx.beans.value.{ChangeListener, ObservableValue} 4 | import javafx.collections.FXCollections 5 | import javafx.event.{ActionEvent, EventHandler} 6 | import javafx.scene.control.{Button, ChoiceBox, Label, ToolBar} 7 | import javafx.util.StringConverter 8 | 9 | import com.sebnozzi.slidingpuzzle.model.structs.GridSize 10 | 11 | class ControlPanel extends ToolBar { 12 | 13 | // TODO: repeated in ScalaJS 14 | val gridSizes = List( 15 | GridSize(3, 2), 16 | GridSize(3, 3), 17 | GridSize(4, 3), 18 | GridSize(6, 4)) 19 | 20 | private val shuffleButton = new Button("Shuffle") 21 | private val resetButton = new Button("Reset") 22 | private val movesLabel = new Label(movesMsg(0)) 23 | private val sizeSelector = new ChoiceBox[GridSize]() 24 | 25 | private var sizeChangeCallback: Option[GridSize => Unit] = None 26 | 27 | setup() 28 | 29 | def onShufflePressed(callback: => Unit): Unit = 30 | addButtonHandler(shuffleButton) { callback } 31 | 32 | def onResetPressed(callback: => Unit): Unit = 33 | addButtonHandler(resetButton) { callback } 34 | 35 | def onSizeChange(callback: GridSize => Unit): Unit = { 36 | sizeChangeCallback = Some(callback) 37 | } 38 | 39 | def setMovesCount(count: Int): Unit = { 40 | movesLabel.setText(movesMsg(count)) 41 | } 42 | 43 | def selectGridSize(gridSize: GridSize): Unit = { 44 | val selectionModel = sizeSelector.getSelectionModel 45 | selectionModel.select(gridSize) 46 | } 47 | 48 | private def movesMsg(count: Int): String = s"Moves: $count" 49 | 50 | private def setup(): Unit = { 51 | ControlPanel.this.getItems.add(sizeSelector) 52 | ControlPanel.this.getItems.add(shuffleButton) 53 | ControlPanel.this.getItems.add(resetButton) 54 | ControlPanel.this.getItems.add(movesLabel) 55 | 56 | sizeSelector.setItems(FXCollections.observableArrayList[GridSize](gridSizes: _*)) 57 | val selectionModel = sizeSelector.getSelectionModel 58 | selectionModel.selectedIndexProperty().addListener(new ChangeListener[Number]() { 59 | def changed(ov: ObservableValue[_ <: Number], 60 | oldValue: Number, newValue: Number): Unit = { 61 | val newSize: GridSize = sizeSelector.getItems.get(newValue.intValue()) 62 | sizeChanged(newSize) 63 | } 64 | }) 65 | sizeSelector.setConverter(new StringConverter[GridSize]() { 66 | def fromString(str: String): GridSize = ??? 67 | def toString(size: GridSize): String = "%dx%d".format(size.columns, size.rows) 68 | }) 69 | } 70 | 71 | private def sizeChanged(newSize: GridSize): Unit = { 72 | if (sizeChangeCallback.isDefined) 73 | sizeChangeCallback.get.apply(newSize) 74 | } 75 | 76 | private def addButtonHandler(button: Button)(block: => Unit): Unit = { 77 | button.setOnAction(new EventHandler[ActionEvent]() { 78 | override def handle(event: ActionEvent): Unit = { 79 | block 80 | } 81 | }) 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /javafx/src/main/scala/com/sebnozzi/slidingpuzzle/ui/javafx/JFXAppController.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui.javafx 2 | 3 | import javafx.scene.image.Image 4 | import javafx.stage.Stage 5 | 6 | import com.sebnozzi.slidingpuzzle.model.structs.GridSize 7 | import com.sebnozzi.slidingpuzzle.ui.{AppController, AppView, PuzzleView} 8 | 9 | class JFXAppController(mainWindow: Stage) extends AppController { 10 | 11 | lazy val img: Image = { 12 | val inputStream = this.getClass.getResourceAsStream("/2322324186_ca41fba641_o.jpg") 13 | new Image(inputStream) 14 | } 15 | 16 | override def createAppView(): AppView = { 17 | new JFXAppView(mainWindow) 18 | } 19 | 20 | override def createPuzzleView(gridSize: GridSize): PuzzleView = { 21 | new JFXPuzzleView(img, gridSize) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /javafx/src/main/scala/com/sebnozzi/slidingpuzzle/ui/javafx/JFXAppView.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui.javafx 2 | 3 | import javafx.application.Platform 4 | import javafx.event.EventHandler 5 | import javafx.scene.{Group, Parent, Scene} 6 | import javafx.scene.input.{KeyCode, KeyEvent} 7 | import javafx.scene.layout.VBox 8 | import javafx.scene.paint.Color 9 | import javafx.stage.{Stage, WindowEvent} 10 | 11 | import com.sebnozzi.slidingpuzzle.model.structs.GridSize 12 | import com.sebnozzi.slidingpuzzle.ui.{AppView, PuzzleView} 13 | import com.sebnozzi.slidingpuzzle.ui.keys.{Down, Left, Right, Up} 14 | 15 | class JFXAppView(window: Stage) extends AppView { 16 | 17 | private val tilesBoardContainer = new Group 18 | private var _controlPanel: ControlPanel = _ 19 | 20 | init() 21 | 22 | private def init(): Unit = { 23 | val mainGroup = new VBox() 24 | _controlPanel = new ControlPanel() 25 | 26 | mainGroup.getChildren.add(_controlPanel) 27 | mainGroup.getChildren.add(tilesBoardContainer) 28 | 29 | _controlPanel.onResetPressed { 30 | this.resetClicked() 31 | } 32 | 33 | _controlPanel.onShufflePressed { 34 | this.shuffleClicked() 35 | } 36 | 37 | _controlPanel.onSizeChange { newSize => 38 | this.newSizeSelected(newSize) 39 | } 40 | 41 | tilesBoardContainer.setOnKeyPressed(new EventHandler[KeyEvent] { 42 | def handle(event: KeyEvent): Unit = { 43 | val maybeMatch = event.getCode match { 44 | case KeyCode.UP => Some(Up) 45 | case KeyCode.DOWN => Some(Down) 46 | case KeyCode.LEFT => Some(Left) 47 | case KeyCode.RIGHT => Some(Right) 48 | case _ => None 49 | } 50 | maybeMatch.foreach { arrowKey => 51 | arrowKeyPressed(arrowKey) 52 | event.consume() 53 | } 54 | } 55 | }) 56 | 57 | setupWithGroup(mainGroup) 58 | } 59 | 60 | def setMovesCount(newCount: Int): Unit = { 61 | _controlPanel.setMovesCount(newCount) 62 | } 63 | 64 | def selectGridSize(newSize: GridSize): Unit = { 65 | _controlPanel.selectGridSize(newSize) 66 | } 67 | 68 | def setPuzzleView(puzzleView: PuzzleView): Unit = { 69 | puzzleView match { 70 | case jfxView: JFXPuzzleView => setPuzzleView(jfxView) 71 | } 72 | } 73 | 74 | def setPuzzleView(puzzleView: JFXPuzzleView): Unit = { 75 | val grpChildren = tilesBoardContainer.getChildren 76 | grpChildren.clear() 77 | grpChildren.add(puzzleView) 78 | } 79 | 80 | def show(): Unit = { 81 | window.show() 82 | } 83 | 84 | private def setupWithGroup(mainGroup: Parent): Unit = { 85 | val scene = new Scene(mainGroup) 86 | scene.setFill(Color.BLACK) 87 | window.setScene(scene) 88 | 89 | window.setTitle("Sliding Puzzle") 90 | window.setX(100) 91 | window.setY(100) 92 | window.setMinWidth(840) 93 | window.setMinHeight(560) 94 | window.setResizable(false) 95 | window.setFullScreen(false) 96 | 97 | window.setOnCloseRequest(new EventHandler[WindowEvent] { 98 | def handle(event: WindowEvent): Unit = { 99 | Platform.exit() 100 | } 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /javafx/src/main/scala/com/sebnozzi/slidingpuzzle/ui/javafx/JFXPuzzleView.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui.javafx 2 | 3 | import javafx.scene.Group 4 | import javafx.scene.image.Image 5 | import com.sebnozzi.slidingpuzzle.model.structs.{GridSize, Position} 6 | import com.sebnozzi.slidingpuzzle.ui.{PuzzleView, TileView} 7 | import com.sebnozzi.slidingpuzzle.ui.javafx.utils.ImageSlicer 8 | 9 | class JFXPuzzleView(img: Image, gridSize: GridSize) extends Group with PuzzleView { 10 | 11 | private val slicer = new ImageSlicer(img, gridSize) 12 | 13 | val tileWidth: Double = slicer.sliceWidth 14 | val tileHeight: Double = slicer.sliceHeight 15 | 16 | addSliceNodes() 17 | 18 | lazy val tileViews: Seq[JFXTileView] = makeTiles() 19 | 20 | override def requestFocus(): Unit = { 21 | super[Group].requestFocus() 22 | } 23 | 24 | private def addSliceNodes(): Unit = { 25 | tileViews.foreach { this.getChildren.add(_) } 26 | } 27 | 28 | private def makeTiles(): Seq[JFXTileView] = { 29 | slicer.slicePositions.map { 30 | case (col, row) => 31 | val imgSlice = slicer.sliceAt(col, row) 32 | new JFXTileView(imgSlice, Position(col, row)) 33 | }.toSeq 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /javafx/src/main/scala/com/sebnozzi/slidingpuzzle/ui/javafx/JFXTileView.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui.javafx 2 | 3 | import javafx.animation.{FadeTransition, TranslateTransition} 4 | import javafx.event.EventHandler 5 | import javafx.scene.Group 6 | import javafx.scene.canvas.Canvas 7 | import javafx.scene.input.MouseEvent 8 | import javafx.util.Duration 9 | 10 | import com.sebnozzi.slidingpuzzle.model.structs.Position 11 | import com.sebnozzi.slidingpuzzle.ui.TileView 12 | 13 | class JFXTileView(imgSlice: Canvas, pos: Position) extends Group with TileView { 14 | 15 | val animationDurationMs = 200 16 | 17 | val initialPosition: Position = pos 18 | val tileWidth: Double = imgSlice.getWidth 19 | val tileHeight: Double = imgSlice.getHeight 20 | 21 | this.getChildren.add(imgSlice) 22 | 23 | setupTranslation() 24 | drawBorders(imgSlice) 25 | setupEventHandler() 26 | 27 | def moveToInitialPosition(): Unit = { 28 | moveTileTo(initialPosition, animate = false) 29 | } 30 | 31 | def moveTileTo(pos: Position, animate: Boolean = true): Unit = { 32 | val destX = (pos.col - 1) * tileWidth 33 | val destY = (pos.row - 1) * tileHeight 34 | 35 | if (animate) { 36 | animateTo(destX, destY) 37 | } else { 38 | translateTo(destX, destY) 39 | } 40 | } 41 | 42 | def makeHidden(): Unit = { 43 | this.setOpacity(0.0) 44 | } 45 | 46 | def makeVisible(animate: Boolean = false): Unit = { 47 | if (animate) { 48 | val toVisibleTransition = new FadeTransition(Duration.seconds(0.3), this) 49 | toVisibleTransition.setDelay(Duration.seconds(0.4)) 50 | toVisibleTransition.setFromValue(0.0) 51 | toVisibleTransition.setToValue(1.0) 52 | toVisibleTransition.play() 53 | } else { 54 | setOpacity(1.0) 55 | } 56 | } 57 | 58 | def animateTo(x: Double, y: Double): Unit = { 59 | val translateTransition = new TranslateTransition(Duration.millis(animationDurationMs), this) 60 | translateTransition.setToX(x) 61 | translateTransition.setToY(y) 62 | translateTransition.play() 63 | } 64 | 65 | def translateTo(x: Double, y: Double): Unit = { 66 | setTranslateX(x) 67 | setTranslateY(y) 68 | } 69 | 70 | private def setupEventHandler(): Unit = { 71 | imgSlice.setOnMousePressed(new EventHandler[MouseEvent]() { 72 | def handle(event: MouseEvent): Unit = { 73 | mousePressed() 74 | } 75 | }) 76 | } 77 | 78 | private def setupTranslation(): Unit = { 79 | val xCoord = (pos.col - 1) * tileWidth 80 | val yCoord = (pos.row - 1) * tileHeight 81 | this.setTranslateX(xCoord) 82 | this.setTranslateY(yCoord) 83 | } 84 | 85 | private def drawBorders(slice: Canvas): Unit = { 86 | val gc = slice.getGraphicsContext2D 87 | gc.beginPath() 88 | gc.rect(0, 0, slice.getWidth, slice.getHeight) 89 | gc.closePath() 90 | gc.stroke() 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /javafx/src/main/scala/com/sebnozzi/slidingpuzzle/ui/javafx/utils/ImageSlicer.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui.javafx.utils 2 | 3 | import javafx.scene.canvas.Canvas 4 | import javafx.scene.image.Image 5 | 6 | import com.sebnozzi.slidingpuzzle.model.structs.GridSize 7 | 8 | class ImageSlicer(val img: Image, gridSize: GridSize) { 9 | 10 | val sliceWidth: Double = img.getWidth / gridSize.columns 11 | val sliceHeight: Double = img.getHeight / gridSize.rows 12 | 13 | val slicePositions: List[(Int, Int)] = { 14 | (for ( 15 | yPos <- 1 to gridSize.rows; 16 | xPos <- 1 to gridSize.columns 17 | ) yield (xPos, yPos)).toList 18 | } 19 | 20 | lazy val allSlices: Seq[Canvas] = { 21 | slicePositions.map { case (xPos, yPos) => makeSliceAt(xPos, yPos) } 22 | } 23 | 24 | def sliceAt(x: Int, y: Int): Canvas = { 25 | val sliceIdx = sliceIndexFor(x, y) 26 | allSlices(sliceIdx) 27 | } 28 | 29 | def coordinatesOfSliceAt(x: Int, y: Int): (Int, Int) = { 30 | val coordX = ((x - 1) * sliceWidth).toInt 31 | val coordY = ((y - 1) * sliceHeight).toInt 32 | (coordX, coordY) 33 | } 34 | 35 | def sliceIndexFor(x: Int, y: Int): Int = { 36 | (x - 1) + (y - 1) * gridSize.columns 37 | } 38 | 39 | private def makeSliceAt(x: Int, y: Int): Canvas = { 40 | val canvas = new Canvas(sliceWidth, sliceHeight) 41 | val grContext = canvas.getGraphicsContext2D 42 | 43 | val (sourceX, sourceY) = coordinatesOfSliceAt(x, y) 44 | val (destX, destY) = (0, 0) 45 | 46 | grContext.drawImage(img, 47 | sourceX, sourceY, sliceWidth, sliceHeight, 48 | destX, destY, sliceWidth, sliceHeight) 49 | 50 | canvas 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /javafx/src/test/resources/size-change.feature: -------------------------------------------------------------------------------- 1 | 2 | Feature: Changing game size 3 | 4 | The user might want to change game size, when this happens a new 5 | game should be presented in solved state. 6 | 7 | It's up to the user to shuffle and start playing. 8 | 9 | Background: 10 | Given I am presented with a game in size "3x3" 11 | 12 | Scenario: Initial moves-count 13 | Then the moves counter should be at 0 14 | 15 | Scenario: Changing size to "4x3" 16 | When I change the size to "4x3" 17 | Then I should see a game with 12 tiles 18 | And the game should be in solved state 19 | And the moves counter should be at 0 20 | -------------------------------------------------------------------------------- /javafx/src/test/scala/com/sebnozzi/slidingpuzzle/ui/javafx/utils/ImageSlicerSuite.scala: -------------------------------------------------------------------------------- 1 | package com.sebnozzi.slidingpuzzle.ui.javafx.utils 2 | 3 | import javafx.scene.canvas.Canvas 4 | import javafx.scene.image.Image 5 | import com.sebnozzi.slidingpuzzle.model.structs.GridSize 6 | import org.scalatest.funsuite.AnyFunSuite 7 | import org.scalatest.{BeforeAndAfter, Ignore} 8 | 9 | @Ignore 10 | class ImageSlicerSuite extends AnyFunSuite with BeforeAndAfter { 11 | 12 | val img: Image = { 13 | val inputStream = this.getClass.getResourceAsStream("/2322324186_ca41fba641_o.jpg") 14 | assert(inputStream != null, "image not found") 15 | new Image(inputStream) 16 | } 17 | 18 | var slicer: ImageSlicer = _ 19 | 20 | before { 21 | slicer = new ImageSlicer(img, GridSize(2, 2)) 22 | } 23 | 24 | test("dimensions") { 25 | assert(img.getWidth === 840) 26 | assert(img.getHeight === 560) 27 | } 28 | 29 | test("slice dimensions") { 30 | assert(slicer.sliceWidth === 420) 31 | assert(slicer.sliceHeight === 280) 32 | } 33 | 34 | test("get coordinates of bottom-right slice") { 35 | val (x: Int, y: Int) = slicer.coordinatesOfSliceAt(x = 2, y = 2) 36 | assert(x === 420) 37 | assert(y === 280) 38 | } 39 | 40 | test("get coordinates of bottom-left slice") { 41 | val (x: Int, y: Int) = slicer.coordinatesOfSliceAt(x = 1, y = 2) 42 | assert(x === 0) 43 | assert(y === 280) 44 | } 45 | 46 | test("get one slice") { 47 | val slice: Canvas = slicer.sliceAt(x = 1, y = 1) 48 | assert(slice.getWidth === 420) 49 | assert(slice.getHeight === 280) 50 | } 51 | 52 | test("get slice positions") { 53 | assert(slicer.slicePositions === List((1, 1), (2, 1), (1, 2), (2, 2))) 54 | } 55 | 56 | test("check dimensions of all slices") { 57 | val slices: Seq[Canvas] = slicer.allSlices 58 | slices.foreach { slice => 59 | assert(slice.getWidth === 420) 60 | assert(slice.getHeight === 280) 61 | } 62 | } 63 | 64 | test("all slices are different instances") { 65 | val slicer = new ImageSlicer(img, GridSize(4, 2)) 66 | slicer.allSlices.combinations(2).foreach { 67 | case Seq(left, right) => 68 | assert(left != right) 69 | } 70 | } 71 | 72 | test("slice number for position") { 73 | val slicer = new ImageSlicer(img, GridSize(4, 2)) 74 | assert(slicer.sliceIndexFor(1, 1) === 0) 75 | assert(slicer.sliceIndexFor(2, 1) === 1) 76 | assert(slicer.sliceIndexFor(3, 1) === 2) 77 | assert(slicer.sliceIndexFor(4, 1) === 3) 78 | assert(slicer.sliceIndexFor(1, 2) === 4) 79 | assert(slicer.sliceIndexFor(2, 2) === 5) 80 | assert(slicer.sliceIndexFor(3, 2) === 6) 81 | assert(slicer.sliceIndexFor(4, 2) === 7) 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.3.10 2 | 3 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.1") 2 | -------------------------------------------------------------------------------- /scalajs/index-dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
17 |
15 | | t |