├── .gitignore ├── README.md ├── canvas-drawing-reference.md ├── images ├── asteroids-screenshot.png ├── lunar-lander-screenshot.png ├── snake-screenshot.png └── space-invaders-screenshot.png ├── livecoded-game ├── game.js └── index.html ├── package.json └── space-invaders ├── index.html ├── src ├── bullet.js ├── game.js ├── invader.js ├── keyboard.js ├── player.js └── run-game.js └── test ├── karma.conf.js └── unit ├── bullet.spec.js ├── game.spec.js └── invader.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build a JavaScript game (Makers Academy workshop) 2 | 3 |  4 | 5 | ## The itinerary 6 | 7 | 1. I'll livecode a small game from scratch. 8 | 2. We'll briefly dissect the architectural approach I used. 9 | 3. We'll talk about an approach for testing game code. 10 | 4. You'll choose what game to implement: Snake, Asteroids, Lunar 11 | Lander. 12 | 5. You'll break into pairs spend the rest of the workshop implementing 13 | your game. I'll be here to help. 14 | 15 | ## The aims of this workshop 16 | 17 | Every game has different, gameplay-specific problems that need to be 18 | solved. The solutions to these problems are different for different 19 | games. If this workshop were about how to do animation, or player 20 | movement, or collision resolution, it would not be very useful. The 21 | techniques I presented would be useful for some games, but not others. 22 | 23 | This workshop is based around letting you practice using an 24 | architectural approach that is good for building 2D action games. 25 | This approach can be applied to many different games. It scales to 26 | ten, a hundred or a thousand times as many lines of code. 27 | 28 | This workshop will also let you complete, or make good progress on, 29 | your first game. 30 | 31 | ## Space Invaders 32 | 33 | The code is in `space-invaders/src/`. 34 | 35 | ## Running the game 36 | 37 | ### Install Python 2 38 | 39 | ### Run Space Invaders game 40 | 41 | To serve the Space Invaders code 42 | 43 | $ cd build-a-javascript-game-workshop/space-invaders/ 44 | $ open index.html 45 | $ python -m SimpleHTTPServer 4000 46 | 47 | ### Run livecoded game 48 | 49 | To serve the livecoded code 50 | 51 | $ cd build-a-javascript-game-workshop/livecoded-game/ 52 | $ open index.html 53 | $ python -m SimpleHTTPServer 4000 54 | 55 | ## Running the tests for Space Invaders 56 | 57 | $ cd build-a-javascript-game-workshop 58 | $ npm install 59 | $ npm test 60 | 61 | ## What game to make? 62 | 63 | ### Snake 64 | 65 | Reasonably simple. Probably the best example for focusing on the 66 | architecture and not getting bogged down in implementation details. 67 | 68 |  69 | 70 | ### Asteroids 71 | 72 | Requries some trigonometry to detect when lines are intersecting. 73 | 74 |  75 | 76 | ### Lunar Lander 77 | 78 | Requires some trigonometry to detect when lines are intersecting. 79 | Also fiddly because the lander has to be able to rest on the landing 80 | pad. 81 | 82 |  83 | 84 | ## How to get started 85 | 86 | 1. Get a black square drawing on the canvas. 87 | 88 | 2. Create a game tick function and get it running many times a second. 89 | 90 | 3. Create a player body and get its `update()` and `draw()` functions 91 | running. 92 | 93 | 4. Use the keyboard to move the player around. 94 | 95 | 5. ... 96 | 97 | ## Canvas drawing reference 98 | 99 | See [canvas-drawing-reference.md](canvas-drawing-reference.md) in the 100 | root of this repo. It has code for drawing lines, polygons and 101 | circles, and setting colours. 102 | -------------------------------------------------------------------------------- /canvas-drawing-reference.md: -------------------------------------------------------------------------------- 1 | # Canvas drawing reference 2 | 3 | ## Get a reference to the canvas drawing context 4 | 5 | This is the object that has drawing functions like `fillRect()`. 6 | 7 | var screen = document.getElementById("screen").getContext("2d"); 8 | 9 | ## Colors 10 | 11 | Do these in CSS-style RGB (red, green, blue). Each part (red, green 12 | or blue) value is a hex number between `00` and `ff`. 13 | 14 | ### Examples 15 | 16 | "#ff0000" // red 17 | "#00ff00" // green 18 | "#0000ff" // blue 19 | 20 | ## Rectangles 21 | 22 | ### Draw rectangle outline 23 | 24 | screen.strokeStyle = "#ff0000"; 25 | screen.strokeRect(10, 10, 50, 70); // top left x, top left y, width, height 26 | 27 | ### Draw solid rectangle 28 | 29 | screen.fillStyle = "#00ff00"; 30 | screen.fillRect(10, 10, 50, 70); // top left x, top left y, width, height 31 | 32 | ## Lines 33 | 34 | ### Draw a red line 35 | 36 | screen.beginPath(); 37 | screen.moveTo(100, 50); // x, y 38 | screen.lineTo(150, 40); // x, y 39 | screen.closePath(); 40 | 41 | screen.lineWidth = 5; 42 | screen.strokeStyle = "#ff0000"; 43 | screen.stroke(); 44 | 45 | ## Polygons 46 | 47 | ### Define a red triangle 48 | 49 | screen.beginPath(); 50 | screen.moveTo(150, 150); // x, y 51 | screen.lineTo(200, 200); // x, y 52 | screen.lineTo(100, 200); 53 | screen.closePath(); 54 | 55 | ### Draw its outline 56 | 57 | screen.lineWidth = 2; 58 | screen.strokeStyle = "#ff0000"; 59 | screen.stroke(); 60 | 61 | ### Fill it with a color 62 | 63 | screen.fillStyle = "#00ff00"; 64 | screen.fill(); 65 | 66 | ## Circles and arcs 67 | 68 | ### Define a red circle 69 | 70 | screen.beginPath(); 71 | screen.arc(100, 100, 30, 0, Math.PI * 2); // center x, center y, radius, 72 | // start angle (radians), end angle 73 | screen.closePath(); 74 | 75 | ### Draw its outline 76 | 77 | screen.strokeStyle = "#ff0000"; 78 | screen.stroke(); 79 | 80 | ### Fill it with a color 81 | 82 | screen.fillStyle = "#00ff00"; 83 | screen.fill(); 84 | -------------------------------------------------------------------------------- /images/asteroids-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryrosecook/build-a-javascript-game-workshop/4c6fbce85b124f796f16df41327f652159e7fb88/images/asteroids-screenshot.png -------------------------------------------------------------------------------- /images/lunar-lander-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryrosecook/build-a-javascript-game-workshop/4c6fbce85b124f796f16df41327f652159e7fb88/images/lunar-lander-screenshot.png -------------------------------------------------------------------------------- /images/snake-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryrosecook/build-a-javascript-game-workshop/4c6fbce85b124f796f16df41327f652159e7fb88/images/snake-screenshot.png -------------------------------------------------------------------------------- /images/space-invaders-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryrosecook/build-a-javascript-game-workshop/4c6fbce85b124f796f16df41327f652159e7fb88/images/space-invaders-screenshot.png -------------------------------------------------------------------------------- /livecoded-game/game.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function() { 2 | var screen = document.getElementById("screen").getContext("2d"); 3 | 4 | var gameSize = { x: screen.canvas.width, y: screen.canvas.height }; 5 | 6 | var game = new Game(gameSize); 7 | game.addBody(new Player(game, gameSize)); 8 | 9 | function tick() { 10 | game.update(); 11 | game.draw(screen); 12 | 13 | requestAnimationFrame(tick); 14 | }; 15 | 16 | tick(); 17 | }); 18 | 19 | function Game(size) { 20 | this.size = size; 21 | this.bodies = []; 22 | }; 23 | 24 | Game.prototype = { 25 | update: function() { 26 | this.bodies.forEach(function(body) { 27 | body.update(); 28 | }); 29 | }, 30 | 31 | draw: function(screen) { 32 | screen.clearRect(0, 0, this.size.x, this.size.y); 33 | 34 | this.bodies.forEach(function(body) { 35 | body.draw(screen); 36 | }); 37 | }, 38 | 39 | addBody: function(body) { 40 | this.bodies.push(body); 41 | } 42 | }; 43 | 44 | function Player(game, gameSize) { 45 | var WIDTH_HEIGHT = 15; 46 | 47 | this.game = game; 48 | this.keyboard = new Keyboard(); 49 | this.center = { x: gameSize.x / 2, y: gameSize.y / 2 }; 50 | this.size = { x: WIDTH_HEIGHT, y: WIDTH_HEIGHT }; 51 | }; 52 | 53 | Player.prototype = { 54 | update: function() { 55 | this.movePlayerInResponseToInput(); 56 | this.shootInResponseToInput(); 57 | }, 58 | 59 | movePlayerInResponseToInput: function() { 60 | if (this.keyboard.isDown(this.keyboard.KEYS.LEFT)) { 61 | this.moveLeft(); 62 | } else if (this.keyboard.isDown(this.keyboard.KEYS.RIGHT)) { 63 | this.moveRight(); 64 | } 65 | }, 66 | 67 | shootInResponseToInput: function() { 68 | if (this.keyboard.isDown(this.keyboard.KEYS.SPACE)) { 69 | this.shoot(); 70 | } 71 | }, 72 | 73 | shoot: function() { 74 | this.game.addBody(new Bullet({ x: this.center.x, y: this.center.y - this.size.y }, 75 | { x: 0, y: -2 })); 76 | }, 77 | 78 | moveLeft: function() { 79 | this.center.x -= 2; 80 | }, 81 | 82 | moveRight: function() { 83 | this.center.x += 2; 84 | }, 85 | 86 | draw: function(screen) { 87 | screen.fillRect(this.center.x - this.size.x / 2, 88 | this.center.y - this.size.y / 2, 89 | this.size.x, 90 | this.size.y); 91 | } 92 | }; 93 | 94 | function Keyboard() { 95 | var keyState = {}; 96 | 97 | window.addEventListener("keydown", function(event) { 98 | keyState[event.keyCode] = true; 99 | }); 100 | 101 | window.addEventListener("keyup", function(event) { 102 | keyState[event.keyCode] = false; 103 | }); 104 | 105 | this.isDown = function(keyCode) { 106 | return keyState[keyCode] === true; 107 | }; 108 | 109 | this.KEYS = { LEFT: 37, RIGHT: 39, SPACE: 32 }; 110 | }; 111 | 112 | function Bullet(center, velocity) { 113 | this.center = center; 114 | this.size = { x: 10, y: 10 }; 115 | this.velocity = velocity; 116 | }; 117 | 118 | Bullet.prototype = { 119 | update: function() { 120 | this.moveWithVelocity(); 121 | }, 122 | 123 | moveWithVelocity: function() { 124 | this.center.x += this.velocity.x; 125 | this.center.y += this.velocity.y; 126 | }, 127 | 128 | draw: function(screen) { 129 | screen.fillRect(this.center.x - this.size.x / 2, 130 | this.center.y - this.size.y / 2, 131 | this.size.x, 132 | this.size.y); 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /livecoded-game/index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build-a-javascript-game-workshop", 3 | "version": "1.0.0", 4 | "description": "A workshop where you build a game in JavaScript", 5 | "main": "game.js", 6 | "scripts": { 7 | "test": "./node_modules/karma/bin/karma start space-invaders/test/karma.conf.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/maryrosecook/build-a-javascript-game-workshop" 12 | }, 13 | "author": "Mary Rose Cook mary@maryrosecook.com", 14 | "license": "MIT", 15 | "dependencies": {}, 16 | "devDependencies": { 17 | "jasmine-core": "^2.4.1", 18 | "karma": "^0.13.22", 19 | "karma-chrome-launcher": "^0.2.3", 20 | "karma-cli": "^0.1.2", 21 | "karma-jasmine": "^0.3.8", 22 | "live-server": "^1.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /space-invaders/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /space-invaders/src/bullet.js: -------------------------------------------------------------------------------- 1 | function Bullet(center, velocity) { 2 | var WIDTH_HEIGHT = 3; 3 | 4 | this.center = center; 5 | this.size = { x: WIDTH_HEIGHT, y: WIDTH_HEIGHT }; 6 | this.velocity = velocity; 7 | }; 8 | 9 | Bullet.prototype = { 10 | update: function() { 11 | this.moveWithVelocity(); 12 | }, 13 | 14 | moveWithVelocity: function() { 15 | this.center.x += this.velocity.x; 16 | this.center.y += this.velocity.y; 17 | }, 18 | 19 | draw: function(screen) { 20 | Game.drawBody(screen, this); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /space-invaders/src/game.js: -------------------------------------------------------------------------------- 1 | function Game(size) { 2 | this.size = size; 3 | this.bodies = []; 4 | }; 5 | 6 | Game.prototype = { 7 | update: function() { 8 | this.bodies = this.bodiesNotColliding(); 9 | 10 | this.bodies.forEach(function(body) { 11 | body.update(); 12 | }); 13 | }, 14 | 15 | draw: function(screen) { 16 | screen.clearRect(0, 0, this.size.x, this.size.y); 17 | 18 | this.bodies.forEach(function(body) { 19 | body.draw(screen); 20 | }); 21 | }, 22 | 23 | addBody: function(body) { 24 | this.bodies.push(body); 25 | }, 26 | 27 | bodiesNotColliding: function() { 28 | var self = this; 29 | return this.bodies.filter(function(b1) { 30 | return self.bodies 31 | .filter(function(b2) { return Game.isColliding(b1, b2); }) 32 | .length === 0; 33 | }); 34 | }, 35 | 36 | invadersBelow: function(invader) { 37 | return this.bodies 38 | .filter(function(b) { 39 | return b instanceof Invader && 40 | b.center.y > invader.center.y && 41 | Math.abs(b.center.x - invader.center.x) < invader.size.x; 42 | }) 43 | .length > 0; 44 | } 45 | }; 46 | 47 | Game.drawBody = function(screen, body) { 48 | screen.fillRect(body.center.x - body.size.x / 2, 49 | body.center.y - body.size.y / 2, 50 | body.size.x, 51 | body.size.y); 52 | }; 53 | 54 | Game.isColliding = function(b1, b2) { 55 | return !( 56 | b1 === b2 || 57 | b1.center.x + b1.size.x / 2 <= b2.center.x - b2.size.x / 2 || 58 | b1.center.y + b1.size.y / 2 <= b2.center.y - b2.size.y / 2 || 59 | b1.center.x - b1.size.x / 2 >= b2.center.x + b2.size.x / 2 || 60 | b1.center.y - b1.size.y / 2 >= b2.center.y + b2.size.y / 2 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /space-invaders/src/invader.js: -------------------------------------------------------------------------------- 1 | function Invader(game, center) { 2 | var WIDTH_HEIGHT = 15; 3 | 4 | this.game = game; 5 | this.center = center; 6 | this.size = { x: WIDTH_HEIGHT, y: WIDTH_HEIGHT }; 7 | this.patrolX = 0; 8 | this.speedX = 0.3; 9 | }; 10 | 11 | Invader.prototype = { 12 | update: function() { 13 | this.patrol(); 14 | 15 | if (this.shouldShoot()) { 16 | this.shoot(); 17 | } 18 | }, 19 | 20 | patrol: function() { 21 | if (this.shouldTurnAround()) { 22 | this.speedX = -this.speedX; 23 | } 24 | 25 | this.center.x += this.speedX; 26 | this.patrolX += this.speedX; 27 | }, 28 | 29 | shouldTurnAround: function() { 30 | return this.patrolX < 0 || this.patrolX > 40; 31 | }, 32 | 33 | shouldShoot: function() { 34 | return Math.random() > 0.995 && !this.game.invadersBelow(this); 35 | }, 36 | 37 | shoot: function() { 38 | this.game.addBody(new Bullet({ x: this.center.x, 39 | y: this.center.y + this.size.x }, 40 | { x: 0, y: 2 })); 41 | }, 42 | 43 | draw: function(screen) { 44 | Game.drawBody(screen, this); 45 | } 46 | }; 47 | 48 | Invader.createAll = function(game) { 49 | var ROWS = 3; 50 | var COLUMNS = 8; 51 | var INVADER_SPACING = 30; 52 | var INVADER_COUNT = 24; 53 | 54 | var invaders = []; 55 | for (var i = 0; i < INVADER_COUNT; i++) { 56 | var x = INVADER_SPACING + i % COLUMNS * INVADER_SPACING; 57 | var y = INVADER_SPACING + i % ROWS * INVADER_SPACING; 58 | 59 | invaders.push(new Invader(game, { x: x, y: y })); 60 | } 61 | 62 | return invaders; 63 | }; 64 | -------------------------------------------------------------------------------- /space-invaders/src/keyboard.js: -------------------------------------------------------------------------------- 1 | function Keyboard() { 2 | var keyState = {}; 3 | 4 | window.addEventListener("keydown", function(e) { 5 | keyState[e.keyCode] = true; 6 | }); 7 | 8 | window.addEventListener("keyup", function(e) { 9 | keyState[e.keyCode] = false; 10 | }); 11 | 12 | this.isDown = function(keyCode) { 13 | return keyState[keyCode] === true; 14 | }; 15 | 16 | this.KEYS = { LEFT: 37, RIGHT: 39, SPACE: 32 }; 17 | }; 18 | -------------------------------------------------------------------------------- /space-invaders/src/player.js: -------------------------------------------------------------------------------- 1 | function Player(game, center) { 2 | var WIDTH_HEIGHT = 15; 3 | 4 | this.game = game; 5 | this.center = { x: center.x, y: center.y - WIDTH_HEIGHT }; 6 | this.size = { x: WIDTH_HEIGHT, y: WIDTH_HEIGHT }; 7 | this.keyboard = new Keyboard(); 8 | }; 9 | 10 | Player.prototype = { 11 | update: function() { 12 | this.respondToUserInput(); 13 | }, 14 | 15 | respondToUserInput: function() { 16 | if (this.keyboard.isDown(this.keyboard.KEYS.LEFT)) { 17 | this.moveLeft(); 18 | } else if (this.keyboard.isDown(this.keyboard.KEYS.RIGHT)) { 19 | this.moveRight(); 20 | } 21 | 22 | if (this.keyboard.isDown(this.keyboard.KEYS.SPACE)) { 23 | this.shoot(); 24 | } 25 | }, 26 | 27 | moveLeft: function() { 28 | this.center.x -= 2; 29 | }, 30 | 31 | moveRight: function() { 32 | this.center.x += 2; 33 | }, 34 | 35 | shoot: function() { 36 | this.game.addBody(new Bullet({ x: this.center.x, 37 | y: this.center.y - this.size.x }, 38 | { x: 0, y: -6 })); 39 | }, 40 | 41 | draw: function(screen) { 42 | Game.drawBody(screen, this); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /space-invaders/src/run-game.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function() { 2 | var screen = document.getElementById("screen").getContext("2d"); 3 | var gameSize = { x: screen.canvas.width, y: screen.canvas.height }; 4 | var game = new Game(gameSize); 5 | 6 | game.addBody(new Player(game, { x: gameSize.x / 2, y: gameSize.y })); 7 | Invader.createAll(game).forEach(function(invader) { 8 | game.addBody(invader); 9 | }); 10 | 11 | function tick() { 12 | game.update(); 13 | game.draw(screen); 14 | requestAnimationFrame(tick); 15 | }; 16 | 17 | tick(); 18 | }); 19 | -------------------------------------------------------------------------------- /space-invaders/test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config){ 2 | config.set({ 3 | 4 | basePath: '../', 5 | 6 | files: [ 7 | 'src/**/!(run-game).js', 8 | 'test/unit/**/*.js' 9 | ], 10 | 11 | autoWatch: true, 12 | 13 | frameworks: ['jasmine'], 14 | 15 | browsers: ['Chrome'], 16 | 17 | plugins: [ 18 | 'karma-chrome-launcher', 19 | 'karma-jasmine' 20 | ] 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /space-invaders/test/unit/bullet.spec.js: -------------------------------------------------------------------------------- 1 | describe("Bullet", function() { 2 | describe("new", function() { 3 | it("should create a bullet", function() { 4 | var center = { x: 1, y: 2 }; 5 | var velocity = { x: 3, y: 4 }; 6 | expect(new Bullet(center, velocity) instanceof Bullet).toEqual(true); 7 | }); 8 | }); 9 | 10 | describe("update", function() { 11 | it("should move bullet one step of the velocity", function() { 12 | var center = { x: 1, y: 2 }; 13 | var velocity = { x: 3, y: 4 }; 14 | var bullet = new Bullet(center, velocity); 15 | bullet.update(); 16 | expect(center.x).toEqual(4); 17 | expect(center.y).toEqual(6); 18 | }); 19 | }); 20 | 21 | describe("draw", function() { 22 | it("should call Game.drawBody with screen and body", function() { 23 | var bullet = new Bullet(); 24 | var screen = {}; 25 | spyOn(Game, ["drawBody"]); 26 | 27 | bullet.draw(screen); 28 | expect(Game.drawBody).toHaveBeenCalledWith(screen, bullet); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /space-invaders/test/unit/game.spec.js: -------------------------------------------------------------------------------- 1 | describe("Game", function() { 2 | describe("new", function() { 3 | it("should create a game", function() { 4 | var size = { x: 1, y: 2 }; 5 | expect(new Game(size) instanceof Game).toEqual(true); 6 | }); 7 | }); 8 | 9 | describe("update", function() { 10 | it("should call update on a body", function() { 11 | var game = new Game(); 12 | var body = jasmine.createSpyObj("body", ["update"]); 13 | 14 | game.addBody(body); 15 | game.update(); 16 | expect(body.update).toHaveBeenCalled(); 17 | }); 18 | }); 19 | 20 | describe("draw", function() { 21 | it("should clear the screen", function() { 22 | var game = new Game({ x: 10, y: 10 }); 23 | var body = jasmine.createSpyObj("body", ["draw"]); 24 | var screen = jasmine.createSpyObj("screen", ["clearRect"]); 25 | 26 | game.draw(screen); 27 | expect(screen.clearRect).toHaveBeenCalled(); 28 | }); 29 | 30 | it("should call draw on a body", function() { 31 | var game = new Game({ x: 10, y: 10 }); 32 | var body = jasmine.createSpyObj("body", ["draw"]); 33 | var screen = jasmine.createSpyObj("screen", ["clearRect"]); 34 | 35 | game.addBody(body); 36 | game.draw(screen); 37 | expect(body.draw).toHaveBeenCalled(); 38 | }); 39 | }); 40 | 41 | describe("bodiesNotColliding", function() { 42 | it("should return no bodies if all colliding", function() { 43 | var game = new Game(); 44 | spyOn(Game, "isColliding").and.returnValue(true); 45 | var body1 = {}; 46 | var body2 = {}; 47 | 48 | game.addBody(body1); 49 | game.addBody(body2); 50 | expect(game.bodiesNotColliding()).toEqual([]); 51 | }); 52 | 53 | it("should return all bodies if none colliding", function() { 54 | var game = new Game(); 55 | spyOn(Game, "isColliding").and.returnValue(false); 56 | var body1 = {}; 57 | var body2 = {}; 58 | 59 | game.addBody(body1); 60 | game.addBody(body2); 61 | expect(game.bodiesNotColliding()).toEqual([body1, body2]); 62 | }); 63 | }); 64 | 65 | describe("addBody", function() { 66 | it("should be able to add body", function() { 67 | var game = new Game(); 68 | var body = jasmine.createSpyObj("body", ["update"]); 69 | 70 | game.addBody(body); 71 | game.update(); 72 | expect(body.update).toHaveBeenCalled(); 73 | }); 74 | }); 75 | 76 | describe("Game.drawBody", function() { 77 | it("should draw a body in the body's position", function() { 78 | var screen = jasmine.createSpyObj("screen", ["fillRect"]); 79 | var body = { center: { x: 1, y: 2 }, size: { x: 3, y: 3 }}; 80 | Game.drawBody(screen, body); 81 | expect(screen.fillRect).toHaveBeenCalledWith(-0.5, 0.5, 3, 3); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /space-invaders/test/unit/invader.spec.js: -------------------------------------------------------------------------------- 1 | describe("Invader", function() { 2 | describe("new", function() { 3 | it("should create an invader", function() { 4 | var game = {}; 5 | var center = { x: 1, y: 2 }; 6 | expect(new Invader(game, center) instanceof Invader).toEqual(true); 7 | }); 8 | }); 9 | 10 | describe("update", function() { 11 | describe("patrolling", function() { 12 | it("should move all the way right, all the way left, all the way right", function() { 13 | var game = jasmine.createSpyObj("game", ["invadersBelow", "addBody"]); 14 | var center = { x: 0, y: 2 }; 15 | var invader = new Invader(game, center); 16 | 17 | // right 18 | for (var i = 0; i < 40; i += 0.3) { 19 | expect(center.x).toEqual(i); 20 | invader.update(); 21 | } 22 | 23 | // left 24 | for (i; i >= 0; i -= 0.3) { 25 | expect(center.x).toEqual(i); 26 | invader.update(); 27 | } 28 | 29 | // right 30 | for (i; i < 40; i += 0.3) { 31 | expect(center.x).toEqual(i); 32 | invader.update(); 33 | } 34 | }); 35 | }); 36 | 37 | // describe("shooting", function() { 38 | // fit("should not shoot if invaders below", function() { 39 | // var game = jasmine.createSpyObj("game", ["addBody"]); 40 | // spyOn(game, "invadersBelow").and.returnValue(true); 41 | 42 | // spyOn(Math, "random").and.returnValue(0.99); 43 | // var center = { x: 0, y: 2 }; 44 | // var invader = new Invader(game, center); 45 | 46 | // }); 47 | // }); 48 | }); 49 | 50 | describe("draw", function() { 51 | it("should call Game.drawBody with screen and body", function() { 52 | var invader = new Invader(); 53 | var screen = {}; 54 | spyOn(Game, ["drawBody"]); 55 | 56 | invader.draw(screen); 57 | expect(Game.drawBody).toHaveBeenCalledWith(screen, invader); 58 | }); 59 | }); 60 | }); 61 | --------------------------------------------------------------------------------