├── project ├── build.properties ├── build.sbt └── Build.scala ├── .gitignore ├── styles.css ├── index.html ├── index-dev.html ├── README.md └── src └── main └── scala └── example ├── Snake.scala ├── Pong.scala ├── Asteroids.scala ├── AstroLander.scala ├── ScalaJSExample.scala ├── BrickBreaker.scala └── Tetris.scala /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .cache 3 | .classpath 4 | .project 5 | .settings/ 6 | .idea/ 7 | .idea_modules/ 8 | -------------------------------------------------------------------------------- /project/build.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-lang.modules.scalajs" % "scalajs-sbt-plugin" % "0.5.0") 2 | 3 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | canvas{ 2 | display: block; 3 | float: left; 4 | margin: 2px 2px 2px 2px; 5 | } 6 | 7 | canvas:focus{ 8 | box-shadow: 0 0 5px 5px gold; 9 | } -------------------------------------------------------------------------------- /project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | import scala.scalajs.sbtplugin.ScalaJSPlugin._ 4 | import ScalaJSKeys._ 5 | 6 | object Build extends sbt.Build { 7 | lazy val root = project.in(file(".")).settings( 8 | scalaJSSettings: _* 9 | ).settings( 10 | name := "games", 11 | libraryDependencies += "org.scala-lang.modules.scalajs" %%% "scalajs-dom" % "0.6" 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scala-Js Games 5 | 6 | 7 | 8 | 9 | 10 |
11 |

Scala-Js Games

12 |

13 | This is a collection of games ported from Java to Scala-Js: the 14 | 15 | source for each game is written in Scala, and 16 | cross compiled to run in the browser targeting the 17 | HTML5 Canvas. The original games can be found 18 | here and targeting Java/Swing. Although this is basically a 19 | complete rewrite, the user-facing side of the game should be exactly 20 | the same. 21 |

22 | 23 |

24 | The games are, in order: 25 | 26 |

34 | 35 |

36 | The controls are generally up-down-left-right and spacebar; they 37 | aren't very complex games. Click on a game to start playing and click 38 | somewhere else to pause it. 39 |

40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /index-dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scala-Js Games 5 | 6 | 7 | 8 | 9 | 10 |
11 |

Scala-Js Games

12 |

13 | This is a collection of games ported from Java to Scala-Js: the 14 | 15 | source for each game is written in Scala, and 16 | cross compiled to run in the browser targeting the 17 | HTML5 Canvas. The original games can be found 18 | here and targeting Java/Swing. Although this is basically a 19 | complete rewrite, the user-facing side of the game should be exactly 20 | the same. 21 |

22 | 23 |

24 | The games are, in order: 25 | 26 |

34 | 35 |

36 | The controls are generally up-down-left-right and spacebar; they 37 | aren't very complex games. Click on a game to start playing and click 38 | somewhere else to pause it. 39 |

40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Scala-Js Games

2 |

3 | This is a collection of games ported from Java to Scala-Js: the 4 | 5 | source for each game is written in Scala, and 6 | cross compiled to run in the browser targeting the 7 | HTML5 Canvas. The original games can be found 8 | here and targeting Java/Swing. Although this is basically a 9 | complete rewrite, the user-facing side of the game should be exactly 10 | the same. Try out the live demo 11 |

12 | 13 |

14 | The games are, in order: 15 | 16 |

24 | 25 |

26 | The controls are generally up-down-left-right and spacebar; they 27 | aren't very complex games. Click on a game to start playing and click 28 | somewhere else to pause it. 29 |

30 | 31 |

How to build

32 | 33 | - Clone the repo 34 | - Hit `sbt packageJS` 35 | - Open `/index-dev.html` in your favorite browser and play the games! 36 | - Hit `sbt optimizeJS`. This may cause SBT to run out of memory. See [here](http://stackoverflow.com/questions/15280839/how-to-set-heap-size-for-sbt) for how to give it more. 37 | - Open `/index.html` in your favorite browser and play the games! 38 | 39 | License 40 | ------- 41 | The MIT License (MIT) 42 | 43 | Copyright (c) 2013 Li Haoyi 44 | 45 | Permission is hereby granted, free of charge, to any person obtaining a copy 46 | of this software and associated documentation files (the "Software"), to deal 47 | in the Software without restriction, including without limitation the rights 48 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 49 | copies of the Software, and to permit persons to whom the Software is 50 | furnished to do so, subject to the following conditions: 51 | 52 | The above copyright notice and this permission notice shall be included in 53 | all copies or substantial portions of the Software. 54 | 55 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 56 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 57 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 58 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 59 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 60 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 61 | THE SOFTWARE. 62 | -------------------------------------------------------------------------------- /src/main/scala/example/Snake.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import org.scalajs.dom 4 | import scala.util.Random 5 | 6 | trait Spot 7 | case class Wall(duration: Int) extends Spot 8 | case class Apple(duration: Int, bonus: Int) extends Spot 9 | case object Empty extends Spot 10 | 11 | case class Snake(bounds: Point, resetGame: () => Unit) extends Game{ 12 | var frameCount = 0 13 | var length = 10 14 | var direction = Point(1, 0) 15 | var position = Point(40, 30) 16 | val grid = { 17 | val spots: Array[Array[Spot]] = Array.fill(80)(Array.fill(60)(Empty)) 18 | for(i <- 0 until 80) { 19 | spots(i)(0) = Wall(Int.MaxValue) 20 | spots(i)(59) = Wall(Int.MaxValue) 21 | } 22 | for(i <- 0 until 60) { 23 | spots(0)(i) = Wall(Int.MaxValue) 24 | spots(79)(i) = Wall(Int.MaxValue) 25 | } 26 | spots 27 | } 28 | def appleCount = { 29 | val apples = for { 30 | col <- grid 31 | spot <- col 32 | } yield spot match{ 33 | case Apple(_, _) => 1 34 | case _ => 0 35 | } 36 | 37 | apples.sum 38 | } 39 | def draw(ctx: dom.CanvasRenderingContext2D) = { 40 | ctx.fillStyle = "rgb(0, 0, 0)" 41 | ctx.fillRect(0, 0, 800, 800) 42 | 43 | for { 44 | i <- 0 until 80 45 | j <- 0 until 60 46 | }{ 47 | grid(i)(j) match{ 48 | case Wall(_) => 49 | ctx.fillStyle = "rgb(200, 200, 200)" 50 | ctx.fillRect(i * 10, j * 10, 10, 10) 51 | case Apple(_, x) => 52 | ctx.fillStyle = x match{ 53 | case 2 => "rgb(255, 0, 0)" 54 | case 5 => "rgb(255, 255, 0)" 55 | } 56 | ctx.fillCircle(i * 10 + 5, j * 10 + 5, 5) 57 | 58 | case Empty => 59 | } 60 | 61 | 62 | } 63 | } 64 | def update(keys: Set[Int]) = { 65 | frameCount += 1 66 | 67 | if (frameCount % 2 == 0){ 68 | 69 | 70 | if (math.random > 0.9 + appleCount/10.0){ 71 | val (x, y) = (Random.nextInt(80), Random.nextInt(60)) 72 | grid(x)(y) match{ 73 | case Empty => 74 | val score = if (Random.nextInt(20) == 0) 5 else 2 75 | grid(x)(y) = Apple(15 * 25, score) 76 | case _ => 77 | } 78 | } 79 | position = position + direction 80 | 81 | grid(position.x.toInt)(position.y.toInt) match{ 82 | case Wall(d) => 83 | result = Some("You hit a wall!") 84 | resetGame() 85 | case x => 86 | x match{ 87 | case Apple(_, s) => length += s 88 | case _ => 89 | } 90 | grid(position.x.toInt)(position.y.toInt) = Wall(length) 91 | } 92 | 93 | val newDirection = 94 | if(keys(37)) Point(-1, 0) 95 | else if (keys(39)) Point(1, 0) 96 | else if (keys(38)) Point(0, -1) 97 | else if (keys(40)) Point(0, 1) 98 | else direction 99 | 100 | if (newDirection + direction != Point(0, 0)) direction = newDirection 101 | 102 | for { 103 | i <- 0 until 80 104 | j <- 0 until 60 105 | }{ 106 | grid(i)(j) = grid(i)(j) match{ 107 | case Wall(1) | Apple(1, _) => Empty 108 | case Wall(d) => Wall(d-1) 109 | case Apple(d, s) => Apple(d-1, s) 110 | case Empty => Empty 111 | } 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/scala/example/Pong.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import org.scalajs.dom 4 | import scala.util.Random 5 | 6 | 7 | class Paddle(var pos: Point, var dims: Point, var direction: Point, val face: Double) 8 | case class Pong(bounds: Point, resetGame: () => Unit) extends Game{ 9 | var (ballPos, ballVel) = initBall 10 | val leftPaddle = new Paddle(Point(40, bounds.y/2), Point(5, 75), Point(0, 0), 0.5) 11 | val rightPaddle = new Paddle(Point(bounds.x - 40, bounds.y/2), Point(5, 75), Point(0, 0), -0.5) 12 | 13 | var leftScore = 0 14 | var rightScore = 0 15 | 16 | var respawnCounter = 60 17 | 18 | var aiCounter = 0 19 | 20 | 21 | def moveAI() = { 22 | val targetY = { 23 | val padelX = rightPaddle.pos.x 24 | val distance = padelX - ballPos.x 25 | val gradient = ballVel.y / ballVel.x 26 | distance * gradient + ballPos.y 27 | } 28 | rightPaddle.direction = Point(0, 0) 29 | if (math.abs(rightPaddle.pos.x - ballPos.x) < ballVel.x * 45 + 25 && ballVel.x > 0){ 30 | if (targetY > rightPaddle.pos.y + rightPaddle.dims.y / 2){ 31 | if (math.abs(targetY - rightPaddle.pos.y - (rightPaddle.dims.y / 2)) > rightPaddle.dims.y / 2 - 10){ 32 | rightPaddle.direction += Point(0, 8) 33 | }else{ 34 | 35 | } 36 | }else if (targetY < rightPaddle.pos.y + rightPaddle.dims.y / 2){ 37 | if (math.abs(targetY - rightPaddle.pos.y - (rightPaddle.dims.y / 2)) > rightPaddle.dims.y / 2 - 10){ 38 | rightPaddle.direction -= Point(0, 8) 39 | }else{ 40 | 41 | } 42 | } 43 | } 44 | } 45 | def initBall = ( 46 | Point(400, 300), 47 | Point(8 * (Random.nextInt(2) - 0.5), 8 * (Random.nextInt(2) - 0.5)) 48 | ) 49 | 50 | def doPaddleCollision(paddle: Paddle) = { 51 | val corner1 = paddle.pos - paddle.dims * paddle.face 52 | val corner2 = paddle.pos + paddle.dims * paddle.face 53 | if (ballPos.within(corner2, corner1, extra = Point(5, 5))){ 54 | ballVel = ballVel.copy( 55 | x = 2 * paddle.face * math.abs(ballVel.x), 56 | y = ballVel.y + paddle.direction.y / 8 57 | ) 58 | } 59 | } 60 | def draw(ctx: dom.CanvasRenderingContext2D) = { 61 | ctx.fillStyle = Color.Black 62 | ctx.fillRect(0, 0, bounds.x, bounds.y) 63 | 64 | ctx.fillStyle = Color.White 65 | ctx.strokeStyle = Color.White 66 | 67 | ctx.fillCircle(ballPos.x, ballPos.y, 5) 68 | 69 | ctx.fillText("Score", 5 * bounds.x / 10, bounds.y / 8) 70 | ctx.fillText(leftScore.toString, 5 * bounds.x / 10 - 20, bounds.y / 8 + 15) 71 | ctx.fillText(rightScore.toString, 5 * bounds.x / 10 + 20, bounds.y / 8 + 15) 72 | 73 | for (p <- Seq(leftPaddle, rightPaddle)){ 74 | 75 | ctx.strokePath( 76 | Point(p.pos.x + p.dims.x * p.face, p.pos.y - p.dims.y * p.face), 77 | Point(p.pos.x + p.dims.x * p.face, p.pos.y + p.dims.y * p.face), 78 | Point(p.pos.x - p.dims.x * p.face, p.pos.y + p.dims.y * p.face) 79 | ) 80 | } 81 | } 82 | def update(keys: Set[Int]): Unit = { 83 | 84 | 85 | leftPaddle.direction = Point(0, 0) 86 | if (keys(38)) leftPaddle.direction -= Point(0, 8) 87 | if (keys(40)) leftPaddle.direction += Point(0, 8) 88 | 89 | moveAI() 90 | ballPos += ballVel 91 | if (ballPos.y <= 0) ballVel = ballVel.copy(y = math.abs(ballVel.y)) 92 | if (ballPos.y >= bounds.y) ballVel = ballVel.copy(y = -math.abs(ballVel.y)) 93 | 94 | if (ballPos.x < 0) { 95 | rightScore += 1 96 | respawnCounter = 100 97 | } 98 | if (ballPos.x > bounds.x) { 99 | leftScore += 1 100 | respawnCounter = 100 101 | } 102 | 103 | if (respawnCounter > 0) { 104 | val (a, b) = initBall 105 | ballPos = a 106 | ballVel = b 107 | respawnCounter -= 1 108 | } else if (respawnCounter == 0){ 109 | respawnCounter = -1 110 | } 111 | for (p <- Seq(leftPaddle, rightPaddle)){ 112 | doPaddleCollision(p) 113 | p.pos += p.direction 114 | p.pos = p.pos.copy(y = math.max(math.min(p.pos.y, bounds.y - p.dims.y / 2), p.dims.y / 2)) 115 | 116 | } 117 | 118 | 119 | 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/scala/example/Asteroids.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import org.scalajs.dom 4 | import scala.util.Random 5 | 6 | case class Asteroids(bounds: Point, resetGame: () => Unit) extends Game{ 7 | 8 | var bullets = Seq.empty[Bullet] 9 | val craft = new Craft(bounds / 2, Point(0, 0), 0) 10 | var frameCount = 0 11 | var asteroids = Seq.fill(10)( 12 | new Asteroid(3, 13 | if (Random.nextBoolean()) Point(0, Random.nextInt(bounds.y.toInt)) 14 | else Point(Random.nextInt(bounds.y.toInt), 0), 15 | Point(Random.nextInt(5), Random.nextInt(5)) - Point(2.5, 2.5) 16 | ) 17 | ) 18 | 19 | def update(keys: Set[Int]) = { 20 | frameCount += 1 21 | 22 | 23 | asteroids.foreach(_.move()) 24 | bullets.foreach(_.move()) 25 | craft.move(keys) 26 | 27 | 28 | 29 | if (keys(32) && bullets.length < 10 && frameCount % 2 == 0){ 30 | bullets = bullets :+ new Bullet( 31 | craft.position, 32 | craft.momentum + Point(15, 0).rotate(craft.theta) 33 | ) 34 | } 35 | 36 | val changes = for{ 37 | b <- bullets 38 | a <- asteroids 39 | if a.contains(b.position) 40 | } yield { 41 | val newAsteroids = 42 | if (a.level == 1) Nil 43 | else { 44 | Seq(30, -30).map(d => 45 | new Asteroid(a.level - 1, a.position, a.momentum.rotate(d*Math.PI/180)) 46 | ) 47 | } 48 | (Seq(a, b), newAsteroids) 49 | } 50 | val (removed, added) = changes.unzip 51 | val flatRemoved = removed.flatten 52 | asteroids = asteroids.filter(!flatRemoved.contains(_)) ++ added.flatten 53 | bullets = 54 | bullets 55 | .filter(!flatRemoved.contains(_)) 56 | .filter(_.position.within(Point(0, 0), bounds)) 57 | 58 | if(asteroids.exists(_.contains(craft.position))){ 59 | result = Some("Your ship hit an asteroid!") 60 | resetGame() 61 | }else if (asteroids.length == 0){ 62 | result = Some("You successfully destroyed every asteroid!") 63 | resetGame() 64 | } 65 | } 66 | 67 | def draw(ctx: dom.CanvasRenderingContext2D) = { 68 | ctx.fillStyle = Color.Black 69 | ctx.fillRect(0, 0, 800, 800) 70 | 71 | ctx.fillStyle = Color.White 72 | ctx.strokeStyle = Color.White 73 | 74 | asteroids.foreach(_.draw(ctx)) 75 | bullets.foreach(_.draw(ctx)) 76 | craft.draw(ctx) 77 | } 78 | 79 | 80 | class Asteroid(val level: Int, var position: Point, val momentum: Point){ 81 | def draw(ctx: dom.CanvasRenderingContext2D) = { 82 | val size = 10*level 83 | ctx.fillRect(position.x - size/2, position.y - size/2, size, size) 84 | } 85 | def move() = { 86 | position += momentum 87 | position += bounds 88 | position %= bounds 89 | } 90 | def contains(other: Point) = { 91 | val min = position - Point(5, 5) * level 92 | val max = position + Point(5, 5) * level 93 | other.within(min, max) 94 | } 95 | } 96 | 97 | class Craft(var position: Point, var momentum: Point, var theta: Double){ 98 | def draw(ctx: dom.CanvasRenderingContext2D) = { 99 | ctx.beginPath() 100 | val pts = Seq( 101 | Point(15, 0).rotate(theta) + position, 102 | Point(7, 0).rotate(theta + 127.5/180 * Math.PI) + position, 103 | Point(7, 0).rotate(theta - 127.5/180 * Math.PI) + position 104 | ) 105 | ctx.moveTo(pts.last.x, pts.last.y) 106 | pts.map(p => ctx.lineTo(p.x, p.y)) 107 | ctx.fill() 108 | } 109 | def move(keys: Set[Int]) = { 110 | position += momentum 111 | position += bounds 112 | position %= bounds 113 | 114 | if (keys(37)) theta -= 0.05 115 | if (keys(38)) momentum += Point(0.2, 0).rotate(theta) 116 | if (keys(39)) theta += 0.05 117 | if (keys(40)) momentum -= Point(0.2, 0).rotate(theta) 118 | } 119 | } 120 | class Bullet(var position: Point, val momentum: Point){ 121 | def draw(ctx: dom.CanvasRenderingContext2D) = { 122 | ctx.beginPath() 123 | ctx.moveTo(position.x, position.y) 124 | val forward = position + momentum * 5.0 / momentum.length 125 | ctx.lineTo(forward.x, forward.y) 126 | ctx.stroke() 127 | } 128 | 129 | def move() = { 130 | position += momentum 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /src/main/scala/example/AstroLander.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import org.scalajs.dom 4 | import scala.util.Random 5 | 6 | case class AstroLander(bounds: Point, resetGame: () => Unit) extends Game{ 7 | val points = { 8 | var current = 450 9 | var pts = List.empty[Point] 10 | val flat = Random.nextInt(21) 11 | val cliff1 = { 12 | var x = 0 13 | do x = Random.nextInt(21) 14 | while(x == flat) 15 | x 16 | } 17 | val cliff2 = { 18 | var x = 0 19 | do x = Random.nextInt(21) 20 | while(x == flat || x == cliff1) 21 | x 22 | } 23 | 24 | (0 to 21).foreach{n => 25 | if (n == flat+1) current = current 26 | else if (n == cliff1+1) current = current - Random.nextInt(25) - 150 27 | else if (n == cliff2+2) current = current - Random.nextInt(25) + 150 28 | else current = current - Random.nextInt(50) + 25 29 | 30 | if (current > bounds.y) current = (2 * bounds.y - current).toInt 31 | 32 | pts = Point(n * 40, current) :: pts 33 | } 34 | 35 | pts.reverse 36 | } 37 | 38 | var craftPos = Point(400, 25) 39 | var craftVel = Point(0, 0) 40 | var theta = -math.Pi / 2 41 | var fuel = 500 42 | 43 | def shipPoints = Seq( 44 | craftPos + Point(15, 0).rotate(theta), 45 | craftPos + Point(7, 0).rotate(theta + 127.5/180 * Math.PI), 46 | craftPos + Point(7, 0).rotate(theta - 127.5/180 * Math.PI) 47 | ) 48 | def draw(ctx: dom.CanvasRenderingContext2D) = { 49 | ctx.textAlign = "left" 50 | ctx.fillStyle = Color.Black 51 | ctx.fillRect(0, 0, bounds.x, bounds.y) 52 | 53 | 54 | ctx.fillStyle = if (craftVel.length < 3) Color.Green else Color.White 55 | ctx.fillText("Speed: " + (craftVel.length * 10).toInt.toDouble / 10, 20, 50) 56 | 57 | ctx.strokeStyle = Color.Green 58 | ctx.strokeRect(20, 60, math.max(1, fuel) * 65 / 500, 15) 59 | ctx.fillStyle = Color.White 60 | ctx.strokeStyle = Color.White 61 | ctx.strokeRect(20, 60, 65, 15) 62 | 63 | ctx.beginPath() 64 | ctx.moveTo(0, bounds.y) 65 | for(p <- points) ctx.lineTo(p.x, p.y) 66 | ctx.lineTo(bounds.x, bounds.y) 67 | ctx.fill() 68 | 69 | ctx.beginPath() 70 | 71 | ctx.moveTo(shipPoints.last.x, shipPoints.last.y) 72 | shipPoints.map(p => ctx.lineTo(p.x, p.y)) 73 | ctx.fill() 74 | def drawFlame(p: Point, angle: Double) = { 75 | val offset = math.Pi * 1.25 76 | def diamond(a: Int, b: Int, c: Int, w: Int) = { 77 | val width = w * math.Pi / 180 78 | 79 | ctx.strokePath( 80 | p + Point(a, a).rotate(angle - offset), 81 | p + Point(b, b).rotate(angle - offset + width), 82 | p + Point(c, c).rotate(angle - offset), 83 | p + Point(b, b).rotate(angle - offset - width), 84 | p + Point(a, a).rotate(angle - offset) 85 | ) 86 | } 87 | diamond(5, 15, 25, 15) 88 | diamond(10, 15, 20, 10) 89 | } 90 | 91 | ctx.strokeStyle = Color.Red 92 | if (fuel > 0){ 93 | if (lastKeys(37)) drawFlame(craftPos, theta + math.Pi / 4) 94 | if (lastKeys(39)) drawFlame(craftPos, theta - math.Pi / 4) 95 | if (lastKeys(40)) drawFlame(craftPos, theta) 96 | } 97 | 98 | } 99 | var lastKeys: Set[Int] = Set() 100 | def update(keys: Set[Int]) = { 101 | lastKeys = keys 102 | if (fuel > 0){ 103 | if (keys(37)) craftVel += Point(0.5, 0).rotate(theta + math.Pi / 4) 104 | if (keys(39)) craftVel += Point(0.5, 0).rotate(theta - math.Pi / 4) 105 | if (keys(40)) craftVel += Point(0.5, 0).rotate(theta) 106 | fuel -= Seq(keys(37), keys(39), keys(40)).count(x => x) 107 | } 108 | 109 | craftVel += Point(0, 0.2) 110 | craftPos += craftVel 111 | 112 | 113 | 114 | val hit = points.flatMap{ p => 115 | val prevIndex = points.lastIndexWhere(_.x < craftPos.x) 116 | if (prevIndex == -1 || prevIndex == 21) None 117 | else{ 118 | val prev = points(prevIndex) 119 | val next = points(prevIndex + 1) 120 | val height = (craftPos.x - prev.x) / (next.x - prev.x) * (next.y - prev.y) + prev.y 121 | if (height > craftPos.y) None 122 | else Some{ 123 | val groundGradient = math.abs((next.y - prev.y) / (next.x - prev.x)) 124 | val landingSkew = math.abs(craftVel.x / craftVel.y) 125 | 126 | if (groundGradient > 0.1) Failure("landing area too steep") 127 | else if (landingSkew > 1) Failure("too much horiontal velocity") 128 | else if(craftVel.length > 3) Failure("coming in too fast") 129 | else Success 130 | } 131 | } 132 | } 133 | 134 | hit.headOption.map{ 135 | case Success => 136 | result = Some("You have landed successfully.") 137 | resetGame() 138 | case Failure(reason) => 139 | result = Some("You have crashed your lander: " + reason) 140 | resetGame() 141 | } 142 | } 143 | } 144 | 145 | trait Collide 146 | case object Success extends Collide 147 | case class Failure(reason: String) extends Collide -------------------------------------------------------------------------------- /src/main/scala/example/ScalaJSExample.scala: -------------------------------------------------------------------------------- 1 | package example 2 | import scala.scalajs.js._ 3 | import org.scalajs.dom 4 | import scala.collection.mutable 5 | import scala.scalajs.js.Any._ 6 | import scala.scalajs.js.Math 7 | import annotation.JSExport 8 | 9 | object Color{ 10 | def rgb(r: Int, g: Int, b: Int) = s"rgb($r, $g, $b)" 11 | val White = rgb(255, 255, 255) 12 | val Red = rgb(255, 0, 0) 13 | val Green = rgb(0, 255, 0) 14 | val Blue = rgb(0, 0, 255) 15 | val Cyan = rgb(0, 255, 255) 16 | val Magenta = rgb(255, 0, 255) 17 | val Yellow = rgb(255, 255, 0) 18 | val Black = rgb(0, 0, 0) 19 | val all = Seq( 20 | White, 21 | Red, 22 | Green, 23 | Blue, 24 | Cyan, 25 | Magenta, 26 | Yellow, 27 | Black 28 | ) 29 | } 30 | 31 | case class Point(x: Double, y: Double){ 32 | def +(other: Point) = Point(x + other.x, y + other.y) 33 | def -(other: Point) = Point(x - other.x, y - other.y) 34 | def %(other: Point) = Point(x % other.x, y % other.y) 35 | def <(other: Point) = x < other.x && y < other.y 36 | def >(other: Point) = x > other.x && y > other.y 37 | def /(value: Double) = Point(x / value, y / value) 38 | def *(value: Double) = Point(x * value, y * value) 39 | def *(other: Point) = x * other.x + y * other.y 40 | def length = Math.sqrt(lengthSquared) 41 | def lengthSquared = x * x + y * y 42 | def within(a: Point, b: Point, extra: Point = Point(0, 0)) = { 43 | import math.{min, max} 44 | x >= min(a.x, b.x) - extra.x && 45 | x < max(a.x, b.x) + extra.y && 46 | y >= min(a.y, b.y) - extra.x && 47 | y < max(a.y, b.y) + extra.y 48 | } 49 | def rotate(theta: Double) = { 50 | val (cos, sin) = (Math.cos(theta), math.sin(theta)) 51 | Point(cos * x - sin * y, sin * x + cos * y) 52 | } 53 | } 54 | 55 | class GameHolder(canvasName: String, gameMaker: (Point, () => Unit) => Game){ 56 | private[this] val canvas = dom.document.getElementById(canvasName).asInstanceOf[dom.HTMLCanvasElement] 57 | private[this] val bounds = Point(canvas.width, canvas.height) 58 | private[this] val keys = mutable.Set.empty[Int] 59 | var game: Game = gameMaker(bounds, () => resetGame()) 60 | 61 | canvas.onkeydown = {(e: dom.KeyboardEvent) => 62 | keys.add(e.keyCode.toInt) 63 | if (Seq(32, 37, 38, 39, 40).contains(e.keyCode.toInt)) e.preventDefault() 64 | message = None 65 | } 66 | canvas.onkeyup = {(e: dom.KeyboardEvent) => 67 | keys.remove(e.keyCode.toInt) 68 | if (Seq(32, 37, 38, 39, 40).contains(e.keyCode.toInt)) e.preventDefault() 69 | } 70 | 71 | canvas.onfocus = {(e: dom.FocusEvent) => 72 | active = true 73 | } 74 | canvas.onblur = {(e: dom.FocusEvent) => 75 | active = false 76 | } 77 | 78 | private[this] val ctx = canvas.getContext("2d").asInstanceOf[dom.CanvasRenderingContext2D] 79 | var active = false 80 | var firstFrame = false 81 | def update() = { 82 | if (!firstFrame){ 83 | game.draw(ctx) 84 | firstFrame = true 85 | } 86 | if (active && message.isEmpty) { 87 | game.draw(ctx) 88 | game.update(keys.toSet) 89 | }else if (message.isDefined){ 90 | ctx.fillStyle = Color.Black 91 | ctx.fillRect(0, 0, bounds.x, bounds.y) 92 | ctx.fillStyle = Color.White 93 | ctx.font = "20pt Arial" 94 | ctx.textAlign = "center" 95 | ctx.fillText(message.get, bounds.x/2, bounds.y/2) 96 | ctx.font = "14pt Arial" 97 | ctx.fillText("Press any key to continue", bounds.x/2, bounds.y/2 + 30) 98 | } 99 | } 100 | 101 | var message: Option[String] = None 102 | def resetGame(): Unit = { 103 | message = game.result 104 | println("MESSAGE " + message) 105 | game = gameMaker(bounds, () => resetGame()) 106 | } 107 | ctx.font = "12pt Arial" 108 | ctx.textAlign = "center" 109 | } 110 | abstract class Game{ 111 | var result: Option[String] = None 112 | def update(keys: Set[Int]): Unit 113 | 114 | def draw(ctx: dom.CanvasRenderingContext2D): Unit 115 | 116 | 117 | implicit class pimpedContext(val ctx: dom.CanvasRenderingContext2D){ 118 | def fillCircle(x: Double, y: Double, r: Double) = { 119 | ctx.beginPath() 120 | ctx.arc(x, y, r, 0, math.Pi * 2) 121 | ctx.fill() 122 | } 123 | def strokePath(points: Point*) = { 124 | 125 | ctx.beginPath() 126 | ctx.moveTo(points.last.x, points.last.y) 127 | for(p <- points){ 128 | ctx.lineTo(p.x, p.y) 129 | } 130 | ctx.stroke() 131 | } 132 | } 133 | } 134 | @JSExport 135 | object ScalaJSExample { 136 | @JSExport 137 | def main(): Unit = { 138 | val asteroids = new GameHolder("asteroids", Asteroids) 139 | val astrolander = new GameHolder("astrolander", AstroLander) 140 | val snake = new GameHolder("snake", Snake) 141 | val pong = new GameHolder("pong", Pong) 142 | val bricks = new GameHolder("bricks", BrickBreaker) 143 | val tetris = new GameHolder("tetris", Tetris) 144 | val games = Seq(asteroids, astrolander, snake, pong, bricks, tetris) 145 | dom.setInterval(() => games.foreach(_.update()), 15) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/scala/example/BrickBreaker.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import org.scalajs.dom 4 | import scala.util.Random 5 | 6 | case class Brick(pos: Point, 7 | color: String) 8 | 9 | case class BrickBreaker(bounds: Point, resetGame: () => Unit) extends Game { 10 | val borderWidth = 175 11 | val brickSize = Point(50, 15) 12 | 13 | val colWidth = bounds.x - borderWidth * 2 14 | var ballsLeft = 3 15 | 16 | var ballVel = Point(0, 0) 17 | var ballPos = Point(0, 0) 18 | var paddlePos = Point(0, 0) 19 | 20 | var respawnCounter = 0 21 | 22 | def reset() = { 23 | 24 | ballVel = Point(0, 0) 25 | paddlePos = Point(bounds.x/2, bounds.y-40) 26 | ballPos = Point(bounds.x/2, paddlePos.y - 5) 27 | respawnCounter = 60 28 | } 29 | def relaunch() = { 30 | 31 | } 32 | reset() 33 | val paddleDims = Point(75, 5) 34 | var paddleDir = Point(0, 0) 35 | 36 | val bricks = { 37 | val colors = Seq( 38 | Color.White, Color.Cyan, Color.Yellow, 39 | Color.Magenta, Color.Red, Color.Green, Color.Blue 40 | ) 41 | val bricks = scala.collection.mutable.Set.empty[Brick] 42 | for { 43 | i <- 0 to (colWidth / brickSize.x).toInt - 1 44 | j <- 3 to 15 45 | }{ 46 | bricks.add(new Brick(Point(i * brickSize.x + borderWidth, j * brickSize.y), colors(Random.nextInt(colors.length)))) 47 | } 48 | bricks 49 | } 50 | def draw(ctx: dom.CanvasRenderingContext2D) = { 51 | ctx.fillStyle = Color.Black 52 | ctx.fillRect(0, 0, bounds.x, bounds.y) 53 | 54 | ctx.fillStyle = Color.White 55 | ctx.strokeStyle = Color.White 56 | ctx.strokeRect(0, 0, borderWidth-1, bounds.y) 57 | ctx.strokeRect(bounds.x - borderWidth + 1, 0, borderWidth-1, bounds.y) 58 | 59 | ctx.fillCircle(ballPos.x - 5, ballPos.y - 5, 5) 60 | 61 | ctx.textAlign = "left" 62 | ctx.fillText("Balls Left: " + ballsLeft, bounds.x - borderWidth / 2 - 40, 4 * bounds.y / 5 - 10) 63 | for (i <- 1 to ballsLeft){ 64 | ctx.fillCircle(bounds.x - borderWidth / 2 - 40 + i * 15, 4 * bounds.y / 5 + 10, 5) 65 | } 66 | for(brick <- bricks){ 67 | ctx.fillStyle = brick.color.replace("255", "128") 68 | ctx.fillRect(brick.pos.x, brick.pos.y, brickSize.x, brickSize.y) 69 | ctx.fillStyle = brick.color 70 | ctx.beginPath() 71 | ctx.moveTo(brick.pos.x + 1, brick.pos.y + brickSize.y - 1) 72 | ctx.lineTo(brick.pos.x + 1, brick.pos.y + 1) 73 | ctx.lineTo(brick.pos.x + brickSize.x - 1, brick.pos.y + 1) 74 | ctx.stroke() 75 | } 76 | 77 | ctx.fillStyle = Color.White.replace("255", "128") 78 | val c1 = paddlePos - paddleDims/2 79 | ctx.fillRect(c1.x, c1.y, paddleDims.x, paddleDims.y) 80 | ctx.fillStyle = Color.White 81 | ctx.strokeRect(c1.x, c1.y, paddleDims.x, paddleDims.y) 82 | } 83 | def update(keys: Set[Int]): Unit = { 84 | 85 | if (respawnCounter > 0) { 86 | respawnCounter -= 1 87 | }else if (respawnCounter == 0){ 88 | ballVel = Point(3.5 * (Random.nextInt(2) - 0.5) * 2, -3.5) 89 | respawnCounter -= 1 90 | }else{ 91 | paddleDir = Point(0, 0) 92 | if (keys(37)) paddleDir = Point(-8, 0) 93 | if (keys(39)) paddleDir = Point(8, 0) 94 | 95 | paddlePos = paddlePos + paddleDir 96 | paddlePos = paddlePos.copy(x = 97 | math.min(math.max(paddlePos.x, paddleDims.x / 2 + borderWidth), bounds.x - paddleDims.x / 2 - borderWidth) 98 | ) 99 | ballPos = ballPos + ballVel 100 | if (ballPos.x + 5 > bounds.x - borderWidth) ballVel = ballVel.copy(x = -math.abs(ballVel.x)) 101 | else if (ballPos.x - 5 < borderWidth) ballVel = ballVel.copy(x = math.abs(ballVel.x)) 102 | else if (ballPos.y - 5 < 0) ballVel = ballVel.copy(y = math.abs(ballVel.y)) 103 | else if (ballPos.y > bounds.y) { 104 | ballsLeft -= 1 105 | if (ballsLeft >= 0) reset() 106 | else { 107 | result = Some("You've run out of balls!") 108 | resetGame() 109 | } 110 | }else{ 111 | if (ballPos.within(paddlePos - paddleDims/2, paddlePos + paddleDims/2, Point(5, 5))){ 112 | ballVel = ballVel.copy( 113 | x = ballVel.x + paddleDir.x / 8, 114 | y = -math.abs(ballVel.y) 115 | ) 116 | } 117 | for (brick <- bricks){ 118 | val points = Seq( 119 | brick.pos, 120 | brick.pos + Point(brickSize.x, 0), 121 | brick.pos + brickSize, 122 | brick.pos + Point(0, brickSize.y) 123 | ) 124 | val lines = Seq( 125 | (points(0), points(1), (p: Point) => p.copy(y = -math.abs(p.y))), 126 | (points(1), points(2), (p: Point) => p.copy(x = math.abs(p.x))), 127 | (points(2), points(3), (p: Point) => p.copy(y = math.abs(p.y))), 128 | (points(3), points(0), (p: Point) => p.copy(x = -math.abs(p.x))) 129 | ) 130 | var hit = false 131 | for ((p1, p2, func) <- lines if !hit){ 132 | val extent = (ballPos - p1) * (p2 - p1) / (p2-p1).length 133 | val perpDist = math.sqrt((ballPos - p1).lengthSquared - extent * extent) 134 | if (!hit && extent > 0 && extent < (p2-p1).length && perpDist < 5){ 135 | ballVel = func(ballVel) 136 | bricks.remove(brick) 137 | hit = true 138 | } 139 | } 140 | for (p <- points if !hit){ 141 | val delta = ballPos - p 142 | if (delta.length < 5){ 143 | val impulse = delta * (ballVel * delta) / delta.lengthSquared 144 | ballVel = ballVel - impulse * 2 145 | bricks.remove(brick) 146 | hit = true 147 | } 148 | } 149 | 150 | } 151 | if(bricks.size == 0){ 152 | result = Some("Success! You've destroyed all the bricks!") 153 | resetGame() 154 | } 155 | } 156 | } 157 | 158 | 159 | 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /src/main/scala/example/Tetris.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import org.scalajs.dom.CanvasRenderingContext2D 4 | import scala.util.Random 5 | 6 | case class Tetris(bounds: Point, resetGame: () => Unit) extends Game{ 7 | val pieces = Seq( 8 | Seq( 9 | Array(0, 1, 0, 0), 10 | Array(0, 1, 0, 0), 11 | Array(0, 1, 0, 0), 12 | Array(0, 1, 0, 0) 13 | ), 14 | Seq( 15 | Array(1, 1), 16 | Array(1, 1) 17 | ), 18 | Seq( 19 | Array(1, 1, 0), 20 | Array(0, 1, 1), 21 | Array(0, 0, 0) 22 | ), 23 | Seq( 24 | Array(0, 1, 1), 25 | Array(1, 1, 0), 26 | Array(0, 0, 0) 27 | ), 28 | Seq( 29 | Array(0, 1, 0), 30 | Array(1, 1, 1), 31 | Array(0, 0, 0) 32 | ), 33 | Seq( 34 | Array(0, 1, 0), 35 | Array(0, 1, 0), 36 | Array(1, 1, 0) 37 | ), 38 | Seq( 39 | Array(0, 1, 0), 40 | Array(0, 1, 0), 41 | Array(0, 1, 1) 42 | ) 43 | ) 44 | def iterator(piece: Seq[Array[Int]], offset: Point = Point(0, 0)) = { 45 | for{ 46 | i <- 0 until piece.length 47 | j <- 0 until piece(0).length 48 | if piece(i)(j) != 0 49 | } yield (i + offset.x.toInt, j + offset.y.toInt) 50 | } 51 | var moveCount = 0 52 | var keyCount = 0 53 | val blockWidth = 20 54 | val gridDims = Point(13, bounds.y / blockWidth) 55 | val leftBorder = (bounds.x - blockWidth * gridDims.x) / 2 56 | var linesCleared = 0 57 | var nextPiece = Random.nextInt(pieces.length) 58 | var currentPiece = Random.nextInt(pieces.length) 59 | var piecePos = Point(gridDims.x/2, 0) 60 | class Position(var filled: String = Color.Black) 61 | 62 | val grid = Array.fill(gridDims.x.toInt, gridDims.y.toInt)(new Position) 63 | var prevKeys = Set.empty[Int] 64 | 65 | def rotate(p: Seq[Array[Int]]) = { 66 | val w = p.length 67 | val h = p(0).length 68 | val center = Point(w - 1, h - 1) / 2 69 | val out = Seq.fill(w)(Array.fill(h)(0)) 70 | 71 | for {i <- 0 until w; j <- 0 until h}{ 72 | val centered = (Point(i, j) - center) 73 | val rotated = Point(centered.y * -1, centered.x * 1) + center 74 | out(rotated.x.toInt)(rotated.y.toInt) = p(i)(j) 75 | } 76 | 77 | for {i <- 0 until w; j <- 0 until h}{ 78 | p(i)(j) = out(i)(j) 79 | } 80 | } 81 | 82 | def findCollisions(offset: Point) = { 83 | val pts = iterator(pieces(currentPiece), piecePos).toArray 84 | for { 85 | index <- 0 until pts.length 86 | (i, j) = pts(index) 87 | newPt = Point(i, j) + offset 88 | if !newPt.within(Point(0, 0), gridDims) || grid(newPt.x.toInt)(newPt.y.toInt).filled != Color.Black 89 | } yield {} 90 | } 91 | 92 | def moveDown() = { 93 | val collisions = findCollisions(Point(0, 1)) 94 | val pts = iterator(pieces(currentPiece), piecePos).toArray 95 | if (collisions.length > 0){ 96 | for (index <- 0 until pts.length) { 97 | val (i, j) = pts(index) 98 | grid(i)(j).filled = Color.all(currentPiece) 99 | } 100 | currentPiece = nextPiece 101 | nextPiece = Random.nextInt(pieces.length) 102 | piecePos = Point(gridDims.x/2, 0) 103 | if (!findCollisions(Point(0, 0)).isEmpty){ 104 | result = Some("The board has filled up!") 105 | resetGame() 106 | } 107 | }else{ 108 | piecePos += Point(0, 1) 109 | } 110 | } 111 | def update(keys: Set[Int]): Unit = { 112 | if (keys(37) && !prevKeys(37) && findCollisions(Point(-1, 0)).isEmpty) piecePos += Point(-1, 0) 113 | if (keys(39) && !prevKeys(39) && findCollisions(Point(1, 0)).isEmpty) piecePos += Point(1, 0) 114 | if (keys(32) && !prevKeys(32)) { 115 | rotate(pieces(currentPiece)) 116 | if (!findCollisions(Point(0, 0)).isEmpty){ 117 | for (i <- 0 until 3) rotate(pieces(currentPiece)) 118 | } 119 | } 120 | if (keys(40)) moveDown() 121 | 122 | prevKeys = keys 123 | 124 | if (moveCount > 0) moveCount -= 1 125 | else{ 126 | moveCount = 15 127 | moveDown() 128 | } 129 | 130 | def row(i: Int) = (0 until gridDims.x.toInt).map(j => grid(j)(i)) 131 | var remaining = for{ 132 | i <- (gridDims.y.toInt-1 to 0 by -1).toList 133 | if !row(i).forall(_.filled != Color.Black) 134 | } yield i 135 | 136 | for(i <- gridDims.y.toInt-1 to 0 by -1) remaining match{ 137 | case first :: rest => 138 | remaining = rest 139 | for ((oldS, newS) <- row(i).zip(row(first))){ 140 | oldS.filled = newS.filled 141 | } 142 | case _ => 143 | linesCleared += 1 144 | for (s <- grid(i)) s.filled = Color.Black 145 | } 146 | } 147 | 148 | def draw(ctx: CanvasRenderingContext2D): Unit = { 149 | ctx.fillStyle = Color.Black 150 | ctx.fillRect(0, 0, bounds.x, bounds.y) 151 | 152 | 153 | ctx.textAlign = "left" 154 | ctx.fillStyle = Color.White 155 | ctx.fillText("Lines Cleared: " + linesCleared, leftBorder * 1.3 + gridDims.x * blockWidth, 100) 156 | ctx.fillText("Next Block", leftBorder * 1.35 + gridDims.x * blockWidth, 150) 157 | 158 | 159 | def fillBlock(i: Int, j: Int, color: String) { 160 | ctx.fillStyle = color.replace("255", "128") 161 | ctx.fillRect(leftBorder + i * blockWidth, 0 + j * blockWidth, blockWidth, blockWidth) 162 | ctx.strokeStyle = color 163 | ctx.strokeRect(leftBorder + i * blockWidth, 0 + j * blockWidth, blockWidth, blockWidth) 164 | } 165 | for{ 166 | i <- 0 until gridDims.x.toInt 167 | j <- 0 until gridDims.y.toInt 168 | }{ 169 | fillBlock(i, j, grid(i)(j).filled) 170 | } 171 | 172 | def draw(p: Int, pos: Point, external: Boolean) = { 173 | val pts = iterator(pieces(p), pos) 174 | for (index <- 0 until pts.length) { 175 | val (i, j) = pts(index) 176 | if (Point(i, j).within(Point(0, 0), gridDims) || external) fillBlock(i, j, Color.all(p)) 177 | } 178 | } 179 | draw(currentPiece, piecePos, external = false) 180 | draw(nextPiece, Point(18, 9), external = true) 181 | 182 | ctx.strokeStyle = Color.White 183 | ctx.strokePath( 184 | Point(leftBorder, 0), 185 | Point(leftBorder, bounds.y) 186 | ) 187 | ctx.strokePath( 188 | Point(bounds.x - leftBorder, 0), 189 | Point(bounds.x - leftBorder, bounds.y) 190 | ) 191 | } 192 | } 193 | --------------------------------------------------------------------------------