diff --git a/.gitignore b/.gitignore index bc50587..5683a64 100644 --- a/.gitignore +++ b/.gitignore @@ -15,11 +15,14 @@ target/ *.sc # Scala-IDE specific +.cache-main +.cache-tests .classpath .project .scala_dependencies .settings .worksheet +bin/ # IntelliJ *.iml diff --git a/.travis.yml b/.travis.yml index bc66c53..176cf7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,19 @@ #https://docs.travis-ci.com/user/languages/scala language: scala +before_install: + # Initilize xvfb for headless testing + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + #- sleep 3 # give xvfb some time to start + script: - - sbt ++$TRAVIS_SCALA_VERSION test + - sbt ++$TRAVIS_SCALA_VERSION firefox:test + + # Trick to avoid unnecessary cache updates + - find $HOME/.sbt -name "*.lock" | xargs rm + scala: - - 2.11.8 + - 2.12.0 jdk: - oraclejdk8 @@ -11,14 +21,12 @@ sudo: false cache: directories: - - $HOME/.ivy2/cache - - $HOME/.sbt/boot/ + - $HOME/.ivy2 + - $HOME/.m2/repository + - $HOME/.sbt env: - CI=travis -#branches: -# only: -# - master - -# - sbt ++$TRAVIS_SCALA_VERSION 'set scalaJSStage in Global := FastOptStage' test 'set scalaJSStage in Global := FullOptStage' test +addons: + firefox: "46.0.1" diff --git a/Licence.md b/Licence.md new file mode 100644 index 0000000..f07d19f --- /dev/null +++ b/Licence.md @@ -0,0 +1,12 @@ + 2016-10-01 Simple Game + ©2016 by F.W. van den Berg Licensed under the EUPL-1.1 + +This Software is provided to You under the terms of the European Union Public License (the "EUPL") version 1.1 +or – as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence") +as published by the European Union. Any use of this Software, other than as authorized under this License is +strictly prohibited (to the extent such use is covered by a right of the copyright holder of this Software). + +This Software is provided under the License on an "AS IS" basis and without warranties of any kind concerning +the Software, including without limitation merchantability, fitness for a particular purpose, absence of defects +or errors, accuracy, and non-infringement of intellectual property rights other than copyright. This disclaimer +of warranty is an essential part of the License and a condition for the grant of any rights to this Software. diff --git a/README.md b/README.md index f837db6..9578875 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,215 @@ HTML5 Powered with CSS3 / Styling, Graphics, 3D & Effects, and Semantics[![Scala.js](https://img.shields.io/badge/scala.js-0.6.10%2B-blue.svg?style=flat)](https://www.scala-js.org) -[![Build Status](https://travis-ci.org/amsterdam-scala/Sjs-Simple-HTML5-canvas-game.svg?branch=master)](https://travis-ci.org/amsterdam-scala/Sjs-Simple-HTML5-canvas-game) +[![Build Status](https://travis-ci.org/amsterdam-scala/Sjs-Simple-HTML5-canvas-game.svg?branch=master_V2)](https://travis-ci.org/amsterdam-scala/Sjs-Simple-HTML5-canvas-game) # Simple HTML5 Canvas game ported to Scala.js +**Featuring Scala.js "in browser testing" by ScalaTest 3.x** -Original tutorial in Javascript : -[How to make a simple HTML5 Canvas game](http://www.lostdecadegames.com/how-to-make-a-simple-html5-canvas-game/) +A Scala hardcore action game where you possess and play as a Hero :smile:. -Play the [live demo](http://goo.gl/oqSFCa). Scala doc is [here](https://amsterdam-scala.github.io/Sjs-Simple-HTML5-canvas-game/docs/api/index.html#nl.amsscala.package). +## Project +This "Simple HTML5 Canvas Game" is a [Scala.js](https://en.wikipedia.org/wiki/Scala.js) project which targets a browser capable displaying HTML5, especially the `` element. +Stored on GitHub.com, due to [sbt](https://en.wikipedia.org/wiki/sbt_(software)) the code is also remote tested on Travis-CI. Also possible on an other continuous integration service. -Further Resources, Notes, and Considerations +This quite super simple game is heavily über engineered. It's certainly not the game that counts but the technology around it, it features: + +1. [HTML5 Canvas](https://en.wikipedia.org/wiki/Canvas_element) controlled by Scala.js +1. Headless canvas [Selenium 2](https://en.wikipedia.org/wiki/Selenium_(software)) "in browser testing" with the recently released ScalaTest 3.x +1. [ScalaTest 3.x](http://www.scalatest.org) featuring "async" testing styles. +1. Scala 2.12 compiler. +1. Exhaustive use of a variety of Scala features, e.g.: + * `Traits`, (`case`) `Class`es and `Object`s (singletons) + * `Future`s sane way to dramatically reduce latency in web requests + * [Generic[T] objects](https://en.wikipedia.org/wiki/Generic_programming) (even in the frenzied Ough). + * [Algebraic Data Types](https://en.wikipedia.org/wiki/Algebraic_data_type) + * [Pattern matching](https://en.wikipedia.org/wiki/Pattern_matching) + * [Lazy evaluation](https://en.wikipedia.org/wiki/Lazy_evaluation) +1. Reactive design instead of continuous polling. +1. Eliminating a continuously redrawn of the canvas saves cpu time and (mobile) power. +1. Tackling [CORS](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image) enabled images. +1. [Scala generated HTML](http://www.lihaoyi.com/scalatags/). +1. CSS Ribbon +1. [Scala 2.12 fresh Scaladoc look.](https://amsterdam-scala.github.io/Sjs-Simple-HTML5-canvas-game/docs/api/index.html). + +## Motivation +Scala.js compile-to-Javascript language is by its compile phase ahead of runtime errors in production. It prevents you of nasty +runtime errors because everything must be ok in the compile phase, specially the types of the functions and variables. + +In the original tutorial in Javascript: [How to make a simple HTML5 Canvas game](http://www.lostdecadegames.com/how-to-make-a-simple-html5-canvas-game/), +a continuous redraw of the canvas was made, which is a simple solution, but resource costly. +## Usage +Play the [live demo](http://goo.gl/oqSFCa). Scaladoc you will find [here](https://amsterdam-scala.github.io/Sjs-Simple-HTML5-canvas-game/docs/api/index.html#nl.amsscala.package). +[Installation instructions here](#installation-instructions) + +## Architecture +![class diagram](https://raw.githubusercontent.com/amsterdam-scala/Sjs-Simple-HTML5-canvas-game/master/doc/HTML5CanvasGame.png) +#### Discussion: + +By the initial call from `SimpleCanvas.main` to `Game.play` its (private) `gameLoop` will periodic started given its `framesPerSec` frequency. +There the status of eventually pressed arrow keys will be tested and per `GameState.keyEffect` converted to a move of the `Hero`. +In an instance of `GameState` the position of the `CanvasComponent`s are immutable recorded. When a change has to be made a new instance will be +generated with only the changed variables adjusted and leaving the rest unchanged by copying the object. + +With the changes in this `CanvasState` a render method of `Page` is only called if the instance is found changed. + +The render method repaints the canvas completely. Successively the background, monster and hero will be painted, so the last image is at the foreground. +The images found are the respectively instances of `CanvasComponent` subclasses `Playground`, `Monster` and `Hero`. +They are asynchronously loaded once at startup by means of the use of `Future`s. + +In spite of the fact that the application is technically one-tier in an MVC design pattern perspective, everything runs in the browser, +the following parts can be identified: + + + + + + + + + + + + + + + + + + + + + + +
PartClassAuxiliary
ModelGameStatePosition
ViewPageCanvasComponents (Playground, Monster and Hero)
ControllerGameGameState
+ +Communication from Game to Page is done by calling with a modified GameState to Page. + +#### Unit Testing + +Unit testing is done with [ScalaTest 3.x](http://www.scalatest.org/) which is completely detached from the JVM system and Java runtime. +Although running sbt, the test code will be executed in a browser. +This is enabled by a [Selenium environment](https://github.com/scala-js/scala-js-env-selenium) interface direct running to [Firefox](http://www.mozilla.org) and via Chrome Driver to a Google [Chrome browser](https://sites.google.com/a/chromium.org/chromedriver/). + +The necessary resources are downloaded from a external server because the test environment lacks a server for this this task. + +The test tasks can be invoked by `chrome:test` for the Google Chrome browser and `firefox:test`, separated configs constructed in `InbrowserTesting.scala`. +As proposed by this [article](http://japgolly.blogspot.nl/2016/03/scalajs-firefox-chrome-sbt.html). + +Unfortunately at [Travis-CI](travis-ci.org/amsterdam-scala/Sjs-Simple-HTML5-canvas-game) it's not possible to run Google Chrome, so `firefox:test` is the only option. +Also unfortunately `chrome:test` fails on `test 10`, so if executed locally `test 10` must be comment out. + + + + + + + + + + + + + + + + + + + + + + + + + + +
Test Class fileCoverageRemarks
CanvasComponentSuitecanvasComponentPlayground, Monster and Hero are concrete classes of CanvasComponent
GameStateSuiteGameState
GameSuiteGame
PageSuitePage
+ +##### CanvasComponentSuite +This excercises a Hero instance against border limitations. +##### GameStateSuite +E.g. GameState equality. +##### GameSuite +Test the effect of the arrow keys on the Hero moves. +##### PageSuite +This is the most interesting unit test, it features: +* Asynchronous non-blocking testing +* Canvas testing + +###### Asynchronous non-blocking testing +ScalaTest 3.x supports asynchronous non-blocking testing by returning a `Future[Assertion]` type. With this the assertion is +postponed as long if the `Future` is not completed. +###### Canvas testing +Because of the difference of processing between various render engines in browsers, is it hard to test the content of a canvas. +One pixel difference becomes immediately a negative test result. However, a couple of techniques can be used by hashing the canvas to +a hash value. The techniques are: +* Exact comparison, only possible if a complete image is rendered in the same sized canvas. In this case no processing (cropping, resizing) is required and therefor not tainted. Only the pixels of original source are used which gives the same result, even in different browsers. Actual it's a property of the source. +* A different hash value per different browser. In this case there are multiple hash values valid, one per browser. +* Tainted canvas. E.g. text on a canvas gives sometimes a slightly different result in pixels by e.g. rounding errors. The only test we can do is to test if the canvas has changed. + +## Installation instructions +1. Clone the Github project to a new directory. This is the project directory which become the working directory of current folder. +1. Naturally, at least a Java SE Runtime Environment (JRE) is installed on your platform and has a path to it enables execution. +1. (Optional) Test this by submitting a `java -version` command in a [Command Line Interface (CLI, terminal)](https://en.wikipedia.org/wiki/Command-line_interface). The output should look like this: +``` +java version "1.8.0_102" +Java(TM) SE Runtime Environment (build 1.8.0_102-b14) +Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode) +``` +1. Make sure sbt is runnable from almost any work directory, use eventually one of the platform depended installers: + 1. [Installing sbt on Mac](http://www.scala-sbt.org/release/docs/Installing-sbt-on-Mac.html) or + 1. [Installing sbt on Windows](http://www.scala-sbt.org/release/docs/Installing-sbt-on-Windows.html) or + 1. [Installing sbt on Linux](http://www.scala-sbt.org/release/docs/Installing-sbt-on-Linux.html) or + 1. [Manual installation](http://www.scala-sbt.org/release/docs/Manual-Installation.html) (not recommended) +1. (Optional ) To test if sbt is effective submit the `sbt sbtVersion` command. The response could look like as this: +``` +[info] Set current project to fransdev (in build file:/C:/Users/FransDev/) +[info] 0.13.12 +``` +Remember shells (CLI's) are not reactive. To pick up the new [environment variables](https://en.wikipedia.org/wiki/Environment_variable) the CLI must be closed and restarted. +1. Run sbt in one of the next modes in a CLI in the working directory or current folder, a compilation will be started and a local web server will be spinned up using: + 1. Inline mode on the command line: `sbt fastOptJS` or + 1. Interactive mode, start first the sbt by hitting in the CLI `sbt` followed by `fastOptJS` on the sbt prompt, or + 1. Triggered execution by a `~` before the command so `~fastOptJS`. This command will execute and wait after the target code is in time behind the source code (Auto build). +1. sbt will give a notice that the server is listening by the message: `Bound to localhost/127.0.0.1:12345` + (Ignore the dead letter notifications with the enter key.) +1. Open this application in a browser on [this given URL](http://localhost:12345/target/scala-2.12/classes/index-dev.html) + +When running this way a tool ["workbench"](https://github.com/lihaoyi/workbench) also will be running in the browser, noticeable by opening the console of the browser. + + + + + +#### Licence +Licensed under the EUPL-1.1 + + +```------------------------------------------------------------------------------- +Language files blank comment code +------------------------------------------------------------------------------- +Scala 6 96 113 261 + + +game.js +------------------------------------------------------------------------------- +Language files blank comment code +------------------------------------------------------------------------------- +JavaScript 1 21 16 93 + +Scala.js minimal project +------------------------------------------------------------------------------- +Language files blank comment code +------------------------------------------------------------------------------- +JavaScript 1 26 1 572 + +------------------------------------------------------------------------------- +Language files blank comment code +------------------------------------------------------------------------------- +JavaScript 2 795 1 15423 +HTML 2 13 25 51 +CSS 1 14 0 49 +------------------------------------------------------------------------------- +SUM: 5 822 26 15523 +------------------------------------------------------------------------------- +``` diff --git a/build.sbt b/build.sbt index d9f2a01..373f002 100644 --- a/build.sbt +++ b/build.sbt @@ -1,59 +1,67 @@ - name := "Simple Game" - version := "0.0" - description := "Simple HTML5 Canvas game ported to Scala.js." - organization := "nl.amsscala" - organizationName := "Amsterdam.scala Meetup Group" -organizationHomepage := Some(url("http://www.meetup.com/amsterdam-scala/")) - homepage := Some(url("http://github.com/amsterdam-scala/Sjs-Full-Window-HTML5-Canvas")) - startYear := Some(2016) - licenses += "EUPL v.1.1" -> url("http://joinup.ec.europa.eu/community/eupl/og_page/european-union-public-licence-eupl-v11") - +lazy val commonSettings = Seq( + name := "Simple HTML5 Canvas Game", + version := "2.0", + description := "Simple HTML5 Canvas game ported to Scala.js.", + organization := "nl.amsscala", + organizationName := "Amsterdam.scala Meetup Group", +organizationHomepage := Some(url("http://www.meetup.com/amsterdam-scala/")), + homepage := Some(url("http://github.com/amsterdam-scala/Sjs-Full-Window-HTML5-Canvas")), + startYear := Some(2016), + licenses += "EUPL-1.1" -> url("http://joinup.ec.europa.eu/community/eupl/og_page/european-union-public-licence-eupl-v11") +) // KEEP THIS normalizedName CONSTANTLY THE SAME, otherwise the outputted JS filename will be changed. normalizedName := "main" // ** Scala dependencies ** -scalaVersion in ThisBuild := "2.11.8" +scalaVersion in ThisBuild := "2.12.0" +scalacOptions in ThisBuild ++= Seq("-unchecked", "-deprecation") +scalacOptions in (Compile,doc) ++= + Seq("-doc-root-content", baseDirectory.value + "/src/main/scala-2.12/root-doc.md", "-groups", "-implicits") libraryDependencies ++= Seq( - "be.doeraene" %%% "scalajs-jquery" % "0.9.0", - "com.lihaoyi" %%% "scalatags" % "0.6.0", +//"be.doeraene" %%% "scalajs-jquery" % "0.9.0", + "com.lihaoyi" %%% "scalatags" % "0.6.2", "org.scala-js" %%% "scalajs-dom" % "0.9.1", "org.scalatest" %%% "scalatest" % "3.0.0" % "test" ) skip in packageJSDependencies := false // All JavaScript dependencies to be concatenated to a single file -scalacOptions in (Compile,doc) ++= Seq("-doc-root-content", baseDirectory.value+"/src/main/scala-2.11/root-doc.md", +scalacOptions in (Compile,doc) ++= Seq("-doc-root-content", baseDirectory.value + "/src/main/scala-2.12/root-doc.md", "-groups", "-implicits") // ** Scala.js configuration ** -// lazy val root = (project in file(".")). -enablePlugins(ScalaJSPlugin) -// Necessary for testing -jsDependencies += RuntimeDOM -scalaJSUseRhino in Global := false +lazy val root: Project = (project in file(".")).enablePlugins(ScalaJSPlugin).settings(commonSettings: _*). + configure(InBrowserTesting.js) + +// jsEnv in Test := new org.scalajs.jsenv.selenium.SeleniumJSEnv(org.scalajs.jsenv.selenium.Chrome()) + +// Firefox works only with FireFox 46.0.1-, and since 48.0 GeckoDriver (aka Marionette) +// (https://ftp.mozilla.org/pub/firefox/releases/46.0.1/win64-EME-free/en-US/Firefox%20Setup%2046.0.1.exe) +// jsEnv in Test := new org.scalajs.jsenv.selenium.SeleniumJSEnv(org.scalajs.jsenv.selenium.Firefox()) // If true, a launcher script src="../[normalizedName]-launcher.js will be generated // that always calls the main def indicated by the used JSApp trait. -persistLauncher := true +persistLauncher in Compile := true persistLauncher in Test := false // Will create [normalizedName]-jsdeps.js containing all JavaScript libraries -// jsDependencies ++= Seq("org.webjars" % "jquery" % "3.1.0" / "3.1.0/jquery.js") +// jsDependencies ++= Seq("org.webjars" % "jquery" % "2.1.4" / "2.1.4/jquery.js") // jsDependencies += "org.webjars" % "bootstrap" % "3.3.6" / "bootstrap.js" minified "bootstrap.min.js" dependsOn "2.2.4/jquery.js" // ScalaTest settings // // testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-oF") -// Workbench settings ** +// Li Haoyi's Workbench settings ** if (sys.env.isDefinedAt("CI")) { - println("Workbench disabled ", sys.env.getOrElse("CI", "?")) + println("[Info] Li Haoyi's workbench disabled ", sys.env.getOrElse("CI", "?")) Seq.empty -} else { - println("Workbench enabled") - workbenchSettings -} +} else workbenchSettings -updateBrowsers <<= updateBrowsers.triggeredBy(fastOptJS in Compile) +if (sys.env.isDefinedAt("CI")) normalizedName := normalizedName.value // Dummy +else // Update without refreshing the page every time fastOptJS completes + refreshBrowsers <<= refreshBrowsers.triggeredBy(fastOptJS in Compile) -bootSnippet := "nl.amsscala.simplegame.SimpleCanvasGame().main();" +if (sys.env.isDefinedAt("CI")) normalizedName := normalizedName.value +else // Workbench has to know how to restart your application + bootSnippet := "nl.amsscala.simplegame.SimpleCanvasGame().main();" diff --git a/project/InBrowserTesting.scala b/project/InBrowserTesting.scala new file mode 100644 index 0000000..4461910 --- /dev/null +++ b/project/InBrowserTesting.scala @@ -0,0 +1,102 @@ +import org.scalajs.jsenv.selenium.{Chrome, Firefox, SeleniumJSEnv} +import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ +import org.scalajs.sbtplugin.ScalaJSPluginInternal._ +import org.scalajs.sbtplugin.cross.CrossProject +import sbt.Keys._ +import sbt.{Configuration, Defaults, Project, TaskKey, Test, config, inConfig} + +object InBrowserTesting { + + lazy val testAll = TaskKey[Unit]("test-all", "Run tests in all test platforms.") + + val ConfigFirefox = config("firefox") + val ConfigChrome = config("chrome") + + def cross: CrossProject => CrossProject = + _.jvmConfigure(jvm).jsConfigure(js) + + def js: Project => Project = + _.configure( + browserConfig(ConfigFirefox, new SeleniumJSEnv(Firefox())), + browserConfig(ConfigChrome, new SeleniumJSEnv(Chrome()))) + .settings( + testAll := { + // (test in Test).value // No jvm test available + (test in ConfigFirefox).value + (test in ConfigChrome).value + }) + + private def browserConfig(cfg: Configuration, env: SeleniumJSEnv): Project => Project = + _.settings( + inConfig(cfg)( + Defaults.testSettings ++ + scalaJSTestSettings ++ + Seq( + + // Scala.JS public settings + checkScalaJSSemantics := (checkScalaJSSemantics in Test).value, + emitSourceMaps := (emitSourceMaps in Test).value, + fastOptJS := (fastOptJS in Test).value, + fullOptJS := (fullOptJS in Test).value, + jsDependencies := (jsDependencies in Test).value, + jsDependencyFilter := (jsDependencyFilter in Test).value, + jsDependencyManifest := (jsDependencyManifest in Test).value, + jsDependencyManifests := (jsDependencyManifests in Test).value, + jsManifestFilter := (jsManifestFilter in Test).value, + // loadedJSEnv := (loadedJSEnv in Test).value, + packageJSDependencies := (packageJSDependencies in Test).value, + packageMinifiedJSDependencies := (packageMinifiedJSDependencies in Test).value, + packageScalaJSLauncher := (packageScalaJSLauncher in Test).value, + persistLauncher := (persistLauncher in Test).value, + relativeSourceMaps := (relativeSourceMaps in Test).value, + resolvedJSDependencies := (resolvedJSDependencies in Test).value, + // resolvedJSEnv := (resolvedJSEnv in Test).value, + // scalaJSConsole := (scalaJSConsole in Test).value, + scalaJSIR := (scalaJSIR in Test).value, + scalaJSLauncher := (scalaJSLauncher in Test).value, + scalaJSLinkedFile := (scalaJSLinkedFile in Test).value, + scalaJSNativeLibraries := (scalaJSNativeLibraries in Test).value, + scalaJSOptimizerOptions := (scalaJSOptimizerOptions in Test).value, + scalaJSOutputMode := (scalaJSOutputMode in Test).value, + scalaJSOutputWrapper := (scalaJSOutputWrapper in Test).value, + scalajsp := (scalajsp in Test).value, + scalaJSSemantics := (scalaJSSemantics in Test).value, + scalaJSStage := (scalaJSStage in Test).value, + + // Scala.JS internal settings + scalaJSClearCacheStats := (scalaJSClearCacheStats in Test).value, + scalaJSEnsureUnforked := (scalaJSEnsureUnforked in Test).value, + scalaJSIRCacheHolder := (scalaJSIRCacheHolder in Test).value, + scalaJSIRCache := (scalaJSIRCache in Test).value, + scalaJSLinker := (scalaJSLinker in Test).value, + scalaJSRequestsDOM := (scalaJSRequestsDOM in Test).value, + sjsirFilesOnClasspath := (sjsirFilesOnClasspath in Test).value, + usesScalaJSLinkerTag := (usesScalaJSLinkerTag in Test).value, + + // SBT test settings + definedTestNames := (definedTestNames in Test).value, + definedTests := (definedTests in Test).value, + // executeTests := (executeTests in Test).value, + // loadedTestFrameworks := (loadedTestFrameworks in Test).value, + // testExecution := (testExecution in Test).value, + // testFilter := (testFilter in Test).value, + testForkedParallel := (testForkedParallel in Test).value, + // testFrameworks := (testFrameworks in Test).value, + testGrouping := (testGrouping in Test).value, + // testListeners := (testListeners in Test).value, + // testLoader := (testLoader in Test).value, + // testOnly := (testOnly in Test).value, + testOptions := (testOptions in Test).value, + // testQuick := (testQuick in Test).value, + testResultLogger := (testResultLogger in Test).value, + // test := (test in Test).value, + + // In-browser settings + jsEnv := env, + requiresDOM := true, + scalaJSUseRhino := false))) + + def jvm: Project => Project = + _.settings( + testAll := (test in Test).value) +} diff --git a/project/build.properties b/project/build.properties index 35c88ba..5f32afe 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.12 +sbt.version=0.13.13 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index 603d413..a9df8b3 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1,4 @@ addSbtPlugin("com.lihaoyi" % "workbench" % "latest.integration") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.11") -addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.6.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.13") +libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "0.1.3" +// addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.6.0") diff --git a/src/main/resources/css/main.css b/src/main/resources/css/main.css index 7a356ec..72eba98 100644 --- a/src/main/resources/css/main.css +++ b/src/main/resources/css/main.css @@ -1,3 +1,63 @@ body { margin: 0 0; } + +.ribbon { + top: 3.2em; + right: -3.7em; + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); + color:#fff; + display: block; + padding: .6em 3.5em; + position: fixed; + text-align: center; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +.bg-navy { + background-color: #001f3f; } + +.bg-blue { + background-color: #0074d9; } + +.bg-teal { + background-color: #39cccc; } + +.bg-olive { + background-color: #3d9970; } + +.bg-green { + background-color: #218F2E; } + +.bg-yellow { + background-color: #ffdc00; } + +.bg-orange { + background-color: #ff851b; } + +.bg-red { + background-color: #ff4136; } + +.bg-fuchsia { + background-color: #f012be; } + +.bg-purple { + background-color: #b10dc9; } + +.bg-maroon { + background-color: #85144b; } + +.bg-gray { + background-color: #aaaaaa; } + +.bg-black { + background-color: #111111; } diff --git a/src/main/resources/index-dev.html b/src/main/resources/index-dev.html index 0b1df81..7c69052 100644 --- a/src/main/resources/index-dev.html +++ b/src/main/resources/index-dev.html @@ -32,6 +32,7 @@ +Under development diff --git a/src/main/resources/index.html b/src/main/resources/index.html index b7000d9..5f41f31 100644 --- a/src/main/resources/index.html +++ b/src/main/resources/index.html @@ -29,7 +29,11 @@ -Fork me on GitHub +Fork me on GitHub diff --git a/src/main/scala-2.11/nl/amsscala/simplegame/Page.scala b/src/main/scala-2.11/nl/amsscala/simplegame/Page.scala deleted file mode 100644 index f7f08aa..0000000 --- a/src/main/scala-2.11/nl/amsscala/simplegame/Page.scala +++ /dev/null @@ -1,79 +0,0 @@ -package nl.amsscala -package simplegame - -import org.scalajs.dom - -import scalatags.JsDom.all._ - -/** All related to Html5 visuals */ -protected trait Page { - // Create the canvas - private[simplegame] val canvas = dom.document.createElement("canvas").asInstanceOf[dom.html.Canvas] - canvas.setAttribute("crossOrigin", "anonymous") - private[simplegame] val ctx = canvas.getContext("2d").asInstanceOf[dom.CanvasRenderingContext2D] - private[this] val (bgImage, heroImage, monsterImage) = (Image("img/background.png"), Image("img/hero.png"), Image("img/monster.png")) - - /** - * Draw everything - * - * @param gs Game state to make graphical - * @return None if not ready else the same GameState if drawn - */ - protected[simplegame] def render(gs: GameState) = { - def gameOverTxt = "Game Over?" - def explainTxt = "Use the arrow keys to\nattack the hidden monster." - - if (bgImage.isReady && heroImage.isReady && monsterImage.isReady) { - ctx.drawImage(bgImage.element, 0, 0, canvas.width, canvas.height) - ctx.drawImage(heroImage.element, gs.hero.pos.x, gs.hero.pos.y) - ctx.drawImage(monsterImage.element, gs.monster.pos.x, gs.monster.pos.y) - - // Score - ctx.fillStyle = "rgb(250, 250, 250)" - ctx.font = "24px Helvetica" - ctx.textAlign = "left" - ctx.textBaseline = "top" - ctx.fillText(f"Goblins caught: ${gs.monstersCaught}%03d", 32, 32) - - if (gs.newGame) { - ctx.textAlign = "center" - ctx.font = "48px Helvetica" - - ctx.fillText( - if (gs.isGameOver) gameOverTxt else { - val txt = explainTxt.split('\n') - ctx.fillText(txt(1), canvas.width / 2, canvas.height / 2 + 32) - txt(0) - }, canvas.width / 2, canvas.height / 2 - 48 - ) - } - - Some(gs) - } else None - } - - private class Image(private[this] val src: String, var isReady: Boolean = false) { - val element = dom.document.createElement("img").asInstanceOf[dom.raw.HTMLImageElement] - // element.setAttribute("crossOrigin", "anonymous") - element.onload = (e: dom.Event) => isReady = true - element.src = src - } - - private[this] object Image { - def apply(src: String) = new Image(src) - } - - canvas.width = dom.window.innerWidth.toInt - canvas.height = dom.window.innerHeight.toInt - 25 - println(s"Dimension of canvas set to ${canvas.width},${canvas.height}") - canvas.textContent = "Your browser doesn't support the HTML5 CANVAS tag." - - dom.document.body.appendChild(div( - cls := "content", style := "text-align:center; background-color:#3F8630;", - canvas, - a(href := "http://www.lostdecadegames.com/how-to-make-a-simple-html5-canvas-game/", "Simple HTML5 Canvas game"), - " ported to ", - a(href := "http://www.scala-js.org/", "ScalaJS"), "." - ).render) - -} diff --git a/src/main/scala-2.11/nl/amsscala/simplegame/game.scala b/src/main/scala-2.11/nl/amsscala/simplegame/game.scala deleted file mode 100644 index b9eaeb4..0000000 --- a/src/main/scala-2.11/nl/amsscala/simplegame/game.scala +++ /dev/null @@ -1,160 +0,0 @@ -package nl.amsscala -package simplegame - -import org.scalajs.dom -import org.scalajs.dom.ext.KeyCode.{ Down, Left, Right, Up } - -import scala.collection.mutable -import scala.scalajs.js - -/** The game with its rules. */ -protected trait Game { - private[this] val framesPerSec = 30 - - /** - * Initialize Game loop - * - * @param canvas The visual html element - * @param headless An option to run for testing - */ - protected def play(canvas: dom.html.Canvas, headless: Boolean) { - // Keyboard events store - val keysPressed: keysBufferType = mutable.Map.empty - var prev = 0D - var oldUpdated: Option[GameState] = None - - // The main game loop - def gameLoop = () => { - val now = js.Date.now() - val delta = now - prev - val updated = oldUpdated.getOrElse(new GameState(canvas, -1, true)).updateGame(delta / 1000, keysPressed, canvas) - - if (oldUpdated.isEmpty || (oldUpdated.get.hero.pos != updated.hero.pos)) - oldUpdated = SimpleCanvasGame.render(updated) - - prev = now - } - - // Let's play this game! - if (!headless) { - // ToDo mobile application navigation - dom.window.setInterval(gameLoop, 1000 / framesPerSec) - - dom.window.addEventListener("keydown", (e: dom.KeyboardEvent) => - e.keyCode match { - case Left | Right | Up | Down if oldUpdated.isDefined => - keysPressed += e.keyCode -> (js.Date.now(), oldUpdated.get.hero.pos) - case _ => - }, useCapture = false) - - dom.window.addEventListener("keyup", (e: dom.KeyboardEvent) => { - keysPressed -= e.keyCode - }, useCapture = false) - } - } -} - -/** - * GameState constructor - * - * @param hero Hero object with its position - * @param monster Monster object with its position - * @param monstersCaught The score - * @param newGame Flags new game - */ -case class GameState( - hero: Hero[Int], - monster: Monster[Int], - monstersCaught: Int = 0, - newGame: Boolean -) { - - /** - * Update game objects according the pressed keys. - * - * @param latency Passed time difference to adjust displacement. - * @param keysDown Collection of key currently pressed. - * @param canvas The visual html element. - * @return Conditional updated GameState, not changed or start GameState. - */ - def updateGame(latency: Double, keysDown: keysBufferType, canvas: dom.html.Canvas): GameState = { - - def directions = Map( // Key to direction translation - Left -> Position(-1, 0), Right -> Position(1, 0), Up -> Position(0, -1), Down -> Position(0, 1) - ).withDefaultValue(Position(0, 0)) - - // Convert pressed keyboard keys to coordinates - def displacements: mutable.Iterable[Position[Int]] = keysDown.map { case (k, _) => directions(k) } - - /* Experimental, does not properly work - def displacements: mutable.Iterable[Position[Int]] = keysDown.map { case (key, (timeAtKPress, posAtKPress)) => - directions(key) * (Hero.speed * (now - timeAtKPress) / 1000 ).toInt + posAtKPress - hero.pos - } - - val newHero = new Hero(displacements.fold(hero.pos)((z, x)=> z + x)) - */ - if (keysDown.isEmpty) this - else { - val newHero = new Hero(displacements.fold(hero.pos) { (z, i) => z + i * (Hero.speed * latency).toInt }) - - if (newHero.pos.isValidPosition(Position(canvas.width, canvas.height), Hero.size)) // Are they touching? - if (newHero.pos.areTouching(monster.pos, Hero.size)) // Reset the game when the player catches a monster - new GameState(canvas, monstersCaught, true) - else copy(hero = newHero, newGame = false) - else this - } - } - - def isGameOver = newGame && monstersCaught != 0 - - /** - * Auxiliary GameState constructor - * - * Creates a start state, Hero centric and Monster random positions - * - * @param canvas The visual html element - * @param oldScore Score accumulator - */ - def this(canvas: dom.html.Canvas, oldScore: Int, newGame: Boolean) { - this( - Hero(canvas.width / 2, canvas.height / 2), - // Throw the monster somewhere on the screen randomly - Monster((math.random * (canvas.width - Hero.size)).toInt, (math.random * (canvas.height - Hero.size)).toInt), - oldScore + 1, - newGame - ) - } -} - -/** - * Monster class, holder for its coordinate, copied as extentension to the Hero class - * - * @param pos Monsters' position - * @tparam T Numeric generic abstraction - */ -class Monster[T: Numeric](val pos: Position[T]) { - override def equals(that: Any): Boolean = that match { - case that: Monster[T] => this.pos == that.pos - case _ => false - } - - override def toString = s"${this.getClass.getSimpleName} $pos" - protected[simplegame] def isValidPosition(canvas: dom.html.Canvas) = - pos.isValidPosition(Position(canvas.width, canvas.height).asInstanceOf[Position[T]], Hero.size.asInstanceOf[T]) -} - -object Monster { - // def apply[T: Numeric](pos: Position[T]) = new Monster(pos) - def apply[T: Numeric](x: T, y: T) = new Monster(Position(x, y)) -} - -class Hero[A: Numeric](override val pos: Position[A]) extends Monster[A](pos) - -/** Compagnion object of class Hero */ -object Hero { - val size = 32 - val speed = 256 - - // def apply[T: Numeric](pos: Position[T]) = new Hero(pos) - def apply[T: Numeric](x: T, y: T) = new Hero(Position(x, y)) -} diff --git a/src/main/scala-2.11/root-doc.md b/src/main/scala-2.11/root-doc.md deleted file mode 100644 index ba527bf..0000000 --- a/src/main/scala-2.11/root-doc.md +++ /dev/null @@ -1,7 +0,0 @@ -This is the documentation for a simple HTML5 Canvas game written in Scala, and cross compiled to run in the browser targeting the HTML5 Canvas. - -== Package structure == - -Notable packages include: - - - [[nl.amsscala.simplegame `nl.amsscala.simplegame`]] diff --git a/src/main/scala-2.12/nl/amsscala/simplegame/Game.scala b/src/main/scala-2.12/nl/amsscala/simplegame/Game.scala new file mode 100644 index 0000000..6513959 --- /dev/null +++ b/src/main/scala-2.12/nl/amsscala/simplegame/Game.scala @@ -0,0 +1,67 @@ +package nl.amsscala +package simplegame + +import org.scalajs.dom +import org.scalajs.dom.ext.KeyCode.{Down, Left, Right, Up} + +import scala.collection.mutable +import scala.concurrent.Future +import scala.scalajs.js + +/** This game with its comprehensible rules. */ +protected trait Game { + private[this] val framesPerSec = 25 + + implicit def executionContext = scala.scalajs.concurrent.JSExecutionContext.Implicits.queue + + /** + * Initialize Game loop + * + * @param canvas The visual html element + * @param headless An option to run for testing + */ + protected def play(canvas: dom.html.Canvas, headless: Boolean) { + // Keyboard events store + val (keysPressed, gameState) = (mutable.Set.empty[Int], GameState[SimpleCanvasGame.T](canvas)) + var prevTimestamp = js.Date.now() + + // Collect all Futures of onload events + val loaders = gameState.pageElements.map(pg => SimpleCanvasGame.imageFuture(pg.src)) + + Future.sequence(loaders).map { load => // Create GameState with loaded images + var prevGS = new GameState(canvas, gameState.pageElements.zip(load).map { case (el, img) => el.copy(img = img) }) + + /** The main game loop, by interval callback invoked. */ + def gameLoop() = { + val nowTimestamp = js.Date.now() + val actualGS = prevGS.keyEffect((nowTimestamp - prevTimestamp) / 1000, keysPressed) + + prevTimestamp = nowTimestamp + + // Render the conditional only by movement of Hero, saves power + if (prevGS != actualGS) prevGS = SimpleCanvasGame.render(actualGS) + } + + SimpleCanvasGame.render(prevGS) // First draw + + // Let's see how this game plays! + if (!headless) {// For test purpose, a facility to silence the listeners. + scala.scalajs.js.timers.setInterval(1000 / framesPerSec)(gameLoop()) + + // TODO: mobile application navigation + + dom.window.addEventListener("keydown", (e: dom.KeyboardEvent) => + e.keyCode match { + case Left | Right | Up | Down => keysPressed += e.keyCode + case _ => + }, useCapture = false) + + dom.window.addEventListener("keyup", (e: dom.KeyboardEvent) => { + keysPressed -= e.keyCode + }, useCapture = false) + } + // Listeners are now obsoleted , so unload them all. + load.foreach(i => i.onload = null) + } + } +} diff --git a/src/main/scala-2.12/nl/amsscala/simplegame/GameState.scala b/src/main/scala-2.12/nl/amsscala/simplegame/GameState.scala new file mode 100644 index 0000000..843261b --- /dev/null +++ b/src/main/scala-2.12/nl/amsscala/simplegame/GameState.scala @@ -0,0 +1,103 @@ +package nl.amsscala +package simplegame + +import org.scalajs.dom + +import scala.collection.mutable + +/** + * Container holding the Game's state. + * @param canvas The visual HTML element + * @param pageElements This member lists the page elements. They are always in this order: Playground, Monster and Hero. + * E.g. pageElements.head is Playground, pageElements(1) is the Monster, pageElements.takes(2) are those both. + * @param monstersCaught + * @param isNewGame Flags game play is just fresh started + * @param isGameOver Flags a new turn + * @param monstersHitTxt + * @param _gameOverTxt + * @param _explainTxt + * @tparam T Numeric generic abstraction + */ +class GameState[T: Numeric](canvas: dom.html.Canvas, + val pageElements: Vector[CanvasComponent[T]], + val isNewGame: Boolean = true, + val isGameOver: Boolean = false, + monstersCaught: Int = 0, + val monstersHitTxt: String = GameState.monsterText(0), + _gameOverTxt: => String = GameState.gameOverTxt, + _explainTxt: => String = GameState.explainTxt + ) { + private[simplegame] def copy(hero: Hero[T]) = new GameState(canvas, + pageElements.take(2) :+ hero, + monstersCaught = monstersCaught, + monstersHitTxt = monstersHitTxt, + isNewGame = false) + + override def equals(that: Any): Boolean = + that match { + case that: GameState[T] => this.pageElements == that.pageElements + case _ => false + } + + def explainTxt = _explainTxt + def gameOverTxt = _gameOverTxt + def hero = pageElements.last.asInstanceOf[Hero[T]] + private def monster = pageElements(1).asInstanceOf[Monster[T]] + private def playGround = pageElements.head.asInstanceOf[Playground[T]] + + /** + * Process on a regular basis the arrow keys pressed. + * + * @param latency + * @param keysDown + * @return a state with the Hero position adjusted. + */ + def keyEffect(latency: Double, keysDown: mutable.Set[Int]): GameState[T] = { + if (keysDown.isEmpty) this + else { + // Get new position according the pressed arrow keys + val newHero = hero.keyEffect(latency, keysDown) + // Are they touching? + val size = Hero.pxSize.asInstanceOf[T] + if (newHero.isValidPosition(canvas)) + if (newHero.pos.areTouching(monster.pos, size)) newGame // Reset the game when the player catches a monster + else copy(hero = newHero) // New position for Hero, with isNewGame reset to false + else this + } + } + + /** + * New game, Monster randomized, Hero centralized, score updated + * @return + */ + private def newGame = new GameState(canvas, + Vector(playGround, monster.copy(canvas), hero.copy(canvas)), + monstersCaught = monstersCaught + 1, + monstersHitTxt = GameState.monsterText(monstersCaught + 1), + isGameOver = true) + + override def toString: String = s"${Position(canvas.width, canvas.height)} $pageElements isNew:$isNewGame $monstersHitTxt" + + require(pageElements.size == 3 && + playGround.isInstanceOf[Playground[T]] && + monster.isInstanceOf[Monster[T]] && + hero.isInstanceOf[Hero[T]], "Page elements are not listed well.") + +} + +/** + * Companion object holding static constant definitions. + */ +object GameState { + + def apply[T: Numeric](canvas: dom.html.Canvas) = + new GameState[T](canvas, Vector(new Playground[T](), Monster[T](canvas, Monster.randomPosition(canvas)), Hero[T](canvas))) + + // Randomness left out for testing + def apply[T: Numeric](canvas: dom.html.Canvas, monsterPos : Position[T], heroPos : Position[T]) = + new GameState[T](canvas, Vector(new Playground[T](), Monster[T](canvas, monsterPos), Hero(heroPos))) + + def explainTxt = "Use the arrow keys to\nattack the hidden monster." + def gameOverTxt = "Game Over?" + def monsterText(score: Int) = f"Goblins caught: $score%03d" +} diff --git a/src/main/scala-2.12/nl/amsscala/simplegame/Page.scala b/src/main/scala-2.12/nl/amsscala/simplegame/Page.scala new file mode 100644 index 0000000..0cadb65 --- /dev/null +++ b/src/main/scala-2.12/nl/amsscala/simplegame/Page.scala @@ -0,0 +1,111 @@ +package nl.amsscala +package simplegame + +import org.scalajs.dom + +import scala.concurrent.{Future, Promise} +import scalatags.JsDom.all._ + +/** Everything related to Html5 visuals as put on a HTML page. */ +trait Page { //Create canvas with a 2D processor + val canvas = dom.document.createElement("canvas").asInstanceOf[dom.html.Canvas] + private [simplegame] val ctx = canvas.getContext("2d").asInstanceOf[dom.CanvasRenderingContext2D] + + private lazy val postponed = // Create the HTML body element with content + dom.document.body.appendChild(div(cls := "content", style := "text-align:center; background-color:#3F8630;", canvas, + a(href := "http://www.lostdecadegames.com/how-to-make-a-simple-html5-canvas-game/", + title := s"This object code is compiled with type parameter ${genericDetect(0D.asInstanceOf[SimpleCanvasGame.T])}.", + "Simple HTML5 Canvas game"), " ported to ", + a(href := "http://www.scala-js.org/", "Scala.js")).render) + + /** + * Draw everything accordingly the given `GameState`. + * + * Order: Playground, Monster, Hero, monstersHitTxt, explainTxt/gameOverTxt + * + * @param gs Game state to make the graphics. + * @return The same gs + */ + def render[T](gs: GameState[T]) = { + postponed + // Draw each page element in the specific list order + gs.pageElements.foreach(pe => { + def drawImage(resize: Position[Int]) = + ctx.drawImage(pe.img, pe.pos.x.asInstanceOf[Int], pe.pos.y.asInstanceOf[Int], resize.x, resize.y) + + drawImage(pe match { + case _: Playground[_] => canvasDim[Int](canvas) + case pm: CanvasComponent[_] => dimension(pm.img) // The otherwise or default clause + }) + }) + + ctx.fillStyle = "rgb(250, 250, 250)" + // Score + if (!gs.monstersHitTxt.isEmpty) { + ctx.font = "24px Helvetica" + ctx.textAlign = "left" + ctx.textBaseline = "top" + ctx.fillText(gs.monstersHitTxt, 32, 32) + } + + if (gs.isNewGame) { + val centr = center(canvas) + + ctx.textAlign = "center" + ctx.font = "48px Helvetica" + + ctx.fillText( + if (gs.isGameOver) gs.gameOverTxt + else { + val txt = gs.explainTxt.split('\n') + ctx.fillText(txt.last, centr.x, centr.y + 32) + txt.head + }, centr.x, centr.y - 48 + ) + } + gs + } + + def center(cnvs: dom.html.Canvas) = Position(canvas.width / 2, canvas.height / 2) + + def canvasDim[D](cnvs: dom.html.Canvas) = Position(cnvs.width, cnvs.height).asInstanceOf[Position[D]] + + canvas.textContent = "Your browser doesn't support the HTML5 CANVAS tag." + resetCanvasWH(canvas, Position(dom.window.innerWidth, dom.window.innerHeight - 25)) + + @inline private def dimension(img: dom.raw.HTMLImageElement) = Position(img.width, img.height) + + /** Convert the onload event of an img tag into a Future */ + def imageFuture(src: String): Future[dom.raw.HTMLImageElement] = { + val img = dom.document.createElement("img").asInstanceOf[dom.raw.HTMLImageElement] + + // Tackling CORS enabled images + img.setAttribute("crossOrigin", "anonymous") + img.src = src + if (img.complete) Future.successful(img) + else { + val p = Promise[dom.raw.HTMLImageElement]() + img.onload = { (e: dom.Event) => p.success(img) } + p.future + } + } + + /** + * Set canvas dimension + * @param cnvs Canvas element + * @param pos Dimension in `Position[P]` + * @tparam P Numeric generic type + */ + @inline + def resetCanvasWH[P: Numeric](cnvs: dom.html.Canvas, pos: Position[P]) = { + cnvs.width = pos.x.asInstanceOf[Int] + cnvs.height = pos.y.asInstanceOf[Int] + } + + private def genericDetect(x: Any) = x match { + case _: Long => "Long" + case _: Int => "Int" + case _: Double => "Double" + case _ => "unknown" + } +} diff --git a/src/main/scala-2.11/nl/amsscala/simplegame/SimpleCanvasGame.scala b/src/main/scala-2.12/nl/amsscala/simplegame/SimpleCanvasGame.scala similarity index 84% rename from src/main/scala-2.11/nl/amsscala/simplegame/SimpleCanvasGame.scala rename to src/main/scala-2.12/nl/amsscala/simplegame/SimpleCanvasGame.scala index 66247a2..3bd4e2e 100644 --- a/src/main/scala-2.11/nl/amsscala/simplegame/SimpleCanvasGame.scala +++ b/src/main/scala-2.12/nl/amsscala/simplegame/SimpleCanvasGame.scala @@ -7,6 +7,8 @@ import scala.scalajs.js.JSApp * Main entry point for application start */ object SimpleCanvasGame extends JSApp with Game with Page { + type T = Long // This sets the generic used by the whole application and tests. + /** * Entry point of execution * called as "nl.amsscala.simplegame.SimpleCanvasGame().main();" @@ -14,4 +16,5 @@ object SimpleCanvasGame extends JSApp with Game with Page { * If `persistLauncher := true` set in sbt build file a `main-launcher.js` launcher is generated. */ def main(): Unit = play(canvas, headless = false) + } diff --git a/src/main/scala-2.12/nl/amsscala/simplegame/canvasComponent.scala b/src/main/scala-2.12/nl/amsscala/simplegame/canvasComponent.scala new file mode 100644 index 0000000..93c45e4 --- /dev/null +++ b/src/main/scala-2.12/nl/amsscala/simplegame/canvasComponent.scala @@ -0,0 +1,133 @@ +package nl.amsscala +package simplegame + +import org.scalajs.dom +import org.scalajs.dom.ext.KeyCode.{Down, Left, Right, Up} + +import scala.collection.mutable + +// TODO: http://stackoverflow.com/questions/12370244/case-class-copy-method-with-superclass + +/** + * Umbrella for `Page`, `Hero` and `Monster` Abstract Data Types. + * @tparam Numeric Numeric generic abstraction + */ +sealed trait CanvasComponent[Numeric] { + val pos: Position[Numeric] + val img: dom.raw.HTMLImageElement + + def copy(img: dom.raw.HTMLImageElement): CanvasComponent[Numeric] + + def src: String + + override def toString = s"${this.getClass.getSimpleName} $pos" + + override def equals(that: Any): Boolean = that match { + case that: CanvasComponent[Numeric] => this.pos == that.pos + case _ => false + } + +} + +/** + * `CanvasComponent`'s implementation for to visual back ground on the canvas. + * + * @param pos Playground position, defaulted to (0,0) + * @param img HTML image + * @tparam G Numeric generic abstraction + */ +final class Playground[G](val pos: Position[G] = Position(0, 0).asInstanceOf[Position[G]], + val img: dom.raw.HTMLImageElement = null) extends CanvasComponent[G] { + + def copy(img: dom.raw.HTMLImageElement): Playground[G] = new Playground(img = img) + + def src = "img/background.png" +} + +/** + * `CanvasComponent`'s implementation for to visual Monster sprite on the canvas. + * @param pos Monsters' position + * @param img HTML image + * @tparam M Numeric generic abstraction + */ +final class Monster[M](val pos: Position[M], val img: dom.raw.HTMLImageElement) extends CanvasComponent[M] { + /** Set a Monster at a (new) random position */ + def copy[D: Numeric](canvas: dom.html.Canvas) = new Monster(Monster.randomPosition[D](canvas), img) + /** Load the img in the Element */ + def copy(image: dom.raw.HTMLImageElement) = new Monster(pos, image) + + def src = "img/monster.png" + +} + +/** + * Companion object of class `Monster` + */ +object Monster { + def apply[M: Numeric](canvas: dom.html.Canvas, randPos: Position[M]) = new Monster(randPos, null) + + private[simplegame] def randomPosition[M: Numeric](canvas: dom.html.Canvas): Position[M] = { + @inline def compute(dim: Int) = (math.random * (dim - Hero.pxSize)).toInt + Position(compute(canvas.width), compute(canvas.height)).asInstanceOf[Position[M]] + } +} + +/** + * `CanvasComponent`'s implementation for to visual Hero sprite on the canvas. + * @param pos Heros' position + * @param img HTML image + * @tparam H Numeric generic abstraction + */ +final class Hero[H: Numeric](val pos: Position[H], val img: dom.raw.HTMLImageElement) extends CanvasComponent[H] { + + def copy(img: dom.raw.HTMLImageElement) = new Hero(pos, img) + + def copy(canvas: dom.html.Canvas) = new Hero(SimpleCanvasGame.center(canvas).asInstanceOf[Position[H]], img) + + def copy(pos: Position[H]) = new Hero(pos, img) + + /** + * Check if the square area is within the rectangle area of the `` + * + * @param canvas Canvas where to + * @return False if a square out of bound + */ + protected[simplegame] def isValidPosition(canvas: dom.html.Canvas) = + pos.isValidPositionEl(SimpleCanvasGame.canvasDim[H](canvas), Hero.pxSize.asInstanceOf[H]) + + def src = "img/hero.png" + + /** + * Compute new position of hero according to the keys pressed + * @param latency Time since previous update. + * @param keysDown Set of the keys pressed. + * @return Computed move Hero + */ + protected[simplegame] def keyEffect(latency: Double, keysDown: mutable.Set[Int]): Hero[H] = { + + // Convert pressed keyboard keys to coordinates + @inline + def displacements: mutable.Set[Position[H]] = { + def dirLookUp = Map(// Key to direction translation + Left -> Position(-1, 0), Right -> Position(1, 0), Up -> Position(0, -1), Down -> Position(0, 1) + ).withDefaultValue(Position(0, 0)) + + keysDown.map { k => dirLookUp(k).asInstanceOf[Position[H]] } + } + + // Compute next position by summing all vectors with the position where the hero is found. + copy(displacements.fold(pos) { (z, vec) => z + vec * (Hero.speed * latency).toInt.asInstanceOf[H] }) + } + +} + +/** Companion object of class `Hero`. */ +object Hero { + protected[simplegame] val (pxSize, speed) = (32, 256) + + /** Hero image centered in the field */ + def apply[H: Numeric](canvas: dom.html.Canvas): Hero[H] = + Hero[H](SimpleCanvasGame.center(canvas).asInstanceOf[Position[H]]) + + def apply[H: Numeric](pos : Position[H]) =new Hero(pos, null) +} diff --git a/src/main/scala-2.11/nl/amsscala/simplegame/package.scala b/src/main/scala-2.12/nl/amsscala/simplegame/package.scala similarity index 51% rename from src/main/scala-2.11/nl/amsscala/simplegame/package.scala rename to src/main/scala-2.12/nl/amsscala/simplegame/package.scala index 773eda6..1a8b530 100644 --- a/src/main/scala-2.11/nl/amsscala/simplegame/package.scala +++ b/src/main/scala-2.12/nl/amsscala/simplegame/package.scala @@ -1,65 +1,66 @@ package nl.amsscala /** - * Provides generic class and operators for dealing with 2D positions. As well dealing with 2D areas. + * This package object provides generic class and operators for 2D `Position`s, as well as dealing with 2D areas. + * + * The package includes externally this package object the main traits `Page`, `Game` and class `GameState` + * as well for the auxiliary trait `CanvasComponent` (overarching for `Hero`, `Monster` and `Playground`). */ package object simplegame { - /** Experimental timestamp and position, displacement is a function of time */ - protected[simplegame]type keysBufferType = scala.collection.mutable.Map[Int, (Double, Position[Int])] /** * Generic base class Position, holding the two ordinates * - * @param x The abscissa - * @param y The ordinate + * @param x The abscissa + * @param y The ordinate * @tparam P Numeric type */ - protected[simplegame] case class Position[P: Numeric](x: P, y: P) { + case class Position[P: Numeric](x: P, y: P) { import Numeric.Implicits.infixNumericOps import Ordering.Implicits.infixOrderingOps - /** Binaire sum operator for two coordinates */ + /** Binaire add operator for two coordinates */ def +(p: Position[P]) = Position(x + p.x, y + p.y) - /** Binaire sum operator e,g. (a, b) + n => (a + n, b +n) */ + /** Binaire add operator e,g. (a, b) + n => (a + n, b + n) */ def +(term: P) = Position(x + term, y + term) /** Binaire subtract operator for the difference of two coordinates */ def -(p: Position[P]) = Position(x - p.x, y - p.y) + /** Binaire subtract operator e,g. (a, b) - n => (a - n, b - n) */ + def -(term: P) = Position(x - term, y - term) + /** Binaire multiply operator for two coordinates, multplies each of the ordinate */ def *(p: Position[P]) = Position(x * p.x, y * p.y) /** Binaire multiply operator e,g. (a, b) * n=> (a * n, b * n) */ def *(factor: P) = Position(x * factor, y * factor) - private def interSectsArea[P: Numeric](p0: Position[P], p1: Position[P], p2: Position[P], p3: Position[P]) = { - @inline def intersectsWith(a0: P, b0: P, a1: P, b1: P) = a0 <= b1 && a1 <= b0 - - intersectsWith(p0.x, p1.x, p2.x, p3.x) && - intersectsWith(p0.y, p1.y, p2.y, p3.y) - } - /** * Check if the square area is within the rectangle area * * @param canvasPos Position of the second square * @param side side of both two squares - * @return False if a square out of bound + * @return False if a square out of bound */ - def isValidPosition(canvasPos: Position[P], side: P): Boolean = { - // println(s"Testing: $x, $y") + def isValidPositionEl(canvasPos: Position[P], side: P): Boolean = { + interSectsArea(Position(0, 0).asInstanceOf[Position[P]], canvasPos, this + side, this) + } - interSectsArea(Position(0, 0).asInstanceOf[Position[P]], canvasPos, this + side, this) + private def interSectsArea(p0: Position[P], p1: Position[P], p2: Position[P], p3: Position[P]) = { + @inline def intersectsWith(a0: P, b0: P, a1: P, b1: P) = a0 <= b1 && a1 <= b0 + // Process the x and y axes + intersectsWith(p0.x, p1.x, p2.x, p3.x) && intersectsWith(p0.y, p1.y, p2.y, p3.y) } /** - * Checks that two squares intersects + * Checks if two squares intersects * * @param posB Position of the second square * @param side side of both two squares - * @return True if a intersection occurs + * @return True if a intersection occurs */ def areTouching(posB: Position[P], side: P): Boolean = interSectsArea(this, this + side, posB, posB + side) } diff --git a/src/main/scala-2.12/root-doc.md b/src/main/scala-2.12/root-doc.md new file mode 100644 index 0000000..123e6d1 --- /dev/null +++ b/src/main/scala-2.12/root-doc.md @@ -0,0 +1,10 @@ +This is the documentation for a simple HTML5 Canvas game written in Scala, and cross compiled to run in the browser targeting the HTML5 Canvas. + +== Package structure == + +Notable packages include: + + - [[nl.amsscala.simplegame `nl.amsscala.simplegame`]] + + + diff --git a/src/test/scala-2.11/nl/amsscala/simplegame/GameSuite.scala b/src/test/scala-2.11/nl/amsscala/simplegame/GameSuite.scala deleted file mode 100644 index 6a3cf38..0000000 --- a/src/test/scala-2.11/nl/amsscala/simplegame/GameSuite.scala +++ /dev/null @@ -1,93 +0,0 @@ -package nl.amsscala -package simplegame - -import org.scalajs.dom -import org.scalajs.dom.ext.KeyCode.{ Down, Left, Right, Up } - -import scala.collection.mutable - -class GameSuite extends SuiteSpec { - - describe("A Hero") { - describe("should tested within the limits") { - val canvas = dom.document.createElement("canvas").asInstanceOf[dom.html.Canvas] - canvas.width = 150 - canvas.height = 100 - it("good path") { - Hero(0, 0).isValidPosition(canvas) shouldBe true - Hero(150 - Hero.size, 100 - Hero.size).isValidPosition(canvas) shouldBe true - } - it("bad path") { - Hero(-1, 0).isValidPosition(canvas) shouldBe false - Hero(4, -1).isValidPosition(canvas) shouldBe false - Hero(0, 101 - Hero.size).isValidPosition(canvas) shouldBe false - Hero(151 - Hero.size, 0).isValidPosition(canvas) shouldBe false - } - - } - } - - describe("The Game") { - describe("should tested by navigation keys") { - import GameSuite.{ dummyTimeStamp, games } - - val canvas = dom.document.createElement("canvas").asInstanceOf[dom.html.Canvas] - canvas.setAttribute("crossOrigin", "anonymous") - canvas.width = 1242 // 1366 - canvas.height = 674 // 768 - - val game = new GameState(canvas, -1, false).copy(monster = Monster(0, 0)) // Keep the monster out of site - - it("good path") { - // No keys, no movement - game.updateGame(1D, mutable.Map.empty, canvas) shouldBe game - - // Opposite horizontal navigation, no movement 1 - game.updateGame(1D, mutable.Map(Left -> dummyTimeStamp, Right -> dummyTimeStamp), canvas) shouldBe game - - // Opposite horizontal navigation, no movement 2 - game.updateGame(1D, mutable.Map(Right -> dummyTimeStamp, Left -> dummyTimeStamp), canvas) shouldBe game - - // Opposite vertical navigation, no movement 1 - game.updateGame(1D, mutable.Map(Up -> dummyTimeStamp, Down -> dummyTimeStamp), canvas) shouldBe game - - // Opposite vertical navigation, no movement 2 - game.updateGame(1D, mutable.Map(Down -> dummyTimeStamp, Up -> dummyTimeStamp), canvas) shouldBe game - - // All four directions, no movement - game.updateGame( - 1D, - mutable.Map(Up -> dummyTimeStamp, Right -> dummyTimeStamp, Left -> dummyTimeStamp, Down -> dummyTimeStamp), - canvas - ) shouldBe game - - games += game.updateGame( - 1D, - mutable.Map(Left -> dummyTimeStamp, Right -> dummyTimeStamp, Up -> dummyTimeStamp, Down -> dummyTimeStamp), - canvas - ) - games.head shouldBe game - games += game.copy(hero = new Hero(game.hero.pos - Position(Hero.speed, Hero.speed))) - // North west navigation - game.updateGame(1D, mutable.Map(Up -> dummyTimeStamp, Left -> dummyTimeStamp), canvas) shouldBe games.last - - games += game.copy(hero = new Hero(game.hero.pos + Position(Hero.speed, Hero.speed))) - // South East navigation - game.updateGame(1D, mutable.Map(Down -> dummyTimeStamp, Right -> dummyTimeStamp), canvas) shouldBe games.last - - } - it("sad path") { // Illegal key code - game.updateGame(1D, mutable.Map(0 -> dummyTimeStamp), canvas) shouldBe game - } - it("bad path") { // No move due a of out canvas limit case - game.updateGame(1.48828125D, mutable.Map(Right -> dummyTimeStamp, Down -> dummyTimeStamp), canvas) shouldBe game - } - - } - } -} - -object GameSuite { - private val dummyTimeStamp = (0D, Position(0, 0)) - private val games = mutable.MutableList.empty[GameState] -} diff --git a/src/test/scala-2.11/nl/amsscala/simplegame/PageSuite.scala b/src/test/scala-2.11/nl/amsscala/simplegame/PageSuite.scala deleted file mode 100644 index 916e235..0000000 --- a/src/test/scala-2.11/nl/amsscala/simplegame/PageSuite.scala +++ /dev/null @@ -1,51 +0,0 @@ -package nl.amsscala.simplegame - -import org.scalajs.dom - -import scala.scalajs.js -import scalatags.JsDom.all._ - -class PageSuite extends SuiteSpec { - val page = new Page { - canvas.width = 1242 // 1366 - canvas.height = 674 // 768 - } - - describe("A Hero") { - describe("should tested within the limits") { - - it("good path") { - { - page.render(GameState(Hero(621, 337), Monster(0, 0), 0, false)) - val imageData: scala.collection.mutable.Seq[Int] = - page.ctx.getImageData(0, 0, page.canvas.width, page.canvas.height).data - - imageData.hashCode() shouldBe -1753260013 - } - - { - page.render(GameState(Hero(365, 81), Monster(0, 0), 0, false)) - - dom.document.body.appendChild(div( - cls := "content", style := "text-align:center; background-color:#3F8630;", - canvas - ).render) - - val imageData = page.ctx.getImageData(0, 0, page.canvas.width, page.canvas.height) - - // imageData.data.sum shouldBe -1753260013 - } - - { - page.render(GameState(Hero(877, 593), Monster(0, 0), 0, false)) - val imageData: scala.collection.mutable.Seq[Int] = - page.ctx.getImageData(0, 0, page.canvas.width, page.canvas.height).data - - imageData.hashCode() shouldBe -1753260013 - } - - } - - } - } -} diff --git a/src/test/scala-2.11/nl/amsscala/simplegame/SuiteSpec.scala b/src/test/scala-2.11/nl/amsscala/simplegame/SuiteSpec.scala deleted file mode 100644 index ab18547..0000000 --- a/src/test/scala-2.11/nl/amsscala/simplegame/SuiteSpec.scala +++ /dev/null @@ -1,6 +0,0 @@ -package nl.amsscala -package simplegame - -import org.scalatest._ - -abstract class SuiteSpec extends FunSpec with Matchers // with OptionValues with Inside with Inspectors diff --git a/src/test/scala-2.12/nl/amsscala/simplegame/CanvasComponentSuite.scala b/src/test/scala-2.12/nl/amsscala/simplegame/CanvasComponentSuite.scala new file mode 100644 index 0000000..d73b592 --- /dev/null +++ b/src/test/scala-2.12/nl/amsscala/simplegame/CanvasComponentSuite.scala @@ -0,0 +1,27 @@ +package nl.amsscala +package simplegame + +import org.scalajs.dom + +class CanvasComponentSuite extends SuiteSpec { + describe("A Hero") { + describe("should tested within the limits") { + it("should be compared") { + assert(new Hero[SimpleCanvasGame.T](Position(0, 0), null) === Hero(Position(0, 0))) + assert(new Hero[SimpleCanvasGame.T](Position(1, 0), null) !== Hero(Position(0, 1))) + } + val canvas = dom.document.createElement("canvas").asInstanceOf[dom.html.Canvas] + SimpleCanvasGame.resetCanvasWH(canvas, Position(150, 100)) + it("good path") { + Hero(Position(0, 0)).isValidPosition(canvas) shouldBe true + Hero(Position(150 - Hero.pxSize, 100 - Hero.pxSize)).isValidPosition(canvas) shouldBe true + } + it("bad path") { + Hero(Position(-1, 0)).isValidPosition(canvas) shouldBe false + Hero(Position(4, -1)).isValidPosition(canvas) shouldBe false + Hero(Position(0, 101 - Hero.pxSize)).isValidPosition(canvas) shouldBe false + Hero(Position(151 - Hero.pxSize, 0)).isValidPosition(canvas) shouldBe false + } + } + } +} diff --git a/src/test/scala-2.12/nl/amsscala/simplegame/GameStateSuite.scala b/src/test/scala-2.12/nl/amsscala/simplegame/GameStateSuite.scala new file mode 100644 index 0000000..7801048 --- /dev/null +++ b/src/test/scala-2.12/nl/amsscala/simplegame/GameStateSuite.scala @@ -0,0 +1,15 @@ +package nl.amsscala +package simplegame + +class GameStateSuite extends SuiteSpec { + val game = GameState(null, Position(0, 0), Position(0, 0)) + + describe("GameState") { + describe("should perform functions") { + it("should be compared") { + assert(GameState(null, Position(0, 0), Position(0, 0)) == game) + assert(GameState(null, Position(0, 0), Position(1, 0)) != game) + } + } + } +} diff --git a/src/test/scala-2.12/nl/amsscala/simplegame/GameSuite.scala b/src/test/scala-2.12/nl/amsscala/simplegame/GameSuite.scala new file mode 100644 index 0000000..ca55c6e --- /dev/null +++ b/src/test/scala-2.12/nl/amsscala/simplegame/GameSuite.scala @@ -0,0 +1,63 @@ +package nl.amsscala +package simplegame + +import org.scalajs.dom.ext.KeyCode.{Down, Left, Right, Up} + +import scala.collection.mutable + +class GameSuite extends SuiteSpec with Page { + val graphField = Position(1242, 674) + resetCanvasWH(canvas, graphField) + + val game = GameState(canvas, Position(0, 0), center(canvas)) + + describe("The Game") { + describe("should tested by navigation keys") { + + it("good path") { + // No keys, no movement + game.keyEffect(1D, mutable.Set.empty) shouldBe game + + // Opposite horizontal navigation, no movement 1 + game.keyEffect(1D, mutable.Set(Left, Right)) shouldBe game + + // Opposite horizontal navigation, no movement 2 + game.keyEffect(1D, mutable.Set(Right, Left)) shouldBe game + + // Opposite vertical navigation, no movement 1 + game.keyEffect(1D, mutable.Set(Up, Down)) shouldBe game + + // Opposite vertical navigation, no movement 2 + game.keyEffect(1D, mutable.Set(Down, Up)) shouldBe game + + // All four directions, no movement + game.keyEffect(1D, mutable.Set(Up, Right, Left, Down)) shouldBe game + + resetCanvasWH(canvas, graphField) + game.keyEffect(1D, mutable.Set(Left)).hero.pos - Position(621, 337) shouldBe Position(-256, 0) + game.keyEffect(1D, mutable.Set(Right)).hero.pos - Position(621, 337) shouldBe Position(256, 0) + game.keyEffect(1D, mutable.Set(Up)).hero.pos - Position(621, 337) shouldBe Position(0, -256) + game.keyEffect(1D, mutable.Set(Down)).hero.pos - Position(621, 337) shouldBe Position(0, 256) + + // North west navigation, etc + game.keyEffect(1D, mutable.Set(Left, Up)).hero.pos - Position(621, 337) shouldBe Position(-256, -256) + game.keyEffect(1D, mutable.Set(Up, Right)).hero.pos - Position(621, 337) shouldBe Position(256, -256) + game.keyEffect(1D, mutable.Set(Down, Right)).hero.pos - Position(621, 337) shouldBe Position(256, 256) + game.keyEffect(1D, mutable.Set(Down, Left)).hero.pos - Position(621, 337) shouldBe Position(-256, 256) + + game.keyEffect(1D, mutable.Set(Up, Right, Left)).hero.pos - Position(621, 337) shouldBe Position(0, -256) + game.keyEffect(1D, mutable.Set(Up, Right, Down)).hero.pos - Position(621, 337) shouldBe Position(256, 0) + game.keyEffect(1D, mutable.Set(Up, Left, Down)).hero.pos - Position(621, 337) shouldBe Position(-256, 0) + game.keyEffect(1D, mutable.Set(Right, Left, Down)).hero.pos - Position(621, 337) shouldBe Position(0, 256) + } + it("sad path") { + // Illegal key code + game.keyEffect(1D, mutable.Set(0)) shouldBe game + } + it("bad path") { + // No move due a of out cnvs limit case + game.keyEffect(1.48828125D, mutable.Set(Right, Down)) shouldBe game + } + } + } +} diff --git a/src/test/scala-2.12/nl/amsscala/simplegame/PageSuite.scala b/src/test/scala-2.12/nl/amsscala/simplegame/PageSuite.scala new file mode 100644 index 0000000..76e7deb --- /dev/null +++ b/src/test/scala-2.12/nl/amsscala/simplegame/PageSuite.scala @@ -0,0 +1,127 @@ +package nl.amsscala +package simplegame + +import org.scalatest.AsyncFlatSpec + +import scala.collection.mutable +import scala.concurrent.Future + +class PageSuite extends AsyncFlatSpec with Page { + // All graphical features are placed just outside the playground + lazy val gameState = GameState[SimpleCanvasGame.T](canvas, doubleInitialLUnder, doubleInitialLUnder) + lazy val loaders = gameState.pageElements.map(pg => + imageFuture((if (Seq(gameState.pageElements.head, gameState.pageElements.last).contains(pg)) urlBase1 else urlBase1) + pg.src)) + // Collect all Futures of onload events + val urlBase0 = "http://lambdalloyd.net23.net/SimpleGame/views/" + val urlBase1 = "https://amsterdam-scala.github.io/Sjs-Simple-HTML5-canvas-game/public/views/" + val initialLUnder = Position(512, 480).asInstanceOf[Position[SimpleCanvasGame.T]] + val doubleInitialLUnder = initialLUnder + initialLUnder + + implicit override def executionContext = scala.scalajs.concurrent.JSExecutionContext.Implicits.queue + + def context2Hashcode[C: Numeric](size: Position[C]) = { + val UintClampedArray: mutable.Seq[Int] = + ctx.getImageData(0, 0, size.x.asInstanceOf[Int], size.y.asInstanceOf[Int]).data + UintClampedArray.hashCode() + } + + def expectedHashCode = Map("background.png" -> 1425165765, "monster.png" -> -277415456, "hero.png" -> -731024817) + //def expectedHashCode = Map("background.png" -> -1768009948, "monster.png" -> 1817836310, "hero.png" -> 1495155181) + def getImgName(url: String) = url.split('/').last + + def testHarness(gs: GameState[SimpleCanvasGame.T], text: String, assertion: () => Boolean) = { + render(gs) + info(text) + assert(assertion(), s"Thrown probably by value ${context2Hashcode(doubleInitialLUnder)}") + } + + // ***** Test the navigation of the Hero character graphical + def navigateHero(gs: GameState[SimpleCanvasGame.T], move: Position[Int]) = + gs.copy(new Hero(initialLUnder + move.asInstanceOf[Position[SimpleCanvasGame.T]], gs.pageElements.last.img)) + + // Don't rely on the browsers defaults + resetCanvasWH(canvas, initialLUnder) + + // You can map assertions onto a Future, then return the resulting Future[Assertion] to ScalaTest: + it should "be remote loaded 3" in { + Future.sequence(loaders).map { imageElements => { + + // Test 00 + info("All images correct loaded") + assert(imageElements.forall { img => { + val pos = Position(img.width, img.height) + resetCanvasWH(canvas, pos) + ctx.drawImage(img, 0, 0, img.width, img.height) + expectedHashCode(getImgName(img.src)) == context2Hashcode(pos) + } + }) + + /* Composite all pictures drawn outside the play field. + * This should result in a hashcode equal as the image of the background. + */ + resetCanvasWH(canvas, initialLUnder) + val loadedAndNoText0 = new GameState(canvas, + gameState.pageElements.zip(imageElements).map { case (el, img) => el.copy(img = img) }, + monstersHitTxt = "", + isNewGame = false) + + // Test 01 + testHarness(loadedAndNoText0, + "Default initial screen everything left out", + () => context2Hashcode(initialLUnder) == expectedHashCode("background.png")) + + // ***** Tests with double canvas size + resetCanvasWH(canvas, doubleInitialLUnder) + + // Test 02 + testHarness(loadedAndNoText0, "Default double size initial screen, no text", + () => Seq( 1355562831 /*Chrome*/ , 1668792783 /*FireFox*/).contains(context2Hashcode(doubleInitialLUnder))) + val ref = context2Hashcode(doubleInitialLUnder) // Register the reference value + + val loadedAndSomeText1 = new GameState(canvas, + gameState.pageElements.zip(imageElements).map { case (el, img) => el.copy(img = img) }, + monstersHitTxt = "Now with text which can differ between browsers", + isNewGame = false) + + val loadedAndSomeText2 = new GameState(canvas, + gameState.pageElements.zip(imageElements).map { case (el, img) => el.copy(img = img) }, + monstersHitTxt = "", + isNewGame = true) + + // Test 03 + testHarness(loadedAndSomeText1, "Test double screen with score text", + () => ref != context2Hashcode(doubleInitialLUnder)) + + // Test 04 + testHarness(loadedAndSomeText2, "Test double screen with explain text put in", + () => ref != context2Hashcode(doubleInitialLUnder)) + + // ***** Test the navigation of the Hero character graphical + + // Test 05 + testHarness(navigateHero(loadedAndNoText0, Position(0, 0)), "Test double screen with centered hero", + () => Seq(1407150772 /*Chrome*/ , -1212284464 /*FireFox*/, 981419409 ).contains(context2Hashcode(doubleInitialLUnder))) + + // Test 06 + testHarness(navigateHero(loadedAndNoText0, Position(1, 0)), "Test double screen with right displaced hero", + () => Seq(-1742535935 /*Chrome*/ ,475868743 /*FireFox*/, -1986372876).contains(context2Hashcode(doubleInitialLUnder))) + + // Test 07 + testHarness(navigateHero(loadedAndNoText0, Position(-1, 0)), "Test double screen with left displaced hero", + () => Seq(2145530953 /*Chrome*/ , 320738379 /*FireFox*/, 214771813).contains(context2Hashcode(doubleInitialLUnder))) + + // Test 08 + testHarness(navigateHero(loadedAndNoText0, Position(0, 1)), "Test double screen with up displaced hero", + () => Seq(-557901336 /*Chrome*/ , -409947707 /*FireFox*/, -1902498081).contains(context2Hashcode(doubleInitialLUnder))) + + // Test 09 + testHarness(navigateHero(loadedAndNoText0, Position(0, -1)), "Test double screen with down displaced hero", + () => Seq(-1996948634 /*Chrome*/ , 1484865515 /*FireFox*/, 954791841).contains(context2Hashcode(doubleInitialLUnder))) + + // Test 10 Doesn't work with Google Chrome + testHarness(loadedAndNoText0, "Test double screen reference still the same.", + () => context2Hashcode(doubleInitialLUnder) == ref) + } + } + } +} diff --git a/src/test/scala-2.12/nl/amsscala/simplegame/SuiteSpec.scala b/src/test/scala-2.12/nl/amsscala/simplegame/SuiteSpec.scala new file mode 100644 index 0000000..9479ab1 --- /dev/null +++ b/src/test/scala-2.12/nl/amsscala/simplegame/SuiteSpec.scala @@ -0,0 +1,8 @@ +package nl.amsscala +package simplegame + +import org.scalatest.{FunSpec, Matchers} + +abstract class SuiteSpec extends FunSpec with Matchers + +// with OptionValues with Inside with Inspectors