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 |
27 |
Asteroids: shoot down the asteroid swarms and avoid getting hit!
28 |
Astrolander: bring your lander to a a gentle landing on flat ground before you run out of fuel.
29 |
Snake: eat apples to grow long and don't crash into walls!
30 |
Pong: outsmart your AI opponent to get the ball past his paddle (right) to score points!
31 |
Brick: use your paddle to bounce the ball up, destroying all the bricks before you run out of balls.
32 |
Tetris: collect points by clearing rows and don't let the screen fill with blocks.
33 |
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 |
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 |
27 |
Asteroids: shoot down the asteroid swarms and avoid getting hit!
28 |
Astrolander: bring your lander to a a gentle landing on flat ground before you run out of fuel.
29 |
Snake: eat apples to grow long and don't crash into walls!
30 |
Pong: outsmart your AI opponent to get the ball past his paddle (right) to score points!
31 |
Brick: use your paddle to bounce the ball up, destroying all the bricks before you run out of balls.
32 |
Tetris: collect points by clearing rows and don't let the screen fill with blocks.
33 |
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 |
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 |
17 |
Asteroids: shoot down the asteroid swarms and avoid getting hit!
18 |
Astrolander: bring your lander to a a gentle landing on flat ground before you run out of fuel.
19 |
Snake: eat apples to grow long and don't crash into walls!
20 |
Pong: outsmart your AI opponent to get the ball past his paddle (right) to score points!
21 |
Brick: use your paddle to bounce the ball up, destroying all the bricks before you run out of balls.
22 |
Tetris: collect points by clearing rows and don't let the screen fill with blocks.
23 |
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 |
--------------------------------------------------------------------------------