Sunday 26 September 2010

Tetris - JavaScript - PART 3 - Glueing the parts together

Here is the game, glue together.

TODO:
  • Add the final two L shape
  • End Game Condition
  • Scoring
  • Put in some pretty graphics
I'm going to stop here, I figure out how to make the game lighting faster in any browser.

Ways to make it faster
  • Instead of creating new object and moving div around, we could initialize the game with all the object it need and use class to activate the object.
  • Some browser are hyperactive and don't delegate their display process and it will slow down the game when there are many object manipulation. This can be over come with grouping of object. Instead of moving the block one by one, we could group them into a row and move the rows.

hope this little game inspire or give some one a cool idea.

Cheers,

Below are the actual game, and it should be running on your browser right now.








/**
 * A little game of falling block, the goal are to score point by
 * controlling the falling block in a way to place them neatly on the bottom
 * of the screen. When the block are filled in contiguously from left to right
 * on either row, score will be earn.
 *
 * @author Do Quoc Cong
 */
Tetris = function(p_holder, p_blockSize, p_xSize, p_ySize) {
    //We don't need multiple instance
    if (window._Tetris != undefined) {
        return window._Tetris;
    }

    window._Tetris = this;
    this.m_keyCode = {
        40 : 'down',
        38 : 'up',
        37 : 'left',
        39 : 'right'
    };
    this.m_holder = p_holder;
    this.m_action = null;
    this.m_blockSize = p_blockSize ? p_blockSize: 32;
    this.m_xSize = p_xSize ? p_xSize: 20;
    this.m_ySize = p_ySize ? p_ySize: 10;
    this.m_grid = Array(p_xSize);
    this.m_falling = Array();
    this.m_timeOut = null;
    this.m_fallingStatue = 0;
    this.m_fallingShape = 0,
    this.m_shape = [
            [[0,0],[0,1],[1,0],[1,1]], // Square
            [[0,0],[1,0],[2,0],[3,0]], // Long Stick
            [[0,0],[1,0],[1,1],[2,1]], // \
            [[0,1],[1,1],[1,0],[2,0]], // /
            [[0,1],[1,0],[1,1],[1,2]]  // Spike shape
        ];
    
    this.m_shapeTranspormation = [
        [],
        [
            [[1,-1],[0,0],[-1,1],[-2,2]],
            [[-1,1],[0,0],[1,-1],[2,-2]]
        ],
        [
            [[2,0],[0,2],[0,0],[0,0]],
            [[-2,0],[0,-2],[0,0],[0,0]]
        ],
        [
            [[2,0],[0,-2],[0,0],[0,0]],
            [[-2,0],[0,2],[0,0],[0,0]]
        ],
        [
            [[1,-1],[1,1],[0,0],[-1,-1]],
            [[1,1],[-1,1],[0,0],[1,-1]],
            [[-1,1],[-1,-1],[0,0],[1,1]],
            [[-1,-1],[1,-1],[0,0],[-1,1]]
        ]
    ];
    $(p_holder).css('width', this.m_blockSize * this.m_ySize + 1 );
    $(p_holder).css('height', this.m_blockSize * this.m_xSize + 1 );

    for (var i = 0; i < p_xSize; i++) {
        this.m_grid[i] = Array(p_ySize);
    }

    // Bind window event to tetris
    $(window).keydown(function(event) {
        return Tetris().keyDown(event);
    });

    // Bind window event to tetris
    $(window).keyup(function(event) {
        return Tetris().keyUp(event);
    });

    //Event handler for KeyDown
    this.keyDown = function(event) {
        if (this.m_keyCode[event.keyCode] != undefined) {
//            console.log(this.m_keyCode[event.keyCode]);
            //Converting keycode into, up, down, left,
            //right action and set tetris action.
            this.m_action = this.m_keyCode[event.keyCode];
            return false;
        } else if(event.keyCode == 27){
            clearTimeout(this.m_timeOut);
        }
        return true;
    };

    //Event handler for KeyUp
    this.keyUp = function(event) {
        this.m_action = null;
        return true;
    }
    /**
     * Add a block to the tetris grid
     *
     * @param p_x the x cordinate to create the block
     * @param p_y the y cordinate to create the block
     * @param p_falling indicated wether this block is falling
     */
    this.addBlock = function(p_x, p_y, p_falling) {
        /**
         * This code is expensive, there are better ways
         * of implementing this section of the code
         * but for simplicity we leaving it
         */
        if (this.validCordinate(p_x, p_y) && !this.blockExist(p_x, p_y)) {
            var block = document.createElement('div');
            this.m_holder.append(block);
            $(block).addClass('block');
            if(this.moveBlock(block,p_x, p_y)){
                block.m_falling = p_falling?p_falling:false;
                if(block.m_falling){
                    $(block).addClass('falling');
                    this.m_falling.push(block);
                }
                return block;
            }
        }
        // There are some error
        return false;
    };
    
    /**
     * Check if the block existed at cordinate x, y
     *
     * @param p_x x cordinate to be check
     * @param p_x y cordinate to be check
     */
    this.blockExist = function(p_x, p_y) {
        if(this.m_grid[p_x]){
            return this.m_grid[p_x][p_y] != undefined && !this.m_grid[p_x][p_y].m_falling;
        } else {
            return false;
        }
    };
    
    /**
     * Check if the cordinate is with in the tetris region.
     * 
     * @param p_x x cordinate to be check
     * @param p_x y cordinate to be check
     */
    this.validCordinate = function(p_x, p_y) {
        return this.m_xSize > p_x && p_x >= 0 && this.m_ySize > p_y && p_y >= 0;
    }

    /**
     * Attemp to move the block to cordinate x and y, if the cordinate are
     * not occupied.
     *
     * @param p_block the block to be move
     * @param p_x the x cordinate to be move too
     * @param p_y the y cordinate to be move too
     */
    this.moveBlock = function(p_block, p_x, p_y) {
        if(!this.blockExist(p_x, p_y) && this.validCordinate(p_x, p_y)){
            if(p_block.m_x != undefined || p_block.m_y != undefined){
                if(this.m_grid[p_block.m_x][p_block.m_y] == p_block){
                    this.m_grid[p_block.m_x][p_block.m_y] = undefined;
                }
            }
            $(p_block).css("top", this.m_blockSize * p_x + 1);
            $(p_block).css("left", this.m_blockSize * p_y + 1);
            p_block.m_x = p_x;
            p_block.m_y = p_y;
            this.m_grid[p_x][p_y] = p_block;
            return true;
        }
        return false;
    };
    
    /**
     * Remove the block pass via parameter
     *
     * @param p_block the block to be remove.
     */
    this.removeBlock = function(p_block) {
        return this.removeCordinate(p_block.m_x, p_block.m_y);
    };
    
    /**
     * Remove the block from x, y cordinate if it existed.
     * 
     * @param p_x x cordinate of block
     * @param p_y y cordinate of block
     */
    this.removeCordinate = function( p_x, p_y){
        if(this.blockExist(p_x, p_y)){
            var block = this.m_grid[p_block.m_x][p_block.m_y];
            this.m_grid[p_block.m_x][p_block.m_y] = undefined;
            block.remove();
            return true;
        } else {
            return false;
        }
    };
    
    /**
     * Make the block that are mark as falling fall.
     */
    this.fall = function() {
        return this.moveFalling(1,0);
    };
    
    /**
     * Convert the falling block to normal blocks
     * And clear out and block when the rows are filled
     */
    this.fallingLanded = function() {
        for(var index in this.m_falling){
            block = this.m_falling[index];
            block.m_falling = false;
            $(block).removeClass('falling');
            
            var emptySlot = false;
            for(var i = 0; i < this.m_ySize ; i++){
                if(this.m_grid[block.m_x][i] == undefined){
                    emptySlot = true;
                }
            }
            
            if(!emptySlot){
                for(var i = 0; i < this.m_ySize ; i++){
                    $(this.m_grid[block.m_x][i]).remove();
                    this.m_grid[block.m_x][i] = undefined;
                }
                
                for(var x = block.m_x; x > 0; x--){
                    for(var y = 0; y < this.m_ySize; y++){
                        if(this.m_grid[x][y] != undefined){
                            this.moveBlock(this.m_grid[x][y],this.m_grid[x][y].m_x+1,this.m_grid[x][y].m_y);
                        };
                    }
                }
            }
        };
        
        this.m_falling = Array();
    };
    
    /**
     * Interperted the user, input and regulate how fast the games response
     * to the user input.
     */
    this.userControl = function(){
        switch(this.m_action){
            case 'left':
                this.moveFalling(0,-1);
                break;
            case 'right':
                this.moveFalling(0,1);
                break;
            case 'down':
                this.fall();
                break;
            case 'up':
                this.rotateFalling();
                break;
            default:
                break;
        }
        this.m_timeOutUserControl = setTimeout(function (){Tetris().userControl()}, 100);
    };
    
    /**
     * Rotate the shape using pre define transformations.
     */
    this.rotateFalling = function(){
        var shapeTransformation = this.m_shapeTranspormation[this.m_fallingShape];
        if(shapeTransformation.length > 1){
            var transformation = shapeTransformation[this.m_fallingStatue];
            
            for(var index in transformation){
                var x = transformation[index][0];
                var y = transformation[index][1];
                var block = this.m_falling[index];
                if(this.blockExist(block.m_x + x, block.m_y + y) || !this.validCordinate(block.m_x + x, block.m_y + y)){
                    return false;
                }
            }
            
            for(var index in transformation){
                var x = transformation[index][0];
                var y = transformation[index][1];
                var block = this.m_falling[index];
                this.moveBlock(block,block.m_x + x, block.m_y + y);
            }
            
            this.m_fallingStatue = (this.m_fallingStatue+1) % shapeTransformation.length;
        } else {
            console.log('NO TRANSFORMATION EXISTED');
        }
    }
    
    /**
     * Add a pre define block to the game.
     *
     * @param p_index the shape to add
     */
    this.addFallingShape = function(p_index) {
        if(p_index == undefined){
            p_index = Math.floor(Math.random()*5);
        }
        
        this.m_fallingShape = p_index;
        this.m_fallingStatue = 0;
        var shape = this.m_shape[p_index];
        
        for(var index in shape){
            this.addBlock(shape[index][0],shape[index][1], true);
        }
    }
    
    /**
     * Move all the falling block by p_x and p_y
     *
     * @param p_x cordinate for the falling blocks to move to
     * @param p_y cordinate for the falling blocks to move to 
     */
    this.moveFalling = function(p_x, p_y){
        for(var index in this.m_falling){
            block = this.m_falling[index];
            if(this.blockExist(block.m_x+p_x,block.m_y+p_y)
                    || !this.validCordinate(block.m_x+p_x, block.m_y+p_y)){
                return false;
            }
        };
        
        for(var index in this.m_falling){
            block = this.m_falling[index];
            this.moveBlock(block,block.m_x+p_x,block.m_y+p_y);
        };
        return true;
    }
    
    /**
     * Game Timer
     */
    this.run = function() {
        if(!this.fall()){
            this.fallingLanded();
            this.addFallingShape();
        };
        this.m_timeOut = setTimeout(function (){Tetris().run()}, 500);
    };
    
    /**
     * Initalize the game.
     */
    this.start = function() {
        this.addFallingShape();
        this.userControl();
        this.run();
    }
};


No comments:

Post a Comment