From ddbf4e550176c7ecf7a72c5aaaf1396c6c9a6b2c Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 8 Nov 2017 14:38:46 +0100 Subject: [PATCH 01/53] upgraded junit version and formatted test --- build.sbt | 2 +- .../htwg/se/learn_duel/model/PlayerSpec.scala | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/build.sbt b/build.sbt index 9d6aa2c..210e2e9 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ scalaVersion := "2.12.4" libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.4" % "test" -libraryDependencies += "junit" % "junit" % "4.8" % "test" +libraryDependencies += "junit" % "junit" % "4.12" % "test" //*******************************************************************************// //Libraries that we will use in later lectures compatible with this scala version diff --git a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala index 97a2f9f..b658027 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala @@ -7,15 +7,15 @@ import org.scalatest.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) class PlayerSpec extends WordSpec with Matchers { - "A Player" when { "new" should { - val player = Player("Your Name") - "have a name" in { - player.name should be("Your Name") + "A Player" when { + "new" should { + val player = Player("Your Name") + "have a name" in { + player.name should be("Your Name") + } + "have a nice String representation" in { + player.toString should be("Your Name") + } + } } - "have a nice String representation" in { - player.toString should be("Your Name") - } - }} - - } From ffeb4373d22cf0e74fc38d03c06fa84f571edfbc Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 6 Dec 2017 10:07:21 +0100 Subject: [PATCH 02/53] added trait for observer pattern --- src/main/scala/de/htwg/se/learn_duel/Observable.scala | 8 ++++++++ src/main/scala/de/htwg/se/learn_duel/Observer.scala | 5 +++++ 2 files changed, 13 insertions(+) create mode 100644 src/main/scala/de/htwg/se/learn_duel/Observable.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/Observer.scala diff --git a/src/main/scala/de/htwg/se/learn_duel/Observable.scala b/src/main/scala/de/htwg/se/learn_duel/Observable.scala new file mode 100644 index 0000000..dbe5f63 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/Observable.scala @@ -0,0 +1,8 @@ +package de.htwg.se.learn_duel + +trait Observable { + private var observers: List[Observer] = List() + def addObserver(observer: Observer): Unit = observers = observer :: observers + def removeObserver(observer: Observer): Unit = observers = observers.filter(_ != observer) + private def notifyObservers: Unit = for (observer <- observers) observer.update +} diff --git a/src/main/scala/de/htwg/se/learn_duel/Observer.scala b/src/main/scala/de/htwg/se/learn_duel/Observer.scala new file mode 100644 index 0000000..a3707e8 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/Observer.scala @@ -0,0 +1,5 @@ +package de.htwg.se.learn_duel + +trait Observer { + def update +} From 619acf90ef3ef728646dd9f625611613e4ae2afa Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 6 Dec 2017 10:09:16 +0100 Subject: [PATCH 03/53] extended model --- .../de/htwg/se/learn_duel/model/Game.scala | 12 ++++++++++ .../de/htwg/se/learn_duel/model/Player.scala | 4 +++- .../htwg/se/learn_duel/model/impl/Game.scala | 23 +++++++++++++++++++ .../se/learn_duel/model/impl/Player.scala | 9 +++++++- 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/main/scala/de/htwg/se/learn_duel/model/Game.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Game.scala b/src/main/scala/de/htwg/se/learn_duel/model/Game.scala new file mode 100644 index 0000000..6d45824 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/model/Game.scala @@ -0,0 +1,12 @@ +package de.htwg.se.learn_duel.model + +trait Game { + protected var _players: List[Player] = List() + def players: List[Player] = _players + + def addPlayer(player: Player): Unit + def removePlayer(player: Player): Unit + def playerCount(): Int + + def getHelp(): String +} diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Player.scala b/src/main/scala/de/htwg/se/learn_duel/model/Player.scala index bd4729a..a1e0885 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/Player.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/Player.scala @@ -1,5 +1,7 @@ package de.htwg.se.learn_duel.model trait Player { - def toString: String + var points: Int = 0 + def name: String + def toString: String } diff --git a/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala new file mode 100644 index 0000000..0eedc2d --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala @@ -0,0 +1,23 @@ +package de.htwg.se.learn_duel.model.impl + +import de.htwg.se.learn_duel.model.{Player => PlayerTrait, Game => GameTrait} + +case class Game() extends GameTrait { + addPlayer(new Player("Player 1")) + + override def addPlayer(player: PlayerTrait): Unit = _players = player :: _players + + override def removePlayer(player: PlayerTrait): Unit = _players = _players.filter(_ != player) + + override def playerCount(): Int = _players.size + + override def getHelp(): String = { + import scala.io.Source + val readmeText: Iterator[String] = Source.fromResource("readme.txt").getLines + readmeText.mkString("\n") + } +} + +object Game { + val maxPlayerCount = 4 +} diff --git a/src/main/scala/de/htwg/se/learn_duel/model/impl/Player.scala b/src/main/scala/de/htwg/se/learn_duel/model/impl/Player.scala index 52ec010..8383f70 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/impl/Player.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/impl/Player.scala @@ -3,6 +3,13 @@ package de.htwg.se.learn_duel.model.impl import de.htwg.se.learn_duel.model.{ Player => PlayerTrait } case class Player(name: String) extends PlayerTrait { - override def toString:String = name + if (name.isEmpty) { + // FIXME error: name can't be empty + } + + override def toString:String = name } +object Player { + val baseName = "Player" +} From 702059cb4acd872823c05b286754ee33ac3eb7b6 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 6 Dec 2017 10:10:02 +0100 Subject: [PATCH 04/53] extended views --- .../scala/de/htwg/se/learn_duel/view/UI.scala | 7 +++ .../de/htwg/se/learn_duel/view/impl/GUI.scala | 16 +++++++ .../de/htwg/se/learn_duel/view/impl/TUI.scala | 45 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 src/main/scala/de/htwg/se/learn_duel/view/UI.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/view/impl/GUI.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala diff --git a/src/main/scala/de/htwg/se/learn_duel/view/UI.scala b/src/main/scala/de/htwg/se/learn_duel/view/UI.scala new file mode 100644 index 0000000..312c760 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/view/UI.scala @@ -0,0 +1,7 @@ +package de.htwg.se.learn_duel.view + +import de.htwg.se.learn_duel.Observer + +trait UI extends Observer { + def displayMenu(): Unit +} diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/GUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/GUI.scala new file mode 100644 index 0000000..9ee96aa --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/GUI.scala @@ -0,0 +1,16 @@ +package de.htwg.se.learn_duel.view.impl + +import de.htwg.se.learn_duel.controller.Controller +import de.htwg.se.learn_duel.view.UI + +class GUI private (controller: Controller) extends UI { + override def displayMenu(): Unit = {} + + override def update: Unit = {} +} + +object GUI { + def create(controller: Controller): GUI = { + new GUI(controller) + } +} diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala new file mode 100644 index 0000000..7d72676 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala @@ -0,0 +1,45 @@ +package de.htwg.se.learn_duel.view.impl + +import de.htwg.se.learn_duel.controller.Controller +import de.htwg.se.learn_duel.view.UI + +class TUI private (controller: Controller) extends UI { + controller.addObserver(this) + displayMenu() + + override def update: Unit = { + + } + + override def displayMenu(): Unit = { + println("") + println("Welcome to Learn Duel") + println("Current players: " + controller.getCurrentPlayers().mkString(", ")) + println("n => new game") + println("a [name] => add player") + println("r [name] => remove player") + println("q => exit") + println("") + } + + def processInputLine(input: String): Boolean = { + var continue = true + val playerPattern = """(?:a|r)(?:\s+(.*))?""".r + input match { + case "q" => continue = false + case "n" => + case playerPattern(name) if input.startsWith("a") => controller.addPlayer(Option(name)) + case playerPattern(name) if input.startsWith("r") => if (name != null) { controller.removePlayer(name) } + case _ => println("Unknown command") + } + + displayMenu() + continue + } +} + +object TUI { + def create(controller: Controller): TUI = { + new TUI(controller) + } +} From 3201e070d89f163d3c5dd5b03f252a0494a5e69e Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 6 Dec 2017 10:10:23 +0100 Subject: [PATCH 05/53] added resource folder/files --- src/main/scala/de/htwg/se/learn_duel/resources/help.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/main/scala/de/htwg/se/learn_duel/resources/help.txt diff --git a/src/main/scala/de/htwg/se/learn_duel/resources/help.txt b/src/main/scala/de/htwg/se/learn_duel/resources/help.txt new file mode 100644 index 0000000..6572659 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/resources/help.txt @@ -0,0 +1 @@ +This is the help for Learn Duel! It is not yet very helpful. :( From da2ac05199e5c5fefc971025d53b803249586aa2 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 6 Dec 2017 10:14:34 +0100 Subject: [PATCH 06/53] extended controller and main game logic --- .../de/htwg/se/learn_duel/LearnDuel.scala | 11 ++++- .../se/learn_duel/controller/Controller.scala | 8 +++- .../controller/impl/Controller.scala | 48 ++++++++++++++++++- .../scala/de/htwg/se/learn_duel/ui/UI.scala | 5 -- .../de/htwg/se/learn_duel/ui/impl/GUI.scala | 7 --- .../de/htwg/se/learn_duel/ui/impl/TUI.scala | 15 ------ .../htwg/se/learn_duel/model/PlayerSpec.scala | 1 + 7 files changed, 63 insertions(+), 32 deletions(-) delete mode 100644 src/main/scala/de/htwg/se/learn_duel/ui/UI.scala delete mode 100644 src/main/scala/de/htwg/se/learn_duel/ui/impl/GUI.scala delete mode 100644 src/main/scala/de/htwg/se/learn_duel/ui/impl/TUI.scala diff --git a/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala b/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala index fd30f48..f196fb7 100644 --- a/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala +++ b/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala @@ -1,9 +1,16 @@ package de.htwg.se.learn_duel -import de.htwg.se.learn_duel.ui.impl.TUI +import de.htwg.se.learn_duel.controller.impl.Controller +import de.htwg.se.learn_duel.model.impl.Game +import de.htwg.se.learn_duel.view.impl.{GUI, TUI} object LearnDuel { def main(args: Array[String]): Unit = { - println(TUI.createTUI.dummyText) + val gameState = Game + val controller = Controller.create(gameState) + val tui = TUI.create(controller) + val gui = GUI.create(controller) + + while(tui.processInputLine(scala.io.StdIn.readLine())) {} } } diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala index 34f133d..85d7fd4 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala @@ -1,5 +1,11 @@ package de.htwg.se.learn_duel.controller -trait Controller { +import de.htwg.se.learn_duel.Observable +trait Controller extends Observable { + def nextPlayerName(): String + def getCurrentPlayers(): List[String] + def addPlayer(name: Option[String]): Unit + def removePlayer(name: String): Unit + def getMaxPlayerCount(): Int } diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala index 1182113..e28e887 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala @@ -1,7 +1,51 @@ package de.htwg.se.learn_duel.controller.impl -import de.htwg.se.learn_duel.controller.{ Controller => ControllerTrait } +import de.htwg.se.learn_duel.controller.{Controller => ControllerTrait} +import de.htwg.se.learn_duel.model.impl.Player +import de.htwg.se.learn_duel.model.impl.Game -class Controller extends ControllerTrait { +class Controller(gameState: Game) private extends ControllerTrait { + override def nextPlayerName(): String = List(Player.baseName, gameState.playerCount + 1).mkString(" ") + override def getCurrentPlayers(): List[String] = { + gameState.players.map(p => p.name) + } + + override def addPlayer(name: Option[String]): Unit = { + var playerName = name match { + case Some(name) => name + case None => nextPlayerName() + } + + if (gameState.playerCount == Game.maxPlayerCount) { + // FIXME error: max player count reached + return + } + else if (gameState.players.exists(p => p.name == name)) { + // FIXME error: player already exists + return + } + + gameState.addPlayer(Player(playerName)) + } + + override def removePlayer(name: String): Unit = { + if (gameState.playerCount == 1) { + // FIXME error: can't remove last player + return + } + + gameState.players.find(p => p.name == name) match { + case Some(p) => gameState.removePlayer(p) + case None => + } + } + + override def getMaxPlayerCount(): Int = Game.maxPlayerCount +} + +object Controller { + def create(gameState: Game): Controller = { + new Controller(gameState) + } } diff --git a/src/main/scala/de/htwg/se/learn_duel/ui/UI.scala b/src/main/scala/de/htwg/se/learn_duel/ui/UI.scala deleted file mode 100644 index f6712de..0000000 --- a/src/main/scala/de/htwg/se/learn_duel/ui/UI.scala +++ /dev/null @@ -1,5 +0,0 @@ -package de.htwg.se.learn_duel.ui - -trait UI { - -} diff --git a/src/main/scala/de/htwg/se/learn_duel/ui/impl/GUI.scala b/src/main/scala/de/htwg/se/learn_duel/ui/impl/GUI.scala deleted file mode 100644 index 27f6d95..0000000 --- a/src/main/scala/de/htwg/se/learn_duel/ui/impl/GUI.scala +++ /dev/null @@ -1,7 +0,0 @@ -package de.htwg.se.learn_duel.ui.impl - -import de.htwg.se.learn_duel.ui.UI - -class GUI extends UI { - -} diff --git a/src/main/scala/de/htwg/se/learn_duel/ui/impl/TUI.scala b/src/main/scala/de/htwg/se/learn_duel/ui/impl/TUI.scala deleted file mode 100644 index 2ccec9f..0000000 --- a/src/main/scala/de/htwg/se/learn_duel/ui/impl/TUI.scala +++ /dev/null @@ -1,15 +0,0 @@ -package de.htwg.se.learn_duel.ui.impl - -import de.htwg.se.learn_duel.controller.Controller -import de.htwg.se.learn_duel.ui.UI - -class TUI(controller: Controller) extends UI { - def dummyText: String = "Just some text." -} - -object TUI { - def createTUI: TUI = { - val controller = new Controller {} - new TUI(controller) - } -} \ No newline at end of file diff --git a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala index b658027..76acc47 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala @@ -19,3 +19,4 @@ class PlayerSpec extends WordSpec with Matchers { } } } + From fdb7cbfa26960edb9b9636955c83440f3a72812b Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 6 Dec 2017 10:27:58 +0100 Subject: [PATCH 07/53] fixed compile error --- build.sbt | 1 + src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala | 2 +- src/main/scala/de/htwg/se/learn_duel/Observable.scala | 2 +- .../de/htwg/se/learn_duel/controller/impl/Controller.scala | 2 +- src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala | 5 +++++ 5 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala diff --git a/build.sbt b/build.sbt index 210e2e9..3305b70 100644 --- a/build.sbt +++ b/build.sbt @@ -3,6 +3,7 @@ organization := "de.htwg.se" version := "0.0.1" scalaVersion := "2.12.4" +libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.4" libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.4" % "test" libraryDependencies += "junit" % "junit" % "4.12" % "test" diff --git a/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala b/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala index f196fb7..94d7e6e 100644 --- a/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala +++ b/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala @@ -6,7 +6,7 @@ import de.htwg.se.learn_duel.view.impl.{GUI, TUI} object LearnDuel { def main(args: Array[String]): Unit = { - val gameState = Game + val gameState = Game() val controller = Controller.create(gameState) val tui = TUI.create(controller) val gui = GUI.create(controller) diff --git a/src/main/scala/de/htwg/se/learn_duel/Observable.scala b/src/main/scala/de/htwg/se/learn_duel/Observable.scala index dbe5f63..13e9f36 100644 --- a/src/main/scala/de/htwg/se/learn_duel/Observable.scala +++ b/src/main/scala/de/htwg/se/learn_duel/Observable.scala @@ -4,5 +4,5 @@ trait Observable { private var observers: List[Observer] = List() def addObserver(observer: Observer): Unit = observers = observer :: observers def removeObserver(observer: Observer): Unit = observers = observers.filter(_ != observer) - private def notifyObservers: Unit = for (observer <- observers) observer.update + private def notifyObservers: Unit = observers.foreach(o => o.update) } diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala index e28e887..455cc89 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala @@ -4,7 +4,7 @@ import de.htwg.se.learn_duel.controller.{Controller => ControllerTrait} import de.htwg.se.learn_duel.model.impl.Player import de.htwg.se.learn_duel.model.impl.Game -class Controller(gameState: Game) private extends ControllerTrait { +class Controller private (gameState: Game) extends ControllerTrait { override def nextPlayerName(): String = List(Player.baseName, gameState.playerCount + 1).mkString(" ") override def getCurrentPlayers(): List[String] = { diff --git a/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala new file mode 100644 index 0000000..e2df2a1 --- /dev/null +++ b/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala @@ -0,0 +1,5 @@ +package de.htwg.se.learn_duel.model + +class GameSpec { + // FIXME write tests for everything +} From d5730bd98f429b2de161519409727af37b0b2219 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 6 Dec 2017 11:36:43 +0100 Subject: [PATCH 08/53] fixed shadowed type in test --- src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala index 76acc47..b24a154 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala @@ -1,6 +1,6 @@ package de.htwg.se.learn_duel.model -import de.htwg.se.learn_duel.model.impl.Player +import de.htwg.se.learn_duel.model.impl.{ Player => PlayerImpl} import org.junit.runner.RunWith import org.scalatest._ import org.scalatest.junit.JUnitRunner @@ -9,7 +9,7 @@ import org.scalatest.junit.JUnitRunner class PlayerSpec extends WordSpec with Matchers { "A Player" when { "new" should { - val player = Player("Your Name") + val player = PlayerImpl("Your Name") "have a name" in { player.name should be("Your Name") } From 96c3f200fd80a09b005ff27df7a9bd9d501ced1c Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Tue, 2 Jan 2018 14:02:04 +0100 Subject: [PATCH 09/53] added necessary dependencies for assignment to build.sbt --- build.sbt | 18 +++-------- .../se/learn_duel => }/resources/help.txt | 0 .../de/htwg/se/learn_duel/view/impl/GUI.scala | 16 ---------- .../se/learn_duel/view/impl/gui/GUI.scala | 32 +++++++++++++++++++ 4 files changed, 36 insertions(+), 30 deletions(-) rename src/main/{scala/de/htwg/se/learn_duel => }/resources/help.txt (100%) delete mode 100644 src/main/scala/de/htwg/se/learn_duel/view/impl/GUI.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala diff --git a/build.sbt b/build.sbt index 3305b70..d7ab227 100644 --- a/build.sbt +++ b/build.sbt @@ -5,19 +5,9 @@ scalaVersion := "2.12.4" libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.4" libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.4" % "test" - libraryDependencies += "junit" % "junit" % "4.12" % "test" -//*******************************************************************************// -//Libraries that we will use in later lectures compatible with this scala version -// uncomment to use!! - -//libraryDependencies += "org.scala-lang.modules" % "scala-swing_2.12" % "2.0.1" - -//libraryDependencies += "com.google.inject" % "guice" % "4.1.0" - -//libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" - -//libraryDependencies += "org.scala-lang.modules" % "scala-xml_2.12" % "1.0.6" - -//libraryDependencies += "com.typesafe.play" %% "play-json" % "2.6.6" +libraryDependencies += "org.scalafx" %% "scalafx" % "8.0.144-R12" +libraryDependencies += "com.google.inject" % "guice" % "4.1.0" +libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" +libraryDependencies += "com.typesafe.play" %% "play-json" % "2.6.6" diff --git a/src/main/scala/de/htwg/se/learn_duel/resources/help.txt b/src/main/resources/help.txt similarity index 100% rename from src/main/scala/de/htwg/se/learn_duel/resources/help.txt rename to src/main/resources/help.txt diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/GUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/GUI.scala deleted file mode 100644 index 9ee96aa..0000000 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/GUI.scala +++ /dev/null @@ -1,16 +0,0 @@ -package de.htwg.se.learn_duel.view.impl - -import de.htwg.se.learn_duel.controller.Controller -import de.htwg.se.learn_duel.view.UI - -class GUI private (controller: Controller) extends UI { - override def displayMenu(): Unit = {} - - override def update: Unit = {} -} - -object GUI { - def create(controller: Controller): GUI = { - new GUI(controller) - } -} diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala new file mode 100644 index 0000000..0d830be --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala @@ -0,0 +1,32 @@ +package de.htwg.se.learn_duel.view.impl + +import javafx.embed.swing.JFXPanel + +import de.htwg.se.learn_duel.Observer +import de.htwg.se.learn_duel.controller.Controller +import de.htwg.se.learn_duel.view.UI + +import scalafx.application.JFXApp + +class GUI private (controller: Controller) extends JFXApp with UI with Observer { + controller.addObserver(this) + displayMenu + + override def displayMenu(): Unit = { + this.stage = new MenuScreen + } + + override def displayGame(): Unit = { + //this.stage = new GameScreen + } + + override def update: Unit = {} +} + +object GUI { + def create(controller: Controller): GUI = { + val gui = new GUI(controller) + gui.main(Array()) + gui + } +} From 66efee34d7e00f0e7be3af86f55ac19bbd32320e Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Tue, 2 Jan 2018 14:03:19 +0100 Subject: [PATCH 10/53] removed warning for println from scalacheck --- scalastyle-config.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scalastyle-config.xml b/scalastyle-config.xml index a06b9c9..d6960d6 100644 --- a/scalastyle-config.xml +++ b/scalastyle-config.xml @@ -73,7 +73,7 @@ - + From 39cb20cf538d5fc4d1033d0dee62eac56e27621c Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Tue, 2 Jan 2018 14:41:43 +0100 Subject: [PATCH 11/53] added update data to observer / observable --- .../scala/de/htwg/se/learn_duel/Observable.scala | 2 +- .../scala/de/htwg/se/learn_duel/Observer.scala | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/scala/de/htwg/se/learn_duel/Observable.scala b/src/main/scala/de/htwg/se/learn_duel/Observable.scala index 13e9f36..4f42607 100644 --- a/src/main/scala/de/htwg/se/learn_duel/Observable.scala +++ b/src/main/scala/de/htwg/se/learn_duel/Observable.scala @@ -4,5 +4,5 @@ trait Observable { private var observers: List[Observer] = List() def addObserver(observer: Observer): Unit = observers = observer :: observers def removeObserver(observer: Observer): Unit = observers = observers.filter(_ != observer) - private def notifyObservers: Unit = observers.foreach(o => o.update) + protected def notifyObservers(updateParam: UpdateData): Unit = observers.foreach(o => o.update(updateParam)) } diff --git a/src/main/scala/de/htwg/se/learn_duel/Observer.scala b/src/main/scala/de/htwg/se/learn_duel/Observer.scala index a3707e8..d522170 100644 --- a/src/main/scala/de/htwg/se/learn_duel/Observer.scala +++ b/src/main/scala/de/htwg/se/learn_duel/Observer.scala @@ -1,5 +1,17 @@ package de.htwg.se.learn_duel +object UpdateAction extends Enumeration { + type UpdateAction = Value + val CLOSE_APPLICATION, START_GAME, SHOW_HELP = Value +} +import UpdateAction._ +import de.htwg.se.learn_duel.model.Game + +class UpdateData(updateAction: UpdateAction, gameState: Option[Game]) { + def getAction(): UpdateAction = updateAction + def getState(): Option[Game] = gameState +} + trait Observer { - def update + def update(updateData: UpdateData): Unit } From 24b3586fa770f4daf6db4c996a8549d371196506 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Tue, 2 Jan 2018 14:42:41 +0100 Subject: [PATCH 12/53] added new exception types to controller --- .../se/learn_duel/controller/ControllerException.scala | 6 ++++++ .../impl/exceptions/ControllerProcedureFailed.scala | 7 +++++++ .../impl/exceptions/NotEnoughPlayersException.scala | 7 +++++++ .../controller/impl/exceptions/PlayerExistsException.scala | 7 +++++++ .../impl/exceptions/PlayerNotExistingException.scala | 7 +++++++ .../impl/exceptions/TooManyPlayersException.scala | 7 +++++++ 6 files changed, 41 insertions(+) create mode 100644 src/main/scala/de/htwg/se/learn_duel/controller/ControllerException.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/ControllerProcedureFailed.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/NotEnoughPlayersException.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/PlayerExistsException.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/PlayerNotExistingException.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/TooManyPlayersException.scala diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/ControllerException.scala b/src/main/scala/de/htwg/se/learn_duel/controller/ControllerException.scala new file mode 100644 index 0000000..5426008 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/controller/ControllerException.scala @@ -0,0 +1,6 @@ +package de.htwg.se.learn_duel.controller + +trait ControllerException { + self: Throwable => + val message: String +} diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/ControllerProcedureFailed.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/ControllerProcedureFailed.scala new file mode 100644 index 0000000..79a0203 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/ControllerProcedureFailed.scala @@ -0,0 +1,7 @@ +package de.htwg.se.learn_duel.controller.impl.exceptions + +import de.htwg.se.learn_duel.controller.ControllerException + +case class ControllerProcedureFailed(message: String) extends Exception(message) with ControllerException { + +} diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/NotEnoughPlayersException.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/NotEnoughPlayersException.scala new file mode 100644 index 0000000..ef19347 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/NotEnoughPlayersException.scala @@ -0,0 +1,7 @@ +package de.htwg.se.learn_duel.controller.impl.exceptions + +import de.htwg.se.learn_duel.controller.ControllerException + +case class NotEnoughPlayersException(message: String) extends Exception(message) with ControllerException { + +} diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/PlayerExistsException.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/PlayerExistsException.scala new file mode 100644 index 0000000..26d5fdb --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/PlayerExistsException.scala @@ -0,0 +1,7 @@ +package de.htwg.se.learn_duel.controller.impl.exceptions + +import de.htwg.se.learn_duel.controller.ControllerException + +case class PlayerExistsException(message: String) extends Exception(message) with ControllerException { + +} diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/PlayerNotExistingException.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/PlayerNotExistingException.scala new file mode 100644 index 0000000..7fd67fa --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/PlayerNotExistingException.scala @@ -0,0 +1,7 @@ +package de.htwg.se.learn_duel.controller.impl.exceptions + +import de.htwg.se.learn_duel.controller.ControllerException + +case class PlayerNotExistingException(message: String) extends Exception(message) with ControllerException { + +} diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/TooManyPlayersException.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/TooManyPlayersException.scala new file mode 100644 index 0000000..ec28238 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/exceptions/TooManyPlayersException.scala @@ -0,0 +1,7 @@ +package de.htwg.se.learn_duel.controller.impl.exceptions + +import de.htwg.se.learn_duel.controller.ControllerException + +case class TooManyPlayersException(message: String) extends Exception(message) with ControllerException { + +} From 8111dcb9789c4bc5b707e50089a5016b329903cd Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Tue, 2 Jan 2018 14:44:01 +0100 Subject: [PATCH 13/53] added exceptions and state update handling to controller, model and view --- .../de/htwg/se/learn_duel/LearnDuel.scala | 8 +- .../se/learn_duel/controller/Controller.scala | 9 ++- .../controller/impl/Controller.scala | 53 ++++++++---- .../de/htwg/se/learn_duel/model/Game.scala | 3 +- .../htwg/se/learn_duel/model/impl/Game.scala | 8 +- .../se/learn_duel/model/impl/Player.scala | 10 ++- .../scala/de/htwg/se/learn_duel/view/UI.scala | 1 + .../de/htwg/se/learn_duel/view/impl/TUI.scala | 80 ++++++++++++++----- .../se/learn_duel/view/impl/gui/GUI.scala | 71 +++++++++++++--- .../learn_duel/view/impl/gui/GameStage.scala | 23 ++++++ .../learn_duel/view/impl/gui/MenuStage.scala | 43 ++++++++++ 11 files changed, 247 insertions(+), 62 deletions(-) create mode 100644 src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GameStage.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala diff --git a/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala b/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala index 94d7e6e..40521b0 100644 --- a/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala +++ b/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala @@ -1,8 +1,12 @@ package de.htwg.se.learn_duel +import java.io.BufferedReader +import javafx.application.Platform + import de.htwg.se.learn_duel.controller.impl.Controller import de.htwg.se.learn_duel.model.impl.Game -import de.htwg.se.learn_duel.view.impl.{GUI, TUI} +import de.htwg.se.learn_duel.view.impl.TUI +import de.htwg.se.learn_duel.view.impl.gui.GUI object LearnDuel { def main(args: Array[String]): Unit = { @@ -11,6 +15,6 @@ object LearnDuel { val tui = TUI.create(controller) val gui = GUI.create(controller) - while(tui.processInputLine(scala.io.StdIn.readLine())) {} + tui.processInput(new BufferedReader(Console.in)) } } diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala index 85d7fd4..68a62f0 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala @@ -3,9 +3,12 @@ package de.htwg.se.learn_duel.controller import de.htwg.se.learn_duel.Observable trait Controller extends Observable { - def nextPlayerName(): String - def getCurrentPlayers(): List[String] + def nextPlayerName: String + def getCurrentPlayers: List[String] def addPlayer(name: Option[String]): Unit def removePlayer(name: String): Unit - def getMaxPlayerCount(): Int + def getMaxPlayerCount: Int + def onHelp: Unit + def onStartGame: Unit + def onClose: Unit } diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala index 455cc89..0accb27 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala @@ -1,50 +1,75 @@ package de.htwg.se.learn_duel.controller.impl +import de.htwg.se.learn_duel.{UpdateAction, UpdateData} +import de.htwg.se.learn_duel.controller.impl.exceptions._ import de.htwg.se.learn_duel.controller.{Controller => ControllerTrait} import de.htwg.se.learn_duel.model.impl.Player import de.htwg.se.learn_duel.model.impl.Game class Controller private (gameState: Game) extends ControllerTrait { - override def nextPlayerName(): String = List(Player.baseName, gameState.playerCount + 1).mkString(" ") + override def nextPlayerName: String = Player.baseName + (gameState.playerCount + 1).toString - override def getCurrentPlayers(): List[String] = { + override def getCurrentPlayers: List[String] = { gameState.players.map(p => p.name) } override def addPlayer(name: Option[String]): Unit = { var playerName = name match { case Some(name) => name - case None => nextPlayerName() + case None => nextPlayerName } if (gameState.playerCount == Game.maxPlayerCount) { - // FIXME error: max player count reached - return + throw TooManyPlayersException("There are too many players to add another one") } - else if (gameState.players.exists(p => p.name == name)) { - // FIXME error: player already exists - return + else if (gameState.players.exists(p => p.name == playerName)) { + throw PlayerExistsException(s"'$playerName' already exists") } - gameState.addPlayer(Player(playerName)) + try { + gameState.addPlayer(Player(playerName)) + } catch { + case e: Throwable => throw ControllerProcedureFailed("Adding player failed: " + e.getMessage) + } + notifyObservers(new UpdateData(UpdateAction.CLOSE_APPLICATION, Option(null))) } override def removePlayer(name: String): Unit = { if (gameState.playerCount == 1) { - // FIXME error: can't remove last player - return + throw NotEnoughPlayersException("There are not enough players to remove one") } gameState.players.find(p => p.name == name) match { - case Some(p) => gameState.removePlayer(p) - case None => + case Some(p) => + gameState.removePlayer(p) + notifyObservers(new UpdateData(UpdateAction.CLOSE_APPLICATION, Option(null))) + case None => throw PlayerNotExistingException(s"Player '$name' does not exist") + } + } + + override def getMaxPlayerCount: Int = Game.maxPlayerCount + + override def onHelp(): Unit = { + if (gameState.helpText.isEmpty) { + import scala.io.Source + val helpText: Iterator[String] = Source.fromResource("help.txt").getLines + gameState.helpText = helpText.mkString("\n") } + + notifyObservers(new UpdateData(UpdateAction.SHOW_HELP, Option(gameState))) } - override def getMaxPlayerCount(): Int = Game.maxPlayerCount + override def onStartGame(): Unit = { + + } + + override def onClose(): Unit = { + notifyObservers(new UpdateData(UpdateAction.CLOSE_APPLICATION, Option(null))) + } } object Controller { + // FIXME Builder Pattern def create(gameState: Game): Controller = { new Controller(gameState) } diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Game.scala b/src/main/scala/de/htwg/se/learn_duel/model/Game.scala index 6d45824..946bda3 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/Game.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/Game.scala @@ -2,11 +2,10 @@ package de.htwg.se.learn_duel.model trait Game { protected var _players: List[Player] = List() + var helpText: String = "" def players: List[Player] = _players def addPlayer(player: Player): Unit def removePlayer(player: Player): Unit def playerCount(): Int - - def getHelp(): String } diff --git a/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala index 0eedc2d..0217dab 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala @@ -3,19 +3,13 @@ package de.htwg.se.learn_duel.model.impl import de.htwg.se.learn_duel.model.{Player => PlayerTrait, Game => GameTrait} case class Game() extends GameTrait { - addPlayer(new Player("Player 1")) + addPlayer(new Player("Player1")) override def addPlayer(player: PlayerTrait): Unit = _players = player :: _players override def removePlayer(player: PlayerTrait): Unit = _players = _players.filter(_ != player) override def playerCount(): Int = _players.size - - override def getHelp(): String = { - import scala.io.Source - val readmeText: Iterator[String] = Source.fromResource("readme.txt").getLines - readmeText.mkString("\n") - } } object Game { diff --git a/src/main/scala/de/htwg/se/learn_duel/model/impl/Player.scala b/src/main/scala/de/htwg/se/learn_duel/model/impl/Player.scala index 8383f70..50fd05c 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/impl/Player.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/impl/Player.scala @@ -1,13 +1,17 @@ package de.htwg.se.learn_duel.model.impl -import de.htwg.se.learn_duel.model.{ Player => PlayerTrait } +import java.security.InvalidParameterException + +import de.htwg.se.learn_duel.model.{Player => PlayerTrait} case class Player(name: String) extends PlayerTrait { if (name.isEmpty) { - // FIXME error: name can't be empty + throw new InvalidParameterException("Player name cannot be empty") + } else if (!name.matches("\\S+")) { + throw new InvalidParameterException("Player name may not contain whitespaces") } - override def toString:String = name + override def toString: String = name } object Player { diff --git a/src/main/scala/de/htwg/se/learn_duel/view/UI.scala b/src/main/scala/de/htwg/se/learn_duel/view/UI.scala index 312c760..6c32806 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/UI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/UI.scala @@ -4,4 +4,5 @@ import de.htwg.se.learn_duel.Observer trait UI extends Observer { def displayMenu(): Unit + def displayGame(): Unit } diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala index 7d72676..d37c2e1 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala @@ -1,41 +1,83 @@ package de.htwg.se.learn_duel.view.impl -import de.htwg.se.learn_duel.controller.Controller -import de.htwg.se.learn_duel.view.UI +import java.io.{BufferedReader, StringReader} -class TUI private (controller: Controller) extends UI { - controller.addObserver(this) - displayMenu() +import de.htwg.se.learn_duel.{Observer, UpdateAction, UpdateData} +import de.htwg.se.learn_duel.controller.{Controller, ControllerException} +import de.htwg.se.learn_duel.view.UI - override def update: Unit = { +import scalafx.collections.ObservableBuffer.Update - } +class TUI private (controller: Controller) extends UI with Observer { + controller.addObserver(this) + var stopProcessingInput = false + displayMenu override def displayMenu(): Unit = { + println("") println("Welcome to Learn Duel") - println("Current players: " + controller.getCurrentPlayers().mkString(", ")) + println("Current players: " + controller.getCurrentPlayers.mkString(", ")) println("n => new game") println("a [name] => add player") println("r [name] => remove player") + println("h => show help") println("q => exit") println("") } - def processInputLine(input: String): Boolean = { - var continue = true - val playerPattern = """(?:a|r)(?:\s+(.*))?""".r - input match { - case "q" => continue = false - case "n" => - case playerPattern(name) if input.startsWith("a") => controller.addPlayer(Option(name)) - case playerPattern(name) if input.startsWith("r") => if (name != null) { controller.removePlayer(name) } - case _ => println("Unknown command") + override def displayGame(): Unit = { + + } + + override def update(updateParam: UpdateData): Unit = { + updateParam.getAction() match { + case UpdateAction.CLOSE_APPLICATION => stopProcessingInput = true + case UpdateAction.SHOW_HELP => { + val helpText = updateParam.getState() match { + case Some(gameState) => gameState.helpText + case None => "No help available." + } + println(helpText) + } } + } + + // scalastyle:off + def processInput(input: BufferedReader): Unit = { + val playerPattern = """(?:a|r)(?:\s+(.*))?""".r + + while (!stopProcessingInput) { + if (input.ready()) { + val line = input.readLine() - displayMenu() - continue + // don't match input if we close the application anyway + try { + line match { + case "q" => { + controller.onClose + } + case "n" => + case playerPattern(name) if line.startsWith("a") => controller.addPlayer(Option(name)) + case playerPattern(name) if line.startsWith("r") => if (name != null) { + controller.removePlayer(name) + } + case "h" => controller.onHelp + case _ => println("Unknown command") + } + } catch { + case e: ControllerException => println(e.getMessage) + } + + // check if the application should be closed after parsing input + // if yes, don't repeat the menu + if (!stopProcessingInput) { + displayMenu + } + } + } } + // scalastyle:on } object TUI { diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala index 0d830be..4172ed8 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala @@ -1,32 +1,79 @@ -package de.htwg.se.learn_duel.view.impl +package de.htwg.se.learn_duel.view.impl.gui -import javafx.embed.swing.JFXPanel +import java.util.concurrent.CountDownLatch +import javafx.application.Platform -import de.htwg.se.learn_duel.Observer +import de.htwg.se.learn_duel.{Observer, UpdateAction, UpdateData} import de.htwg.se.learn_duel.controller.Controller import de.htwg.se.learn_duel.view.UI +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global import scalafx.application.JFXApp +import scalafx.application.JFXApp.PrimaryStage +import scalafx.scene.control.{Button, ButtonType, Dialog} +import scalafx.stage.Modality -class GUI private (controller: Controller) extends JFXApp with UI with Observer { +class GUI private (controller: Controller, latch: CountDownLatch) extends JFXApp with UI with Observer { controller.addObserver(this) - displayMenu + displayMenu() + this.stage.onCloseRequest = {(_) => + controller.onClose + } + + // signal initialization down + latch.countDown() override def displayMenu(): Unit = { - this.stage = new MenuScreen + + this.stage = new MenuStage( + - => { + controller.onStartGame + }, + _ => { + controller.onHelp + }) } override def displayGame(): Unit = { - //this.stage = new GameScreen + this.stage = new GameStage } - override def update: Unit = {} + override def update(updateParam: UpdateData): Unit = { + // every update needs to be run on the JavaFX Application thread + Platform.runLater {() => + updateParam.getAction() match { + case UpdateAction.CLOSE_APPLICATION => this.stage.close() + case UpdateAction.SHOW_HELP => { + val helpText = updateParam.getState() match { + case Some(gameState) => gameState.helpText + case None => "No help available." + } + val dlg = new Dialog[Unit] { + title = "Learn Duel Help" + contentText = helpText + dialogPane.value.getButtonTypes.add(ButtonType.OK) + initModality(Modality.None) + } + + dlg.show + } + } + } + } } object GUI { - def create(controller: Controller): GUI = { - val gui = new GUI(controller) - gui.main(Array()) - gui + def create(controller: Controller): Unit = { + val latch = new CountDownLatch(1) + val gui = new GUI(controller, latch) + + // run GUI on its own thread + Future { + gui.main(Array()) + } + + // wait for initialization of JFXApp to be done + latch.await() } } diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GameStage.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GameStage.scala new file mode 100644 index 0000000..ab4b1ed --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GameStage.scala @@ -0,0 +1,23 @@ +package de.htwg.se.learn_duel.view.impl.gui + +import scalafx.Includes._ +import scalafx.application.JFXApp.PrimaryStage +import scalafx.scene.Scene +import scalafx.scene.paint.Color._ +import scalafx.scene.shape.Rectangle + +class GameStage extends PrimaryStage { + title.value = "Game" + width = 480 + height = 640 + scene = new Scene { + fill = LightGreen + content = new Rectangle { + x = 25 + y = 40 + width = 100 + height = 100 + fill <== when(hover) choose Yellow otherwise Blue + } + } +} diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala new file mode 100644 index 0000000..b74bad0 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala @@ -0,0 +1,43 @@ +package de.htwg.se.learn_duel.view.impl.gui + +import javafx.event.{ActionEvent, EventHandler} + +import scalafx.Includes._ +import scalafx.application.JFXApp.PrimaryStage +import scalafx.scene.Scene +import scalafx.scene.control.Button +import scalafx.scene.layout.VBox +import scalafx.scene.paint.Color._ +import scalafx.scene.text.Text + +class MenuStage( + newGameAction: EventHandler[ActionEvent], + helpAction: EventHandler[ActionEvent] +) extends PrimaryStage { + title.value = "Learn Duel Menu" + resizable = false + width = 480 + height = 640 + + scene = new Scene { + fill = White + root = new VBox { + children += new Text("Learn Duel") + + val newGameButton = new Button { + text = "New Game" + onAction = newGameAction + } + children += newGameButton + + + val helpButton = new Button { + text = "?" + onAction = helpAction + } + + children += helpButton + } + + } +} From 21433b904d1c25b073a7b6c7c68121c1bd1d0743 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Tue, 2 Jan 2018 15:02:54 +0100 Subject: [PATCH 14/53] added a little info dialog for user information --- .../scala/de/htwg/se/learn_duel/view/impl/TUI.scala | 1 + .../de/htwg/se/learn_duel/view/impl/gui/GUI.scala | 9 ++------- .../htwg/se/learn_duel/view/impl/gui/InfoPopup.scala | 11 +++++++++++ 3 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 src/main/scala/de/htwg/se/learn_duel/view/impl/gui/InfoPopup.scala diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala index d37c2e1..927e03b 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala @@ -40,6 +40,7 @@ class TUI private (controller: Controller) extends UI with Observer { } println(helpText) } + case _ => } } diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala index 4172ed8..e06daee 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala @@ -49,15 +49,10 @@ class GUI private (controller: Controller, latch: CountDownLatch) extends JFXApp case Some(gameState) => gameState.helpText case None => "No help available." } - val dlg = new Dialog[Unit] { - title = "Learn Duel Help" - contentText = helpText - dialogPane.value.getButtonTypes.add(ButtonType.OK) - initModality(Modality.None) - } - + val dlg = new InfoPopup("Learn Duel Help", helpText) dlg.show } + case _ => } } } diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/InfoPopup.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/InfoPopup.scala new file mode 100644 index 0000000..247cb2a --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/InfoPopup.scala @@ -0,0 +1,11 @@ +package de.htwg.se.learn_duel.view.impl.gui + +import scalafx.scene.control.{ButtonType, Dialog} +import scalafx.stage.Modality + +class InfoPopup(titleText: String, content: String) extends Dialog[Unit] { + title = titleText + contentText = content + dialogPane.value.getButtonTypes.add(ButtonType.OK) + initModality(Modality.None) +} From 04bca67f4b4bdb99125255bd8f15ce5bdb63c6b7 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 3 Jan 2018 18:51:21 +0100 Subject: [PATCH 15/53] introduced new models for questions and answers --- .../de/htwg/se/learn_duel/model/Answer.scala | 24 +++++++++++++ .../htwg/se/learn_duel/model/Question.scala | 36 +++++++++++++++++++ .../se/learn_duel/model/impl/Answer.scala | 6 ++++ .../se/learn_duel/model/impl/Question.scala | 13 +++++++ 4 files changed, 79 insertions(+) create mode 100644 src/main/scala/de/htwg/se/learn_duel/model/Answer.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/model/Question.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/model/impl/Answer.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/model/impl/Question.scala diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Answer.scala b/src/main/scala/de/htwg/se/learn_duel/model/Answer.scala new file mode 100644 index 0000000..96d69e1 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/model/Answer.scala @@ -0,0 +1,24 @@ +package de.htwg.se.learn_duel.model + +import de.htwg.se.learn_duel.model.impl.{Answer => AnswerImpl} +import play.api.libs.json.{JsPath, Json, Reads, Writes} +import play.api.libs.functional.syntax._ + +trait Answer { + val id: Int + val text: String +} + +object Answer { + implicit val answerWrites = new Writes[Answer] { + def writes(answer: Answer) = Json.obj( + "id" -> answer.id, + "text" -> answer.text, + ) + } + + implicit val answerReads: Reads[Answer] = ( + (JsPath \ "id").read[Int] and + (JsPath \ "text").read[String] + )(AnswerImpl.apply _) +} diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Question.scala b/src/main/scala/de/htwg/se/learn_duel/model/Question.scala new file mode 100644 index 0000000..d8db187 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/model/Question.scala @@ -0,0 +1,36 @@ +package de.htwg.se.learn_duel.model + +import de.htwg.se.learn_duel.model.impl.{Question => QuestionImpl} +import play.api.libs.json.{JsPath, Json, Reads, Writes} +import play.api.libs.functional.syntax._ + +trait Question { + val id: Int + val text: String + val points: Int + val answers: List[Answer] + val correctAnswer: Int + val time: Int +} + +object Question { + implicit val questionWrites = new Writes[Question] { + def writes(question: Question) = Json.obj( + "id" -> question.id, + "text" -> question.text, + "points" -> question.points, + "answers" -> question.answers, + "correctAnswer" -> question.correctAnswer, + "time" -> question.time + ) + } + + implicit val questionReads: Reads[Question] = ( + (JsPath \ "id").read[Int] and + (JsPath \ "text").read[String] and + (JsPath \ "points").read[Int] and + (JsPath \ "answers").read[List[Answer]] and + (JsPath \ "correctAnswer").read[Int] and + (JsPath \ "time").read[Int] + )(QuestionImpl.apply _) +} diff --git a/src/main/scala/de/htwg/se/learn_duel/model/impl/Answer.scala b/src/main/scala/de/htwg/se/learn_duel/model/impl/Answer.scala new file mode 100644 index 0000000..3413ab1 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/model/impl/Answer.scala @@ -0,0 +1,6 @@ +package de.htwg.se.learn_duel.model.impl + +import de.htwg.se.learn_duel.model.{Answer => AnswerTrait} + +// needed for json (de)serialization +case class Answer(id: Int, text: String) extends AnswerTrait {} diff --git a/src/main/scala/de/htwg/se/learn_duel/model/impl/Question.scala b/src/main/scala/de/htwg/se/learn_duel/model/impl/Question.scala new file mode 100644 index 0000000..dc24711 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/model/impl/Question.scala @@ -0,0 +1,13 @@ +package de.htwg.se.learn_duel.model.impl + +import de.htwg.se.learn_duel.model.{Answer => AnswerTrait, Question => QuestionTrait} + +// needed for json (de)serialization +case class Question( + id: Int, + text: String, + points: Int, + answers: List[AnswerTrait], + correctAnswer: Int, + time: Int +) extends QuestionTrait {} From e8723d2a5c6e87fdb5d324a50ec6c7d1a62dc65c Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 3 Jan 2018 18:51:52 +0100 Subject: [PATCH 16/53] added sample file with questions for game --- src/main/resources/questions.json | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/main/resources/questions.json diff --git a/src/main/resources/questions.json b/src/main/resources/questions.json new file mode 100644 index 0000000..c71f13a --- /dev/null +++ b/src/main/resources/questions.json @@ -0,0 +1,52 @@ +[ + { + "id": 1, + "text": "What is 1+1?", + "points": 20, + "answers": [ + { + "id": 1, + "text": "1" + }, + { + "id": 2, + "text": "2" + }, + { + "id": 3, + "text": "3" + }, + { + "id": 4, + "text": "4" + } + ], + "correctAnswer": 2, + "time": 5 + }, + { + "id": 2, + "text": "What is 5*20?", + "points": 50, + "answers": [ + { + "id": 1, + "text": "168" + }, + { + "id": 2, + "text": "934" + }, + { + "id": 3, + "text": "236" + }, + { + "id": 4, + "text": "100" + } + ], + "correctAnswer": 4, + "time": 120 + } +] From cac0f872d81b3a2dcb903f9ddf237a36c430303f Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 3 Jan 2018 18:54:01 +0100 Subject: [PATCH 17/53] changed readme to represent the help file in markdown --- README.md | 43 ++++++++++++------------------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 8890f06..ce33594 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,16 @@ -# Htwg Scala Seed Project -===================================================== -## the easiest way to start a project for the lecture SE +# Learn Duel +## About +Learn Duel is based on QuizDuel and works in a similar fashion, but with a new twist: +You play with questions based on your school or study assignments. -This is a seed project to create a basic scala project as used in the -class Software Engineering at the University of Applied Science HTWG Konstanz. - -It requires Java 8 on your local platform. -The project has -* a folder structure prepared for a MVC-style application -* *ScalaTest* and as dependency aswell as dependencies to other libraries in the build.sbt (commented out at start). -* *scalastyle-sbt-plugin* and *sbt-scoverage* sbt plugins -* .gitignore defaults - -The easiest way to create your own project from this seed is -* create an account on github -* navigate to this project on github (https://github.com/markoboger/htwg-scala-seed) -* fork this project on github (button on top right "Fork") -* copy the URL to your fork -* clone the forked project from your local git (git clone ) or IDE (IDEA: File > New > Import from Version Controll > github) - -After that, please rename -* the Project name from htwg-scala-seed to -* the the package structure from de.htwg.se.yourgame to de.htwg.se. -* the name of the project in the build.sbt file from htwg-scala-seed to -* the main Class YourGame to - -Then -* push to git -* add team partner to your project on github (Settings > Collaborators) -* clone on partners account -* push and pull back and forth +## Current features +For now, there is only local play, but online features will be added later. +If you are playing alone, the answers can be selected with the mouse or the keys 1-4. +In local multiplayer mode player 1 can specify his answer with the keys 1-4 and +player 2 with the keys 6-9. +## Future features: + - define your own questions + - play with up to 3 friends online and compete against each other From 2b80b1b67d32c5e363f14fccd6c2c85c5b3c59ce Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 3 Jan 2018 18:55:00 +0100 Subject: [PATCH 18/53] added scala logging to dependencies --- .gitignore | 2 +- build.sbt | 2 ++ src/main/resources/logback.xml | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/logback.xml diff --git a/.gitignore b/.gitignore index 5a3f3eb..eac3d09 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,4 @@ project/.sbtserver .sonar/ # Application -yourgame.log \ No newline at end of file +learn-duel.log diff --git a/build.sbt b/build.sbt index d7ab227..7f1bd0b 100644 --- a/build.sbt +++ b/build.sbt @@ -11,3 +11,5 @@ libraryDependencies += "org.scalafx" %% "scalafx" % "8.0.144-R12" libraryDependencies += "com.google.inject" % "guice" % "4.1.0" libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" libraryDependencies += "com.typesafe.play" %% "play-json" % "2.6.6" +libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3" +libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2" diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..a54f28e --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,21 @@ + + + + %msg%n + + + + + + learn-duel.log + false + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + From c125d90820558ddbb8e256551c95e97c5f20f474 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 3 Jan 2018 18:55:45 +0100 Subject: [PATCH 19/53] changed help file --- src/main/resources/help.txt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/resources/help.txt b/src/main/resources/help.txt index 6572659..b91bd67 100644 --- a/src/main/resources/help.txt +++ b/src/main/resources/help.txt @@ -1 +1,13 @@ -This is the help for Learn Duel! It is not yet very helpful. :( +Learn Duel Help + +Learn Duel is based on QuizDuel and works in a similar fashion, but with a new twist: +You play with questions based on your school or study assignments. + +For now, there is only local play, but online features will be added later. +If you are playing alone, the answers can be selected with the mouse or the keys 1-4. +In local multiplayer mode player 1 can specify his answer with the keys 1-4 and +player 2 with the keys 6-9. + +Future features: + - define your own questions + - play with up to 3 friends online and compete against each other From 4debe49389aac1941518c75797c338030ccb692d Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 3 Jan 2018 18:57:09 +0100 Subject: [PATCH 20/53] game now loads questions from json file --- .../de/htwg/se/learn_duel/LearnDuel.scala | 20 ++++++++++++------- .../de/htwg/se/learn_duel/Observer.scala | 7 ++++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala b/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala index 40521b0..7ad72eb 100644 --- a/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala +++ b/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala @@ -1,19 +1,25 @@ package de.htwg.se.learn_duel +import de.htwg.se.learn_duel.controller.Controller +import de.htwg.se.learn_duel.model.impl.Game import java.io.BufferedReader -import javafx.application.Platform -import de.htwg.se.learn_duel.controller.impl.Controller -import de.htwg.se.learn_duel.model.impl.Game -import de.htwg.se.learn_duel.view.impl.TUI -import de.htwg.se.learn_duel.view.impl.gui.GUI +import de.htwg.se.learn_duel.model.Question +import de.htwg.se.learn_duel.view.{GUI, TUI} +import play.api.libs.json.Json + +import scala.io.Source object LearnDuel { def main(args: Array[String]): Unit = { - val gameState = Game() + val jsonString = Source.fromResource("questions.json").getLines.mkString("\n") + val json = Json.parse(jsonString) + val questions = Json.fromJson[List[Question]](json).getOrElse(List()) + + val gameState = Game(questions = questions) val controller = Controller.create(gameState) val tui = TUI.create(controller) - val gui = GUI.create(controller) + GUI.create(controller) tui.processInput(new BufferedReader(Console.in)) } diff --git a/src/main/scala/de/htwg/se/learn_duel/Observer.scala b/src/main/scala/de/htwg/se/learn_duel/Observer.scala index d522170..a930f44 100644 --- a/src/main/scala/de/htwg/se/learn_duel/Observer.scala +++ b/src/main/scala/de/htwg/se/learn_duel/Observer.scala @@ -2,14 +2,15 @@ package de.htwg.se.learn_duel object UpdateAction extends Enumeration { type UpdateAction = Value - val CLOSE_APPLICATION, START_GAME, SHOW_HELP = Value + val CLOSE_APPLICATION, PLAYER_UPDATE, SHOW_HELP, + SHOW_GAME, SHOW_RESULT, UPDATE_TIMER = Value } import UpdateAction._ import de.htwg.se.learn_duel.model.Game -class UpdateData(updateAction: UpdateAction, gameState: Option[Game]) { +class UpdateData(updateAction: UpdateAction, gameState: Game) { def getAction(): UpdateAction = updateAction - def getState(): Option[Game] = gameState + def getState(): Game = gameState } trait Observer { From 6a1ccfccf565d4a93123f33189dc6d54abae05b5 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 3 Jan 2018 19:00:03 +0100 Subject: [PATCH 21/53] changed model classes for json (de)serialization and new spec --- .../de/htwg/se/learn_duel/model/Game.scala | 38 +++++++++++++++++-- .../de/htwg/se/learn_duel/model/Player.scala | 33 +++++++++++++++- .../htwg/se/learn_duel/model/impl/Game.scala | 28 ++++++++++---- .../se/learn_duel/model/impl/Player.scala | 13 ++++--- 4 files changed, 94 insertions(+), 18 deletions(-) diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Game.scala b/src/main/scala/de/htwg/se/learn_duel/model/Game.scala index 946bda3..1d3cdd3 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/Game.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/Game.scala @@ -1,11 +1,43 @@ package de.htwg.se.learn_duel.model +import de.htwg.se.learn_duel.model.impl.{Game => GameImpl, Player => PlayerImpl, Question => QuestionImpl} +import play.api.libs.json.{JsPath, Json, Reads, Writes} +import play.api.libs.functional.syntax._ + trait Game { - protected var _players: List[Player] = List() - var helpText: String = "" - def players: List[Player] = _players + var helpText: String + var players: List[Player] + var questions: List[Question] + var currentQuestion: Option[Question] + var currentQuestionTime: Option[Int] def addPlayer(player: Player): Unit def removePlayer(player: Player): Unit def playerCount(): Int + + def addQuestion(question: Question): Unit + def removeQuestion(question: Question): Unit + def questionCount(): Int +} + +object Game { + val maxPlayerCount = 2 + + implicit val gameWrites = new Writes[Game] { + def writes(game: Game) = Json.obj( + "helpText" -> game.helpText, + "players" -> game.players, + "questions" -> game.questions, + "currentQuestion" -> game.currentQuestion, + "currentQuestionTime" -> game.currentQuestionTime + ) + } + + implicit val questionReads: Reads[Game] = ( + (JsPath \ "helpText").read[String] and + (JsPath \ "players").read[List[Player]] and + (JsPath \ "questions").read[List[Question]] and + (JsPath \ "currentQuestion").readNullable[Question] and + (JsPath \ "currentQuestionTime").readNullable[Int] + )(GameImpl.apply _) } diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Player.scala b/src/main/scala/de/htwg/se/learn_duel/model/Player.scala index a1e0885..824ff56 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/Player.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/Player.scala @@ -1,7 +1,36 @@ package de.htwg.se.learn_duel.model +import de.htwg.se.learn_duel.model.impl.{Player => PlayerImpl} +import play.api.libs.json.{JsPath, Json, Reads, Writes} +import play.api.libs.functional.syntax._ + trait Player { - var points: Int = 0 - def name: String + val name: String + var points: Int + var correctAnswers: List[Question] + var wrongAnswers: List[Question] def toString: String } + +object Player { + val baseName = "Player" + def create(name: String): PlayerImpl = { + PlayerImpl(name) + } + + implicit val playerWrites = new Writes[Player] { + def writes(player: Player) = Json.obj( + "name" -> player.name, + "points" -> player.points, + "correctAnswers" -> player.correctAnswers, + "wrongAnswers" -> player.wrongAnswers + ) + } + + implicit val playerReads: Reads[Player] = ( + (JsPath \ "name").read[String] and + (JsPath \ "points").read[Int] and + (JsPath \ "correctAnswers").read[List[Question]] and + (JsPath \ "wrongAnswers").read[List[Question]] + )(PlayerImpl.apply _) +} diff --git a/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala index 0217dab..d194ffb 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala @@ -1,17 +1,29 @@ package de.htwg.se.learn_duel.model.impl -import de.htwg.se.learn_duel.model.{Player => PlayerTrait, Game => GameTrait} +import java.security.InvalidParameterException -case class Game() extends GameTrait { +import de.htwg.se.learn_duel.model.{Game => GameTrait, Player => PlayerTrait, Question => QuestionTrait} + +case class Game( + var helpText: String = "", + var players: List[PlayerTrait] = List(), + var questions: List[QuestionTrait] = List(), + var currentQuestion: Option[QuestionTrait] = None, + var currentQuestionTime: Option[Int] = None +) extends GameTrait { addPlayer(new Player("Player1")) - override def addPlayer(player: PlayerTrait): Unit = _players = player :: _players + override def addPlayer(player: PlayerTrait): Unit = players = players :+ player - override def removePlayer(player: PlayerTrait): Unit = _players = _players.filter(_ != player) + override def removePlayer(player: PlayerTrait): Unit = players = players.filter(_ != player) - override def playerCount(): Int = _players.size -} + override def playerCount(): Int = players.size + + override def addQuestion(question: QuestionTrait): Unit = questions :+ question -object Game { - val maxPlayerCount = 4 + override def removeQuestion(question: QuestionTrait): Unit = questions = questions.filter(_ != question) + + override def questionCount(): Int = questions.size } + + diff --git a/src/main/scala/de/htwg/se/learn_duel/model/impl/Player.scala b/src/main/scala/de/htwg/se/learn_duel/model/impl/Player.scala index 50fd05c..c2079f3 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/impl/Player.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/impl/Player.scala @@ -2,9 +2,14 @@ package de.htwg.se.learn_duel.model.impl import java.security.InvalidParameterException -import de.htwg.se.learn_duel.model.{Player => PlayerTrait} +import de.htwg.se.learn_duel.model.{Player => PlayerTrait, Question => QuestionTrait} -case class Player(name: String) extends PlayerTrait { +case class Player( + name: String, + var points: Int = 0, + var correctAnswers: List[QuestionTrait] = List(), + var wrongAnswers: List[QuestionTrait] = List() +) extends PlayerTrait { if (name.isEmpty) { throw new InvalidParameterException("Player name cannot be empty") } else if (!name.matches("\\S+")) { @@ -14,6 +19,4 @@ case class Player(name: String) extends PlayerTrait { override def toString: String = name } -object Player { - val baseName = "Player" -} + From d91fa548ce2333c1cce930ea2f0765ad5c9cb933 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 3 Jan 2018 19:00:42 +0100 Subject: [PATCH 22/53] added new controller functionality (almost complete) --- .../se/learn_duel/controller/Controller.scala | 13 +- .../controller/impl/Controller.scala | 148 +++++++++++++++--- 2 files changed, 141 insertions(+), 20 deletions(-) diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala index 68a62f0..51ab807 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala @@ -1,14 +1,23 @@ package de.htwg.se.learn_duel.controller import de.htwg.se.learn_duel.Observable +import de.htwg.se.learn_duel.controller.impl.{Controller => ControllerImpl} +import de.htwg.se.learn_duel.model.{Game, Player, Question} trait Controller extends Observable { - def nextPlayerName: String + def nextPlayerName: Option[String] def getCurrentPlayers: List[String] def addPlayer(name: Option[String]): Unit def removePlayer(name: String): Unit - def getMaxPlayerCount: Int + def maxPlayerCount: Int def onHelp: Unit def onStartGame: Unit def onClose: Unit + def answerChosen(input: Int) +} + +object Controller { + def create(gameState: Game): ControllerImpl = { + new ControllerImpl(gameState) + } } diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala index 0accb27..8e11d3e 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala @@ -1,22 +1,26 @@ package de.htwg.se.learn_duel.controller.impl -import de.htwg.se.learn_duel.{UpdateAction, UpdateData} +import java.util.{Timer, TimerTask} + import de.htwg.se.learn_duel.controller.impl.exceptions._ import de.htwg.se.learn_duel.controller.{Controller => ControllerTrait} -import de.htwg.se.learn_duel.model.impl.Player -import de.htwg.se.learn_duel.model.impl.Game +import de.htwg.se.learn_duel.model.{Game, Player, Question} +import de.htwg.se.learn_duel.{UpdateAction, UpdateData} +import sun.plugin.dom.exception.InvalidStateException -class Controller private (gameState: Game) extends ControllerTrait { - override def nextPlayerName: String = Player.baseName + (gameState.playerCount + 1).toString +class Controller(gameState: Game) extends ControllerTrait { + protected var questionIter: Iterator[Question] = Iterator.empty + protected var timer: Option[Timer] = None override def getCurrentPlayers: List[String] = { gameState.players.map(p => p.name) } + // FIXME undo / redo pattern for player adding and removing override def addPlayer(name: Option[String]): Unit = { var playerName = name match { case Some(name) => name - case None => nextPlayerName + case None => nextPlayerName.getOrElse("") // will not be used if None } if (gameState.playerCount == Game.maxPlayerCount) { @@ -27,13 +31,22 @@ class Controller private (gameState: Game) extends ControllerTrait { } try { - gameState.addPlayer(Player(playerName)) + gameState.addPlayer(Player.create(playerName)) } catch { case e: Throwable => throw ControllerProcedureFailed("Adding player failed: " + e.getMessage) } - notifyObservers(new UpdateData(UpdateAction.CLOSE_APPLICATION, Option(null))) + notifyObservers(new UpdateData(UpdateAction.PLAYER_UPDATE, gameState)) } + override def nextPlayerName: Option[String] = { + gameState.playerCount match { + case c if c < maxPlayerCount => Some(Player.baseName + (gameState.playerCount + 1).toString) + case _ => None + } + } + + override def maxPlayerCount: Int = Game.maxPlayerCount + override def removePlayer(name: String): Unit = { if (gameState.playerCount == 1) { throw NotEnoughPlayersException("There are not enough players to remove one") @@ -42,13 +55,11 @@ class Controller private (gameState: Game) extends ControllerTrait { gameState.players.find(p => p.name == name) match { case Some(p) => gameState.removePlayer(p) - notifyObservers(new UpdateData(UpdateAction.CLOSE_APPLICATION, Option(null))) + notifyObservers(new UpdateData(UpdateAction.PLAYER_UPDATE, gameState)) case None => throw PlayerNotExistingException(s"Player '$name' does not exist") } } - override def getMaxPlayerCount: Int = Game.maxPlayerCount - override def onHelp(): Unit = { if (gameState.helpText.isEmpty) { import scala.io.Source @@ -56,21 +67,122 @@ class Controller private (gameState: Game) extends ControllerTrait { gameState.helpText = helpText.mkString("\n") } - notifyObservers(new UpdateData(UpdateAction.SHOW_HELP, Option(gameState))) + notifyObservers(new UpdateData(UpdateAction.SHOW_HELP, gameState)) } override def onStartGame(): Unit = { + resetQuestionIterator() + if (!questionIter.hasNext) { + throw new InvalidStateException("Can't start game without questions"); + } + showGame() } override def onClose(): Unit = { - notifyObservers(new UpdateData(UpdateAction.CLOSE_APPLICATION, Option(null))) + notifyObservers(new UpdateData(UpdateAction.CLOSE_APPLICATION, gameState)) + } + + override def answerChosen(input: Int): Unit = { + val currentQuestion = gameState.currentQuestion.get + val correctAnswer = currentQuestion.correctAnswer + val player = input match { + case x if 0 until 5 contains x => Some(gameState.players(0)) + case x if (6 until 10 contains x) && (gameState.players.length > 1 )=> Some(gameState.players(1)) + case _ => None + } + + if (player.isDefined && !playerAnsweredQuestion(player.get, currentQuestion.id)) { + if (input == correctAnswer) { + player.get.points += currentQuestion.points + player.get.correctAnswers = player.get.correctAnswers :+ currentQuestion + } else { + player.get.wrongAnswers = player.get.wrongAnswers :+ currentQuestion + } + } + + // check if question was answered by all players + val allAnswered = gameState.players.forall(p => { + (p.correctAnswers ::: p.wrongAnswers).exists(q => { + q.id == currentQuestion.id + }) + }) + + if (allAnswered) { + nextQuestion + } + } + + protected def resetQuestionIterator(): Unit = { + questionIter = gameState.questions.iterator + } + + protected def stopTimer(): Unit = { + if (timer.isDefined) { + timer.get.cancel() + } + } + + protected def setUpTimer(): Unit = { + timer = Some(new Timer(true)) + val localTimer = timer.get + localTimer.scheduleAtFixedRate(new TimerTask { + override def run(): Unit = { + gameState.currentQuestionTime = gameState.currentQuestionTime match { + case Some(time) => { + val newTime = time - 1 + if (newTime == 0) { + nextQuestion + None + } else { + Some(newTime) + } + } + case None => None + } + + gameState.currentQuestionTime match { + case Some(time) if time % 5 == 0 => { + notifyObservers(new UpdateData(UpdateAction.UPDATE_TIMER, gameState)) + } + case None => stopTimer + case _ => + } + } + }, 1000, 1000) + } + + protected def nextQuestion(): Unit = { + // implicitely add not given answer as wrong answer + val currentQuestion = gameState.currentQuestion.get + val noAnswerPlayers: List[Player] = gameState.players.filter(p => { + !playerAnsweredQuestion(p, currentQuestion.id) + }) + + noAnswerPlayers.foreach(p => p.wrongAnswers = p.wrongAnswers :+ currentQuestion) + + if (questionIter.hasNext) { + showGame() + } else { + stopTimer() + notifyObservers(new UpdateData(UpdateAction.SHOW_RESULT, gameState)) + } + } + + protected def showGame(): Unit = { + stopTimer() + + val nextQuestion = questionIter.next() + gameState.currentQuestion = Some(nextQuestion) + gameState.currentQuestionTime = Some(nextQuestion.time) + setUpTimer() + + notifyObservers((new UpdateData(UpdateAction.SHOW_GAME, gameState))) } -} -object Controller { - // FIXME Builder Pattern - def create(gameState: Game): Controller = { - new Controller(gameState) + protected def playerAnsweredQuestion(p: Player, questionId: Int): Boolean = { + (p.correctAnswers ::: p.wrongAnswers).exists(q => { + q.id == questionId + }) } } From d4c0cd39b038d043142541c7bd3e98d889ac7544 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 3 Jan 2018 19:01:09 +0100 Subject: [PATCH 23/53] changed and added views to display new controller functionality --- .../scala/de/htwg/se/learn_duel/view/UI.scala | 33 +++- .../de/htwg/se/learn_duel/view/impl/TUI.scala | 175 +++++++++++++----- .../se/learn_duel/view/impl/gui/GUI.scala | 103 +++++++---- .../learn_duel/view/impl/gui/GameStage.scala | 110 +++++++++-- .../learn_duel/view/impl/gui/MenuStage.scala | 50 ++++- .../view/impl/gui/ResultStage.scala | 51 +++++ 6 files changed, 418 insertions(+), 104 deletions(-) create mode 100644 src/main/scala/de/htwg/se/learn_duel/view/impl/gui/ResultStage.scala diff --git a/src/main/scala/de/htwg/se/learn_duel/view/UI.scala b/src/main/scala/de/htwg/se/learn_duel/view/UI.scala index 6c32806..dc06d87 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/UI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/UI.scala @@ -1,8 +1,39 @@ package de.htwg.se.learn_duel.view +import java.util.concurrent.CountDownLatch + import de.htwg.se.learn_duel.Observer +import de.htwg.se.learn_duel.controller.Controller +import de.htwg.se.learn_duel.model.{Player, Question} +import de.htwg.se.learn_duel.view.impl.TUI +import de.htwg.se.learn_duel.view.impl.gui.GUI + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future trait UI extends Observer { def displayMenu(): Unit - def displayGame(): Unit + def displayGame(currentQuestion: Question, multiplayer: Boolean): Unit + def displayResult(players: List[Player]): Unit +} + +object TUI { + def create(controller: Controller): TUI = { + new TUI(controller) + } +} + +object GUI { + def create(controller: Controller): Unit = { + val latch = new CountDownLatch(1) + val gui = new GUI(controller, latch) + + // run GUI on its own thread + Future { + gui.main(Array()) + } + + // wait for initialization of JFXApp to be done + latch.await() + } } diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala index 927e03b..4e6d1c5 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala @@ -1,88 +1,167 @@ package de.htwg.se.learn_duel.view.impl -import java.io.{BufferedReader, StringReader} +import java.io.BufferedReader +import java.util.{Timer, TimerTask} +import com.typesafe.scalalogging.LazyLogging import de.htwg.se.learn_duel.{Observer, UpdateAction, UpdateData} import de.htwg.se.learn_duel.controller.{Controller, ControllerException} +import de.htwg.se.learn_duel.model.{Player, Question} import de.htwg.se.learn_duel.view.UI -import scalafx.collections.ObservableBuffer.Update - -class TUI private (controller: Controller) extends UI with Observer { +class TUI (controller: Controller) extends UI with Observer with LazyLogging { controller.addObserver(this) var stopProcessingInput = false + var inMenu = true + var inGame = false displayMenu + def displayPlayers(): Unit = { + logger.info("Current players: " + controller.getCurrentPlayers.mkString(", ")) + } + override def displayMenu(): Unit = { + logger.info("") + logger.info("Welcome to Learn Duel") + displayPlayers() + logger.info("n => new game") + logger.info("a [name] => add player") + logger.info("r [name] => remove player") + logger.info("h => show help") + logger.info("q => exit") + logger.info("") + } - println("") - println("Welcome to Learn Duel") - println("Current players: " + controller.getCurrentPlayers.mkString(", ")) - println("n => new game") - println("a [name] => add player") - println("r [name] => remove player") - println("h => show help") - println("q => exit") - println("") + override def displayGame(question: Question, multiplayer: Boolean): Unit = { + logger.info(question.text) + question.answers.zipWithIndex.foreach {case (ans, i) => { + logger.info((i + 1) + "/" + (i + 5) + ": " + ans.text) + }} } - override def displayGame(): Unit = { + override def displayResult(players: List[Player]): Unit = { + logger.info("") + logger.info("RESULT:") + players.foreach(p => { + logger.info("Player '" + p.name + "':") + logger.info("Points: " + p.points) + logger.info("Correct answers:") + p.correctAnswers.foreach(q => { + logger.info("\t" + q.text) + }) + logger.info("Wrong answers:") + p.wrongAnswers.foreach(q => { + logger.info("\t" + q.text) + val correctAnswer = q.answers.find(a => a.id == q.correctAnswer).get + logger.info("\tcorrect answer is: " + correctAnswer.text) + }) + }) + + val player = players.max[Player]{ case (p1: Player, p2: Player) => { + p1.points.compareTo(p2.points) + }} + + logger.info("") + logger.info("'" + player.name + "' won the game!") + logger.info("") } + // scalastyle:off override def update(updateParam: UpdateData): Unit = { updateParam.getAction() match { case UpdateAction.CLOSE_APPLICATION => stopProcessingInput = true case UpdateAction.SHOW_HELP => { - val helpText = updateParam.getState() match { - case Some(gameState) => gameState.helpText - case None => "No help available." - } - println(helpText) + logger.info(updateParam.getState().helpText) + } + case UpdateAction.PLAYER_UPDATE => displayPlayers + case UpdateAction.SHOW_GAME => { + displayGamePretty( + updateParam.getState().currentQuestion.get, + updateParam.getState().players.length > 1, + updateParam.getState().currentQuestionTime.get + ) + inMenu = false; + inGame = true; + } + case UpdateAction.UPDATE_TIMER => { + displayGamePretty( + updateParam.getState().currentQuestion.get, + updateParam.getState().players.length > 1, + updateParam.getState().currentQuestionTime.get + ) + } + case UpdateAction.SHOW_RESULT => { + displayResult(updateParam.getState().players) } case _ => } } - // scalastyle:off def processInput(input: BufferedReader): Unit = { - val playerPattern = """(?:a|r)(?:\s+(.*))?""".r - while (!stopProcessingInput) { if (input.ready()) { val line = input.readLine() - - // don't match input if we close the application anyway - try { - line match { - case "q" => { - controller.onClose - } - case "n" => - case playerPattern(name) if line.startsWith("a") => controller.addPlayer(Option(name)) - case playerPattern(name) if line.startsWith("r") => if (name != null) { - controller.removePlayer(name) - } - case "h" => controller.onHelp - case _ => println("Unknown command") - } - } catch { - case e: ControllerException => println(e.getMessage) + if (inMenu){ + processMenuInput(line) + } else if (inGame) { + processGameInput(line) + } else { + processResultInput(line) } - - // check if the application should be closed after parsing input - // if yes, don't repeat the menu - if (!stopProcessingInput) { - displayMenu + } else { + Thread.sleep(200) // don't waste cpu cycles if no input is given + } + } + } + protected def processMenuInput(line: String): Unit = { + val playerPattern = """(?:a|r)(?:\s+(.*))?""".r + try { + line match { + case "q" => controller.onClose; return + case "n" => controller.onStartGame; return + case playerPattern(name) if line.startsWith("a") => controller.addPlayer(Option(name)) + case playerPattern(name) if line.startsWith("r") => if (name != null) { + controller.removePlayer(name) } + case "h" => controller.onHelp; + case _ => logger.info("Unknown command") } + } catch { + case e: ControllerException => logger.error(e.getMessage) + } + + displayMenu + } + + protected def processGameInput(line: String): Unit = { + line match { + case "q" => controller.onClose + case "1" => controller.answerChosen(1) + case "2" => controller.answerChosen(2) + case "3" => controller.answerChosen(3) + case "4" => controller.answerChosen(4) + case "6" => controller.answerChosen(6) + case "7" => controller.answerChosen(7) + case "8" => controller.answerChosen(8) + case "9" => controller.answerChosen(9) + case _ => logger.info("Unknown command") + } + } + + protected def processResultInput(line: String): Unit = { + line match { + case "q" => controller.onClose + case _ => logger.info("Unknown command") } } // scalastyle:on -} -object TUI { - def create(controller: Controller): TUI = { - new TUI(controller) + protected def displayGamePretty(question: Question, multiplayer: Boolean, timeRemaining: Int): Unit = { + logger.info("") + displayGame(question, multiplayer) + logger.info("Time remaining: " + timeRemaining + "s") + logger.info("") } + } diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala index e06daee..4f46d9d 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala @@ -1,74 +1,97 @@ package de.htwg.se.learn_duel.view.impl.gui +import java.io.{PrintWriter, StringWriter} import java.util.concurrent.CountDownLatch import javafx.application.Platform -import de.htwg.se.learn_duel.{Observer, UpdateAction, UpdateData} -import de.htwg.se.learn_duel.controller.Controller +import com.typesafe.scalalogging.LazyLogging +import de.htwg.se.learn_duel.controller.{Controller, ControllerException} +import de.htwg.se.learn_duel.model.{Player, Question} import de.htwg.se.learn_duel.view.UI +import de.htwg.se.learn_duel.{Observer, UpdateAction, UpdateData} -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global import scalafx.application.JFXApp -import scalafx.application.JFXApp.PrimaryStage -import scalafx.scene.control.{Button, ButtonType, Dialog} -import scalafx.stage.Modality -class GUI private (controller: Controller, latch: CountDownLatch) extends JFXApp with UI with Observer { +class GUI (controller: Controller, latch: CountDownLatch) extends JFXApp with UI with Observer with LazyLogging { controller.addObserver(this) displayMenu() - this.stage.onCloseRequest = {(_) => + this.stage.onCloseRequest = { (_) => controller.onClose } + // handle self defined exception in a 'global' exception handler + Thread.currentThread().setUncaughtExceptionHandler((t: Thread, e: Throwable) => { + e.getCause match { + case cause: ControllerException => { + new InfoPopup("Error", cause.message).show + } + case _ => { + val sw = new StringWriter() + e.printStackTrace(new PrintWriter(sw)) + logger.error(Console.RED + sw.toString + Console.RESET) + controller.onClose + } + } + }) + // signal initialization down latch.countDown() - override def displayMenu(): Unit = { - - this.stage = new MenuStage( - - => { - controller.onStartGame - }, - _ => { - controller.onHelp - }) - } - - override def displayGame(): Unit = { - this.stage = new GameStage - } - override def update(updateParam: UpdateData): Unit = { // every update needs to be run on the JavaFX Application thread - Platform.runLater {() => + Platform.runLater { () => updateParam.getAction() match { case UpdateAction.CLOSE_APPLICATION => this.stage.close() case UpdateAction.SHOW_HELP => { - val helpText = updateParam.getState() match { - case Some(gameState) => gameState.helpText - case None => "No help available." - } + val helpText = updateParam.getState().helpText val dlg = new InfoPopup("Learn Duel Help", helpText) dlg.show } + case UpdateAction.PLAYER_UPDATE => displayMenu + case UpdateAction.SHOW_GAME => { + displayGame( + updateParam.getState().currentQuestion.get, + updateParam.getState().players.length > 1 + ) + } + case UpdateAction.UPDATE_TIMER => { + updateParam.getState().currentQuestionTime match { + case Some(time) => { + if (this.stage.isInstanceOf[GameStage]) { + this.stage.asInstanceOf[GameStage].updateTime(time) + } + } + case _ => + } + } + case UpdateAction.SHOW_RESULT => { + displayResult(updateParam.getState().players) + } case _ => } } } -} -object GUI { - def create(controller: Controller): Unit = { - val latch = new CountDownLatch(1) - val gui = new GUI(controller, latch) + override def displayMenu(): Unit = { + this.stage = new MenuStage( + - => controller.onStartGame, + _ => controller.onHelp, + (controller.getCurrentPlayers, controller.nextPlayerName), + (name) => controller.addPlayer(Some(name)), + controller.removePlayer + ) + } - // run GUI on its own thread - Future { - gui.main(Array()) - } + override def displayGame(question: Question, multiplayer: Boolean): Unit = { + this.stage = new GameStage( + question, + !multiplayer, + controller.answerChosen + ) + } - // wait for initialization of JFXApp to be done - latch.await() + override def displayResult(players: List[Player]): Unit = { + this.stage = new ResultStage(players) } } + diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GameStage.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GameStage.scala index ab4b1ed..0bdf425 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GameStage.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GameStage.scala @@ -1,23 +1,109 @@ package de.htwg.se.learn_duel.view.impl.gui +import java.util.{Timer, TimerTask} +import javafx.beans.property.SimpleStringProperty +import javafx.scene.input.KeyCode + +import de.htwg.se.learn_duel.model.Question + import scalafx.Includes._ import scalafx.application.JFXApp.PrimaryStage import scalafx.scene.Scene +import scalafx.scene.control.Button +import scalafx.scene.layout.{TilePane, VBox} +import scalafx.scene.paint.Color import scalafx.scene.paint.Color._ -import scalafx.scene.shape.Rectangle +import scalafx.scene.text.Text + +class GameStage( + question: Question, + allowMouseInput: Boolean, + onInput: Function[Int, Unit] +) extends PrimaryStage { + var timeRemaining = question.time + var timerText = new SimpleStringProperty { + "Time remaining: " + timeRemaining + "s" + } + var timer: Option[Timer] = None + + title.value = "Learn Duel Game" + width = 640 + height = 480 -class GameStage extends PrimaryStage { - title.value = "Game" - width = 480 - height = 640 scene = new Scene { - fill = LightGreen - content = new Rectangle { - x = 25 - y = 40 - width = 100 - height = 100 - fill <== when(hover) choose Yellow otherwise Blue + fill = White + root = new VBox { + children += new Text(question.text) + + val pane = new TilePane { + prefColumns = 2 + + question.answers.zipWithIndex.foreach { case (ans, i) => + val btn = new Button { + text = "Answer " + (i + 1) + ": " + ans.text + if (allowMouseInput) { + onAction = _ => onInput(ans.id) + } + minWidth = 200 + } + + children += btn + } + } + children += pane + + val timer = new Text() + timer.text.bind(timerText) + children += timer + + if (!allowMouseInput) { + val warning = new Text { + text = "Mouse input not allowed, use keyboard instead" + fill = Color.RED + } + + children += warning + } + } + + onKeyReleased = { e => { + e.getCode() match { + case KeyCode.DIGIT1 => onInput(1) + case KeyCode.DIGIT2 => onInput(2) + case KeyCode.DIGIT3 => onInput(3) + case KeyCode.DIGIT4 => onInput(4) + case KeyCode.DIGIT6 => onInput(6) + case KeyCode.DIGIT7 => onInput(7) + case KeyCode.DIGIT8 => onInput(8) + case KeyCode.DIGIT9 => onInput(9) + case _ => + } + }} + } + + setUpTimer() + + def updateTime(timeRemaining: Int): Unit = { + if (timer.isDefined) { + timer.get.cancel() } + + this.timeRemaining = timeRemaining + setUpTimer() + } + + protected def setUpTimer(): Unit = { + timer = Some(new Timer(true)) + val localTimer = timer.get + + localTimer.scheduleAtFixedRate(new TimerTask{ + override def run(): Unit = { + timerText.set("Time remaining: " + timeRemaining + "s") + timeRemaining -= 1 + if (timeRemaining < 0) { + localTimer.cancel() + } + } + }, 0, 1000) } } diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala index b74bad0..811c345 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala @@ -5,14 +5,17 @@ import javafx.event.{ActionEvent, EventHandler} import scalafx.Includes._ import scalafx.application.JFXApp.PrimaryStage import scalafx.scene.Scene -import scalafx.scene.control.Button -import scalafx.scene.layout.VBox +import scalafx.scene.control.{Button, TextField} +import scalafx.scene.layout.{HBox, VBox} import scalafx.scene.paint.Color._ import scalafx.scene.text.Text class MenuStage( newGameAction: EventHandler[ActionEvent], - helpAction: EventHandler[ActionEvent] + helpAction: EventHandler[ActionEvent], + playerInfo: (List[String], Option[String]), + playerAddAction: Function[String, Unit], + playerRemoveAction: Function[String, Unit] ) extends PrimaryStage { title.value = "Learn Duel Menu" resizable = false @@ -30,6 +33,47 @@ class MenuStage( } children += newGameButton + for (playerName <- playerInfo._1) { + val hBox = new HBox { + val txtFld = new TextField { + text = playerName + editable = false + } + children += txtFld + + val removeBtn = new Button { + text = "-" + onAction = - => playerRemoveAction(playerName) + } + children += removeBtn + } + children += hBox + } + + playerInfo._2 match { + case Some(nextPlayername) => { + val hBox = new HBox { + val txtField = new TextField { + promptText = nextPlayername + } + children += txtField + + val addBtn = new Button { + text = "+" + onAction = - => playerAddAction( + if (txtField.getText.isEmpty) { + txtField.getPromptText + } else { + txtField.getText + } + ) + } + children += addBtn + } + children += hBox + } + case _ => + } val helpButton = new Button { text = "?" diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/ResultStage.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/ResultStage.scala new file mode 100644 index 0000000..d23de85 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/ResultStage.scala @@ -0,0 +1,51 @@ +package de.htwg.se.learn_duel.view.impl.gui + +import de.htwg.se.learn_duel.model.Player + +import scalafx.Includes._ +import scalafx.application.JFXApp.PrimaryStage +import scalafx.scene.{Node, Scene} +import scalafx.scene.layout.VBox +import scalafx.scene.paint.Color._ +import scalafx.scene.text.Text + +class ResultStage( + players: List[Player] +) extends PrimaryStage { + title.value = "Learn Duel Result" + width = 480 + height = 640 + + scene = new Scene { + fill = White + root = new VBox { + players.foreach { p => { + children.addAll( + new Text("Player '" + p.name + "': "), + new Text("Points: " + p.points), + new Text("Correct answers:") + ) + + p.correctAnswers.foreach(q => { + children.add(new Text("\t" + q.text)) + }) + + children.add(new Text("Wrong answers:")) + + p.wrongAnswers.foreach(q => { + val correctAnswer = q.answers.find(a => a.id == q.correctAnswer).get + children.addAll( + new Text("\t" + q.text), + new Text("\tcorrect answer is: " + correctAnswer.text) + ) + }) + }} + + val player = players.max[Player]{ case (p1: Player, p2: Player) => { + p1.points.compareTo(p2.points) + }} + + children.add(new Text("'" + player.name + "' won the game!")) + } + } +} From b4c5ce3e1a9cee91f382ea15c8ed1117793dd272 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 3 Jan 2018 19:04:38 +0100 Subject: [PATCH 24/53] fixed some minor errors --- .../se/learn_duel/controller/impl/Controller.scala | 3 +-- src/main/scala/de/htwg/se/learn_duel/view/UI.scala | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala index 8e11d3e..6a3eee6 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala @@ -6,7 +6,6 @@ import de.htwg.se.learn_duel.controller.impl.exceptions._ import de.htwg.se.learn_duel.controller.{Controller => ControllerTrait} import de.htwg.se.learn_duel.model.{Game, Player, Question} import de.htwg.se.learn_duel.{UpdateAction, UpdateData} -import sun.plugin.dom.exception.InvalidStateException class Controller(gameState: Game) extends ControllerTrait { protected var questionIter: Iterator[Question] = Iterator.empty @@ -73,7 +72,7 @@ class Controller(gameState: Game) extends ControllerTrait { override def onStartGame(): Unit = { resetQuestionIterator() if (!questionIter.hasNext) { - throw new InvalidStateException("Can't start game without questions"); + throw new IllegalStateException("Can't start game without questions"); } showGame() diff --git a/src/main/scala/de/htwg/se/learn_duel/view/UI.scala b/src/main/scala/de/htwg/se/learn_duel/view/UI.scala index dc06d87..76733a5 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/UI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/UI.scala @@ -5,8 +5,8 @@ import java.util.concurrent.CountDownLatch import de.htwg.se.learn_duel.Observer import de.htwg.se.learn_duel.controller.Controller import de.htwg.se.learn_duel.model.{Player, Question} -import de.htwg.se.learn_duel.view.impl.TUI -import de.htwg.se.learn_duel.view.impl.gui.GUI +import de.htwg.se.learn_duel.view.impl.{ TUI => TUIImpl } +import de.htwg.se.learn_duel.view.impl.gui.{ GUI => GUIImpl } import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -18,15 +18,15 @@ trait UI extends Observer { } object TUI { - def create(controller: Controller): TUI = { - new TUI(controller) + def create(controller: Controller): TUIImpl = { + new TUIImpl(controller) } } object GUI { def create(controller: Controller): Unit = { val latch = new CountDownLatch(1) - val gui = new GUI(controller, latch) + val gui = new GUIImpl(controller, latch) // run GUI on its own thread Future { From c7ff4685cbe264409f6e8cda29106cb672b074d6 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Fri, 5 Jan 2018 14:42:21 +0100 Subject: [PATCH 25/53] added undo/redo with command pattern --- .../se/learn_duel/controller/Controller.scala | 10 ++- .../controller/impl/Controller.scala | 87 ++++++++++++------- .../se/learn_duel/model/command/Command.scala | 7 ++ .../model/command/CommandInvoker.scala | 17 ++++ .../model/command/impl/CommandInvoker.scala | 28 ++++++ .../model/command/impl/PlayerAddCommand.scala | 24 +++++ .../command/impl/PlayerRemoveCommand.scala | 22 +++++ .../de/htwg/se/learn_duel/view/impl/TUI.scala | 37 ++++---- .../se/learn_duel/view/impl/gui/GUI.scala | 9 +- 9 files changed, 182 insertions(+), 59 deletions(-) create mode 100644 src/main/scala/de/htwg/se/learn_duel/model/command/Command.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/model/command/CommandInvoker.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/model/command/impl/CommandInvoker.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerAddCommand.scala create mode 100644 src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerRemoveCommand.scala diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala index 51ab807..0535869 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala @@ -6,14 +6,16 @@ import de.htwg.se.learn_duel.model.{Game, Player, Question} trait Controller extends Observable { def nextPlayerName: Option[String] - def getCurrentPlayers: List[String] - def addPlayer(name: Option[String]): Unit - def removePlayer(name: String): Unit + def getPlayerNames: List[String] def maxPlayerCount: Int + def onAddPlayer(name: Option[String]): Unit + def onRemovePlayer(name: String): Unit + def onPlayerActionUndo: Unit + def onPlayerActionRedo: Unit def onHelp: Unit def onStartGame: Unit def onClose: Unit - def answerChosen(input: Int) + def onAnswerChosen(input: Int) } object Controller { diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala index 6a3eee6..02e23aa 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala @@ -4,37 +4,34 @@ import java.util.{Timer, TimerTask} import de.htwg.se.learn_duel.controller.impl.exceptions._ import de.htwg.se.learn_duel.controller.{Controller => ControllerTrait} +import de.htwg.se.learn_duel.model.command.CommandInvoker +import de.htwg.se.learn_duel.model.command.impl.{PlayerAddCommand, PlayerRemoveCommand} import de.htwg.se.learn_duel.model.{Game, Player, Question} import de.htwg.se.learn_duel.{UpdateAction, UpdateData} class Controller(gameState: Game) extends ControllerTrait { protected var questionIter: Iterator[Question] = Iterator.empty protected var timer: Option[Timer] = None + protected val invoker = CommandInvoker.create - override def getCurrentPlayers: List[String] = { + override def getPlayerNames: List[String] = { gameState.players.map(p => p.name) } + + override def onAddPlayer(name: Option[String]) : Unit = { + invoker.execute(new PlayerAddCommand(name, addPlayer, removePlayer)) + } - // FIXME undo / redo pattern for player adding and removing - override def addPlayer(name: Option[String]): Unit = { - var playerName = name match { - case Some(name) => name - case None => nextPlayerName.getOrElse("") // will not be used if None - } + override def onRemovePlayer(name: String): Unit = { + invoker.execute(new PlayerRemoveCommand(name, removePlayer, addPlayer)) + } - if (gameState.playerCount == Game.maxPlayerCount) { - throw TooManyPlayersException("There are too many players to add another one") - } - else if (gameState.players.exists(p => p.name == playerName)) { - throw PlayerExistsException(s"'$playerName' already exists") - } + override def onPlayerActionUndo: Unit = { + invoker.undo + } - try { - gameState.addPlayer(Player.create(playerName)) - } catch { - case e: Throwable => throw ControllerProcedureFailed("Adding player failed: " + e.getMessage) - } - notifyObservers(new UpdateData(UpdateAction.PLAYER_UPDATE, gameState)) + override def onPlayerActionRedo: Unit = { + invoker.redo } override def nextPlayerName: Option[String] = { @@ -46,19 +43,6 @@ class Controller(gameState: Game) extends ControllerTrait { override def maxPlayerCount: Int = Game.maxPlayerCount - override def removePlayer(name: String): Unit = { - if (gameState.playerCount == 1) { - throw NotEnoughPlayersException("There are not enough players to remove one") - } - - gameState.players.find(p => p.name == name) match { - case Some(p) => - gameState.removePlayer(p) - notifyObservers(new UpdateData(UpdateAction.PLAYER_UPDATE, gameState)) - case None => throw PlayerNotExistingException(s"Player '$name' does not exist") - } - } - override def onHelp(): Unit = { if (gameState.helpText.isEmpty) { import scala.io.Source @@ -82,7 +66,8 @@ class Controller(gameState: Game) extends ControllerTrait { notifyObservers(new UpdateData(UpdateAction.CLOSE_APPLICATION, gameState)) } - override def answerChosen(input: Int): Unit = { + // scalastyle:off + override def onAnswerChosen(input: Int): Unit = { val currentQuestion = gameState.currentQuestion.get val correctAnswer = currentQuestion.correctAnswer val player = input match { @@ -111,6 +96,42 @@ class Controller(gameState: Game) extends ControllerTrait { nextQuestion } } + // scalastyle:on + + protected def addPlayer(name: Option[String]): String = { + var playerName = name match { + case Some(name) => name + case None => nextPlayerName.getOrElse("") // will not be used if None + } + + if (gameState.playerCount == Game.maxPlayerCount) { + throw TooManyPlayersException("There are too many players to add another one") + } + else if (gameState.players.exists(p => p.name == playerName)) { + throw PlayerExistsException(s"'$playerName' already exists") + } + + try { + gameState.addPlayer(Player.create(playerName)) + } catch { + case e: Throwable => throw ControllerProcedureFailed("Adding player failed: " + e.getMessage) + } + notifyObservers(new UpdateData(UpdateAction.PLAYER_UPDATE, gameState)) + playerName + } + + protected def removePlayer(name: String): Unit = { + if (gameState.playerCount == 1) { + throw NotEnoughPlayersException("There are not enough players to remove one") + } + + gameState.players.find(p => p.name == name) match { + case Some(p) => + gameState.removePlayer(p) + notifyObservers(new UpdateData(UpdateAction.PLAYER_UPDATE, gameState)) + case None => throw PlayerNotExistingException(s"Player '$name' does not exist") + } + } protected def resetQuestionIterator(): Unit = { questionIter = gameState.questions.iterator diff --git a/src/main/scala/de/htwg/se/learn_duel/model/command/Command.scala b/src/main/scala/de/htwg/se/learn_duel/model/command/Command.scala new file mode 100644 index 0000000..e1fdbe1 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/model/command/Command.scala @@ -0,0 +1,7 @@ +package de.htwg.se.learn_duel.model.command + +trait Command { + def execute(): Unit + def undo(): Unit + def redo(): Unit +} diff --git a/src/main/scala/de/htwg/se/learn_duel/model/command/CommandInvoker.scala b/src/main/scala/de/htwg/se/learn_duel/model/command/CommandInvoker.scala new file mode 100644 index 0000000..75c004f --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/model/command/CommandInvoker.scala @@ -0,0 +1,17 @@ +package de.htwg.se.learn_duel.model.command + +import de.htwg.se.learn_duel.model.command.impl.{ CommandInvoker => CommandInvokerImpl } + +trait CommandInvoker { + var undoCommands: List[Command] = List() + var redoCommands: List[Command] = List() + def execute(cmd: Command): Unit + def undo(): Unit + def redo(): Unit +} + +object CommandInvoker { + def create(): CommandInvokerImpl = { + CommandInvokerImpl() + } +} diff --git a/src/main/scala/de/htwg/se/learn_duel/model/command/impl/CommandInvoker.scala b/src/main/scala/de/htwg/se/learn_duel/model/command/impl/CommandInvoker.scala new file mode 100644 index 0000000..1882b28 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/model/command/impl/CommandInvoker.scala @@ -0,0 +1,28 @@ +package de.htwg.se.learn_duel.model.command.impl + +import de.htwg.se.learn_duel.model.command.{Command, CommandInvoker => CommandInvokerTrait} + +case class CommandInvoker() extends CommandInvokerTrait { + override def execute(cmd: Command): Unit = { + cmd.execute() + undoCommands = undoCommands :+ cmd + redoCommands = List() + } + + override def undo(): Unit = { + if (undoCommands.nonEmpty) { + val lastCommand = undoCommands.last + lastCommand.undo + redoCommands = redoCommands :+ lastCommand + undoCommands = undoCommands diff List(lastCommand) + } + } + + override def redo(): Unit = { + if (redoCommands.nonEmpty) { + redoCommands.head.redo + undoCommands = undoCommands :+ redoCommands.head + redoCommands = redoCommands diff List(redoCommands.head) + } + } +} diff --git a/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerAddCommand.scala b/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerAddCommand.scala new file mode 100644 index 0000000..f4c2fe1 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerAddCommand.scala @@ -0,0 +1,24 @@ +package de.htwg.se.learn_duel.model.command.impl + +import de.htwg.se.learn_duel.controller.Controller +import de.htwg.se.learn_duel.model.command.Command + +case class PlayerAddCommand( + name: Option[String], + addPlayer: Function[Option[String], String], + removePlayer: Function[String, Unit] +) extends Command { + var actualName: String = "" + + override def execute(): Unit = { + actualName = addPlayer(name) + } + + override def undo(): Unit = { + removePlayer(actualName) + } + + override def redo(): Unit = { + execute() + } +} diff --git a/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerRemoveCommand.scala b/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerRemoveCommand.scala new file mode 100644 index 0000000..22c4dfa --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerRemoveCommand.scala @@ -0,0 +1,22 @@ +package de.htwg.se.learn_duel.model.command.impl + +import de.htwg.se.learn_duel.controller.Controller +import de.htwg.se.learn_duel.model.command.Command + +case class PlayerRemoveCommand( + name: String, + remvoePlayer: Function[String, Unit], + addPlayer: Function[Option[String], String] +) extends Command { + override def execute(): Unit = { + remvoePlayer(name) + } + + override def undo(): Unit = { + addPlayer(Some(name)) + } + + override def redo(): Unit = { + execute() + } +} diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala index 4e6d1c5..fdb91e8 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala @@ -17,7 +17,7 @@ class TUI (controller: Controller) extends UI with Observer with LazyLogging { displayMenu def displayPlayers(): Unit = { - logger.info("Current players: " + controller.getCurrentPlayers.mkString(", ")) + logger.info("Current players: " + controller.getPlayerNames.mkString(", ")) } override def displayMenu(): Unit = { @@ -118,33 +118,36 @@ class TUI (controller: Controller) extends UI with Observer with LazyLogging { val playerPattern = """(?:a|r)(?:\s+(.*))?""".r try { line match { - case "q" => controller.onClose; return - case "n" => controller.onStartGame; return - case playerPattern(name) if line.startsWith("a") => controller.addPlayer(Option(name)) + case "q" => controller.onClose + case "n" => controller.onStartGame + case playerPattern(name) if line.startsWith("a") => controller.onAddPlayer(Option(name)) case playerPattern(name) if line.startsWith("r") => if (name != null) { - controller.removePlayer(name) + controller.onRemovePlayer(name) + } + case "h" => controller.onHelp + case "u" => controller.onPlayerActionUndo + case "U" => controller.onPlayerActionRedo + case _ => { + logger.info("Unknown command") + displayMenu } - case "h" => controller.onHelp; - case _ => logger.info("Unknown command") } } catch { case e: ControllerException => logger.error(e.getMessage) } - - displayMenu } protected def processGameInput(line: String): Unit = { line match { case "q" => controller.onClose - case "1" => controller.answerChosen(1) - case "2" => controller.answerChosen(2) - case "3" => controller.answerChosen(3) - case "4" => controller.answerChosen(4) - case "6" => controller.answerChosen(6) - case "7" => controller.answerChosen(7) - case "8" => controller.answerChosen(8) - case "9" => controller.answerChosen(9) + case "1" => controller.onAnswerChosen(1) + case "2" => controller.onAnswerChosen(2) + case "3" => controller.onAnswerChosen(3) + case "4" => controller.onAnswerChosen(4) + case "6" => controller.onAnswerChosen(6) + case "7" => controller.onAnswerChosen(7) + case "8" => controller.onAnswerChosen(8) + case "9" => controller.onAnswerChosen(9) case _ => logger.info("Unknown command") } } diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala index 4f46d9d..cb27099 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala @@ -76,9 +76,9 @@ class GUI (controller: Controller, latch: CountDownLatch) extends JFXApp with UI this.stage = new MenuStage( - => controller.onStartGame, _ => controller.onHelp, - (controller.getCurrentPlayers, controller.nextPlayerName), - (name) => controller.addPlayer(Some(name)), - controller.removePlayer + (controller.getPlayerNames, controller.nextPlayerName), + (name) => controller.onAddPlayer(Some(name)), + controller.onRemovePlayer ) } @@ -86,7 +86,7 @@ class GUI (controller: Controller, latch: CountDownLatch) extends JFXApp with UI this.stage = new GameStage( question, !multiplayer, - controller.answerChosen + controller.onAnswerChosen ) } @@ -94,4 +94,3 @@ class GUI (controller: Controller, latch: CountDownLatch) extends JFXApp with UI this.stage = new ResultStage(players) } } - From 872f6d1eed007cb0f3b2b5c0f138feb699007708 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Sat, 6 Jan 2018 16:32:22 +0100 Subject: [PATCH 26/53] made update method for observers protected --- src/main/scala/de/htwg/se/learn_duel/Observer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/de/htwg/se/learn_duel/Observer.scala b/src/main/scala/de/htwg/se/learn_duel/Observer.scala index a930f44..fd453a8 100644 --- a/src/main/scala/de/htwg/se/learn_duel/Observer.scala +++ b/src/main/scala/de/htwg/se/learn_duel/Observer.scala @@ -14,5 +14,5 @@ class UpdateData(updateAction: UpdateAction, gameState: Game) { } trait Observer { - def update(updateData: UpdateData): Unit + protected def update(updateData: UpdateData): Unit } From a99d6b93424b462721c811cf0d8846a5f7399fc2 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 10 Jan 2018 10:18:39 +0100 Subject: [PATCH 27/53] changed initial menu display to controller update --- .../de/htwg/se/learn_duel/LearnDuel.scala | 1 + .../de/htwg/se/learn_duel/Observer.scala | 4 +- .../se/learn_duel/controller/Controller.scala | 1 + .../controller/impl/Controller.scala | 88 +++++++++++-------- .../de/htwg/se/learn_duel/model/Game.scala | 2 +- .../model/command/impl/PlayerAddCommand.scala | 1 - .../de/htwg/se/learn_duel/view/impl/TUI.scala | 4 +- .../se/learn_duel/view/impl/gui/GUI.scala | 12 +-- 8 files changed, 66 insertions(+), 47 deletions(-) diff --git a/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala b/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala index 7ad72eb..00b2961 100644 --- a/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala +++ b/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala @@ -21,6 +21,7 @@ object LearnDuel { val tui = TUI.create(controller) GUI.create(controller) + controller.requestUpdate tui.processInput(new BufferedReader(Console.in)) } } diff --git a/src/main/scala/de/htwg/se/learn_duel/Observer.scala b/src/main/scala/de/htwg/se/learn_duel/Observer.scala index fd453a8..fe2ff6f 100644 --- a/src/main/scala/de/htwg/se/learn_duel/Observer.scala +++ b/src/main/scala/de/htwg/se/learn_duel/Observer.scala @@ -2,8 +2,8 @@ package de.htwg.se.learn_duel object UpdateAction extends Enumeration { type UpdateAction = Value - val CLOSE_APPLICATION, PLAYER_UPDATE, SHOW_HELP, - SHOW_GAME, SHOW_RESULT, UPDATE_TIMER = Value + val BEGIN, CLOSE_APPLICATION, PLAYER_UPDATE, SHOW_HELP, + SHOW_GAME, SHOW_RESULT, TIMER_UPDATE = Value } import UpdateAction._ import de.htwg.se.learn_duel.model.Game diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala index 0535869..f175d36 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala @@ -8,6 +8,7 @@ trait Controller extends Observable { def nextPlayerName: Option[String] def getPlayerNames: List[String] def maxPlayerCount: Int + def requestUpdate: Unit def onAddPlayer(name: Option[String]): Unit def onRemovePlayer(name: String): Unit def onPlayerActionUndo: Unit diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala index 02e23aa..40c5abb 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala @@ -12,30 +12,35 @@ import de.htwg.se.learn_duel.{UpdateAction, UpdateData} class Controller(gameState: Game) extends ControllerTrait { protected var questionIter: Iterator[Question] = Iterator.empty protected var timer: Option[Timer] = None - protected val invoker = CommandInvoker.create + protected var lastUpdate: UpdateData = new UpdateData(UpdateAction.BEGIN, gameState) + protected val invoker: CommandInvoker = CommandInvoker.create + + override def requestUpdate: Unit = { + notifyObserversAndSaveUpdate(lastUpdate) + } override def getPlayerNames: List[String] = { gameState.players.map(p => p.name) } override def onAddPlayer(name: Option[String]) : Unit = { - invoker.execute(new PlayerAddCommand(name, addPlayer, removePlayer)) + invoker.execute(PlayerAddCommand(name, addPlayer, removePlayer)) } override def onRemovePlayer(name: String): Unit = { - invoker.execute(new PlayerRemoveCommand(name, removePlayer, addPlayer)) + invoker.execute(PlayerRemoveCommand(name, removePlayer, addPlayer)) } override def onPlayerActionUndo: Unit = { - invoker.undo + invoker.undo() } override def onPlayerActionRedo: Unit = { - invoker.redo + invoker.redo() } override def nextPlayerName: Option[String] = { - gameState.playerCount match { + gameState.playerCount() match { case c if c < maxPlayerCount => Some(Player.baseName + (gameState.playerCount + 1).toString) case _ => None } @@ -43,27 +48,27 @@ class Controller(gameState: Game) extends ControllerTrait { override def maxPlayerCount: Int = Game.maxPlayerCount - override def onHelp(): Unit = { + override def onHelp: Unit = { if (gameState.helpText.isEmpty) { import scala.io.Source val helpText: Iterator[String] = Source.fromResource("help.txt").getLines gameState.helpText = helpText.mkString("\n") } - notifyObservers(new UpdateData(UpdateAction.SHOW_HELP, gameState)) + notifyObserversAndSaveUpdate(new UpdateData(UpdateAction.SHOW_HELP, gameState)) } - override def onStartGame(): Unit = { + override def onStartGame: Unit = { resetQuestionIterator() if (!questionIter.hasNext) { - throw new IllegalStateException("Can't start game without questions"); + throw new IllegalStateException("Can't start game without questions") } - showGame() + nextQuestion() } - override def onClose(): Unit = { - notifyObservers(new UpdateData(UpdateAction.CLOSE_APPLICATION, gameState)) + override def onClose: Unit = { + notifyObserversAndSaveUpdate(new UpdateData(UpdateAction.CLOSE_APPLICATION, gameState)) } // scalastyle:off @@ -71,7 +76,7 @@ class Controller(gameState: Game) extends ControllerTrait { val currentQuestion = gameState.currentQuestion.get val correctAnswer = currentQuestion.correctAnswer val player = input match { - case x if 0 until 5 contains x => Some(gameState.players(0)) + case x if 0 until 5 contains x => Some(gameState.players.head) case x if (6 until 10 contains x) && (gameState.players.length > 1 )=> Some(gameState.players(1)) case _ => None } @@ -93,14 +98,14 @@ class Controller(gameState: Game) extends ControllerTrait { }) if (allAnswered) { - nextQuestion + nextQuestion() } } // scalastyle:on protected def addPlayer(name: Option[String]): String = { var playerName = name match { - case Some(name) => name + case Some(n) => n case None => nextPlayerName.getOrElse("") // will not be used if None } @@ -116,7 +121,9 @@ class Controller(gameState: Game) extends ControllerTrait { } catch { case e: Throwable => throw ControllerProcedureFailed("Adding player failed: " + e.getMessage) } - notifyObservers(new UpdateData(UpdateAction.PLAYER_UPDATE, gameState)) + + notifyObserversAndSaveUpdate(new UpdateData(UpdateAction.PLAYER_UPDATE, gameState)) + playerName } @@ -128,7 +135,7 @@ class Controller(gameState: Game) extends ControllerTrait { gameState.players.find(p => p.name == name) match { case Some(p) => gameState.removePlayer(p) - notifyObservers(new UpdateData(UpdateAction.PLAYER_UPDATE, gameState)) + notifyObserversAndSaveUpdate(new UpdateData(UpdateAction.PLAYER_UPDATE, gameState)) case None => throw PlayerNotExistingException(s"Player '$name' does not exist") } } @@ -144,15 +151,16 @@ class Controller(gameState: Game) extends ControllerTrait { } protected def setUpTimer(): Unit = { - timer = Some(new Timer(true)) - val localTimer = timer.get + val localTimer = new Timer(true) + timer = Some(localTimer) + localTimer.scheduleAtFixedRate(new TimerTask { override def run(): Unit = { - gameState.currentQuestionTime = gameState.currentQuestionTime match { + val newTime = gameState.currentQuestionTime match { case Some(time) => { val newTime = time - 1 if (newTime == 0) { - nextQuestion + nextQuestion() None } else { Some(newTime) @@ -161,11 +169,13 @@ class Controller(gameState: Game) extends ControllerTrait { case None => None } - gameState.currentQuestionTime match { - case Some(time) if time % 5 == 0 => { - notifyObservers(new UpdateData(UpdateAction.UPDATE_TIMER, gameState)) + newTime match { + case Some(time) => { + gameState.currentQuestionTime = newTime + if (time % 5 == 0) { + notifyObserversAndSaveUpdate(new UpdateData(UpdateAction.TIMER_UPDATE, gameState)) + } } - case None => stopTimer case _ => } } @@ -173,31 +183,32 @@ class Controller(gameState: Game) extends ControllerTrait { } protected def nextQuestion(): Unit = { + stopTimer() + // implicitely add not given answer as wrong answer - val currentQuestion = gameState.currentQuestion.get - val noAnswerPlayers: List[Player] = gameState.players.filter(p => { - !playerAnsweredQuestion(p, currentQuestion.id) - }) + if (gameState.currentQuestion.isDefined) { + val currentQuestion = gameState.currentQuestion.get + val noAnswerPlayers: List[Player] = gameState.players.filter(p => { + !playerAnsweredQuestion(p, currentQuestion.id) + }) - noAnswerPlayers.foreach(p => p.wrongAnswers = p.wrongAnswers :+ currentQuestion) + noAnswerPlayers.foreach(p => p.wrongAnswers = p.wrongAnswers :+ currentQuestion) + } if (questionIter.hasNext) { showGame() } else { - stopTimer() - notifyObservers(new UpdateData(UpdateAction.SHOW_RESULT, gameState)) + notifyObserversAndSaveUpdate(new UpdateData(UpdateAction.SHOW_RESULT, gameState)) } } protected def showGame(): Unit = { - stopTimer() - val nextQuestion = questionIter.next() gameState.currentQuestion = Some(nextQuestion) gameState.currentQuestionTime = Some(nextQuestion.time) setUpTimer() - notifyObservers((new UpdateData(UpdateAction.SHOW_GAME, gameState))) + notifyObserversAndSaveUpdate(new UpdateData(UpdateAction.SHOW_GAME, gameState)) } protected def playerAnsweredQuestion(p: Player, questionId: Int): Boolean = { @@ -205,4 +216,9 @@ class Controller(gameState: Game) extends ControllerTrait { q.id == questionId }) } + + protected def notifyObserversAndSaveUpdate(data: UpdateData): Unit = { + lastUpdate = data + notifyObservers(data) + } } diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Game.scala b/src/main/scala/de/htwg/se/learn_duel/model/Game.scala index 1d3cdd3..72a4fc0 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/Game.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/Game.scala @@ -1,6 +1,6 @@ package de.htwg.se.learn_duel.model -import de.htwg.se.learn_duel.model.impl.{Game => GameImpl, Player => PlayerImpl, Question => QuestionImpl} +import de.htwg.se.learn_duel.model.impl.{Game => GameImpl} import play.api.libs.json.{JsPath, Json, Reads, Writes} import play.api.libs.functional.syntax._ diff --git a/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerAddCommand.scala b/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerAddCommand.scala index f4c2fe1..65e3132 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerAddCommand.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerAddCommand.scala @@ -1,6 +1,5 @@ package de.htwg.se.learn_duel.model.command.impl -import de.htwg.se.learn_duel.controller.Controller import de.htwg.se.learn_duel.model.command.Command case class PlayerAddCommand( diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala index fdb91e8..f0969b1 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala @@ -14,7 +14,6 @@ class TUI (controller: Controller) extends UI with Observer with LazyLogging { var stopProcessingInput = false var inMenu = true var inGame = false - displayMenu def displayPlayers(): Unit = { logger.info("Current players: " + controller.getPlayerNames.mkString(", ")) @@ -70,6 +69,7 @@ class TUI (controller: Controller) extends UI with Observer with LazyLogging { // scalastyle:off override def update(updateParam: UpdateData): Unit = { updateParam.getAction() match { + case UpdateAction.BEGIN => displayMenu case UpdateAction.CLOSE_APPLICATION => stopProcessingInput = true case UpdateAction.SHOW_HELP => { logger.info(updateParam.getState().helpText) @@ -84,7 +84,7 @@ class TUI (controller: Controller) extends UI with Observer with LazyLogging { inMenu = false; inGame = true; } - case UpdateAction.UPDATE_TIMER => { + case UpdateAction.TIMER_UPDATE => { displayGamePretty( updateParam.getState().currentQuestion.get, updateParam.getState().players.length > 1, diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala index cb27099..88da8fa 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala @@ -14,10 +14,6 @@ import scalafx.application.JFXApp class GUI (controller: Controller, latch: CountDownLatch) extends JFXApp with UI with Observer with LazyLogging { controller.addObserver(this) - displayMenu() - this.stage.onCloseRequest = { (_) => - controller.onClose - } // handle self defined exception in a 'global' exception handler Thread.currentThread().setUncaughtExceptionHandler((t: Thread, e: Throwable) => { @@ -41,6 +37,12 @@ class GUI (controller: Controller, latch: CountDownLatch) extends JFXApp with UI // every update needs to be run on the JavaFX Application thread Platform.runLater { () => updateParam.getAction() match { + case UpdateAction.BEGIN => { + displayMenu() + this.stage.onCloseRequest = { (_) => + controller.onClose + } + } case UpdateAction.CLOSE_APPLICATION => this.stage.close() case UpdateAction.SHOW_HELP => { val helpText = updateParam.getState().helpText @@ -54,7 +56,7 @@ class GUI (controller: Controller, latch: CountDownLatch) extends JFXApp with UI updateParam.getState().players.length > 1 ) } - case UpdateAction.UPDATE_TIMER => { + case UpdateAction.TIMER_UPDATE => { updateParam.getState().currentQuestionTime match { case Some(time) => { if (this.stage.isInstanceOf[GameStage]) { From cce446d6715c421082bde0c367c2ea1705bdf802 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 11 Jan 2018 21:34:44 +0100 Subject: [PATCH 28/53] made game resettable and fixed a few warnings --- .../de/htwg/se/learn_duel/Observer.scala | 2 +- .../se/learn_duel/controller/Controller.scala | 21 ++++++++-------- .../controller/impl/Controller.scala | 25 +++++++++++-------- .../de/htwg/se/learn_duel/model/Answer.scala | 6 ++--- .../de/htwg/se/learn_duel/model/Game.scala | 8 +++--- .../de/htwg/se/learn_duel/model/Player.scala | 6 ++--- .../htwg/se/learn_duel/model/Question.scala | 6 ++--- .../htwg/se/learn_duel/model/Resettable.scala | 5 ++++ .../htwg/se/learn_duel/model/impl/Game.scala | 11 ++++++-- .../se/learn_duel/view/impl/gui/GUI.scala | 4 +-- .../learn_duel/view/impl/gui/MenuStage.scala | 4 +-- .../view/impl/gui/ResultStage.scala | 12 +++++++-- 12 files changed, 68 insertions(+), 42 deletions(-) create mode 100644 src/main/scala/de/htwg/se/learn_duel/model/Resettable.scala diff --git a/src/main/scala/de/htwg/se/learn_duel/Observer.scala b/src/main/scala/de/htwg/se/learn_duel/Observer.scala index fe2ff6f..72aabbb 100644 --- a/src/main/scala/de/htwg/se/learn_duel/Observer.scala +++ b/src/main/scala/de/htwg/se/learn_duel/Observer.scala @@ -14,5 +14,5 @@ class UpdateData(updateAction: UpdateAction, gameState: Game) { } trait Observer { - protected def update(updateData: UpdateData): Unit + def update(updateData: UpdateData): Unit } diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala index f175d36..06a42b6 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/Controller.scala @@ -2,20 +2,21 @@ package de.htwg.se.learn_duel.controller import de.htwg.se.learn_duel.Observable import de.htwg.se.learn_duel.controller.impl.{Controller => ControllerImpl} -import de.htwg.se.learn_duel.model.{Game, Player, Question} +import de.htwg.se.learn_duel.model.{Game, Resettable} -trait Controller extends Observable { - def nextPlayerName: Option[String] +trait Controller extends Observable with Resettable { + def nextPlayerName(): Option[String] def getPlayerNames: List[String] - def maxPlayerCount: Int - def requestUpdate: Unit + def maxPlayerCount(): Int + def requestUpdate(): Unit + def reset(): Unit def onAddPlayer(name: Option[String]): Unit def onRemovePlayer(name: String): Unit - def onPlayerActionUndo: Unit - def onPlayerActionRedo: Unit - def onHelp: Unit - def onStartGame: Unit - def onClose: Unit + def onPlayerActionUndo(): Unit + def onPlayerActionRedo(): Unit + def onHelp(): Unit + def onStartGame(): Unit + def onClose(): Unit def onAnswerChosen(input: Int) } diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala index 40c5abb..8966fc0 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala @@ -13,12 +13,17 @@ class Controller(gameState: Game) extends ControllerTrait { protected var questionIter: Iterator[Question] = Iterator.empty protected var timer: Option[Timer] = None protected var lastUpdate: UpdateData = new UpdateData(UpdateAction.BEGIN, gameState) - protected val invoker: CommandInvoker = CommandInvoker.create + protected val invoker: CommandInvoker = CommandInvoker.create() - override def requestUpdate: Unit = { + override def requestUpdate(): Unit = { notifyObserversAndSaveUpdate(lastUpdate) } + override def reset(): Unit = { + gameState.reset() + notifyObservers(new UpdateData(UpdateAction.BEGIN, gameState)) + } + override def getPlayerNames: List[String] = { gameState.players.map(p => p.name) } @@ -31,24 +36,24 @@ class Controller(gameState: Game) extends ControllerTrait { invoker.execute(PlayerRemoveCommand(name, removePlayer, addPlayer)) } - override def onPlayerActionUndo: Unit = { + override def onPlayerActionUndo(): Unit = { invoker.undo() } - override def onPlayerActionRedo: Unit = { + override def onPlayerActionRedo(): Unit = { invoker.redo() } - override def nextPlayerName: Option[String] = { + override def nextPlayerName(): Option[String] = { gameState.playerCount() match { case c if c < maxPlayerCount => Some(Player.baseName + (gameState.playerCount + 1).toString) case _ => None } } - override def maxPlayerCount: Int = Game.maxPlayerCount + override def maxPlayerCount(): Int = Game.maxPlayerCount - override def onHelp: Unit = { + override def onHelp(): Unit = { if (gameState.helpText.isEmpty) { import scala.io.Source val helpText: Iterator[String] = Source.fromResource("help.txt").getLines @@ -58,7 +63,7 @@ class Controller(gameState: Game) extends ControllerTrait { notifyObserversAndSaveUpdate(new UpdateData(UpdateAction.SHOW_HELP, gameState)) } - override def onStartGame: Unit = { + override def onStartGame(): Unit = { resetQuestionIterator() if (!questionIter.hasNext) { throw new IllegalStateException("Can't start game without questions") @@ -67,7 +72,7 @@ class Controller(gameState: Game) extends ControllerTrait { nextQuestion() } - override def onClose: Unit = { + override def onClose(): Unit = { notifyObserversAndSaveUpdate(new UpdateData(UpdateAction.CLOSE_APPLICATION, gameState)) } @@ -106,7 +111,7 @@ class Controller(gameState: Game) extends ControllerTrait { protected def addPlayer(name: Option[String]): String = { var playerName = name match { case Some(n) => n - case None => nextPlayerName.getOrElse("") // will not be used if None + case None => nextPlayerName().getOrElse("") // will not be used if None } if (gameState.playerCount == Game.maxPlayerCount) { diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Answer.scala b/src/main/scala/de/htwg/se/learn_duel/model/Answer.scala index 96d69e1..d37be9b 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/Answer.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/Answer.scala @@ -1,7 +1,7 @@ package de.htwg.se.learn_duel.model import de.htwg.se.learn_duel.model.impl.{Answer => AnswerImpl} -import play.api.libs.json.{JsPath, Json, Reads, Writes} +import play.api.libs.json._ import play.api.libs.functional.syntax._ trait Answer { @@ -10,8 +10,8 @@ trait Answer { } object Answer { - implicit val answerWrites = new Writes[Answer] { - def writes(answer: Answer) = Json.obj( + implicit val answerWrites: Writes[Answer] = new Writes[Answer] { + def writes(answer: Answer): JsObject = Json.obj( "id" -> answer.id, "text" -> answer.text, ) diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Game.scala b/src/main/scala/de/htwg/se/learn_duel/model/Game.scala index 72a4fc0..0a4e63f 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/Game.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/Game.scala @@ -1,10 +1,10 @@ package de.htwg.se.learn_duel.model import de.htwg.se.learn_duel.model.impl.{Game => GameImpl} -import play.api.libs.json.{JsPath, Json, Reads, Writes} +import play.api.libs.json._ import play.api.libs.functional.syntax._ -trait Game { +trait Game extends Resettable { var helpText: String var players: List[Player] var questions: List[Question] @@ -23,8 +23,8 @@ trait Game { object Game { val maxPlayerCount = 2 - implicit val gameWrites = new Writes[Game] { - def writes(game: Game) = Json.obj( + implicit val gameWrites: Writes[Game] = new Writes[Game] { + def writes(game: Game): JsObject = Json.obj( "helpText" -> game.helpText, "players" -> game.players, "questions" -> game.questions, diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Player.scala b/src/main/scala/de/htwg/se/learn_duel/model/Player.scala index 824ff56..856c293 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/Player.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/Player.scala @@ -1,7 +1,7 @@ package de.htwg.se.learn_duel.model import de.htwg.se.learn_duel.model.impl.{Player => PlayerImpl} -import play.api.libs.json.{JsPath, Json, Reads, Writes} +import play.api.libs.json._ import play.api.libs.functional.syntax._ trait Player { @@ -18,8 +18,8 @@ object Player { PlayerImpl(name) } - implicit val playerWrites = new Writes[Player] { - def writes(player: Player) = Json.obj( + implicit val playerWrites: Writes[Player] = new Writes[Player] { + def writes(player: Player): JsObject = Json.obj( "name" -> player.name, "points" -> player.points, "correctAnswers" -> player.correctAnswers, diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Question.scala b/src/main/scala/de/htwg/se/learn_duel/model/Question.scala index d8db187..2d5dd35 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/Question.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/Question.scala @@ -1,7 +1,7 @@ package de.htwg.se.learn_duel.model import de.htwg.se.learn_duel.model.impl.{Question => QuestionImpl} -import play.api.libs.json.{JsPath, Json, Reads, Writes} +import play.api.libs.json._ import play.api.libs.functional.syntax._ trait Question { @@ -14,8 +14,8 @@ trait Question { } object Question { - implicit val questionWrites = new Writes[Question] { - def writes(question: Question) = Json.obj( + implicit val questionWrites: Writes[Question] = new Writes[Question] { + def writes(question: Question): JsObject = Json.obj( "id" -> question.id, "text" -> question.text, "points" -> question.points, diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Resettable.scala b/src/main/scala/de/htwg/se/learn_duel/model/Resettable.scala new file mode 100644 index 0000000..9961ffa --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/model/Resettable.scala @@ -0,0 +1,5 @@ +package de.htwg.se.learn_duel.model + +trait Resettable { + def reset(): Unit +} diff --git a/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala index d194ffb..662a6f8 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala @@ -2,7 +2,7 @@ package de.htwg.se.learn_duel.model.impl import java.security.InvalidParameterException -import de.htwg.se.learn_duel.model.{Game => GameTrait, Player => PlayerTrait, Question => QuestionTrait} +import de.htwg.se.learn_duel.model.{Resettable, Game => GameTrait, Player => PlayerTrait, Question => QuestionTrait} case class Game( var helpText: String = "", @@ -11,7 +11,8 @@ case class Game( var currentQuestion: Option[QuestionTrait] = None, var currentQuestionTime: Option[Int] = None ) extends GameTrait { - addPlayer(new Player("Player1")) + protected val initialQuestions: List[QuestionTrait] = questions + reset() override def addPlayer(player: PlayerTrait): Unit = players = players :+ player @@ -24,6 +25,12 @@ case class Game( override def removeQuestion(question: QuestionTrait): Unit = questions = questions.filter(_ != question) override def questionCount(): Int = questions.size + + override def reset(): Unit = { + players = List() + addPlayer(new Player("Player1")) + questions = initialQuestions + } } diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala index 88da8fa..2d91402 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala @@ -76,7 +76,7 @@ class GUI (controller: Controller, latch: CountDownLatch) extends JFXApp with UI override def displayMenu(): Unit = { this.stage = new MenuStage( - - => controller.onStartGame, + _ => controller.onStartGame, _ => controller.onHelp, (controller.getPlayerNames, controller.nextPlayerName), (name) => controller.onAddPlayer(Some(name)), @@ -93,6 +93,6 @@ class GUI (controller: Controller, latch: CountDownLatch) extends JFXApp with UI } override def displayResult(players: List[Player]): Unit = { - this.stage = new ResultStage(players) + this.stage = new ResultStage(players, controller.reset) } } diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala index 811c345..df0359f 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala @@ -43,7 +43,7 @@ class MenuStage( val removeBtn = new Button { text = "-" - onAction = - => playerRemoveAction(playerName) + onAction = _ => playerRemoveAction(playerName) } children += removeBtn } @@ -60,7 +60,7 @@ class MenuStage( val addBtn = new Button { text = "+" - onAction = - => playerAddAction( + onAction = _ => playerAddAction( if (txtField.getText.isEmpty) { txtField.getPromptText } else { diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/ResultStage.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/ResultStage.scala index d23de85..c234885 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/ResultStage.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/ResultStage.scala @@ -4,13 +4,15 @@ import de.htwg.se.learn_duel.model.Player import scalafx.Includes._ import scalafx.application.JFXApp.PrimaryStage -import scalafx.scene.{Node, Scene} +import scalafx.scene.control.Button +import scalafx.scene.Scene import scalafx.scene.layout.VBox import scalafx.scene.paint.Color._ import scalafx.scene.text.Text class ResultStage( - players: List[Player] + players: List[Player], + backAction: Function0[Unit] ) extends PrimaryStage { title.value = "Learn Duel Result" width = 480 @@ -46,6 +48,12 @@ class ResultStage( }} children.add(new Text("'" + player.name + "' won the game!")) + + val backButton = new Button { + text = "Back" + onAction = backAction + } + children += backButton } } } From 6ff5ea324aa5a969c847afe2ac322537338cfbef Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Sat, 13 Jan 2018 16:02:34 +0100 Subject: [PATCH 29/53] resetting game state was incomplete --- .../de/htwg/se/learn_duel/controller/impl/Controller.scala | 2 +- src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala index 8966fc0..aa09b9c 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala @@ -81,7 +81,7 @@ class Controller(gameState: Game) extends ControllerTrait { val currentQuestion = gameState.currentQuestion.get val correctAnswer = currentQuestion.correctAnswer val player = input match { - case x if 0 until 5 contains x => Some(gameState.players.head) + case x if (0 until 5 contains x) => Some(gameState.players.head) case x if (6 until 10 contains x) && (gameState.players.length > 1 )=> Some(gameState.players(1)) case _ => None } diff --git a/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala index 662a6f8..ada9479 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala @@ -27,9 +27,11 @@ case class Game( override def questionCount(): Int = questions.size override def reset(): Unit = { + questions = initialQuestions + currentQuestion = None + currentQuestionTime = None players = List() addPlayer(new Player("Player1")) - questions = initialQuestions } } From b98dcc4b18cb23fbbf22e7f5581c450449c9f120 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Mon, 15 Jan 2018 10:50:02 +0100 Subject: [PATCH 30/53] fixed wrong mapping from input of second player to correct answer --- src/main/resources/questions.json | 4 ++-- .../htwg/se/learn_duel/controller/impl/Controller.scala | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/resources/questions.json b/src/main/resources/questions.json index c71f13a..e68976c 100644 --- a/src/main/resources/questions.json +++ b/src/main/resources/questions.json @@ -22,7 +22,7 @@ } ], "correctAnswer": 2, - "time": 5 + "time": 60 }, { "id": 2, @@ -47,6 +47,6 @@ } ], "correctAnswer": 4, - "time": 120 + "time": 60 } ] diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala index aa09b9c..6ace39b 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala @@ -80,14 +80,14 @@ class Controller(gameState: Game) extends ControllerTrait { override def onAnswerChosen(input: Int): Unit = { val currentQuestion = gameState.currentQuestion.get val correctAnswer = currentQuestion.correctAnswer - val player = input match { - case x if (0 until 5 contains x) => Some(gameState.players.head) - case x if (6 until 10 contains x) && (gameState.players.length > 1 )=> Some(gameState.players(1)) + val (player: Option[Player], userInput: Int) = input match { + case x if (0 until 5 contains x) => (Some(gameState.players.head), input) + case x if (6 until 10 contains x) && (gameState.players.length > 1 )=> (Some(gameState.players(1)), input-5) // FIXME magic number -> local mp will be removed anyway case _ => None } if (player.isDefined && !playerAnsweredQuestion(player.get, currentQuestion.id)) { - if (input == correctAnswer) { + if (userInput == correctAnswer) { player.get.points += currentQuestion.points player.get.correctAnswers = player.get.correctAnswers :+ currentQuestion } else { From 936ea6ac3ffff17762a0313738f071e1bd15456e Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 24 Jan 2018 20:35:21 +0100 Subject: [PATCH 31/53] improved gui appearance --- src/main/resources/help.txt | 13 -- src/main/resources/styles.css | 196 ++++++++++++++++++ .../de/htwg/se/learn_duel/GuiceModule.scala | 21 ++ .../de/htwg/se/learn_duel/LearnDuel.scala | 8 +- .../controller/impl/Controller.scala | 17 +- .../de/htwg/se/learn_duel/model/Game.scala | 4 +- .../htwg/se/learn_duel/model/impl/Game.scala | 15 +- .../scala/de/htwg/se/learn_duel/view/UI.scala | 5 +- .../de/htwg/se/learn_duel/view/impl/TUI.scala | 2 +- .../se/learn_duel/view/impl/gui/GUI.scala | 9 +- .../learn_duel/view/impl/gui/GameStage.scala | 30 ++- .../learn_duel/view/impl/gui/InfoPopup.scala | 8 +- .../learn_duel/view/impl/gui/MenuStage.scala | 103 +++++---- .../view/impl/gui/ResultStage.scala | 110 ++++++++-- 14 files changed, 427 insertions(+), 114 deletions(-) delete mode 100644 src/main/resources/help.txt create mode 100644 src/main/resources/styles.css create mode 100644 src/main/scala/de/htwg/se/learn_duel/GuiceModule.scala diff --git a/src/main/resources/help.txt b/src/main/resources/help.txt deleted file mode 100644 index b91bd67..0000000 --- a/src/main/resources/help.txt +++ /dev/null @@ -1,13 +0,0 @@ -Learn Duel Help - -Learn Duel is based on QuizDuel and works in a similar fashion, but with a new twist: -You play with questions based on your school or study assignments. - -For now, there is only local play, but online features will be added later. -If you are playing alone, the answers can be selected with the mouse or the keys 1-4. -In local multiplayer mode player 1 can specify his answer with the keys 1-4 and -player 2 with the keys 6-9. - -Future features: - - define your own questions - - play with up to 3 friends online and compete against each other diff --git a/src/main/resources/styles.css b/src/main/resources/styles.css new file mode 100644 index 0000000..526db8c --- /dev/null +++ b/src/main/resources/styles.css @@ -0,0 +1,196 @@ +/* @import url('https://fonts.googleapis.com/css?family=Roboto:100,300,400,700'); */ + +.headline { + -fx-font-size: 36px; + -fx-font-family: 'Roboto', sans-serif +} + +/* ============================================ */ +/* MENU */ +/* ============================================ */ + +.menu { + background-color: #fff; + -fx-font-family: 'Roboto', sans-serif; + -fx-alignment: center; + -fx-spacing: 50px; +} + +.play-button { + -fx-text-fill: #fff; + -fx-min-width: 430px; + -fx-min-height: 60px; + -fx-background-color: #259A40; + -fx-cursor: hand; +} + +.play-button:hover { + -fx-background-color: #1e7e34; +} + +.player-rows-container { + -fx-spacing: 10px; +} + +.player-container { + -fx-alignment: center; + -fx-spacing: 10px; +} + +.player-textfield { + -fx-min-width: 360px; + -fx-min-height: 30px; + -fx-faint-focus-color: transparent; + -fx-focus-color: gray; +} + +.player-textfield-non-editable { + -fx-focus-color: rgb(221,221,221); + -fx-background-color: rgb(221,221,221); +} + +.add-remove-buttons { + -fx-min-width: 60px; + -fx-min-height: 30px; + -fx-background-color: rgb(221,221,221); + -fx-cursor: hand; +} + +.help-button { + -fx-min-width: 430px; + -fx-min-height: 30px; + -fx-background-color: rgb(221,221,221); + -fx-cursor: hand; +} + +.add-remove-buttons:hover, .help-button:hover { + -fx-background-color: lightgray; +} + +/* ============================================ */ +/* GAME */ +/* ============================================ */ + +.game { + -fx-background-color: #fff; + -fx-font-family: 'Roboto', sans-serif; + -fx-alignment: center; + -fx-spacing: 50px; +} + +.answer-container { + -fx-alignment: center; + -fx-spacing: 10px; +} + +.answer-button { + -fx-min-width: 430px; + -fx-min-height: 50px; + -fx-background-color: rgb(221,221,221); + -fx-cursor: hand; +} + +.answer-button:hover { + -fx-background-color: lightgray; +} + +.remaining-time { + -fx-font-family: 'Roboto', sans-serif; + -fx-font-size: 20px; +} + +/* ============================================ */ +/* RESULT */ +/* ============================================ */ + +.result { + -fx-background-color: #fff; + -fx-font-family: 'Roboto', sans-serif; + -fx-alignment: center; + -fx-spacing: 50px; +} + +.player-results { + -fx-alignment: center; + -fx-spacing: 35px; +} + +.player-result { + -fx-alignment: center; + -fx-spacing: 20px; +} + +.player-name { + -fx-font-size: 20px; +} + +.player-points { + -fx-font-size: 12px; + -fx-font-weight: bold; +} + +.correct-container { + -fx-alignment: center; +} + +.correct-text { + -fx-font-size: 14px; + -fx-fill: #28a745; +} + +.correct-answer { } + +.wrong-container { + -fx-alignment: center; +} + +.wrong-text { + -fx-font-size: 14px; + -fx-fill: #d43f3a; +} + +.wrong-answer {} + +.winner { + -fx-font-size: 14px; +} + +.back-button { + -fx-min-width: 430px; + -fx-min-height: 50px; + -fx-background-color: rgb(221,221,221); + -fx-cursor: hand; +} + +.back-button:hover { + -fx-background-color: lightgray; +} + +/* ============================================ */ +/* HELP */ +/* ============================================ */ + +.dialog-pane { + -fx-background-color: white; +} + +.dialog-pane > .content { + -fx-background-color: white; + -fx-background-insets: 10px 20px; + -fx-padding: 10px 20px; +} + +.dialog-pane > .button-bar > .container { + -fx-background-color: white; +} + +.dialog-pane > .button-bar > .container > .button { + -fx-min-width: 430px; + -fx-min-height: 30px; + -fx-background-color: rgb(221,221,221); + -fx-cursor: hand; +} + +.dialog-pane > .button-bar > .container > .button:hover { + -fx-background-color: lightgray; +} diff --git a/src/main/scala/de/htwg/se/learn_duel/GuiceModule.scala b/src/main/scala/de/htwg/se/learn_duel/GuiceModule.scala new file mode 100644 index 0000000..5bc5aa6 --- /dev/null +++ b/src/main/scala/de/htwg/se/learn_duel/GuiceModule.scala @@ -0,0 +1,21 @@ +package de.htwg.se.learn_duel + +import com.google.inject.AbstractModule +import de.htwg.se.learn_duel.controller.Controller +import de.htwg.se.learn_duel.controller.impl.{Controller => ControllerImpl} +import de.htwg.se.learn_duel.model.{Game, Question} +import de.htwg.se.learn_duel.model.impl.{Game => GameImpl} +import play.api.libs.json.{JsValue, Json} + +import scala.io.Source + +class GuiceModule extends AbstractModule { + val jsonString: String = Source.fromResource("questions.json").getLines.mkString("\n") + val json: JsValue = Json.parse(jsonString) + val questions: List[Question] = Json.fromJson[List[Question]](json).getOrElse(List()) + + override def configure(): Unit = { + bind(classOf[Game]).toInstance(GameImpl(questions = questions)) + bind(classOf[Controller]).to(classOf[ControllerImpl]) + } +} diff --git a/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala b/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala index 00b2961..17ffe1e 100644 --- a/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala +++ b/src/main/scala/de/htwg/se/learn_duel/LearnDuel.scala @@ -4,6 +4,7 @@ import de.htwg.se.learn_duel.controller.Controller import de.htwg.se.learn_duel.model.impl.Game import java.io.BufferedReader +import com.google.inject.Guice import de.htwg.se.learn_duel.model.Question import de.htwg.se.learn_duel.view.{GUI, TUI} import play.api.libs.json.Json @@ -12,12 +13,9 @@ import scala.io.Source object LearnDuel { def main(args: Array[String]): Unit = { - val jsonString = Source.fromResource("questions.json").getLines.mkString("\n") - val json = Json.parse(jsonString) - val questions = Json.fromJson[List[Question]](json).getOrElse(List()) + val injector = Guice.createInjector(new GuiceModule()) + val controller = injector.getInstance(classOf[Controller]) - val gameState = Game(questions = questions) - val controller = Controller.create(gameState) val tui = TUI.create(controller) GUI.create(controller) diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala index 6ace39b..4bf6005 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala @@ -2,6 +2,7 @@ package de.htwg.se.learn_duel.controller.impl import java.util.{Timer, TimerTask} +import com.google.inject.Inject import de.htwg.se.learn_duel.controller.impl.exceptions._ import de.htwg.se.learn_duel.controller.{Controller => ControllerTrait} import de.htwg.se.learn_duel.model.command.CommandInvoker @@ -9,7 +10,7 @@ import de.htwg.se.learn_duel.model.command.impl.{PlayerAddCommand, PlayerRemoveC import de.htwg.se.learn_duel.model.{Game, Player, Question} import de.htwg.se.learn_duel.{UpdateAction, UpdateData} -class Controller(gameState: Game) extends ControllerTrait { +class Controller @Inject() (gameState: Game) extends ControllerTrait { protected var questionIter: Iterator[Question] = Iterator.empty protected var timer: Option[Timer] = None protected var lastUpdate: UpdateData = new UpdateData(UpdateAction.BEGIN, gameState) @@ -54,12 +55,6 @@ class Controller(gameState: Game) extends ControllerTrait { override def maxPlayerCount(): Int = Game.maxPlayerCount override def onHelp(): Unit = { - if (gameState.helpText.isEmpty) { - import scala.io.Source - val helpText: Iterator[String] = Source.fromResource("help.txt").getLines - gameState.helpText = helpText.mkString("\n") - } - notifyObserversAndSaveUpdate(new UpdateData(UpdateAction.SHOW_HELP, gameState)) } @@ -80,10 +75,10 @@ class Controller(gameState: Game) extends ControllerTrait { override def onAnswerChosen(input: Int): Unit = { val currentQuestion = gameState.currentQuestion.get val correctAnswer = currentQuestion.correctAnswer - val (player: Option[Player], userInput: Int) = input match { + var (player: Option[Player], userInput: Int) = input match { case x if (0 until 5 contains x) => (Some(gameState.players.head), input) case x if (6 until 10 contains x) && (gameState.players.length > 1 )=> (Some(gameState.players(1)), input-5) // FIXME magic number -> local mp will be removed anyway - case _ => None + case _ => (None, input) } if (player.isDefined && !playerAnsweredQuestion(player.get, currentQuestion.id)) { @@ -177,9 +172,7 @@ class Controller(gameState: Game) extends ControllerTrait { newTime match { case Some(time) => { gameState.currentQuestionTime = newTime - if (time % 5 == 0) { - notifyObserversAndSaveUpdate(new UpdateData(UpdateAction.TIMER_UPDATE, gameState)) - } + notifyObserversAndSaveUpdate(new UpdateData(UpdateAction.TIMER_UPDATE, gameState)) } case _ => } diff --git a/src/main/scala/de/htwg/se/learn_duel/model/Game.scala b/src/main/scala/de/htwg/se/learn_duel/model/Game.scala index 0a4e63f..9d4eee8 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/Game.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/Game.scala @@ -5,7 +5,7 @@ import play.api.libs.json._ import play.api.libs.functional.syntax._ trait Game extends Resettable { - var helpText: String + var helpText: List[String] var players: List[Player] var questions: List[Question] var currentQuestion: Option[Question] @@ -34,7 +34,7 @@ object Game { } implicit val questionReads: Reads[Game] = ( - (JsPath \ "helpText").read[String] and + (JsPath \ "helpText").read[List[String]] and (JsPath \ "players").read[List[Player]] and (JsPath \ "questions").read[List[Question]] and (JsPath \ "currentQuestion").readNullable[Question] and diff --git a/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala index ada9479..f976c53 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala @@ -2,10 +2,10 @@ package de.htwg.se.learn_duel.model.impl import java.security.InvalidParameterException -import de.htwg.se.learn_duel.model.{Resettable, Game => GameTrait, Player => PlayerTrait, Question => QuestionTrait} +import de.htwg.se.learn_duel.model.{Game => GameTrait, Player => PlayerTrait, Question => QuestionTrait} -case class Game( - var helpText: String = "", +case class Game ( + var helpText: List[String] = List(), var players: List[PlayerTrait] = List(), var questions: List[QuestionTrait] = List(), var currentQuestion: Option[QuestionTrait] = None, @@ -31,7 +31,14 @@ case class Game( currentQuestion = None currentQuestionTime = None players = List() - addPlayer(new Player("Player1")) + addPlayer(Player("Player1")) + helpText = List( + "Learn Duel is based on QuizDuel and works in a similar fashion, but with a new twist:\nYou play with questions based on your school or study assignments.", + "For now, there is only local play, but online features will be added later.\nIf you are playing alone, the answers can be selected with the mouse or the keys 1-4.\nIn local multiplayer mode player 1 can specify his answer with the keys 1-4 and\nplayer 2 with the keys 6-9.", + "Future features:", + "* define your own questions", + "* play with up to 3 friends online and compete against each other" + ) } } diff --git a/src/main/scala/de/htwg/se/learn_duel/view/UI.scala b/src/main/scala/de/htwg/se/learn_duel/view/UI.scala index 76733a5..dfbd13f 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/UI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/UI.scala @@ -2,11 +2,12 @@ package de.htwg.se.learn_duel.view import java.util.concurrent.CountDownLatch +import com.google.inject.Inject import de.htwg.se.learn_duel.Observer import de.htwg.se.learn_duel.controller.Controller import de.htwg.se.learn_duel.model.{Player, Question} -import de.htwg.se.learn_duel.view.impl.{ TUI => TUIImpl } -import de.htwg.se.learn_duel.view.impl.gui.{ GUI => GUIImpl } +import de.htwg.se.learn_duel.view.impl.{TUI => TUIImpl} +import de.htwg.se.learn_duel.view.impl.gui.{GUI => GUIImpl} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala index f0969b1..f65212d 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/TUI.scala @@ -72,7 +72,7 @@ class TUI (controller: Controller) extends UI with Observer with LazyLogging { case UpdateAction.BEGIN => displayMenu case UpdateAction.CLOSE_APPLICATION => stopProcessingInput = true case UpdateAction.SHOW_HELP => { - logger.info(updateParam.getState().helpText) + logger.info(updateParam.getState().helpText.mkString("\n\n")) } case UpdateAction.PLAYER_UPDATE => displayPlayers case UpdateAction.SHOW_GAME => { diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala index 2d91402..1c1d168 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GUI.scala @@ -19,7 +19,9 @@ class GUI (controller: Controller, latch: CountDownLatch) extends JFXApp with UI Thread.currentThread().setUncaughtExceptionHandler((t: Thread, e: Throwable) => { e.getCause match { case cause: ControllerException => { - new InfoPopup("Error", cause.message).show + val infoPopup = new InfoPopup("Error", cause.message) + infoPopup.getDialogPane().getStylesheets().add("styles.css") + infoPopup.show } case _ => { val sw = new StringWriter() @@ -33,6 +35,7 @@ class GUI (controller: Controller, latch: CountDownLatch) extends JFXApp with UI // signal initialization down latch.countDown() + // scalastyle:off override def update(updateParam: UpdateData): Unit = { // every update needs to be run on the JavaFX Application thread Platform.runLater { () => @@ -46,7 +49,8 @@ class GUI (controller: Controller, latch: CountDownLatch) extends JFXApp with UI case UpdateAction.CLOSE_APPLICATION => this.stage.close() case UpdateAction.SHOW_HELP => { val helpText = updateParam.getState().helpText - val dlg = new InfoPopup("Learn Duel Help", helpText) + val dlg = new InfoPopup("Learn Duel Help", helpText.mkString("\n\n")) + dlg.getDialogPane().getStylesheets().add("styles.css") dlg.show } case UpdateAction.PLAYER_UPDATE => displayMenu @@ -73,6 +77,7 @@ class GUI (controller: Controller, latch: CountDownLatch) extends JFXApp with UI } } } + // scalastyle:on override def displayMenu(): Unit = { this.stage = new MenuStage( diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GameStage.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GameStage.scala index 0bdf425..34f0a69 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GameStage.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/GameStage.scala @@ -28,38 +28,52 @@ class GameStage( title.value = "Learn Duel Game" width = 640 - height = 480 + height = allowMouseInput match { + case true => 480 + case false => 520 + } scene = new Scene { fill = White + stylesheets += "styles.css" + root = new VBox { - children += new Text(question.text) + styleClass += "game" + + val questionProp = new Text { + text = question.text + styleClass += "headline" + } + children += questionProp - val pane = new TilePane { - prefColumns = 2 + val answerBox = new VBox { + styleClass += "answer-container" question.answers.zipWithIndex.foreach { case (ans, i) => val btn = new Button { + styleClass += "answer-button" + text = "Answer " + (i + 1) + ": " + ans.text if (allowMouseInput) { onAction = _ => onInput(ans.id) } - minWidth = 200 } children += btn } } - children += pane + children += answerBox - val timer = new Text() + val timer = new Text { + styleClass += "remaining-time" + } timer.text.bind(timerText) children += timer if (!allowMouseInput) { val warning = new Text { text = "Mouse input not allowed, use keyboard instead" - fill = Color.RED + fill = Color.Red } children += warning diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/InfoPopup.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/InfoPopup.scala index 247cb2a..50cc19e 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/InfoPopup.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/InfoPopup.scala @@ -1,11 +1,13 @@ package de.htwg.se.learn_duel.view.impl.gui -import scalafx.scene.control.{ButtonType, Dialog} +import scalafx.geometry.Insets +import scalafx.scene.control.{ButtonType, Dialog, DialogPane} import scalafx.stage.Modality -class InfoPopup(titleText: String, content: String) extends Dialog[Unit] { +class InfoPopup(titleText: String, text: String) extends Dialog[Unit] { title = titleText - contentText = content + contentText = text + dialogPane.value.getButtonTypes.add(ButtonType.OK) initModality(Modality.None) } diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala index df0359f..299e2b5 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/MenuStage.scala @@ -4,11 +4,13 @@ import javafx.event.{ActionEvent, EventHandler} import scalafx.Includes._ import scalafx.application.JFXApp.PrimaryStage +import scalafx.geometry.{Insets, Pos} import scalafx.scene.Scene import scalafx.scene.control.{Button, TextField} -import scalafx.scene.layout.{HBox, VBox} +import scalafx.scene.layout._ +import scalafx.scene.paint.Color import scalafx.scene.paint.Color._ -import scalafx.scene.text.Text +import scalafx.scene.text.{Text, TextAlignment} class MenuStage( newGameAction: EventHandler[ActionEvent], @@ -20,68 +22,91 @@ class MenuStage( title.value = "Learn Duel Menu" resizable = false width = 480 - height = 640 + height = 540 scene = new Scene { fill = White + stylesheets += "styles.css" + root = new VBox { - children += new Text("Learn Duel") + styleClass += "menu" + + val headLine = new Text { + text = "Learn Duel" + styleClass += "headline" + } + children += headLine val newGameButton = new Button { text = "New Game" onAction = newGameAction + styleClass += "play-button" } children += newGameButton - for (playerName <- playerInfo._1) { - val hBox = new HBox { - val txtFld = new TextField { - text = playerName - editable = false - } - children += txtFld + val playerContainer = new VBox { + styleClass += "player-rows-container" - val removeBtn = new Button { - text = "-" - onAction = _ => playerRemoveAction(playerName) - } - children += removeBtn - } - children += hBox - } - - playerInfo._2 match { - case Some(nextPlayername) => { + for (playerName <- playerInfo._1) { val hBox = new HBox { - val txtField = new TextField { - promptText = nextPlayername + styleClass += "player-container" + + val txtFld = new TextField { + text = playerName + editable = false + styleClass += "player-textfield" + styleClass += "player-textfield-non-editable" } - children += txtField - - val addBtn = new Button { - text = "+" - onAction = _ => playerAddAction( - if (txtField.getText.isEmpty) { - txtField.getPromptText - } else { - txtField.getText - } - ) + children += txtFld + + val removeBtn = new Button { + text = "Remove" + onAction = _ => playerRemoveAction(playerName) + styleClass += "add-remove-buttons" } - children += addBtn + children += removeBtn } children += hBox } - case _ => + + playerInfo._2 match { + case Some(nextPlayername) => { + val hBox = new HBox { + styleClass += "player-container" + + val txtField = new TextField { + promptText = nextPlayername + styleClass += "player-textfield" + } + children += txtField + + val addBtn = new Button { + text = "Add" + onAction = _ => playerAddAction( + if (txtField.getText.isEmpty) { + txtField.getPromptText + } else { + txtField.getText + } + ) + styleClass += "add-remove-buttons" + } + children += addBtn + } + children += hBox + } + case _ => + } } + children += playerContainer val helpButton = new Button { - text = "?" + text = "Help" onAction = helpAction + styleClass += "help-button" } children += helpButton } - } } diff --git a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/ResultStage.scala b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/ResultStage.scala index c234885..7e2c81c 100644 --- a/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/ResultStage.scala +++ b/src/main/scala/de/htwg/se/learn_duel/view/impl/gui/ResultStage.scala @@ -7,6 +7,7 @@ import scalafx.application.JFXApp.PrimaryStage import scalafx.scene.control.Button import scalafx.scene.Scene import scalafx.scene.layout.VBox +import scalafx.scene.paint.Color import scalafx.scene.paint.Color._ import scalafx.scene.text.Text @@ -16,42 +17,105 @@ class ResultStage( ) extends PrimaryStage { title.value = "Learn Duel Result" width = 480 - height = 640 + height = players.length match { + case 1 => 560 + case _ => 720 + } scene = new Scene { fill = White + stylesheets += "styles.css" + root = new VBox { - players.foreach { p => { - children.addAll( - new Text("Player '" + p.name + "': "), - new Text("Points: " + p.points), - new Text("Correct answers:") - ) - - p.correctAnswers.foreach(q => { - children.add(new Text("\t" + q.text)) - }) - - children.add(new Text("Wrong answers:")) - - p.wrongAnswers.foreach(q => { - val correctAnswer = q.answers.find(a => a.id == q.correctAnswer).get - children.addAll( - new Text("\t" + q.text), - new Text("\tcorrect answer is: " + correctAnswer.text) - ) - }) - }} + styleClass += "result" + + val headline = new Text { + text = "Result" + styleClass += "headline" + } + children += headline + + val playerContainer = new VBox { + styleClass += "player-results" + + players.foreach { p => { + val singlePlayerContainer = new VBox { + styleClass += "player-result" + + val player = new Text { + text = p.name + styleClass += "player-name" + } + children += player + + val points = new Text { + text = "Points: " + p.points + styleClass += "player-points" + } + children += points + + val correctContainer = new VBox { + styleClass += "correct-container" + + if (p.correctAnswers.length > 0) { + val correctText = new Text { + text = "Correct answers" + styleClass += "correct-text" + } + children += correctText + + p.correctAnswers.foreach(q => { + val correctQuestion = new Text { + text = q.text + styleClass += "correct-answer" + } + children += correctQuestion + }) + } + } + children += correctContainer + + val wrongContainer = new VBox { + styleClass += "wrong-container" + + if (p.wrongAnswers.length > 0) { + val wrongText = new Text { + text = "Wrong answers" + styleClass += "wrong-text" + } + children += wrongText + + p.wrongAnswers.foreach(q => { + val correctAnswer = q.answers.find(a => a.id == q.correctAnswer).get + val answerText = new Text { + text = q.text + " (correct answer: " + correctAnswer.text + ")" + styleClass += "wrong-answer" + } + children += answerText + }) + } + } + children += wrongContainer + } + children += singlePlayerContainer + }} + } + children += playerContainer val player = players.max[Player]{ case (p1: Player, p2: Player) => { p1.points.compareTo(p2.points) }} - children.add(new Text("'" + player.name + "' won the game!")) + val winner = new Text { + text = "'" + player.name + "' won the game!" + styleClass += "winner" + } + children += winner val backButton = new Button { text = "Back" onAction = backAction + styleClass += "back-button" } children += backButton } From 16af23e2bd5ed7b9e5423fa263c3178daee37ea9 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 24 Jan 2018 20:35:36 +0100 Subject: [PATCH 32/53] added travis and coveralls config --- .coveralls.yml | 1 + .travis.yml | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 .coveralls.yml create mode 100644 .travis.yml diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..6e64999 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +service_name: travis-ci \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..52378d5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: scala + +scala: + - 2.12.4 + +script: "sbt clean coverage test" +after_success: "sbt coverageReport coveralls" \ No newline at end of file From 309001d6a3379aab92e1146a42e0b643095d5603 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 24 Jan 2018 20:35:57 +0100 Subject: [PATCH 33/53] added empty test files --- .../de/htwg/se/learn_duel/controller/ControllerSpec.scala | 5 +++++ src/test/scala/de/htwg/se/learn_duel/model/AnswerSpec.scala | 5 +++++ .../scala/de/htwg/se/learn_duel/model/QuestionSpec.scala | 5 +++++ .../se/learn_duel/model/command/CommandInvokerSpec.scala | 5 +++++ .../se/learn_duel/model/command/PlayerAddCommandSpec.scala | 5 +++++ .../learn_duel/model/command/PlayerRemoveCommandSpec.scala | 5 +++++ 6 files changed, 30 insertions(+) create mode 100644 src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala create mode 100644 src/test/scala/de/htwg/se/learn_duel/model/AnswerSpec.scala create mode 100644 src/test/scala/de/htwg/se/learn_duel/model/QuestionSpec.scala create mode 100644 src/test/scala/de/htwg/se/learn_duel/model/command/CommandInvokerSpec.scala create mode 100644 src/test/scala/de/htwg/se/learn_duel/model/command/PlayerAddCommandSpec.scala create mode 100644 src/test/scala/de/htwg/se/learn_duel/model/command/PlayerRemoveCommandSpec.scala diff --git a/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala new file mode 100644 index 0000000..6bfa4ff --- /dev/null +++ b/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala @@ -0,0 +1,5 @@ +package de.htwg.se.learn_duel.controller + +class ControllerSpec { + +} diff --git a/src/test/scala/de/htwg/se/learn_duel/model/AnswerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/AnswerSpec.scala new file mode 100644 index 0000000..7856192 --- /dev/null +++ b/src/test/scala/de/htwg/se/learn_duel/model/AnswerSpec.scala @@ -0,0 +1,5 @@ +package de.htwg.se.learn_duel.model + +class AnswerSpec { + +} diff --git a/src/test/scala/de/htwg/se/learn_duel/model/QuestionSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/QuestionSpec.scala new file mode 100644 index 0000000..8b0e11a --- /dev/null +++ b/src/test/scala/de/htwg/se/learn_duel/model/QuestionSpec.scala @@ -0,0 +1,5 @@ +package de.htwg.se.learn_duel.model + +class QuestionSpec { + +} diff --git a/src/test/scala/de/htwg/se/learn_duel/model/command/CommandInvokerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/command/CommandInvokerSpec.scala new file mode 100644 index 0000000..a82b417 --- /dev/null +++ b/src/test/scala/de/htwg/se/learn_duel/model/command/CommandInvokerSpec.scala @@ -0,0 +1,5 @@ +package de.htwg.se.learn_duel.model.command + +class CommandInvokerSpec { + +} diff --git a/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerAddCommandSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerAddCommandSpec.scala new file mode 100644 index 0000000..0d2954b --- /dev/null +++ b/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerAddCommandSpec.scala @@ -0,0 +1,5 @@ +package de.htwg.se.learn_duel.model.command + +class PlayerAddCommandSpec { + +} diff --git a/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerRemoveCommandSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerRemoveCommandSpec.scala new file mode 100644 index 0000000..b674f84 --- /dev/null +++ b/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerRemoveCommandSpec.scala @@ -0,0 +1,5 @@ +package de.htwg.se.learn_duel.model.command + +class PlayerRemoveCommandSpec { + +} From 22d29b6b11c2e6c197366d34e6f28ddc1146c325 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 24 Jan 2018 21:12:31 +0100 Subject: [PATCH 34/53] added model/player test --- .../htwg/se/learn_duel/model/PlayerSpec.scala | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala index b24a154..47df4ee 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala @@ -1,6 +1,8 @@ package de.htwg.se.learn_duel.model -import de.htwg.se.learn_duel.model.impl.{ Player => PlayerImpl} +import java.security.InvalidParameterException + +import de.htwg.se.learn_duel.model.impl.{Player => PlayerImpl} import org.junit.runner.RunWith import org.scalatest._ import org.scalatest.junit.JUnitRunner @@ -9,14 +11,37 @@ import org.scalatest.junit.JUnitRunner class PlayerSpec extends WordSpec with Matchers { "A Player" when { "new" should { - val player = PlayerImpl("Your Name") + val player = PlayerImpl("YourName") "have a name" in { - player.name should be("Your Name") + player.name should be("YourName") } "have a nice String representation" in { - player.toString should be("Your Name") + player.toString should be("YourName") + } + "have no points" in { + player.points should be(0) + } + "have no correct answers" in { + player.correctAnswers.length should be (0) + } + "have no wrong answers" in { + player.wrongAnswers.length should be (0) + } + } + + "not accept emtpy names" in { + assertThrows[InvalidParameterException] { + val player = PlayerImpl("") + } + } + + "not accept names with whitespace" in { + assertThrows[InvalidParameterException] { + val player = PlayerImpl("Your Name") } } + + } } From a3391083265928091845115d6bc86c4792d7d8e0 Mon Sep 17 00:00:00 2001 From: DonatJR Date: Wed, 24 Jan 2018 21:19:23 +0100 Subject: [PATCH 35/53] added travis and coverall badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce33594..eba1ed6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Learn Duel +# Learn Duel [![Build Status](https://travis-ci.org/DonatJR/learn-duel.svg?branch=develop)](https://travis-ci.org/DonatJR/learn-duel) [![Coverage Status](https://coveralls.io/repos/github/DonatJR/learn-duel/badge.svg?branch=develop)](https://coveralls.io/github/DonatJR/learn-duel?branch=develop) ## About Learn Duel is based on QuizDuel and works in a similar fashion, but with a new twist: From 7f3ca60ff5e53719b9339f1a1cdead6c46934340 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 24 Jan 2018 21:46:30 +0100 Subject: [PATCH 36/53] added model/answer test --- .../htwg/se/learn_duel/model/AnswerSpec.scala | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/test/scala/de/htwg/se/learn_duel/model/AnswerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/AnswerSpec.scala index 7856192..0df4acb 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/AnswerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/AnswerSpec.scala @@ -1,5 +1,33 @@ package de.htwg.se.learn_duel.model -class AnswerSpec { +import de.htwg.se.learn_duel.model.impl.{Answer => AnswerImpl} +import org.junit.runner.RunWith +import org.scalatest._ +import org.scalatest.junit.JUnitRunner +import play.api.libs.json.Json +@RunWith(classOf[JUnitRunner]) +class AnswerSpec extends WordSpec with Matchers { + "A Answer" when { + "new" should { + val answer = AnswerImpl(0, "text") + "a ID" in { + answer.id should be(0) + } + "have a text" in { + answer.text should be("text") + } + } + } + + "A Answer" should { + "be serializable to JSON" in { + val answer = AnswerImpl(0, "text") + + val jsonValue = Json.parse("{\"id\": 0, \"text\": \"text\"}") + + Json.toJson(answer) should be(jsonValue) + } + } } + From 3d8878f545307de0be03277c7c1078f90eb965d8 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 24 Jan 2018 21:46:53 +0100 Subject: [PATCH 37/53] improved model/player test --- .../scala/de/htwg/se/learn_duel/model/PlayerSpec.scala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala index 47df4ee..49ff286 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala @@ -6,6 +6,7 @@ import de.htwg.se.learn_duel.model.impl.{Player => PlayerImpl} import org.junit.runner.RunWith import org.scalatest._ import org.scalatest.junit.JUnitRunner +import play.api.libs.json.Json @RunWith(classOf[JUnitRunner]) class PlayerSpec extends WordSpec with Matchers { @@ -40,8 +41,16 @@ class PlayerSpec extends WordSpec with Matchers { val player = PlayerImpl("Your Name") } } + } + + "A Player" should { + "be serializable to JSON" in { + val player = PlayerImpl("YourName") + val jsonValue = Json.parse("{ \"name\": \"YourName\", \"points\": 0, \"correctAnswers\": [], \"wrongAnswers\": [] }") + Json.toJson(player) should be(jsonValue) + } } } From b67a1d578cd673627146eb1813a978163c2d38ff Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Wed, 24 Jan 2018 21:47:26 +0100 Subject: [PATCH 38/53] added model/question test --- .../se/learn_duel/model/QuestionSpec.scala | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/test/scala/de/htwg/se/learn_duel/model/QuestionSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/QuestionSpec.scala index 8b0e11a..44a139e 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/QuestionSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/QuestionSpec.scala @@ -1,5 +1,47 @@ package de.htwg.se.learn_duel.model -class QuestionSpec { +import de.htwg.se.learn_duel.model.impl.{Answer => AnswerImpl, Question => QuestionImpl} +import org.junit.runner.RunWith +import org.scalatest._ +import org.scalatest.junit.JUnitRunner +import play.api.libs.json.Json +@RunWith(classOf[JUnitRunner]) +class QuestionSpec extends WordSpec with Matchers { + "A Question" when { + "new" should { + val answers = List(AnswerImpl(0, "text"), AnswerImpl(1, "text2")) + val question = QuestionImpl(0, "text", 20, answers, 0, 20) + "a ID" in { + question.id should be(0) + } + "have a text" in { + question.text should be("text") + } + "have a point value" in { + question.points should be(20) + } + "have a list of answers" in { + question.answers should be (answers) + } + "a correct answer" in { + question.correctAnswer should be(0) + } + "a time" in { + question.time should be(20) + } + } + } + + "A Question" should { + "be serializable to JSON" in { + val answers = List(AnswerImpl(0, "text")) + val question = QuestionImpl(0, "text", 20, answers, 0, 20) + + val jsonValue = Json.parse("{\"id\": 0, \"text\": \"text\", \"points\": 20, \"answers\": [{ \"id\": 0, \"text\": \"text\" }],\"correctAnswer\": 0, \"time\": 20}") + + Json.toJson(question) should be(jsonValue) + } + } } + From 528e5441f49b591bb6470cb8aa23794faa6a7582 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 10:52:46 +0100 Subject: [PATCH 39/53] added model/game tests and improved other tests --- .coveralls.yml | 1 - .travis.yml | 2 +- .../htwg/se/learn_duel/model/impl/Game.scala | 3 +- .../htwg/se/learn_duel/model/AnswerSpec.scala | 15 ++- .../htwg/se/learn_duel/model/GameSpec.scala | 93 ++++++++++++++++++- .../htwg/se/learn_duel/model/PlayerSpec.scala | 29 +++--- .../se/learn_duel/model/QuestionSpec.scala | 21 ++--- 7 files changed, 127 insertions(+), 37 deletions(-) delete mode 100644 .coveralls.yml diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 6e64999..0000000 --- a/.coveralls.yml +++ /dev/null @@ -1 +0,0 @@ -service_name: travis-ci \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 52378d5..036d8e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,4 @@ scala: - 2.12.4 script: "sbt clean coverage test" -after_success: "sbt coverageReport coveralls" \ No newline at end of file +after_success: "sbt coveralls" diff --git a/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala index f976c53..ec33dd7 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/impl/Game.scala @@ -4,6 +4,7 @@ import java.security.InvalidParameterException import de.htwg.se.learn_duel.model.{Game => GameTrait, Player => PlayerTrait, Question => QuestionTrait} +// FIXME find better way for resetting or do not let all props be specified in constructor (currently needed for JSON reader) case class Game ( var helpText: List[String] = List(), var players: List[PlayerTrait] = List(), @@ -20,7 +21,7 @@ case class Game ( override def playerCount(): Int = players.size - override def addQuestion(question: QuestionTrait): Unit = questions :+ question + override def addQuestion(question: QuestionTrait): Unit = questions = questions :+ question override def removeQuestion(question: QuestionTrait): Unit = questions = questions.filter(_ != question) diff --git a/src/test/scala/de/htwg/se/learn_duel/model/AnswerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/AnswerSpec.scala index 0df4acb..7bb7930 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/AnswerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/AnswerSpec.scala @@ -11,22 +11,21 @@ class AnswerSpec extends WordSpec with Matchers { "A Answer" when { "new" should { val answer = AnswerImpl(0, "text") - "a ID" in { + "have an ID" in { answer.id should be(0) } "have a text" in { answer.text should be("text") } } - } - - "A Answer" should { - "be serializable to JSON" in { - val answer = AnswerImpl(0, "text") + "serialized to JSON" should { + "be correct" in { + val answer = AnswerImpl(0, "text") - val jsonValue = Json.parse("{\"id\": 0, \"text\": \"text\"}") + val jsonValue = Json.parse("{\"id\": 0, \"text\": \"text\"}") - Json.toJson(answer) should be(jsonValue) + Json.toJson(answer) should be(jsonValue) + } } } } diff --git a/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala index e2df2a1..9feb09f 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala @@ -1,5 +1,94 @@ package de.htwg.se.learn_duel.model -class GameSpec { - // FIXME write tests for everything +import java.security.InvalidParameterException + +import de.htwg.se.learn_duel.model.impl.{Answer => AnswerImpl, Game => GameImpl, Player => PlayerImpl, Question => QuestionImpl} +import org.junit.runner.RunWith +import org.scalatest._ +import org.scalatest.junit.JUnitRunner +import play.api.libs.json.Json + +@RunWith(classOf[JUnitRunner]) +class GameSpec extends WordSpec with Matchers { + "A Game" when { + "new" should { + val game = GameImpl() + + "have no questions" in { + game.questions.length should be(0) + } + var players: List[Player] = List() + var helpText: List[String] = List() + var currentQuestion: Option[Question] = None + var currentQuestionTime: Option[Int] = None + "have a standard player" in { + game.players.length should be(1) + } + "have no current question" in { + game.currentQuestion should be(None) + } + "have no current question time" in { + game.currentQuestionTime should be(None) + } + "should have a help text" in { + game.helpText should be(List( + "Learn Duel is based on QuizDuel and works in a similar fashion, but with a new twist:\nYou play with questions based on your school or study assignments.", + "For now, there is only local play, but online features will be added later.\nIf you are playing alone, the answers can be selected with the mouse or the keys 1-4.\nIn local multiplayer mode player 1 can specify his answer with the keys 1-4 and\nplayer 2 with the keys 6-9.", + "Future features:", + "* define your own questions", + "* play with up to 3 friends online and compete against each other" + )) + } + } + "new with supplied questions" should { + val answers = List(AnswerImpl(0, "text"), AnswerImpl(1, "text2")) + val question1 = QuestionImpl(0, "text1", 20, answers, 0, 20) + val question2 = QuestionImpl(1, "text2", 30, answers, 0, 30) + val questionList = List(question1, question2) + val game = GameImpl(questions = questionList) + + "have these questions" in { + game.questions should be(questionList) + } + } + "serialized to JSON" should { + "be correct" in { + val game = GameImpl() + + val jsonValue = Json.parse("{\"helpText\":[\"Learn Duel is based on QuizDuel and works in a similar fashion, but with a new twist:\\nYou play with questions based on your school or study assignments.\",\"For now, there is only local play, but online features will be added later.\\nIf you are playing alone, the answers can be selected with the mouse or the keys 1-4.\\nIn local multiplayer mode player 1 can specify his answer with the keys 1-4 and\\nplayer 2 with the keys 6-9.\",\"Future features:\",\"* define your own questions\",\"* play with up to 3 friends online and compete against each other\"],\"players\":[{\"name\":\"Player1\",\"points\":0,\"correctAnswers\":[],\"wrongAnswers\":[]}],\"questions\":[],\"currentQuestion\":null,\"currentQuestionTime\":null}") + + Json.toJson(game) should be(jsonValue) + } + } + "when constructed" should { + val player = PlayerImpl("bla") + val question = QuestionImpl(0, "text1", 20, List(), 0, 20) + val game = GameImpl() + + "correctly add players" in { + game.playerCount() should be(1) + game.addPlayer(player) + game.playerCount() should be(2) + } + + "correctly remove players" in { + game.playerCount() should be(2) + game.removePlayer(player) + game.playerCount() should be(1) + } + + "correclty add questions" in { + game.questionCount() should be(0) + game.addQuestion(question) + game.questionCount() should be(1) + } + + "correctly remove questions" in { + game.questionCount() should be(1) + game.removeQuestion(question) + game.questionCount() should be(0) + } + } + } } + diff --git a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala index 49ff286..4d29685 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala @@ -30,26 +30,29 @@ class PlayerSpec extends WordSpec with Matchers { } } - "not accept emtpy names" in { - assertThrows[InvalidParameterException] { - val player = PlayerImpl("") + "constructed without name" should { + "not accept emtpy names" in { + assertThrows[InvalidParameterException] { + val player = PlayerImpl("") + } } } - "not accept names with whitespace" in { - assertThrows[InvalidParameterException] { - val player = PlayerImpl("Your Name") + "constructed with a name with withspace" should { + "not accept names with whitespace" in { + assertThrows[InvalidParameterException] { + val player = PlayerImpl("Your Name") + } } } - } - - "A Player" should { - "be serializable to JSON" in { - val player = PlayerImpl("YourName") + "serialized to JSON" should { + "be correct" in { + val player = PlayerImpl("YourName") - val jsonValue = Json.parse("{ \"name\": \"YourName\", \"points\": 0, \"correctAnswers\": [], \"wrongAnswers\": [] }") + val jsonValue = Json.parse("{ \"name\": \"YourName\", \"points\": 0, \"correctAnswers\": [], \"wrongAnswers\": [] }") - Json.toJson(player) should be(jsonValue) + Json.toJson(player) should be(jsonValue) + } } } } diff --git a/src/test/scala/de/htwg/se/learn_duel/model/QuestionSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/QuestionSpec.scala index 44a139e..68fb9dc 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/QuestionSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/QuestionSpec.scala @@ -12,7 +12,7 @@ class QuestionSpec extends WordSpec with Matchers { "new" should { val answers = List(AnswerImpl(0, "text"), AnswerImpl(1, "text2")) val question = QuestionImpl(0, "text", 20, answers, 0, 20) - "a ID" in { + "have a ID" in { question.id should be(0) } "have a text" in { @@ -24,23 +24,22 @@ class QuestionSpec extends WordSpec with Matchers { "have a list of answers" in { question.answers should be (answers) } - "a correct answer" in { + "have a correct answer" in { question.correctAnswer should be(0) } - "a time" in { + "have a time" in { question.time should be(20) } } - } - - "A Question" should { - "be serializable to JSON" in { - val answers = List(AnswerImpl(0, "text")) - val question = QuestionImpl(0, "text", 20, answers, 0, 20) + "serialized to JSON" should { + "be correct" in { + val answers = List(AnswerImpl(0, "text")) + val question = QuestionImpl(0, "text", 20, answers, 0, 20) - val jsonValue = Json.parse("{\"id\": 0, \"text\": \"text\", \"points\": 20, \"answers\": [{ \"id\": 0, \"text\": \"text\" }],\"correctAnswer\": 0, \"time\": 20}") + val jsonValue = Json.parse("{\"id\": 0, \"text\": \"text\", \"points\": 20, \"answers\": [{ \"id\": 0, \"text\": \"text\" }],\"correctAnswer\": 0, \"time\": 20}") - Json.toJson(question) should be(jsonValue) + Json.toJson(question) should be(jsonValue) + } } } } From b64b2fe9032e2f2d2edfd0a5f46db3bc9422ad8e Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 11:01:20 +0100 Subject: [PATCH 40/53] sbt-coveralls to dependencies and improved travis script --- .travis.yml | 7 +++++-- build.sbt | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 036d8e5..38845c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,5 +3,8 @@ language: scala scala: - 2.12.4 -script: "sbt clean coverage test" -after_success: "sbt coveralls" +script: + - sbt clean coverage test coverageReport && + sbt coverageAggregate +after_success: + - sbt coveralls diff --git a/build.sbt b/build.sbt index 7f1bd0b..ee59cbd 100644 --- a/build.sbt +++ b/build.sbt @@ -13,3 +13,6 @@ libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" libraryDependencies += "com.typesafe.play" %% "play-json" % "2.6.6" libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3" libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2" + +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") +addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.2") From 06bade410c411c9d19ffe9863eb04b37eaa85847 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 11:26:34 +0100 Subject: [PATCH 41/53] hopefully got coveralls to work --- .travis.yml | 3 +-- build.sbt | 3 --- project/plugins.sbt | 1 + 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 38845c9..cf8e207 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ scala: - 2.12.4 script: - - sbt clean coverage test coverageReport && - sbt coverageAggregate + - sbt clean coverage test coverageReport after_success: - sbt coveralls diff --git a/build.sbt b/build.sbt index ee59cbd..7f1bd0b 100644 --- a/build.sbt +++ b/build.sbt @@ -13,6 +13,3 @@ libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" libraryDependencies += "com.typesafe.play" %% "play-json" % "2.6.6" libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3" libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2" - -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") -addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.2") diff --git a/project/plugins.sbt b/project/plugins.sbt index 619b061..a6019ce 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,3 +3,4 @@ resolvers += Classpaths.sbtPluginReleases addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") +addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.2") From 1d282304c6ece5394fd61ee9296271890b7ba1c4 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 11:26:48 +0100 Subject: [PATCH 42/53] added tests for commands --- .../command/impl/PlayerRemoveCommand.scala | 4 +- .../htwg/se/learn_duel/model/GameSpec.scala | 4 +- .../model/command/PlayerAddCommandSpec.scala | 41 ++++++++++++++++++- .../command/PlayerRemoveCommandSpec.scala | 39 +++++++++++++++++- 4 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerRemoveCommand.scala b/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerRemoveCommand.scala index 22c4dfa..2b0bda2 100644 --- a/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerRemoveCommand.scala +++ b/src/main/scala/de/htwg/se/learn_duel/model/command/impl/PlayerRemoveCommand.scala @@ -5,11 +5,11 @@ import de.htwg.se.learn_duel.model.command.Command case class PlayerRemoveCommand( name: String, - remvoePlayer: Function[String, Unit], + removePlayer: Function[String, Unit], addPlayer: Function[Option[String], String] ) extends Command { override def execute(): Unit = { - remvoePlayer(name) + removePlayer(name) } override def undo(): Unit = { diff --git a/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala index 9feb09f..3f1441a 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala @@ -60,8 +60,8 @@ class GameSpec extends WordSpec with Matchers { Json.toJson(game) should be(jsonValue) } } - "when constructed" should { - val player = PlayerImpl("bla") + "constructed" should { + val player = PlayerImpl("player") val question = QuestionImpl(0, "text1", 20, List(), 0, 20) val game = GameImpl() diff --git a/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerAddCommandSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerAddCommandSpec.scala index 0d2954b..7ebaf73 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerAddCommandSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerAddCommandSpec.scala @@ -1,5 +1,42 @@ -package de.htwg.se.learn_duel.model.command +package de.htwg.se.learn_duel.model -class PlayerAddCommandSpec { +import java.security.InvalidParameterException +import de.htwg.se.learn_duel.model.command.impl.PlayerAddCommand +import org.junit.runner.RunWith +import org.scalatest._ +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class PlayerAddCommandSpec extends WordSpec with Matchers { + "A PlayerAddCommand" when { + "constructed" should { + var addCalled = false + var removeCalled = false + val nameAfterAdd = "playerAdd" + val playerAddCommand = PlayerAddCommand( + Some("player"), + _ => { addCalled = true; nameAfterAdd }, + _ => { removeCalled = true; } + ) + + "call the add function on execute" in { + playerAddCommand.execute() + addCalled should be(true) + playerAddCommand.actualName should be(nameAfterAdd) + } + + "call the remove function on undo" in { + playerAddCommand.undo() + removeCalled should be(true) + } + + "call the add function on redo" in { + addCalled = false + playerAddCommand.redo() + addCalled should be(true) + } + } + } } + diff --git a/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerRemoveCommandSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerRemoveCommandSpec.scala index b674f84..a202cdc 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerRemoveCommandSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerRemoveCommandSpec.scala @@ -1,5 +1,40 @@ -package de.htwg.se.learn_duel.model.command +package de.htwg.se.learn_duel.model -class PlayerRemoveCommandSpec { +import java.security.InvalidParameterException +import de.htwg.se.learn_duel.model.command.impl.PlayerRemoveCommand +import org.junit.runner.RunWith +import org.scalatest._ +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class PlayerRemoveCommandSpec extends WordSpec with Matchers { + "A PlayerRemoveCommand" when { + "constructed" should { + var addCalled = false + var removeCalled = false + val playerRemoveCommand = PlayerRemoveCommand( + "player", + _ => { removeCalled = true; }, + _ => { addCalled = true; "player" } + ) + + "call the remove function on execute" in { + playerRemoveCommand.execute() + removeCalled should be(true) + } + + "call the add function on undo" in { + playerRemoveCommand.undo() + addCalled should be(true) + } + + "call the remove function on redo" in { + removeCalled = false + playerRemoveCommand.redo() + removeCalled should be(true) + } + } + } } + From 2871d9d7652ab2b5791c4764d24c72a0ef89853a Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 11:35:38 +0100 Subject: [PATCH 43/53] excluded view from coveralls coverage --- build.sbt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.sbt b/build.sbt index 7f1bd0b..2b75a40 100644 --- a/build.sbt +++ b/build.sbt @@ -13,3 +13,7 @@ libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" libraryDependencies += "com.typesafe.play" %% "play-json" % "2.6.6" libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3" libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2" + +import org.scoverage.coveralls.Imports.CoverallsKeys._ +coverageExcludedPackages := ".*view.*" +coverageEnabled := true From 15d5f74971c682b7fc54e6389728138226c04b03 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 11:41:35 +0100 Subject: [PATCH 44/53] excluded files from test coverage --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 2b75a40..4c8aaa4 100644 --- a/build.sbt +++ b/build.sbt @@ -15,5 +15,5 @@ libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3" libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2" import org.scoverage.coveralls.Imports.CoverallsKeys._ -coverageExcludedPackages := ".*view.*" +coverageExcludedPackages := ".*view.*;GuiceModule.scala;LearnDuel.scala" coverageEnabled := true From 39a2d9e3b5f4cd10ae173599bbeb94b840f1cae4 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 11:42:10 +0100 Subject: [PATCH 45/53] improved test for model/player --- .../de/htwg/se/learn_duel/model/PlayerSpec.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala index 4d29685..fb515d4 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/PlayerSpec.scala @@ -30,6 +30,17 @@ class PlayerSpec extends WordSpec with Matchers { } } + "constructed by factory method" should { + "be the same as constructing it directly" in { + val player = Player.create("YourName") + player.name should be("YourName") + player.toString should be("YourName") + player.points should be(0) + player.correctAnswers.length should be (0) + player.wrongAnswers.length should be (0) + } + } + "constructed without name" should { "not accept emtpy names" in { assertThrows[InvalidParameterException] { From a5684a3e3bcebbe61490be59338a591bffd6bfe7 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 11:46:38 +0100 Subject: [PATCH 46/53] previous exclusion of coverage did not work --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 4c8aaa4..076ad2c 100644 --- a/build.sbt +++ b/build.sbt @@ -15,5 +15,5 @@ libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3" libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2" import org.scoverage.coveralls.Imports.CoverallsKeys._ -coverageExcludedPackages := ".*view.*;GuiceModule.scala;LearnDuel.scala" +coverageExcludedPackages := ".*view.*;.*GuiceModule.scala;.*LearnDuel.scala" coverageEnabled := true From 4053d111495214947952cec788edb0b0688f62c1 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 11:57:30 +0100 Subject: [PATCH 47/53] next try --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 076ad2c..95bfea3 100644 --- a/build.sbt +++ b/build.sbt @@ -15,5 +15,5 @@ libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3" libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2" import org.scoverage.coveralls.Imports.CoverallsKeys._ -coverageExcludedPackages := ".*view.*;.*GuiceModule.scala;.*LearnDuel.scala" +coverageExcludedPackages := ".*view.*;.*GuiceModule\\.scala;.*LearnDuel\\.scala" coverageEnabled := true From d3843b0b8cb6dc13ad378cc2c00f73de9726e7af Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 12:11:23 +0100 Subject: [PATCH 48/53] added model/command/commandinvoker tests --- build.sbt | 3 +- .../model/command/CommandInvokerSpec.scala | 73 ++++++++++++++++++- .../model/command/PlayerAddCommandSpec.scala | 2 +- .../command/PlayerRemoveCommandSpec.scala | 2 +- 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index 95bfea3..44f7367 100644 --- a/build.sbt +++ b/build.sbt @@ -15,5 +15,6 @@ libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3" libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2" import org.scoverage.coveralls.Imports.CoverallsKeys._ -coverageExcludedPackages := ".*view.*;.*GuiceModule\\.scala;.*LearnDuel\\.scala" +coverageExcludedPackages := ".*view.*;.*GuiceModule.*;.*LearnDuel.*" coverageEnabled := true +coverageHighlighting := true diff --git a/src/test/scala/de/htwg/se/learn_duel/model/command/CommandInvokerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/command/CommandInvokerSpec.scala index a82b417..bb0bbed 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/command/CommandInvokerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/command/CommandInvokerSpec.scala @@ -1,5 +1,76 @@ package de.htwg.se.learn_duel.model.command -class CommandInvokerSpec { +import de.htwg.se.learn_duel.model.command.impl.{ CommandInvoker => CommandInvokerImpl, PlayerAddCommand } +import org.junit.runner.RunWith +import org.scalatest._ +import org.scalatest.junit.JUnitRunner +@RunWith(classOf[JUnitRunner]) +class CommandInvokerSpec extends WordSpec with Matchers { + "A CommandInvoker" when { + "constructed" should { + var addCalled = false + var removeCalled = false + val nameAfterAdd = "playerAdd" + val playerAddCommand = PlayerAddCommand( + Some("player"), + _ => { addCalled = true; nameAfterAdd }, + _ => { removeCalled = true; } + ) + val commandInvoker = CommandInvokerImpl() + + "have no saved commands" in { + commandInvoker.undoCommands.length should be(0) + commandInvoker.redoCommands.length should be(0) + } + "execute given commands and save them" in { + addCalled should be(false) + + commandInvoker.execute(playerAddCommand) + + addCalled should be(true) + commandInvoker.redoCommands.length should be(0) + commandInvoker.undoCommands.length should be(1) + } + "be able to undo command" in { + removeCalled should be(false) + commandInvoker.redoCommands.length should be(0) + commandInvoker.undoCommands.length should be(1) + + commandInvoker.undo() + + removeCalled should be(true) + commandInvoker.redoCommands.length should be(1) + commandInvoker.undoCommands.length should be(0) + } + "not do anything on undo when there are no commands to undo" in { + commandInvoker.undoCommands.length should be(0) + + removeCalled = false + commandInvoker.undo() + + removeCalled should be(false) + } + "be able to redo command" in { + addCalled = false + commandInvoker.redoCommands.length should be(1) + commandInvoker.undoCommands.length should be(0) + + commandInvoker.redo() + + addCalled should be(true) + commandInvoker.redoCommands.length should be(0) + commandInvoker.undoCommands.length should be(1) + } + } + + "constructed with factory method" should { + val commandInvoker = CommandInvoker.create() + "have no saved commands" in { + commandInvoker.undoCommands.length should be(0) + commandInvoker.redoCommands.length should be(0) + } + } + } } + diff --git a/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerAddCommandSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerAddCommandSpec.scala index 7ebaf73..bc407f3 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerAddCommandSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerAddCommandSpec.scala @@ -1,4 +1,4 @@ -package de.htwg.se.learn_duel.model +package de.htwg.se.learn_duel.model.command import java.security.InvalidParameterException diff --git a/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerRemoveCommandSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerRemoveCommandSpec.scala index a202cdc..f73e135 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerRemoveCommandSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/command/PlayerRemoveCommandSpec.scala @@ -1,4 +1,4 @@ -package de.htwg.se.learn_duel.model +package de.htwg.se.learn_duel.model.command import java.security.InvalidParameterException From 798a4632f521ee583e3ebfb294a800a1a4349cd2 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 12:17:32 +0100 Subject: [PATCH 49/53] tested missing line in command invoker --- .../se/learn_duel/model/command/CommandInvokerSpec.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/scala/de/htwg/se/learn_duel/model/command/CommandInvokerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/command/CommandInvokerSpec.scala index bb0bbed..013d76f 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/command/CommandInvokerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/command/CommandInvokerSpec.scala @@ -62,6 +62,14 @@ class CommandInvokerSpec extends WordSpec with Matchers { commandInvoker.redoCommands.length should be(0) commandInvoker.undoCommands.length should be(1) } + "not do anything on redo when there are no commands to redo" in { + commandInvoker.redoCommands.length should be(0) + + addCalled = false + commandInvoker.redo() + + addCalled should be(false) + } } "constructed with factory method" should { From 389dc50555cf00f752d397e18ee06644c334fc80 Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 12:46:29 +0100 Subject: [PATCH 50/53] added a few controller tests --- .../controller/impl/Controller.scala | 2 +- .../controller/ControllerSpec.scala | 47 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala index 4bf6005..e6f39a3 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala @@ -22,7 +22,7 @@ class Controller @Inject() (gameState: Game) extends ControllerTrait { override def reset(): Unit = { gameState.reset() - notifyObservers(new UpdateData(UpdateAction.BEGIN, gameState)) + notifyObserversAndSaveUpdate(new UpdateData(UpdateAction.BEGIN, gameState)) } override def getPlayerNames: List[String] = { diff --git a/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala index 6bfa4ff..9d6b775 100644 --- a/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala @@ -1,5 +1,50 @@ package de.htwg.se.learn_duel.controller -class ControllerSpec { +import com.google.inject.Guice +import de.htwg.se.learn_duel.GuiceModule +import de.htwg.se.learn_duel.model.Game +import de.htwg.se.learn_duel.controller.impl.{Controller => ControllerImpl} +import org.junit.runner.RunWith +import org.scalatest._ +import org.scalatest.junit.JUnitRunner +@RunWith(classOf[JUnitRunner]) +class ControllerSpec extends WordSpec with Matchers { + "A Controller" when { + val injector = Guice.createInjector(new GuiceModule()) + val gameState = injector.getInstance(classOf[Game]) + + "constructed" should { + val controller = new ControllerImpl(gameState) + + "be able to generate a list of player names" in { + controller.getPlayerNames should be(gameState.players.map(p => p.name)) + } + "have a max player count" in { + controller.maxPlayerCount() should be (Game.maxPlayerCount) + } + "be able to add a player" in { + gameState.playerCount() should be(1) + controller.onAddPlayer(Some("player")) + gameState.playerCount() should be(2) + } + "be able to remove a player" in { + gameState.playerCount() should be(2) + controller.onRemovePlayer("player") + gameState.playerCount() should be(1) + } + "be able to add a player with a default name" in { + gameState.playerCount() should be(1) + controller.onAddPlayer(None) + gameState.playerCount() should be(2) + } + } + + "constructed with factory method" should { + val manualController = new ControllerImpl(gameState) + "be valid" in { + val controller = Controller.create(gameState) + } + } + } } From fe25496333353b0ec09a0fd5a4b48bb894bd930e Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 13:16:37 +0100 Subject: [PATCH 51/53] added more controller tests --- .../controller/ControllerSpec.scala | 62 ++++++++++++++++++- .../htwg/se/learn_duel/model/GameSpec.scala | 4 -- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala index 9d6b775..97478ea 100644 --- a/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala @@ -1,22 +1,37 @@ package de.htwg.se.learn_duel.controller import com.google.inject.Guice -import de.htwg.se.learn_duel.GuiceModule +import de.htwg.se.learn_duel.{GuiceModule, Observer, UpdateAction, UpdateData} import de.htwg.se.learn_duel.model.Game import de.htwg.se.learn_duel.controller.impl.{Controller => ControllerImpl} import org.junit.runner.RunWith import org.scalatest._ import org.scalatest.junit.JUnitRunner +class DummyObserver extends Observer { + var updateData: Option[UpdateData] = None + override def update(updateData: UpdateData): Unit = { + this.updateData = Some(updateData) + } +} + @RunWith(classOf[JUnitRunner]) class ControllerSpec extends WordSpec with Matchers { "A Controller" when { val injector = Guice.createInjector(new GuiceModule()) val gameState = injector.getInstance(classOf[Game]) + val dummyObserver = new DummyObserver() "constructed" should { val controller = new ControllerImpl(gameState) + "update observers with the last update on request to do so" in { + controller.addObserver(dummyObserver) + dummyObserver.updateData should be(None) + controller.requestUpdate() + dummyObserver.updateData.isDefined should be(true) + dummyObserver.updateData.get.getAction() should be(UpdateAction.BEGIN) + } "be able to generate a list of player names" in { controller.getPlayerNames should be(gameState.players.map(p => p.name)) } @@ -38,6 +53,51 @@ class ControllerSpec extends WordSpec with Matchers { controller.onAddPlayer(None) gameState.playerCount() should be(2) } + "be able do undo and redo a player action" in { + gameState.playerCount() should be(2) + controller.onPlayerActionUndo() + gameState.playerCount() should be(1) + controller.onPlayerActionRedo() + gameState.playerCount() should be(2) + } + "not supply a next player name if max player count is reached" in { + gameState.playerCount() should be(2) + val nextName = controller.nextPlayerName() + nextName should be(None) + } + "supply a next player name if max player count was not reached" in { + controller.onPlayerActionUndo() + gameState.playerCount() should be(1) + val nextName = controller.nextPlayerName() + nextName should be(Some("Player2")) + } + "indicate to observers that they should show the help" in { + controller.onHelp() + dummyObserver.updateData.get.getAction() should be(UpdateAction.SHOW_HELP) + } + "indicate to observers that they should start the game" in { + controller.onStartGame() + dummyObserver.updateData.get.getAction() should be(UpdateAction.SHOW_GAME) + gameState.currentQuestion should not be(None) + gameState.currentQuestionTime should not be(None) + } + "indicate to observers that they should close" in { + controller.onClose() + dummyObserver.updateData.get.getAction() should be(UpdateAction.CLOSE_APPLICATION) + } + "be able to reset game state" in { + controller.onPlayerActionRedo() + gameState.playerCount() should be(2) + gameState.currentQuestion should not be(None) + gameState.currentQuestionTime should not be(None) + dummyObserver.updateData.get.getAction() should not be(UpdateAction.BEGIN) + + controller.reset() + gameState.playerCount() should be(1) + gameState.currentQuestion should be(None) + gameState.currentQuestionTime should be(None) + dummyObserver.updateData.get.getAction() should be(UpdateAction.BEGIN) + } } "constructed with factory method" should { diff --git a/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala b/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala index 3f1441a..05e253c 100644 --- a/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/model/GameSpec.scala @@ -17,10 +17,6 @@ class GameSpec extends WordSpec with Matchers { "have no questions" in { game.questions.length should be(0) } - var players: List[Player] = List() - var helpText: List[String] = List() - var currentQuestion: Option[Question] = None - var currentQuestionTime: Option[Int] = None "have a standard player" in { game.players.length should be(1) } From 0df9f27bf93fa9748ffac24aefc7520753f522cc Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 13:46:31 +0100 Subject: [PATCH 52/53] added even more controller tests --- src/main/resources/test_questions.json | 52 ++++++++++++++++ .../controller/impl/Controller.scala | 3 +- .../controller/ControllerSpec.scala | 59 +++++++++++++++++-- 3 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 src/main/resources/test_questions.json diff --git a/src/main/resources/test_questions.json b/src/main/resources/test_questions.json new file mode 100644 index 0000000..e68976c --- /dev/null +++ b/src/main/resources/test_questions.json @@ -0,0 +1,52 @@ +[ + { + "id": 1, + "text": "What is 1+1?", + "points": 20, + "answers": [ + { + "id": 1, + "text": "1" + }, + { + "id": 2, + "text": "2" + }, + { + "id": 3, + "text": "3" + }, + { + "id": 4, + "text": "4" + } + ], + "correctAnswer": 2, + "time": 60 + }, + { + "id": 2, + "text": "What is 5*20?", + "points": 50, + "answers": [ + { + "id": 1, + "text": "168" + }, + { + "id": 2, + "text": "934" + }, + { + "id": 3, + "text": "236" + }, + { + "id": 4, + "text": "100" + } + ], + "correctAnswer": 4, + "time": 60 + } +] diff --git a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala index e6f39a3..0299b75 100644 --- a/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala +++ b/src/main/scala/de/htwg/se/learn_duel/controller/impl/Controller.scala @@ -60,7 +60,8 @@ class Controller @Inject() (gameState: Game) extends ControllerTrait { override def onStartGame(): Unit = { resetQuestionIterator() - if (!questionIter.hasNext) { + + if (questionIter.isEmpty) { throw new IllegalStateException("Can't start game without questions") } diff --git a/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala index 97478ea..a6acc0e 100644 --- a/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala @@ -1,12 +1,15 @@ package de.htwg.se.learn_duel.controller -import com.google.inject.Guice -import de.htwg.se.learn_duel.{GuiceModule, Observer, UpdateAction, UpdateData} -import de.htwg.se.learn_duel.model.Game +import de.htwg.se.learn_duel.{Observer, UpdateAction, UpdateData} +import de.htwg.se.learn_duel.model.{Game, Question} +import de.htwg.se.learn_duel.model.impl.{Game => GameImpl} import de.htwg.se.learn_duel.controller.impl.{Controller => ControllerImpl} import org.junit.runner.RunWith import org.scalatest._ import org.scalatest.junit.JUnitRunner +import play.api.libs.json.{JsValue, Json} + +import scala.io.Source class DummyObserver extends Observer { var updateData: Option[UpdateData] = None @@ -18,8 +21,10 @@ class DummyObserver extends Observer { @RunWith(classOf[JUnitRunner]) class ControllerSpec extends WordSpec with Matchers { "A Controller" when { - val injector = Guice.createInjector(new GuiceModule()) - val gameState = injector.getInstance(classOf[Game]) + val jsonString: String = Source.fromResource("test_questions.json").getLines.mkString("\n") + val json: JsValue = Json.parse(jsonString) + val questionList: List[Question] = Json.fromJson[List[Question]](json).getOrElse(List()) + val gameState = GameImpl(questions = questionList) val dummyObserver = new DummyObserver() "constructed" should { @@ -31,6 +36,7 @@ class ControllerSpec extends WordSpec with Matchers { controller.requestUpdate() dummyObserver.updateData.isDefined should be(true) dummyObserver.updateData.get.getAction() should be(UpdateAction.BEGIN) + dummyObserver.updateData.get.getState() should be(gameState) } "be able to generate a list of player names" in { controller.getPlayerNames should be(gameState.players.map(p => p.name)) @@ -98,8 +104,51 @@ class ControllerSpec extends WordSpec with Matchers { gameState.currentQuestionTime should be(None) dummyObserver.updateData.get.getAction() should be(UpdateAction.BEGIN) } + "add correctly and wrongly answered questions to player and move on to next question" in { + gameState.players.head.correctAnswers.length should be(0) + gameState.players.head.wrongAnswers.length should be(0) + controller.onStartGame() + + val question1 = gameState.currentQuestion + + controller.onAnswerChosen(2) + + gameState.players.head.correctAnswers.length should be(1) + + controller.onAnswerChosen(1) + + gameState.players.head.wrongAnswers.length should be(1) + dummyObserver.updateData.get.getAction() should be(UpdateAction.SHOW_RESULT) + } + "should update question timer" in { + controller.reset() + controller.onStartGame() + val time = gameState.currentQuestionTime + Thread.sleep(2000) + gameState.currentQuestionTime should not be(time) + } + "remove observers correctly" in { + controller.onHelp() + val currentAction = dummyObserver.updateData.get.getAction() + currentAction should not be(UpdateAction.SHOW_GAME) + controller.removeObserver(dummyObserver) + controller.onStartGame() + dummyObserver.updateData.get.getAction() should not be(UpdateAction.SHOW_GAME) + dummyObserver.updateData.get.getAction() should be(currentAction) + } } + "constructed with no questions" should { + val newState = GameImpl(questions = questionList) + newState.questions = List() + + val controller = new ControllerImpl(newState) + "throw on start game" in { + assertThrows[IllegalStateException] { + controller.onStartGame() + } + } + } "constructed with factory method" should { val manualController = new ControllerImpl(gameState) "be valid" in { From 4b7a5227801e145d328710477dad0051ac7d485b Mon Sep 17 00:00:00 2001 From: Jonas Reinwald Date: Thu, 25 Jan 2018 14:10:04 +0100 Subject: [PATCH 53/53] added more controller tests --- .../controller/ControllerSpec.scala | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala b/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala index a6acc0e..8f3918c 100644 --- a/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala +++ b/src/test/scala/de/htwg/se/learn_duel/controller/ControllerSpec.scala @@ -1,5 +1,6 @@ package de.htwg.se.learn_duel.controller +import de.htwg.se.learn_duel.controller.impl.exceptions._ import de.htwg.se.learn_duel.{Observer, UpdateAction, UpdateData} import de.htwg.se.learn_duel.model.{Game, Question} import de.htwg.se.learn_duel.model.impl.{Game => GameImpl} @@ -120,7 +121,7 @@ class ControllerSpec extends WordSpec with Matchers { gameState.players.head.wrongAnswers.length should be(1) dummyObserver.updateData.get.getAction() should be(UpdateAction.SHOW_RESULT) } - "should update question timer" in { + "update question timer" in { controller.reset() controller.onStartGame() val time = gameState.currentQuestionTime @@ -128,6 +129,7 @@ class ControllerSpec extends WordSpec with Matchers { gameState.currentQuestionTime should not be(time) } "remove observers correctly" in { + controller.reset() controller.onHelp() val currentAction = dummyObserver.updateData.get.getAction() currentAction should not be(UpdateAction.SHOW_GAME) @@ -136,6 +138,53 @@ class ControllerSpec extends WordSpec with Matchers { dummyObserver.updateData.get.getAction() should not be(UpdateAction.SHOW_GAME) dummyObserver.updateData.get.getAction() should be(currentAction) } + "throw when adding or removing too many players" in { + controller.reset() + controller.onAddPlayer(None) + assertThrows[TooManyPlayersException] { + controller.onAddPlayer(Some("invalidthirdplayer")) + } + + controller.reset() + assertThrows[NotEnoughPlayersException] { + controller.onRemovePlayer("Player1") + } + } + "throw when adding a already existing or removing a non existing player" in { + assertThrows[PlayerExistsException] { + controller.onAddPlayer(Some("Player1")) + } + + controller.onAddPlayer(None) + assertThrows[PlayerNotExistingException] { + controller.onRemovePlayer("nonexistingplayer") + } + } + "throw when internal objects throw" in { + controller.reset() + assertThrows[ControllerProcedureFailed] { + controller.onAddPlayer(Some("Player With Space")) + } + } + } + "constructed with very small questions timers" should { + "show next question and results when timer runs out" in { + val tempJsonString = jsonString.replace("\"time\": 60", "\"time\": 1") + val tempJson = Json.parse(tempJsonString) + val tempList = Json.fromJson[List[Question]](tempJson).getOrElse(List()) + val tempGame = GameImpl(questions = tempList) + val tempController = Controller.create(tempGame) + val tempObserver = new DummyObserver() + + tempController.addObserver(tempObserver) + tempController.onAddPlayer(None) + + tempController.onStartGame() + Thread.sleep(3000) + + tempObserver.updateData.get.getAction() should be(UpdateAction.SHOW_RESULT) + tempGame.players.foreach(p => p.wrongAnswers.length should be(2)) + } } "constructed with no questions" should { val newState = GameImpl(questions = questionList)