Thursday, May 9, 2019

Tetris challenge - reloaded (Javascript)

Years ago I've blogged on a challenge which was to implement a simple tetris game. This new reloaded version is a direct one-to-one translation to vanilla Javascript and runs in a browser. Feel free to learn from the code and/or reuse it in any way. Note that the UP key has to be pressed to start the game.
(Re)start: up
Moves: left/right/down/space
The code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>

    <script>

        var pieceMove = {
            left:   'left',
            right:  'right',
            up:     'up',
            down:   'down',
            rotate: 'rotate'
        };
    
        function board() {
            this.MARGIN      = 5;
            this.SIZEX       = 10;
            this.SIZEY       = 40;
            this.BLOCKSIZE   = 10;
            this.PIECESTARTX = 8;
    
            var i, j;
    
            this.squares = new Array( this.MARGIN + this.SIZEX + this.MARGIN );
            for ( i=0; i < this.squares.length; i++ )
                this.squares[i] = new Array( this.MARGIN + this.SIZEY + this.MARGIN );
    
            // fill the margin 
            for ( i=0; i < this.MARGIN + this.SIZEX + this.MARGIN; i++ )
                for ( j=0; j < this.MARGIN; j++ )
                    this.squares[i][j] = this.squares[i][j + this.SIZEY + this.MARGIN] = true;
            for ( i=0; i < this.MARGIN; i++ )
                for ( j=0; j < this.MARGIN + this.SIZEY + this.MARGIN; j++ )
                    this.squares[i][j] = this.squares[i + this.SIZEX + this.MARGIN][j] = true;        
    
            this.currentPiece  = piece.getRandomPiece();
            this.currentPieceX = this.PIECESTARTX;
            this.currentPieceY = this.MARGIN;        
    
            this.score         = 0;
        }
    
        board.prototype.obtainContext = function(canvas) {
            this.context       = canvas.getContext('2d');     
            this.contextWidth  = canvas.width;
            this.contextHeight = canvas.height;       
        }
    
        board.prototype.paint = function() {
            
            var i, j;
    
            this.context.fillStyle = 'white';
            this.context.fillRect(0, 0, this.contextWidth, this.contextHeight);
    
            // background
            this.context.fillStyle = 'black';
    
            for ( i=0; i < this.SIZEX; i++ )
                for ( j=0; j < this.SIZEY; j++ )
                    if ( this.squares[this.MARGIN + i][this.MARGIN + j] )
                        this.context.fillRect( i * this.BLOCKSIZE, j * this.BLOCKSIZE, this.BLOCKSIZE, this.BLOCKSIZE );
    
            // piece
            if ( this.currentPiece ) {
                for ( i=0; i < this.currentPiece.SIZE; i++ )
                    for ( j=0; j < this.currentPiece.SIZE; j++ )
                        if ( this.currentPiece.blockData[i][j] ) {
                            this.context.fillRect( (this.currentPieceX - this.MARGIN + i) * this.BLOCKSIZE, ( this.currentPieceY - this.MARGIN + j ) * this.BLOCKSIZE, 
                                                    this.BLOCKSIZE, this.BLOCKSIZE);
                        }
            }    

            // score
            this.context.fillStyle = 'red';
            this.context.fillText( this.score, 5, 10 );
        }
    
        /* Advance one step of the game */
        board.prototype.autoUpdate = function() {

            this.removeFullLines();
    
            if ( this.currentPiece ) {
                var _newX = this.currentPieceX;
                var _newY = this.currentPieceY + 1;
    
                if ( this.isLegalPosition( this.currentPiece, _newX, _newY ) ) {
                    this.currentPieceY = _newY;
                }
                else {
                    this.absorbCurrentPiece();
    
                    this.currentPiece  = piece.getRandomPiece();
                    this.currentPieceX = this.PIECESTARTX;
                    this.currentPieceY = this.MARGIN;
    
                    if ( !this.isLegalPosition( this.currentPiece, this.currentPieceX, this.currentPieceY ) ) {
                        this.score = 'Game over: ' + this.score;
                        this.currentPiece = null;

                        return false;
                    }
                }
            }    

            return true;
        }
    
        /* Remove all full lines */
        board.prototype.removeFullLines = function() {
            var fullLineNumber = -1;
            do
            {
                fullLineNumber = this.getPossibleFullLine();
                if ( fullLineNumber >= 0 )
                    this.removeLine( fullLineNumber );
            } while ( fullLineNumber >= 0 );
        }
    
        /* Check if one of lines is possibly filled */
        board.prototype.getPossibleFullLine = function() {
            for ( var j = 0; j < this.SIZEY; j++ )
            {
                var isFull = true;
                for ( var i = 0; i < this.SIZEX; i++ )
                    isFull &= this.squares[this.MARGIN + i][this.MARGIN + j];
    
                if ( isFull )
                    return j + this.MARGIN;
            }
    
            return -1;
        }
    
        /* Remove selected line */
        board.prototype.removeLine = function( lineNumber ) {
            for ( var j = lineNumber; j > this.MARGIN; j-- )
                for ( var i=this.MARGIN; i < this.SIZEX + this.MARGIN; i++ )
                    this.squares[i][j] = this.squares[i][j - 1];
    
            this.score += 1;
        }
    
        /* Copy piece data to board's block data. Called only when piece becomes "solid", non-movable. */
        board.prototype.absorbCurrentPiece = function() {
            if ( this.currentPiece )
                for ( var i=0; i < this.currentPiece.SIZE; i++ )
                    for ( var j=0; j < this.currentPiece.SIZE; j++ )
                        if ( this.currentPiece.blockData[i][j] )
                            this.squares[i + this.currentPieceX][j + this.currentPieceY] = true;
        }
    
        /* Moves current piece into a new position, if possible */
        board.prototype.moveCurrentPiece = function(move) {
    
            var _newPiece = this.currentPiece;
            var _newX     = this.currentPieceX;
            var _newY     = this.currentPieceY;
    
            if ( this.currentPiece )
            {
                if ( move == pieceMove.rotate ) _newPiece = piece.rotate( this.currentPiece );
                if ( move == pieceMove.left )   _newX = this.currentPieceX - 1;
                if ( move == pieceMove.right )  _newX = this.currentPieceX + 1;
                if ( move == pieceMove.down )   _newY = this.currentPieceY + 1;
    
                // move only if legal position
                if ( this.isLegalPosition( _newPiece, _newX, _newY ) )
                {
                    this.currentPiece  = _newPiece;
                    this.currentPieceX = _newX;
                    this.currentPieceY = _newY;
                }
            }
        }
    
        /* Is the new possible position of a piece "legal" in a sense that the piece doesn't overlap with non-empty cells */
        board.prototype.isLegalPosition = function( piece, pieceX, pieceY ) {
            for ( var i=0; i < piece.SIZE; i++ )
                for ( var j=0; j < piece.SIZE; j++ )
                    if ( piece.blockData[i][j] &&
                         this.squares[i + pieceX][j + pieceY]
                        )
                        return false;
    
                return true;
        }
    
        function piece(r0, r1, r2, r3, r4) {
            this.SIZE = 5;
    
            var i;
    
            this.blockData = new Array( this.SIZE );
            for ( i=0; i < this.SIZE; i++ )
                this.blockData[i] = new Array( this.SIZE );
    
            for ( i=0; i<this.SIZE; i++ ) {
                this.blockData[i][0] = r0[i];
                this.blockData[i][1] = r1[i];
                this.blockData[i][2] = r2[i];
                this.blockData[i][3] = r3[i];
                this.blockData[i][4] = r4[i];
            }
        }
    
        piece.rotate = function( source ) {
            if ( source == piece.i_horiz ) return piece.i_vert;
            if ( source == piece.i_vert )  return piece.i_horiz;
    
            if ( source == piece.z_lefthoriz ) return piece.z_leftvert;
            if ( source == piece.z_leftvert )  return piece.z_lefthoriz;
    
            if ( source == piece.z_righthoriz ) return piece.z_rightvert;
            if ( source == piece.z_rightvert )  return piece.z_righthoriz;
    
            if ( source == piece.l_l1 ) return piece.l_l2;
            if ( source == piece.l_l2 ) return piece.l_l3;
            if ( source == piece.l_l3 ) return piece.l_l4;
            if ( source == piece.l_l4 ) return piece.l_l1;
    
            if ( source == piece.r_l1 ) return piece.r_l2;
            if ( source == piece.r_l2 ) return piece.r_l3;
            if ( source == piece.r_l3 ) return piece.r_l4;
            if ( source == piece.r_l4 ) return piece.r_l1;
    
            return piece.box;
        }
    
        piece.getRandomPiece = function() {
            var _next = Math.floor( Math.random() * 6 );
            switch ( _next ) {
                case 1  : return piece.z_lefthoriz;
                case 2  : return piece.z_righthoriz;
                case 3  : return piece.l_l1;
                case 4  : return piece.r_l1;
                case 5  : return piece.i_horiz;
                default : return piece.box;
            }
        }
    
        piece.box = new piece(
            [0, 0, 0, 0, 0 ],
            [0, 1, 1, 0, 0 ],
            [0, 1, 1, 0, 0 ],
            [0, 0, 0, 0, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.i_horiz = new piece(
            [0, 0, 1, 0, 0 ],
            [0, 0, 1, 0, 0 ],
            [0, 0, 1, 0, 0 ],
            [0, 0, 1, 0, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.i_vert = new piece(
            [0, 0, 0, 0, 0 ],
            [0, 1, 1, 1, 1 ],
            [0, 0, 0, 0, 0 ],
            [0, 0, 0, 0, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.z_lefthoriz = new piece(
            [0, 0, 0, 0, 0 ],
            [0, 1, 1, 0, 0 ],
            [0, 0, 1, 1, 0 ],
            [0, 0, 0, 0, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.z_leftvert = new piece(
            [0, 0, 0, 0, 0 ],
            [0, 0, 1, 0, 0 ],
            [0, 1, 1, 0, 0 ],
            [0, 1, 0, 0, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.z_righthoriz = new piece(
            [0, 0, 0, 0, 0 ],
            [0, 0, 1, 1, 0 ],
            [0, 1, 1, 0, 0 ],
            [0, 0, 0, 0, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.z_rightvert = new piece(
            [0, 0, 0, 0, 0 ],
            [0, 0, 1, 0, 0 ],
            [0, 0, 1, 1, 0 ],
            [0, 0, 0, 1, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.l_l1 = new piece(
            [0, 0, 0, 0, 0 ],
            [0, 1, 0, 0, 0 ],
            [0, 1, 1, 1, 0 ],
            [0, 0, 0, 0, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.l_l2 = new piece(
            [0, 0, 0, 0, 0 ],
            [0, 1, 1, 0, 0 ],
            [0, 1, 0, 0, 0 ],
            [0, 1, 0, 0, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.l_l3 = new piece(
            [0, 0, 0, 0, 0 ],
            [1, 1, 1, 0, 0 ],
            [0, 0, 1, 0, 0 ],
            [0, 0, 0, 0, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.l_l4 = new piece(
            [0, 0, 1, 0, 0 ],
            [0, 0, 1, 0, 0 ],
            [0, 1, 1, 0, 0 ],
            [0, 0, 0, 0, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.r_l1 = new piece(
            [0, 0, 0, 0, 0 ],
            [0, 0, 0, 1, 0 ],
            [0, 1, 1, 1, 0 ],
            [0, 0, 0, 0, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.r_l2 = new piece(
            [0, 0, 0, 0, 0 ],
            [0, 0, 1, 1, 0 ],
            [0, 0, 0, 1, 0 ],
            [0, 0, 0, 1, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.r_l3 = new piece(
            [0, 0, 0, 0, 0 ],
            [0, 0, 1, 1, 1 ],
            [0, 0, 1, 0, 0 ],
            [0, 0, 0, 0, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
        piece.r_l4 = new piece(
            [0, 0, 1, 0, 0 ],
            [0, 0, 1, 0, 0 ],
            [0, 0, 1, 1, 0 ],
            [0, 0, 0, 0, 0 ],
            [0, 0, 0, 0, 0 ]        
        );
    
        
        var _board;
    
        var _interval;              // current frame interval
        var _speedinterval = 15000; // speed up every 15 sec

        var _intervalHandle;
    
        /*
            * This advances one frame
            */
        function tick() {
            _board.paint();
            if ( _board.autoUpdate() ) {
                _intervalHandle = setTimeout( tick, _interval);
            }
        }
    
        /*
            * This fires every 15 seconds and decreases the timeout of the other timer
            */
        function speedup() {
            if ( _interval > 20 )
                _interval -= 10;
        }
    
        function init(canvas) {
            _interval      = 200;

            _board = new board();
            _board.obtainContext(canvas);
        
            _intervalHandle = setTimeout( tick, _interval ); 
        }

        (function main(){
                        
            window.addEventListener('load', function() {
                var canvas = document.getElementById('tetris');

                canvas.addEventListener('click', function() {
                    if ( _intervalHandle )
                        clearTimeout( _intervalHandle );
                    init(canvas);
                });
                
                setInterval( speedup, _speedinterval );
            } );
    
            window.addEventListener( 
                'keydown', 
                function(e) {
                    //e.preventDefault();
                    if ( e.keyCode == 37 ) // arrow left
                        _board.moveCurrentPiece( pieceMove.left );
                    if ( e.keyCode == 39 ) // arrow right
                    _board.moveCurrentPiece( pieceMove.right );
                    if ( e.keyCode == 40 ) // arrow down
                        _board.moveCurrentPiece( pieceMove.down );
                    if ( e.keyCode == 32 ) // space
                        _board.moveCurrentPiece( pieceMove.rotate );
    
                    _board.paint();
                }
            );

        })();
    
    </script>
    <style>
        #welcome {
            border: 1px solid black;
            font: 10px tahoma;
            width: 100px;
        }
        #tetris {
            border: 1px solid black;
        }
    </style>    
</head>
<body>

    <div id="welcome">
        Moves: left/right/down/space
        Click: (re)start.
    </div>
    <div>
        <canvas id="tetris" width="100" height="400">
        </canvas>
    </div>

</body>
</html>

No comments: