((function ( $ ) { "use strict"; $.widget('aerolab.blockrain', { options: { autoplay: false, // Let a bot play the game autoplayRestart: true, // Restart the game automatically once a bot loses showFieldOnStart: true, // Show a bunch of random blocks on the start screen (it looks nice) theme: null, // The theme name or a theme object blockWidth: 10, // How many blocks wide the field is (The standard is 10 blocks) autoBlockWidth: false, // The blockWidth is dinamically calculated based on the autoBlockSize. Disabled blockWidth. Useful for responsive backgrounds autoBlockSize: 24, // The max size of a block for autowidth mode difficulty: 'normal', // Difficulty (normal|nice|evil). speed: 20, // The speed of the game. The higher, the faster the pieces go. asdwKeys: true, // Enable ASDW keys //Highest score highestScore: 0, // Copy playButtonText: 'Play', gameOverText: 'Game Over', restartButtonText: 'Play Again', scoreText: 'Score', highscoreText: 'High Score', // High score text // Basic Callbacks onStart: function(){}, onRestart: function(){}, onGameOver: function(score){}, // When a block is placed onPlaced: function(){}, // When a line is made. Returns the number of lines, score assigned and total score onLine: function(lines, scoreIncrement, score){} }, /** * Start/Restart Game */ start: function() { this._doStart(); this.options.onStart.call(this.element); }, restart: function() { this.options.onRestart.call(this.element); this._doStart(); }, gameover: function() { this.showGameOverMessage(); this._board.gameover = true; this.options.onGameOver.call(this.element, this._filled.score); }, _doStart: function() { this._filled.clearAll(); this._filled._resetScore(); this._filled._showHighScore(); // shows high score at the beginning of game. Initial high score is 0 this._board.cur = this._board.nextShape(); this._board.started = true; this._board.gameover = false; this._board.dropDelay = 5; this._board.render(true); this._board.animate(); this._$start.fadeOut(150); this._$gameover.fadeOut(150); this._$score.fadeIn(150); this._$highscore.fadeIn(150); }, pause: function() { this._board.paused = true; }, resume: function() { this._board.paused = false; }, autoplay: function(enable) { if( typeof enable !== 'boolean' ){ enable = true; } // On autoplay, start the game right away this.options.autoplay = enable; if( enable && ! this._board.started ) { this._doStart(); } this._setupControls( ! enable ); this._setupTouchControls( ! enable ); }, controls: function(enable) { if( typeof enable !== 'boolean' ){ enable = true; } this._setupControls(enable); }, touchControls: function(enable) { if( typeof enable !== 'boolean' ){ enable = true; } this._setupTouchControls(enable); }, score: function(newScore) { if( typeof newScore !== 'undefined' && parseInt(newScore) >= 0 ) { this._filled.score = parseInt(newScore); this._$scoreText.text(this._filled_score); } return this._filled.score; }, highscore: function() { this._filled.highscore = parseInt(game.options.highestScore); this._$highscoreText.text(this._filled_highscore); return this._filled.highscore; }, freesquares: function() { return this._filled.getFreeSpaces(); }, showStartMessage: function() { this._$start.show(); }, showGameOverMessage: function() { this._$gameover.show(); }, /** * Update the sizes of the renderer (this makes the game responsive) */ updateSizes: function() { this._PIXEL_WIDTH = this.element.innerWidth(); this._PIXEL_HEIGHT = this.element.innerHeight(); this._BLOCK_WIDTH = this.options.blockWidth; this._BLOCK_HEIGHT = Math.floor(this.element.innerHeight() / this.element.innerWidth() * this._BLOCK_WIDTH); this._block_size = Math.floor(this._PIXEL_WIDTH / this._BLOCK_WIDTH); this._border_width = 2; // Recalculate the pixel width and height so the canvas always has the best possible size this._PIXEL_WIDTH = this._block_size * this._BLOCK_WIDTH; this._PIXEL_HEIGHT = this._block_size * this._BLOCK_HEIGHT; this._$canvas .attr('width', this._PIXEL_WIDTH) .attr('height', this._PIXEL_HEIGHT); }, theme: function(newTheme){ if( typeof newTheme === 'undefined' ) { return this.options.theme || this._theme; } // Setup the theme properly if( typeof newTheme === 'string' ) { this.options.theme = newTheme; this._theme = $.extend(true, {}, BlockrainThemes[newTheme]); } else { this.options.theme = null; this._theme = newTheme; } if( typeof this._theme === 'undefined' || this._theme === null ) { this._theme = $.extend(true, {}, BlockrainThemes['retro']); this.options.theme = 'retro'; } if( isNaN(parseInt(this._theme.strokeWidth)) || typeof parseInt(this._theme.strokeWidth) !== 'number' ) { this._theme.strokeWidth = 2; } // Load the image assets this._preloadThemeAssets(); if( this._board !== null ) { if( typeof this._theme.background === 'string' ) { this._$canvas.css('background-color', this._theme.background); } this._board.render(); } }, // Theme _theme: { }, // UI Elements _$game: null, _$canvas: null, _$gameholder: null, _$start: null, _$gameover: null, _$score: null, _$scoreText: null, _$highscore: null, _$highscoreText: null, // Canvas _canvas: null, _ctx: null, // Initialization _create: function() { var game = this; this.theme(this.options.theme); this._createHolder(); this._createUI(); this._refreshBlockSizes(); this.updateSizes(); $(window).resize(function(){ //game.updateSizes(); }); this._SetupShapeFactory(); this._SetupFilled(); this._SetupInfo(); this._SetupBoard(); this._info.init(); this._board.init(); var renderLoop = function(){ requestAnimationFrame(renderLoop); game._board.render(); }; renderLoop(); if( this.options.autoplay ) { this.autoplay(true); this._setupTouchControls(false); } else { this._setupControls(true); this._setupTouchControls(false); } }, _checkCollisions: function(x, y, blocks, checkDownOnly) { // x & y should be aspirational values var i = 0, len = blocks.length, a, b; for (; i= this._BLOCK_HEIGHT || this._filled.check(a, b)) { return true; } else if (!checkDownOnly && a < 0 || a >= this._BLOCK_WIDTH) { return true; } } return false; }, _board: null, _info: null, _filled: null, /** * Draws the background */ _drawBackground: function() { if( typeof this._theme.background !== 'string' ) { return; } if( this._theme.backgroundGrid instanceof Image ) { // Not loaded if( this._theme.backgroundGrid.width === 0 || this._theme.backgroundGrid.height === 0 ){ return; } this._ctx.globalAlpha = 1.0; for( var x=0; x maxx) { maxx = blocks[i]; } if (blocks[i+1] < miny) { miny = blocks[i+1]; } if (blocks[i+1] > maxy) { maxy = blocks[i+1]; } } return { left: minx, right: maxx, top: miny, bottom: maxy, width: maxx - minx, height: maxy - miny }; } }); return this.init(); }; this._shapeFactory = { line: function() { return new Shape(game, game._shapes.line, false, 'line'); }, square: function() { return new Shape(game, game._shapes.square, false, 'square'); }, arrow: function() { return new Shape(game, game._shapes.arrow, false, 'arrow'); }, leftHook: function() { return new Shape(game, game._shapes.leftHook, false, 'leftHook'); }, rightHook: function() { return new Shape(game, game._shapes.rightHook, false, 'rightHook'); }, leftZag: function() { return new Shape(game, game._shapes.leftZag, false, 'leftZag'); }, rightZag: function() { return new Shape(game, game._shapes.rightZag, false, 'rightZag'); } }; }, _SetupFilled: function() { var game = this; if( this._filled !== null ){ return; } this._filled = { data: new Array(game._BLOCK_WIDTH * game._BLOCK_HEIGHT), score: 0, highscore: game.options.highestScore, toClear: {}, check: function(x, y) { return this.data[this.asIndex(x, y)]; }, add: function(x, y, blockType, blockVariation, blockIndex, blockOrientation) { if (x >= 0 && x < game._BLOCK_WIDTH && y >= 0 && y < game._BLOCK_HEIGHT) { this.data[this.asIndex(x, y)] = { blockType: blockType, blockVariation: blockVariation, blockIndex: blockIndex, blockOrientation: blockOrientation }; } }, getFreeSpaces: function() { var count = 0; for( var i=0; i=0; i--) { this.data[i] = (i >= game._BLOCK_WIDTH ? this.data[i-game._BLOCK_WIDTH] : undefined); } }, checkForClears: function() { var startLines = game._board.lines; var rows = [], i, len, count, mod; for (i=0, len=this.data.length; i 1 ) { game._board.dropDelay *= 0.9; } } var clearedLines = game._board.lines - startLines; this._updateScore(clearedLines); //Updates high score when score is more than the current high score if (this.score > this.highscore) { this._updateHighScore(); } }, _updateScore: function(numLines) { if( numLines <= 0 ) { return; } var scores = [0,400,1000,3000,12000]; if( numLines >= scores.length ){ numLines = scores.length-1 } this.score += scores[numLines]; game._$scoreText.text(this.score); game.options.onLine.call(game.element, numLines, scores[numLines], this.score); }, _resetScore: function() { this.score = 0; game._$scoreText.text(this.score); }, _showHighScore: function() { game._$highscoreText.text(this.highscore); }, _updateHighScore: function() { this.highscore = this.score; game._$highscoreText.text(this.highscore); }, draw: function() { for (var i=0, len=this.data.length, row, color; i= this.dropDelay) || (game.options.autoplay) || (this.holding.drop && (now - this.holding.drop) >= this.holdingThreshold) ) { drop = true; moved = true; this.dropCount = 0; } // Move Left by holding if( this.holding.left && (now - this.holding.left) >= this.holdingThreshold ) { moved = true; this.cur.moveLeft(); } // Move Right by holding if( this.holding.right && (now - this.holding.right) >= this.holdingThreshold ) { moved = true; this.cur.moveRight(); } // Test for a collision, add the piece to the filled blocks and fetch the next one if (drop) { var cur = this.cur, x = cur.x, y = cur.y, blocks = cur.getBlocks(); if (game._checkCollisions(x, y+1, blocks, true)) { drop = false; var blockIndex = 0; for (var i=0; i 0) { return blockTheme[0]; } else { return null; } } else { return blockTheme; } } if( typeof falling !== 'boolean' ){ falling = true; } if( falling ) { if( typeof game._theme.primary === 'string' && game._theme.primary !== '' ) { return game._theme.primary; } else if( typeof game._theme.blocks !== 'undefined' && game._theme.blocks !== null ) { return getBlockVariation(game._theme.blocks[blockType], blockVariation); } else { return getBlockVariation(game._theme.complexBlocks[blockType], blockVariation); } } else { if( typeof game._theme.secondary === 'string' && game._theme.secondary !== '' ) { return game._theme.secondary; } else if( typeof game._theme.blocks !== 'undefined' && game._theme.blocks !== null ) { return getBlockVariation(game._theme.blocks[blockType], blockVariation); } else { return getBlockVariation(game._theme.complexBlocks[blockType], blockVariation); } } } }; game._niceShapes = game._getNiceShapes(); }, // Utility Functions _randInt: function(a, b) { return a + Math.floor(Math.random() * (1 + b - a)); }, _randSign: function() { return this._randInt(0, 1) * 2 - 1; }, _randChoice: function(choices) { return choices[this._randInt(0, choices.length-1)]; }, /** * Find base64 encoded images and load them as image objects, which can be used by the canvas renderer */ _preloadThemeAssets: function() { var game = this; var hexColorcheck = new RegExp('^#[A-F0-9+]{3,6}', 'i'); var base64check = new RegExp('^data:image/(png|gif|jpg);base64,', 'i'); var handleAssetLoad = function() { // Rerender the board as soon as an asset loads if( game._board ) { game._board.render(true); } }; var loadAsset = function(src) { var plainSrc = src; if( ! hexColorcheck.test( plainSrc ) ) { // It's an image src = new Image(); src.src = plainSrc; src.onload = handleAssetLoad; } else { // It's a color src = plainSrc; } return src; }; var startAssetLoad = function(block) { // Assets can be an array of variation so they can change color/design randomly if( $.isArray(block) && block.length > 0 ) { for( var i=0; i'); this._$gameholder.css('position', 'relative').css('width', '100%').css('height', '100%'); this.element.html('').append(this._$gameholder); // Create the game canvas and context this._$canvas = $(''); if( typeof this._theme.background === 'string' ) { this._$canvas.css('background-color', this._theme.background); } this._$gameholder.append(this._$canvas); this._canvas = this._$canvas.get(0); this._ctx = this._canvas.getContext('2d'); }, _createUI: function() { var game = this; // Score game._$score = $( '
'+ '
'+ '
'+ this.options.scoreText +'
'+ '
0
'+ '
'+ '
').hide(); game._$scoreText = game._$score.find('.blockrain-score-num'); game._$gameholder.append(game._$score); // High Score game._$highscore = $( '
'+ '
'+ '
'+ this.options.highscoreText +'
'+ '
0
'+ '
'+ '
').hide(); game._$highscoreText = game._$highscore.find('.blockrain-Hscore-num'); game._$gameholder.append(game._$highscore); // Create the start menu game._$start = $( '').hide(); game._$gameholder.append(game._$start); game._$start.find('.blockrain-start-btn').click(function(event){ event.preventDefault(); game.start(); }); // Create the game over menu game._$gameover = $( '
'+ '
'+ '
'+ this.options.gameOverText +'
'+ ''+ this.options.restartButtonText +''+ '
'+ '
').hide(); game._$gameover.find('.blockrain-game-over-btn').click(function(event){ event.preventDefault(); game.restart(); }); game._$gameholder.append(game._$gameover); this._createControls(); }, _createControls: function() { var game = this; game._$touchLeft = $('').appendTo(game._$gameholder); game._$touchRight = $('').appendTo(game._$gameholder); game._$touchRotateRight = $('').appendTo(game._$gameholder); game._$touchRotateLeft = $('').appendTo(game._$gameholder); game._$touchDrop = $('').appendTo(game._$gameholder); }, _refreshBlockSizes: function() { if( this.options.autoBlockWidth ) { this.options.blockWidth = Math.ceil( this.element.width() / this.options.autoBlockSize ); } }, _getNiceShapes: function() { /* * Things I need for this to work... * - ability to test each shape with this._filled data * - maybe give empty spots scores? and try to maximize the score? */ var game = this; var shapes = {}, attr; for( var attr in this._shapeFactory ) { shapes[attr] = this._shapeFactory[attr](); } function scoreBlocks(possibles, blocks, x, y, filled, width, height) { var i, len=blocks.length, score=0, bottoms = {}, tx, ty, overlaps; // base score for (i=0; i best_score_for_shape) { best_score_for_shape = score; best_orientation_for_shape = i; best_x_for_shape = x; } break; } } } } if ((evil && best_score_for_shape < best_score) || (!evil && best_score_for_shape > best_score)) { best_shape = shape; best_score = best_score_for_shape; best_orientation = best_orientation_for_shape; best_x = best_x_for_shape; } } best_shape.best_orientation = best_orientation; best_shape.best_x = best_x; return best_shape; }; func.no_preview = true; return func; }, _randomShapes: function() { // Todo: The shapefuncs should be cached. var shapeFuncs = []; $.each(this._shapeFactory, function(k,v) { shapeFuncs.push(v); }); return this._randChoice(shapeFuncs); }, /** * Controls */ _setupControls: function(enable) { var game = this; var moveLeft = function(start) { if( ! start ) { game._board.holding.left = null; return; } if( ! game._board.holding.left ) { game._board.cur.moveLeft(); game._board.holding.left = Date.now(); game._board.holding.right = null; } } var moveRight = function(start) { if( ! start ) { game._board.holding.right = null; return; } if( ! game._board.holding.right ) { game._board.cur.moveRight(); game._board.holding.right = Date.now(); game._board.holding.left = null; } } var drop = function(start) { if( ! start ) { game._board.holding.drop = null; return; } if( ! game._board.holding.drop ) { game._board.cur.drop(); game._board.holding.drop = Date.now(); } } var rotateLeft = function() { game._board.cur.rotate('left'); } var rotateRight = function() { game._board.cur.rotate('right'); } // Handlers: These are used to be able to bind/unbind controls var handleKeyDown = function(evt) { if( ! game._board.cur ) { return true; } var caught = false; caught = true; if (game.options.asdwKeys) { switch(evt.keyCode) { case 65: /*a*/ moveLeft(true); break; case 68: /*d*/ moveRight(true); break; case 83: /*s*/ drop(true); break; case 87: /*w*/ game._board.cur.rotate('right'); break; } } switch(evt.keyCode) { case 37: /*left*/ moveLeft(true); break; case 39: /*right*/ moveRight(true); break; case 40: /*down*/ drop(true); break; case 38: /*up*/ game._board.cur.rotate('right'); break; case 88: /*x*/ game._board.cur.rotate('right'); break; case 90: /*z*/ game._board.cur.rotate('left'); break; default: caught = false; } if (caught) evt.preventDefault(); return !caught; }; var handleKeyUp = function(evt) { if( ! game._board.cur ) { return true; } var caught = false; caught = true; if (game.options.asdwKeys) { switch(evt.keyCode) { case 65: /*a*/ moveLeft(false); break; case 68: /*d*/ moveRight(false); break; case 83: /*s*/ drop(false); break; } } switch(evt.keyCode) { case 37: /*left*/ moveLeft(false); break; case 39: /*right*/ moveRight(false); break; case 40: /*down*/ drop(false); break; default: caught = false; } if (caught) evt.preventDefault(); return !caught; }; function isStopKey(evt) { var cfg = { stopKeys: {37:1, 38:1, 39:1, 40:1} }; var isStop = (cfg.stopKeys[evt.keyCode] || (cfg.moreStopKeys && cfg.moreStopKeys[evt.keyCode])); if (isStop) evt.preventDefault(); return isStop; } function getKey(evt) { return 'safekeypress.' + evt.keyCode; } function keydown(evt) { var key = getKey(evt); $.data(this, key, ($.data(this, key) || 0) - 1); return handleKeyDown.call(this, evt); } function keyup(evt) { $.data(this, getKey(evt), 0); handleKeyUp.call(this, evt); return isStopKey(evt); } // Unbind everything by default // Use event namespacing so we don't ruin other keypress events $(document) .unbind('keydown.blockrain') .unbind('keyup.blockrain'); if( ! game.options.autoplay ) { if( enable ) { $(document) .bind('keydown.blockrain', keydown) .bind('keyup.blockrain', keyup); } } }, _setupTouchControls: function(enable) { var game = this; // Movements can be held for faster movement var moveLeft = function(event){ event.preventDefault(); game._board.cur.moveLeft(); game._board.holding.left = Date.now(); game._board.holding.right = null; game._board.holding.drop = null; }; var moveRight = function(event){ event.preventDefault(); game._board.cur.moveRight(); game._board.holding.right = Date.now(); game._board.holding.left = null; game._board.holding.drop = null; }; var drop = function(event){ event.preventDefault(); game._board.cur.drop(); game._board.holding.drop = Date.now(); }; var endMoveLeft = function(event){ event.preventDefault(); game._board.holding.left = null; }; var endMoveRight = function(event){ event.preventDefault(); game._board.holding.right = null; }; var endDrop = function(event){ event.preventDefault(); game._board.holding.drop = null; }; // Rotations can't be held var rotateLeft = function(event){ event.preventDefault(); game._board.cur.rotate('left'); }; var rotateRight = function(event){ event.preventDefault(); game._board.cur.rotate('right'); }; // Unbind everything by default game._$touchLeft.unbind('touchstart touchend click'); game._$touchRight.unbind('touchstart touchend click'); game._$touchRotateLeft.unbind('touchstart touchend click'); game._$touchRotateRight.unbind('touchstart touchend click'); game._$touchDrop.unbind('touchstart touchend click'); if( ! game.options.autoplay && enable ) { game._$touchLeft.show().bind('touchstart click', moveLeft).bind('touchend', endMoveLeft); game._$touchRight.show().bind('touchstart click', moveRight).bind('touchend', endMoveRight); game._$touchDrop.show().bind('touchstart click', drop).bind('touchend', endDrop); game._$touchRotateLeft.show().bind('touchstart click', rotateLeft); game._$touchRotateRight.show().bind('touchstart click', rotateRight); } else { game._$touchLeft.hide(); game._$touchRight.hide(); game._$touchRotateLeft.hide(); game._$touchRotateRight.hide(); game._$touchDrop.hide(); } $("#left-arrow").bind('touchstart click', function(event) {moveLeft(event); endMoveLeft(event)}); $("#right-arrow").bind('touchstart click', function(event) {moveRight(event); endMoveRight(event)}); $("#up-arrow").bind('touchstart click', rotateRight); $("#down-arrow").bind('touchstart click', function(event) {drop(event); endDrop(event)}); } }); })(jQuery));