((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 // Copy playText: 'Let\'s play some Tetris', playButtonText: 'Play', gameOverText: 'Game Over', restartButtonText: 'Play Again', scoreText: 'Score', // Basic Callbacks onStart: function(){}, onRestart: function(){}, onGameOver: function(score){}, // 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._doStart(); this.options.onRestart.call(this.element); }, 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._board.cur = this._board.nextShape(); this._board.started = true; this._board.gameover = false; this._board.animate(); this._$start.fadeOut(150); this._$gameover.fadeOut(150); this._$score.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 ); }, controls: function(enable) { if( typeof enable !== 'boolean' ){ enable = true; } this._setupControls(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; }, 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 = BlockrainThemes[newTheme]; } else { this.options.theme = null; this._theme = newTheme; } if( typeof this._theme === 'undefined' || this._theme === null ) { this._theme = 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, // 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(); if( this.options.autoplay ) { this.autoplay(true); } else { this._setupControls(true); } }, _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() { /* * X X * O XOXX O XOXX * X X * X X */ var ver = [0, -1, 0, -2, 0, -3, 0, -4], hor = [-1, -2, 0, -2, 1, -2, 2, -2]; return new Shape(game, [ver, hor, ver, hor], true, 'line'); }, square: function() { /* * XX * XX */ var s = [0, 0, 1, 0, 0, -1, 1, -1]; return new Shape(game, [s, s, s, s], true, 'square'); }, arrow: function() { /* * X X X * XOX OX XOX XO * X X X */ return new Shape(game, [ [0, -1, 1, -1, 2, -1, 1, -2], [1, -2, 1, -1, 1, 0, 2, -1], [0, -1, 1, -1, 2, -1, 1, 0], [0, -1, 1, -1, 1, -2, 1, 0] ], false, 'arrow'); }, rightHook: function() { /* * XX X X * XOX O XOX O * X X XX */ return new Shape(game, [ [0, 0, 0, -1, 1, -1, 2, -1], [0, -2, 1, 0, 1, -1, 1, -2], [0, -1, 1, -1, 2, -1, 2, -2], [0, -2, 0, -1, 0, 0, 1, 0] ], false, 'rightHook'); }, leftHook: function() { /* * X X XX * XOX O XOX O * X XX X */ return new Shape(game, [ [2, 0, 0, -1, 1, -1, 2, -1], [0, 0, 1, 0, 1, -1, 1, -2], [0, -2, 0, -1, 1, -1, 2, -1], [0, 0, 0, -1, 0, -2, 1, -2] ], false, 'leftHook'); }, leftZag: function() { /* * X * XO OX * XX X */ var ver = [0, 0, 0, -1, 1, -1, 1, -2], hor = [0, -1, 1, -1, 1, 0, 2, 0]; return new Shape(game, [hor, ver, hor, ver], true, 'leftZag'); }, rightZag: function() { /* * X * OX OX * XX X */ var ver = [0, -2, 0, -1, 1, -1, 1, 0], hor = [0, 0, 1, 0, 1, -1, 2, -1]; return new Shape(game, [hor, ver, hor, ver], true, 'rightZag'); } }; }, _SetupFilled: function() { var game = this; if( this._filled !== null ){ return; } this._filled = { data: new Array(game._BLOCK_WIDTH * game._BLOCK_HEIGHT), score: 0, toClear: {}, check: function(x, y) { return this.data[this.asIndex(x, y)]; }, add: function(x, y, blockType) { if (x >= 0 && x < game._BLOCK_WIDTH && y >= 0 && y < game._BLOCK_HEIGHT) { this.data[this.asIndex(x, y)] = blockType; } }, asIndex: function(x, y) { return x + y*game._BLOCK_WIDTH; }, asX: function(index) { return index % game._BLOCK_WIDTH; }, asY: function(index) { return Math.floor(index / game._BLOCK_WIDTH); }, clearAll: function() { delete this.data; this.data = new Array(game._BLOCK_WIDTH * game._BLOCK_HEIGHT); }, _popRow: function(row_to_pop) { for (var i=game._BLOCK_WIDTH*(row_to_pop+1) - 1; 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 ) { //board.dropDelay -= 2; } } var clearedLines = game._board.lines - startLines; this._updateScore(clearedLines); }, _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); }, draw: function() { for (var i=0, len=this.data.length, row, color; i= this.dropDelay || game.options.autoplay ) { drop = true; this.dropCount = 0; } // test for a collision 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; 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); // Create the start menu game._$start = $( '
'+ '
'+ '
'+ this.options.playText +'
'+ ''+ this.options.playButtonText +''+ '
'+ '
').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); }, _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; // Handlers: These are used to be able to bind/unbind controls var handleKeyPress = function(evt) { var caught = false; if (game._board.cur) { caught = true; if (game.options.asdwKeys) { switch(evt.keyCode) { case 65: /*a*/ game._board.cur.moveLeft(); break; case 87: /*w*/ game._board.cur.rotate(true); break; case 68: /*d*/ game._board.cur.moveRight(); break; case 83: /*s*/ game._board.dropCount = game._board.dropDelay; break; } } switch(evt.keyCode) { case 37: /*left*/ game._board.cur.moveLeft(); break; case 38: /*up*/ game._board.cur.rotate(true); break; case 39: /*right*/ game._board.cur.moveRight(); break; case 40: /*down*/ game._board.dropCount = game._board.dropDelay; break; case 88: /*x*/ game._board.cur.rotate(true); break; case 90: /*z*/ game._board.cur.rotate(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 keypress(evt) { var key = getKey(evt), val = ($.data(this, key) || 0) + 1; $.data(this, key, val); if (val > 0) return handleKeyPress.call(this, evt); return isStopKey(evt); } function keydown(evt) { var key = getKey(evt); $.data(this, key, ($.data(this, key) || 0) - 1); return handleKeyPress.call(this, evt); } function keyup(evt) { $.data(this, getKey(evt), 0); return isStopKey(evt); } // Unbind everything by default // Use event namespacing so we don't ruin other keypress events $(document).unbind('keypress.blockrain').unbind('keydown.blockrain').unbind('keyup.blockrain'); if( ! game.options.autoplay ) { if( enable ) { $(document).bind('keypress.blockrain', keypress).bind('keydown.blockrain', keydown).bind('keyup.blockrain', keyup); } } } }); })(jQuery));