Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building Tetris.js

Building Tetris.js

Building Tetris using: javascript & canvas drawing, and testing with node+jasmine

Code at: https://github.com/pedrocunha/tetris.js
Live at: http://tetris-js.herokuapp.com/

Avatar for Pedro Cunha

Pedro Cunha

October 24, 2013
Tweet

Other Decks in Programming

Transcript

  1. You never did a game before! ... but you are

    a programmer and everything is possible right?
  2. A tetromino is a geometric shape composed of four squares,

    connected orthogonally. This, like dominoes and pentominoes, is a particular type of polyomino. The corresponding polycube, called a tetracube, is a geometric shape composed of fourcubes connected orthogonally.
  3. describe('Tetromino', function() { var tetromino; beforeEach(function() { tetromino = new

    Tetromino([[1,0], [2,0], [0,1], [1, 1]]); }); ! describe('#new', function(){ ... it('sets a default color', function(){ expect(tetromino.color).toEqual('#FFFFFF') }) ... describe('#rotateRight', function(){ beforeEach(function(){ tetromino.rotateRight(); }) ! it('rotates the tetromino to the right', function(){ // - X X // X X - ! // X - // X X // - X expect(tetromino.grid[0][0]).toBe(1); expect(tetromino.grid[0][1]).toBe(0); ! expect(tetromino.grid[1][0]).toBe(1); expect(tetromino.grid[1][1]).toBe(1); ! expect(tetromino.grid[2][0]).toBe(0); expect(tetromino.grid[2][1]).toBe(1); }) It’s very easy to break things ( syntax issues, bad scoping, etc..). - Go mad on TDD. ! Jasmine is great for unit testing spec/tetromino_spec.js
  4. js/tetromino.js Class style conventions: - Constructor function - Prototype for

    public & “private” methods - private methods start with “_” ! On the bottom: - Class methods function Tetromino(coordinates, color){ this.coordinates = coordinates; this.color = color || '#FFFFFF'; this.grid = []; ! this._initializeGrid(); } ! Tetromino.prototype = { ... rotateRight: function(commit){ var i = this.grid.length - 1, j = 0, newGrid = []; ! for ( ; i >= 0; --i ) { for ( var j = 0; j < this.grid[i].length; ++j ) { if ( newGrid[j] == null ) newGrid[j] = []; newGrid[j][this.grid.length - 1 - i] = this.grid[i][j]; } } ! if ( commit === undefined || commit === true ) this.grid = newGrid; ! return newGrid; }, ... } ! // Class methods Tetromino.all = [ new Tetromino([[0,0], [1,0], [2,0], [3, 0]], '#e33100'), ...
  5. #moveDown - Game keeps a reference for the currentX and

    currentY. ! - Increment currentY and check if the future game grid positions are 0. If so means the Tetromino can safely move. it('returns false when row 0th is all filled', function(){ // Fill the first row of grid with non-nil // values. moveDown returns false because // he can't move down. for(var i = 0; i < Game.HORIZONTAL_SPACES; ++i ) game.grid[0][i] = 1 ! expect(game.moveDown()).toBe(false); }) it('returns false when there is an obstacle on the way', function(){ game.moveDown(); game.moveDown(); // - - - X - - - - - - // - - - X X X - - - - ! // Introduce the obstacle // - - - X - - - - - - // - - o X X X - - - - // - - o o - - - - - - game.grid[1][2] = 1 game.grid[2][2] = 1 game.grid[2][3] = 1 ! expect(game.moveDown()).toBe(false) })
  6. #removeCompletedRows - For the game class perspective it just removes

    elements from the grid. This will be important for painting later. - Tetromino grid is still present on it’s inner object but it’s irrelevant tetromino will have 0 references so it will be garbage collected tetromino still has a reference so it’s kept
  7. UI

  8. You should try to aim 60 fps > game algorithms

    should have lowest complexity as possible > drawing needs to be snappy > Problem: only measurable through your experience ! Backend is all built > we just need to draw ! Options: > CSS and messing around with positions ( only if you are crazy ) > WebGL ( weird and unsupported APIs ) > Canvas
  9. requestAnimationFrame: - API for animation - For changes on: DOM

    styling || Canvas || WebGL - Optimises concurrent animations into a single reflow and repaint cycle - If you are running the animation loop in a tab that’s not visible, the browser won’t keep it running
  10. var mainloop = function() { updateGame(); drawGame(); }; ! var

    animFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || null ; ! if(animFrame !== null) { var recursiveAnim = function() { mainloop(); animFrame(recursiveAnim, canvas); }; animFrame(recursiveAnim, canvas); } else { var ONE_FRAME_TIME = 1000.0 / 60.0 ; setInterval(mainloop, ONE_FRAME_TIME); } ! function updateGame(){ if(!game.canMoveDown()){ game.removeCompletedRows(); game.next(); } } ! function drawGame(){ gamePresenter.clear(); gamePresenter.draw(); } All browsers have their variant of animation frame • Unsupported browsers it does still work but animation might be worse Easiest way to draw is in every loop just clean everything and draw again Mainloop • Do game logic ✓Handle user events (left, right, …) ✓Handle game events (completed rows) • Draw current game logic Drawing
  11. js/game_presenter.js Handles all game drawing logic: - User event listeners

    - Repaint logic - Auto move down ! Very important: - canvas.height - canvas.width - everything drawn inside is calculated based on that GamePresenter.prototype = { prepare: function(){ ... this.canvas.height = GamePresenter.gameHeight; this.canvas.width = GamePresenter.gameWidth; this._autoMoveDown(); }, ! clear: function(){ this.context.fillStyle = "#FFFFFF"; this.context.fillRect(0,0, GamePresenter.gameWidth, GamePresenter.gameHeight); }, ! draw: function(){ var i = null, j = null; ! for (i = 0 ; i < this.game.grid.length; ++i) for (j = 0; j < this.game.grid[i].length; ++j) if ( this.game.grid[i][j] != undefined ) this.tetrominoPresenter.draw( game.grid[i][j], j * TetrominoPresenter.BLOCK_SIZE, i * TetrominoPresenter.BLOCK_SIZE ); }, ...
  12. js/tetromino_presenter.js Draws tetrominos that’s it. function TetrominoPresenter(context) { this.context =

    context; } ! TetrominoPresenter.prototype = { draw: function(tetromino, xPos, yPos){ this.context.fillStyle = tetromino.color; this.context.fillRect( xPos, yPos, TetrominoPresenter.BLOCK_SIZE, TetrominoPresenter.BLOCK_SIZE ); } } ! TetrominoPresenter.BLOCK_SIZE = 30;
  13. Hard to do integration testing - Break down the app

    as much as possible to do intensive Unit Testing - Build for a Game API
  14. Node testing { "dependencies": { "jasmine-node": "*" }, "scripts": {

    "test": "jasmine-node spec" } } brew install npm ! npm install - Installs dependencies on package.json ! npm test - Runs all jasmine tests - Described on package.json var fs = require('fs'); var vm = require('vm'); ! function include(path) { var code = fs.readFileSync(path, 'utf-8'); vm.runInThisContext(code, path); } ! include('./js/underscore.min.js'); include('./js/game.js'); include('./js/tetromino.js'); spec/spec_helper.js package.json
  15. Travis CI .................................................... .........! ! Finished in 0.022 seconds! 61

    tests, 179 assertions, 0 failures, 0 skipped! 1 language: node_js! 2 node_js:! 3 - "0.11"! Make it public and benefit from the easy testing provided by Travis CI that can run node out of the box. ! It installs all dependencies on the package.json and runs by default npm test. .travis.yml