From 6287184c24a8f58fb8dc8d44f7cca994baece10a Mon Sep 17 00:00:00 2001 From: Gustavo Mazzoni Date: Fri, 29 Jul 2016 13:47:00 -0300 Subject: [PATCH] Updated version; Added ability to share game board with scores on twitter and facebook. --- Gruntfile.js | 53 +- app/index.html | 51 +- app/scripts/app.js | 54 +- app/scripts/game/game.js | 65 ++- app/scripts/grid/grid.js | 21 +- .../social/facebook/facebook_directive.html | 1 + .../social/facebook/facebook_directive.js | 26 + app/scripts/social/social.js | 3 + .../social/twitter/twitter_directive.html | 1 + .../social/twitter/twitter_directive.js | 36 ++ app/styles/main.scss | 1 + app/styles/social.scss | 9 + app/vendors/lz-string/lz-string.js | 510 ++++++++++++++++++ app/views/main.html | 20 + bower.json | 12 +- karma.conf.js | 1 + package.json | 14 +- test/unit/game/game_spec.js | 62 ++- test/unit/keyboard/keyboard_spec.js | 42 ++ 19 files changed, 922 insertions(+), 60 deletions(-) create mode 100644 app/scripts/social/facebook/facebook_directive.html create mode 100644 app/scripts/social/facebook/facebook_directive.js create mode 100644 app/scripts/social/social.js create mode 100644 app/scripts/social/twitter/twitter_directive.html create mode 100644 app/scripts/social/twitter/twitter_directive.js create mode 100644 app/styles/social.scss create mode 100644 app/vendors/lz-string/lz-string.js create mode 100644 test/unit/keyboard/keyboard_spec.js diff --git a/Gruntfile.js b/Gruntfile.js index 8115142..d717562 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -29,7 +29,7 @@ module.exports = function (grunt) { watch: { bower: { files: ['bower.json'], - tasks: ['bowerInstall'] + tasks: ['wiredep'] }, js: { files: ['<%= yeoman.app %>/scripts/{,*/}*.js'], @@ -119,8 +119,8 @@ module.exports = function (grunt) { dest: '<%= yeoman.dist %>/scripts/templates.js', options: { cwd: '<%= yeoman.app %>', - url: function(url) { - return url.replace('app/', ''); + url: function(url) { + return url.replace('app/', ''); }, usemin: 'scripts/templates.js', htmlmin: '<%= htmlmin.app %>' @@ -174,14 +174,31 @@ module.exports = function (grunt) { }, // Automatically inject Bower components into the app - bowerInstall: { - app: { - src: ['<%= yeoman.app %>/index.html'], - ignorePath: '<%= yeoman.app %>/' - }, - sass: { - src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], - ignorePath: '<%= yeoman.app %>/bower_components/' + // bowerInstall: { + // app: { + // src: ['<%= yeoman.app %>/index.html'], + // ignorePath: '<%= yeoman.app %>/' + // }, + // sass: { + // src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], + // ignorePath: '<%= yeoman.app %>/bower_components/' + // } + // }, + wiredep: { + task: { + // Point to the files that should be updated when + // you run `grunt wiredep` + src: [ + '<%= yeoman.app %>/index.html', // .html support... + '<%= yeoman.app %>/styles/main.scss' // .scss & .sass support... + ], + + options: { + // See wiredep's configuration documentation for the options + // you may pass: + + // https://github.com/taptapship/wiredep#configuration + } } }, // Renames files for browser caching purposes @@ -285,7 +302,7 @@ module.exports = function (grunt) { }] } }, - + // Replace Google CDN references cdnify: { dist: { @@ -366,6 +383,9 @@ module.exports = function (grunt) { // }, // Test settings + /** + * The Karma configurations. + */ karma: { unit: { configFile: 'karma.conf.js', @@ -382,7 +402,8 @@ module.exports = function (grunt) { grunt.task.run([ 'clean:server', - 'bowerInstall', + // 'bowerInstall', + 'wiredep', 'concurrent:server', 'autoprefixer', 'connect:livereload', @@ -405,7 +426,7 @@ module.exports = function (grunt) { grunt.registerTask('build', [ 'clean:dist', - 'bowerInstall', + 'wiredep', 'useminPrepare', 'concurrent:dist', 'autoprefixer', @@ -427,3 +448,7 @@ module.exports = function (grunt) { 'build' ]); }; + + + + diff --git a/app/index.html b/app/index.html index 2c51056..509219d 100644 --- a/app/index.html +++ b/app/index.html @@ -8,12 +8,13 @@ 2048 + + - - + - - - + + + + + + + + + + + + + + + + - - - - - - - - + + + + - - - - + + + + + + + + + + + diff --git a/app/scripts/app.js b/app/scripts/app.js index e3a33a4..6100fd9 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -1,26 +1,60 @@ 'use strict'; angular -.module('twentyfourtyeightApp', ['Game', 'Grid', 'Keyboard', 'ngAnimate', 'ngCookies']) +.module('twentyfourtyeightApp', ['Game', 'Grid', 'Keyboard', 'Social', 'ngAnimate', 'ngCookies', 'ui.router']) .config(function(GridServiceProvider) { GridServiceProvider.setSize(4); }) -.controller('GameController', function(GameManager, KeyboardService) { +.config(function($stateProvider, $urlRouterProvider, $locationProvider) { + $stateProvider.state( 'app', { + url: '/?board', + controller: 'GameController' + }); - this.game = GameManager; + // For any unmatched url, + $urlRouterProvider.otherwise('/'); + // use the HTML5 History API so we can remove # (hashtag) from the URL + $locationProvider.html5Mode(true).hashPrefix('!'); +}) +.controller('GameController', function(GameManager, KeyboardService, $stateParams, $location) { + var vm = this; + + vm.game = GameManager; - this.newGame = function() { + // Internal function. It will not be exposed in Controller API + var init = function() { KeyboardService.init(); - this.game.newGame(); - this.startGame(); + // if board parameter is not empty + if ($stateParams.board && $stateParams.board.length > 0) { + vm.restoreGame(); + } else { + vm.newGame(); + } + vm.startGame(); }; - this.startGame = function() { - var self = this; + vm.newGame = function() { + vm.game.newGame(); + }; + + vm.startGame = function() { KeyboardService.on(function(key) { - self.game.move(key); + vm.game.move(key).then(function() {}); }); }; - this.newGame(); + vm.restoreGame = function() { + vm.game.restoreGame($stateParams.board); + }; + + vm.generateShareableLink = function() { + var url = $location.protocol() + '://' + $location.host() + ':' + $location.port(); + return url + '/?board=' + vm.game.encodeGame(); + }; + + vm.getShareableText = function() { + return 'My ' + vm.game.currentScore + ' scores at'; + }; + + init(); }); diff --git a/app/scripts/game/game.js b/app/scripts/game/game.js index 0f4a9a1..242fd48 100644 --- a/app/scripts/game/game.js +++ b/app/scripts/game/game.js @@ -1,7 +1,7 @@ 'use strict'; -angular.module('Game', ['Grid', 'ngCookies']) -.service('GameManager', function($q, $timeout, GridService, $cookieStore) { +angular.module('Game', ['Grid', 'ngCookies', 'LZString']) +.service('GameManager', function($q, $timeout, GridService, $cookieStore, LZString) { this.getHighScore = function() { return parseInt($cookieStore.get('highScore')) || 0; @@ -18,6 +18,8 @@ angular.module('Game', ['Grid', 'ngCookies']) this.win = false; this.currentScore = 0; this.highScore = this.getHighScore(); + // + this.encrypted = null; }; this.reinit(); @@ -27,6 +29,64 @@ angular.module('Game', ['Grid', 'ngCookies']) this.reinit(); }; + // Restore the game board with the data sent by the user via URL + this.restoreGame = function(gameEncrypted) { + GridService.buildEmptyGameBoard(); + if (gameEncrypted) { + // decode the data + var game = this.decodeGame(gameEncrypted); + + if (game) { + GridService.restoreGameBoard(game.tiles); + this.updateScore(game.currentScore); + return true; + } + } + // Couldn't restore, start in initial state + GridService.buildStartingPosition(); + return false; + }; + + this.encodeGame = function() { + // minify tiles object + var tiles = this.tiles.map(function(tile) { + return tile ? tile.value : null; + }); + var game = [this.currentScore, tiles]; + // encode game data + var encryptedGame = LZString.compressToEncodedURIComponent(JSON.stringify(game)); + return encryptedGame; + }; + + this.decodeGame = function(encryptedGame) { + var decompressed = LZString.decompressFromEncodedURIComponent(encryptedGame); + if (decompressed) { + var temp = JSON.parse(decompressed); + if (this.validateDecodedGame(temp)) { + var game = { + currentScore: temp[0], + tiles: temp[1] + }; + return game; + } + } + return null; + }; + + this.validateDecodedGame = function(game) { + if (game.length === 2 && game[1].length === 16) { + // Check if its in the right structure + for (var i = 0, l = game[1].length; i < l; i++) { + var item = game[1][i]; + if (Array.isArray(item)) { + return false; + } + } + return true; + } + return false; + }; + /* * The game loop * @@ -117,6 +177,7 @@ angular.module('Game', ['Grid', 'ngCookies']) this.updateScore = function(newScore) { this.currentScore = newScore; + if(this.currentScore > this.getHighScore()) { this.highScore = newScore; $cookieStore.put('highScore', newScore); diff --git a/app/scripts/grid/grid.js b/app/scripts/grid/grid.js index e4dd2be..d11d64a 100644 --- a/app/scripts/grid/grid.js +++ b/app/scripts/grid/grid.js @@ -7,7 +7,7 @@ angular.module('Grid', []) // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript var d = new Date().getTime(); var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = (d + Math.random()*16)%16 | 0; + var r = (d + Math.random()*16) %16 | 0; d = Math.floor(d/16); return (c === 'x' ? r : (r&0x7|0x8)).toString(16); }); @@ -91,10 +91,11 @@ angular.module('Grid', []) this.getSize = function() { return service.size; }; - - // Build game board + + // Build empty game board this.buildEmptyGameBoard = function() { var self = this; + // Initialize our grid for (var x = 0; x < service.size * service.size; x++) { this.grid[x] = null; @@ -105,6 +106,20 @@ angular.module('Grid', []) }); }; + // Restore game board from array of values + this.restoreGameBoard = function(tilesValues) { + var self = this, + i = 0; + this.forEach(function(x,y) { + var value = tilesValues[i]; + if (value) { + var tile = self.newTile({x:x,y:y}, value); + self.insertTile(tile); + } + i++; + }); + }; + /* * Prepare for traversal */ diff --git a/app/scripts/social/facebook/facebook_directive.html b/app/scripts/social/facebook/facebook_directive.html new file mode 100644 index 0000000..0e7abc0 --- /dev/null +++ b/app/scripts/social/facebook/facebook_directive.html @@ -0,0 +1 @@ +Facebook diff --git a/app/scripts/social/facebook/facebook_directive.js b/app/scripts/social/facebook/facebook_directive.js new file mode 100644 index 0000000..3469a42 --- /dev/null +++ b/app/scripts/social/facebook/facebook_directive.js @@ -0,0 +1,26 @@ +'use strict'; + +angular.module('Facebook', []) +.directive('facebookShare', function($window) { + function link(scope, element) { + element.bind('click', function() { + var urlString = 'https://www.facebook.com/sharer/sharer.php?'; + //default to the current page if a URL isn't specified + urlString += 'u=' + encodeURIComponent(scope.facebookUrl || $window.location.href); + + $window.open( + urlString, + 'Facebook', 'toolbar=0,status=0,resizable=yes,width=500,height=400' + + ',top=' + ($window.innerHeight - 400) / 2 + ',left=' + ($window.innerWidth - 500) / 2 + ); + }); + } + return { + restrict: 'A', + scope: { + facebookUrl: '=' + }, + link: link, + templateUrl: 'scripts/social/facebook/facebook_directive.html' + }; +}); \ No newline at end of file diff --git a/app/scripts/social/social.js b/app/scripts/social/social.js new file mode 100644 index 0000000..c53e9f0 --- /dev/null +++ b/app/scripts/social/social.js @@ -0,0 +1,3 @@ +'use strict'; + +angular.module('Social', ['Twitter', 'Facebook']); diff --git a/app/scripts/social/twitter/twitter_directive.html b/app/scripts/social/twitter/twitter_directive.html new file mode 100644 index 0000000..f6f96a5 --- /dev/null +++ b/app/scripts/social/twitter/twitter_directive.html @@ -0,0 +1 @@ +Twitter diff --git a/app/scripts/social/twitter/twitter_directive.js b/app/scripts/social/twitter/twitter_directive.js new file mode 100644 index 0000000..e3c98c5 --- /dev/null +++ b/app/scripts/social/twitter/twitter_directive.js @@ -0,0 +1,36 @@ +'use strict'; + +angular.module('Twitter', []) +.directive('twitterShare', function($window) { + function link(scope, element) { + element.bind('click', function() { + var urlString = 'https://www.twitter.com/intent/tweet?'; + //default to the current page if a URL isn't specified + urlString += 'url=' + encodeURIComponent(scope.twitterUrl || $window.location.href); + + if (scope.twitterText) { + urlString += '&text=' + encodeURIComponent(scope.twitterText); + } + + if (scope.twitterHashtags) { + urlString += '&hashtags=' + encodeURIComponent(scope.twitterHashtags); + } + + $window.open( + urlString, + 'Twitter', 'toolbar=0,status=0,resizable=yes,width=500,height=400' + + ',top=' + ($window.innerHeight - 400) / 2 + ',left=' + ($window.innerWidth - 500) / 2 + ); + }); + } + return { + restrict: 'A', + scope: { + twitterText: '=', + twitterUrl: '=', + twitterHashtags: '=' + }, + link: link, + templateUrl: 'scripts/social/twitter/twitter_directive.html' + }; +}); \ No newline at end of file diff --git a/app/styles/main.scss b/app/styles/main.scss index ccd7686..abae6b7 100644 --- a/app/styles/main.scss +++ b/app/styles/main.scss @@ -3,6 +3,7 @@ // endbower @import "_functions"; +@import "social.scss"; $width: 400px; $min-tile-count: 3; diff --git a/app/styles/social.scss b/app/styles/social.scss new file mode 100644 index 0000000..8779337 --- /dev/null +++ b/app/styles/social.scss @@ -0,0 +1,9 @@ +.social { + a { + text-decoration: none; + } + + img { + width: 40px; + } +} diff --git a/app/vendors/lz-string/lz-string.js b/app/vendors/lz-string/lz-string.js new file mode 100644 index 0000000..b9f6943 --- /dev/null +++ b/app/vendors/lz-string/lz-string.js @@ -0,0 +1,510 @@ +'use strict'; + +// Forked by @gustavo.mazzoni to generate an Angular version +// -- +// Copyright (c) 2013 Pieroxy +// This work is free. You can redistribute it and/or modify it +// under the terms of the WTFPL, Version 2 +// For more information see LICENSE.txt or http://www.wtfpl.net/ +// +// For more information, the home page: +// http://pieroxy.net/blog/pages/lz-string/testing.html +// +// LZ-based compression algorithm, version 1.4.4 +var LZString = (function() { + + // private property + var f = String.fromCharCode; + var keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"; + var baseReverseDic = {}; + + function getBaseValue(alphabet, character) { + if (!baseReverseDic[alphabet]) { + baseReverseDic[alphabet] = {}; + for (var i=0 ; i>> 8; + buf[i*2+1] = current_value % 256; + } + return buf; + }, + + //decompress from uint8array (UCS-2 big endian format) + decompressFromUint8Array:function (compressed) { + if (compressed===null || compressed===undefined){ + return LZString.decompress(compressed); + } else { + var buf=new Array(compressed.length/2); // 2 bytes per character + for (var i=0, TotalLen=buf.length; i> 1; + } + } else { + value = 1; + for (i=0 ; i> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + delete context_dictionaryToCreate[context_w]; + } else { + value = context_dictionary[context_w]; + for (i=0 ; i> 1; + } + + + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + // Add wc to the dictionary. + context_dictionary[context_wc] = context_dictSize++; + context_w = String(context_c); + } + } + + // Output the code for w. + if (context_w !== "") { + if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) { + if (context_w.charCodeAt(0)<256) { + for (i=0 ; i> 1; + } + } else { + value = 1; + for (i=0 ; i> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + delete context_dictionaryToCreate[context_w]; + } else { + value = context_dictionary[context_w]; + for (i=0 ; i> 1; + } + + + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + } + + // Mark the end of the stream + value = 2; + for (i=0 ; i> 1; + } + + // Flush the last char + while (true) { + context_data_val = (context_data_val << 1); + if (context_data_position == bitsPerChar-1) { + context_data.push(getCharFromInt(context_data_val)); + break; + } + else context_data_position++; + } + return context_data.join(''); + }, + + decompress: function (compressed) { + if (compressed == null) return ""; + if (compressed == "") return null; + return LZString._decompress(compressed.length, 32768, function(index) { return compressed.charCodeAt(index); }); + }, + + _decompress: function (length, resetValue, getNextValue) { + var dictionary = [], + next, + enlargeIn = 4, + dictSize = 4, + numBits = 3, + entry = "", + result = [], + i, + w, + bits, resb, maxpower, power, + c, + data = {val:getNextValue(0), position:resetValue, index:1}; + + for (i = 0; i < 3; i += 1) { + dictionary[i] = i; + } + + bits = 0; + maxpower = Math.pow(2,2); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + switch (next = bits) { + case 0: + bits = 0; + maxpower = Math.pow(2,8); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + case 1: + bits = 0; + maxpower = Math.pow(2,16); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + case 2: + return ""; + } + dictionary[3] = c; + w = c; + result.push(c); + while (true) { + if (data.index > length) { + return ""; + } + + bits = 0; + maxpower = Math.pow(2,numBits); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + switch (c = bits) { + case 0: + bits = 0; + maxpower = Math.pow(2,8); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + dictionary[dictSize++] = f(bits); + c = dictSize-1; + enlargeIn--; + break; + case 1: + bits = 0; + maxpower = Math.pow(2,16); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + dictionary[dictSize++] = f(bits); + c = dictSize-1; + enlargeIn--; + break; + case 2: + return result.join(''); + } + + if (enlargeIn == 0) { + enlargeIn = Math.pow(2, numBits); + numBits++; + } + + if (dictionary[c]) { + entry = dictionary[c]; + } else { + if (c === dictSize) { + entry = w + w.charAt(0); + } else { + return null; + } + } + result.push(entry); + + // Add w+entry[0] to the dictionary. + dictionary[dictSize++] = w + entry.charAt(0); + enlargeIn--; + + w = entry; + + if (enlargeIn == 0) { + enlargeIn = Math.pow(2, numBits); + numBits++; + } + + } + } + }; + return LZString; +})(); + +if (typeof define === 'function' && define.amd) { + define(function () { return LZString; }); +} else if( typeof module !== 'undefined' && module != null ) { + module.exports = LZString; +} else if( typeof angular !== 'undefined' && angular != null ) { + angular.module('LZString', []) + .factory('LZString', function () { + return LZString; + }); +} diff --git a/app/views/main.html b/app/views/main.html index 1317ccf..14cc544 100644 --- a/app/views/main.html +++ b/app/views/main.html @@ -23,6 +23,16 @@

ng-2048

+ +
@@ -31,6 +41,16 @@

ng-2048

+ +
diff --git a/bower.json b/bower.json index 1a81d59..326514b 100644 --- a/bower.json +++ b/bower.json @@ -2,17 +2,17 @@ "name": "twentyfourtyeight", "version": "0.0.0", "dependencies": { - "angular": "1.3.0-beta.4", + "angular": "~1.5.6", "json3": "~3.2.6", "es5-shim": "~2.1.0", "jquery": "~1.11.0", "bootstrap-sass-official": "~3.1.0", - "angular-route": "1.3.0-beta.4", - "angular-cookies": "1.3.0-beta.4", - "angular-animate": "1.3.0-beta.4" + "angular-cookies": "^1.5.7", + "angular-animate": "^1.5.7", + "angular-ui-router": "^0.3.1" }, "devDependencies": { - "angular-mocks": "1.2.15", - "angular-scenario": "1.2.15" + "angular-mocks": "~1.5.6", + "angular-scenario": "^1.5.7" } } diff --git a/karma.conf.js b/karma.conf.js index 34930ca..a65d29b 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -14,6 +14,7 @@ module.exports = function(config) { 'app/bower_components/angular/angular.js', 'app/bower_components/angular-mocks/angular-mocks.js', 'app/bower_components/angular-cookies/angular-cookies.js', + 'app/vendors/lz-string/lz-string.js', 'app/scripts/**/*.js', 'test/unit/**/*.js' ], diff --git a/package.json b/package.json index c2ad697..403ad33 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "dependencies": {}, "devDependencies": { "grunt": "~0.4.1", + "grunt-angular-templates": "~0.5.4", "grunt-autoprefixer": "~0.4.0", - "grunt-bower-install": "~1.0.0", "grunt-concurrent": "~0.5.0", "grunt-contrib-clean": "~0.5.0", "grunt-contrib-concat": "~0.3.0", @@ -18,18 +18,20 @@ "grunt-contrib-uglify": "~0.2.0", "grunt-contrib-watch": "~0.5.2", "grunt-google-cdn": "~0.2.0", + "grunt-karma": "^2.0.0", "grunt-newer": "~0.6.1", "grunt-ngmin": "~0.0.2", "grunt-rev": "~0.1.0", + "grunt-sass": "^1.2.0", "grunt-svgmin": "~0.2.0", "grunt-usemin": "~2.0.0", + "grunt-wiredep": "^3.0.1", "jshint-stylish": "~0.1.3", - "load-grunt-tasks": "~0.4.0", - "time-grunt": "~0.2.1", - "grunt-sass": "~0.12.1", + "karma": "^0.13.0", + "karma-chrome-launcher": "^1.0.1", "karma-jasmine": "~0.1.5", - "karma-chrome-launcher": "~0.1.3", - "grunt-angular-templates": "~0.5.4" + "load-grunt-tasks": "~0.4.0", + "time-grunt": "~0.2.1" }, "engines": { "node": ">=0.10.0" diff --git a/test/unit/game/game_spec.js b/test/unit/game/game_spec.js index 1f1f297..6d175af 100644 --- a/test/unit/game/game_spec.js +++ b/test/unit/game/game_spec.js @@ -1,9 +1,14 @@ describe('Game', function() { describe('GameManager', function() { + + beforeEach(function () { + //mock the LZString lib: + angular.module("LZString", []); + }); beforeEach(module('Game')); - - var gameManager, _cookieStore, _gridService, provide, $rootScope; + + var gameManager, _cookieStore, _gridService, provide, $rootScope, _lzString; beforeEach(module(function($provide) { _cookieStore = { @@ -20,8 +25,13 @@ describe('Game', function() { tileMatchesAvailable: angular.noop, getSize: function() { return 4; } }; + _lzString = { + compressToEncodedURIComponent: function() { return 'NoFgbATANMB2CuAbRUHNUlaXGgBigEYBdG'; }, + decompressFromEncodedURIComponent: function(param) {} + }; $provide.value('$cookieStore', _cookieStore); $provide.value('GridService', _gridService); + $provide.value('LZString', _lzString); provide = $provide; })); @@ -69,6 +79,54 @@ describe('Game', function() { }); }); + describe('.encodeGame', function() { + it('should call LZString to compress and encode tiles and currentScore data and return encoded result', function() { + var game = { + tiles: [ + {id:1,value:4,x:0,y:3}, + {id:2,value:8,x:2,y:2} + ], + currentScore: 12 + }; + gameManager.tiles = game.tiles; + gameManager.currentScore = game.currentScore; + spyOn(_lzString, 'compressToEncodedURIComponent').andCallThrough(); + var result = gameManager.encodeGame(); + expect(_lzString.compressToEncodedURIComponent).toHaveBeenCalled(); + expect(result).toBeTruthy(); + }); + }); + + describe('.decodeGame', function() { + it('should call LZString to decompress and decode argument received', function() { + var param = 'NoFgbATANMB2CuAbRUHNUlaXGgBigEYBdG'; + spyOn(_lzString, 'decompressFromEncodedURIComponent').andCallThrough(); + gameManager.decodeGame(param); + expect(_lzString.decompressFromEncodedURIComponent).toHaveBeenCalledWith(param); + }); + }); + + describe('.restoreGame', function() { + it('should call GridService and decodeGame with argument received', function() { + var param = 'NoFgbATANMB2CuAbRUHNUlaXGgBigEYBdG'; + spyOn(_gridService, 'buildEmptyGameBoard').andCallThrough(); + spyOn(gameManager, 'decodeGame').andCallThrough(); + gameManager.restoreGame(param); + expect(_gridService.buildEmptyGameBoard).toHaveBeenCalled(); + expect(gameManager.decodeGame).toHaveBeenCalledWith(param); + }); + }); + + describe('.restoreGame', function() { + it('should call GridService buildStartingPosition and return false if argument is null', function() { + var param = null; + spyOn(_gridService, 'buildStartingPosition').andCallThrough(); + var result = gameManager.restoreGame(param); + expect(_gridService.buildStartingPosition).toHaveBeenCalled(); + expect(result).toBeFalsy(); + }); + }); + describe('.move', function() { it('should return false if the user has already won the game', function() { gameManager.win = true; diff --git a/test/unit/keyboard/keyboard_spec.js b/test/unit/keyboard/keyboard_spec.js new file mode 100644 index 0000000..37df978 --- /dev/null +++ b/test/unit/keyboard/keyboard_spec.js @@ -0,0 +1,42 @@ +describe('Keyboard', function() { + + beforeEach(module('Keyboard')); + + describe('KeyboardService', function() { + + var keyboardService, _document, _event; + beforeEach( inject( function(_KeyboardService_, $document) { + keyboardService = _KeyboardService_; + _document = $document; + })); + + describe('.init', function() { + + beforeEach(function() { + // create keyboard event + _event = new window.KeyboardEvent('keydown', { + bubbles: true, + cancelable: true, + shiftKey: true + }); + + delete _event.which; + // define property 'which' with UP keyboard code + Object.defineProperty(_event, 'which', {'value': 38}); + }); + it('should bind keydown event listener to document DOM', function() { + // call method to be tested + keyboardService.init(); + // spy on a follow up method + spyOn(keyboardService, '_handleKeyEvent').andCallThrough(); + // dispatch the event + _document[0].dispatchEvent(_event); + // check if it was called + expect(keyboardService._handleKeyEvent).toHaveBeenCalled(); + }); + + }); + + }); + +}); \ No newline at end of file