Search code examples
javascriptarraystetris

JavaScript Tetris tetrominoes move apart after movement and rotation


Learning JavaScript by making a tetris game.

Problem: When I try to move (left, right or down from the starting position) a piece and then rotate it, then the rotated piece stretches appart. When I go back with it t the starting position, everything works fine. Also when I do not rotate the piece but only move it to the left/right/down then everything is also fine. Am thinking that I have the center of rotation ankered to the grid but not to the piece.

Here you can play the game: Here

Here is my github: Here

Temporary controlls:

Enter and after up arrow: start the game

left arrow: move left

right arrow: move right

up arrow: rotate

down arrow: move one row down

Description:

My tetrominoes and my grid are made of arrays (class based). The grid comes from SimpleBlock{} and GridBlock{}. My tetrominoes are made by Simple Block{} and

    class SimpleBlock{
    constructor(tempSquareColor, boardPosX, boardPosY){
        this.x = boardPosX;
        this.y = boardPosY;
        this.squareColor = tempSquareColor;
    }
}

class GridBlock extends SimpleBlock{
    constructor(tempSquareColor, boardPosX, boardPosY){
        super(tempSquareColor, boardPosX, boardPosY);

        ctx.fillStyle = this.squareColor;
        ctx.strokeStyle = "black";
        ctx.lineWidth = 3;
        ctx.fillRect(this.x * squareSize, this.y * squareSize, squareSize, squareSize);
        ctx.strokeRect(this.x * squareSize, this.y * squareSize, squareSize, squareSize);
    }
}

var gameBoardSquared = [];

function drawSquaredGameBoard() {
    for(var row = 0; row < gameBoardRows; row++){
        gameBoardSquared[row] = [];
        for(var col = 0; col < gameBoardColumns; col++){
            gameBoardSquared[row][col] = new GridBlock("white", row, col);
        }
    }
}

My Tetrominoes:

class BasicBlock extends SimpleBlock{
    constructor(tempSquareColor, boardPosX, boardPosY){
        super(tempSquareColor, boardPosX, boardPosY);
    }

    drawBlock(){
        ctx.fillStyle = this.squareColor;
        ctx.strokeStyle = "black";
        ctx.lineWidth = 3;
        ctx.fillRect(this.x * squareSize, this.y * squareSize, squareSize, squareSize);
        ctx.strokeRect(this.x * squareSize, this.y * squareSize, squareSize, squareSize);
    }

    undrawBlock(){
        ctx.fillStyle = "white";
        ctx.strokeStyle = "black";
        ctx.lineWidth = 3;
        ctx.fillRect(this.x * squareSize, this.y * squareSize, squareSize, squareSize);
        ctx.strokeRect(this.x * squareSize, this.y * squareSize, squareSize, squareSize);
    }

    moveLeft(){
        this.x--;
    }

    moveRight(){
        this.x++;
    }

    slowFall(){
        this.y++;
    }
}

//Tetrominos

//Declaration of variables, of a [4x4] tetromino array. Excluding the cells, that will never be used (like tetro3)
var tetrominoes = [];
var tetrominoI = [];
var tetrominoJ = [];
var tetrominoL = [];
var tetrominoO = [];
var tetrominoS = [];
var tetrominoT = [];
var tetrominoZ = [];

function makeNewRandomTetromino(){
    var tetro = [];
    tetro[0] = new BasicBlock("blue", 4, 0);
    tetro[1] = new BasicBlock("blue", 5, 0);
    tetro[2] = new BasicBlock("blue", 6, 0);
    tetro[4] = new BasicBlock("blue", 4, 1);
    tetro[5] = new BasicBlock("blue", 5, 1);
    tetro[6] = new BasicBlock("blue", 6, 1);
    tetro[7] = new BasicBlock("blue", 7, 1);
    tetro[8] = new BasicBlock("blue", 4, 2);
    tetro[9] = new BasicBlock("blue", 5, 2);
    tetro[10] = new BasicBlock("blue", 6, 2);
    tetro[11] = new BasicBlock("blue", 7, 2);
    tetro[13] = new BasicBlock("blue", 5, 3);
    tetro[14] = new BasicBlock("blue", 6, 3);

    // var tetrominoJ0 = [        
    //     tetro[1].tempSquareColor = "yellow",
    //     tetro[5].tempSquareColor = "yellow",
    //     tetro[8].tempSquareColor = "yellow",
    //     tetro[9].tempSquareColor = "yellow",
    // ];


    // var tetrominoJ1 = [        
    //     tetro[4].tempSquareColor = "yellow",
    //     tetro[5].tempSquareColor = "yellow",
    //     tetro[6].tempSquareColor = "yellow",
    //     tetro[10].tempSquareColor = "yellow",
    // ];

    // var tetrominoI = [];

    tetrominoI[0] = [tetro[1], tetro[5], tetro[9], tetro[13]];
    tetrominoI[1] = [tetro[8], tetro[9], tetro[10], tetro[11]];
    tetrominoI[2] = [tetro[2], tetro[6], tetro[10], tetro[14]];
    tetrominoI[3] = [tetro[4], tetro[5], tetro[6], tetro[7]];

    // for(var i of tetrominoI){
    //     i.squareColor == "magenta";
    // }

    // var tetrominoJ = [];

    tetrominoJ[0] = [tetro[1], tetro[5], tetro[8], tetro[9]];
    tetrominoJ[1] = [tetro[6], tetro[4], tetro[5], tetro[10]];
    tetrominoJ[2] = [tetro[1], tetro[2], tetro[5], tetro[9]];
    tetrominoJ[3] = [tetro[0], tetro[4], tetro[5], tetro[6]];

    // var tetrominoL = [];

    tetrominoL[0] = [tetro[0], tetro[1], tetro[5], tetro[9]];
    tetrominoL[1] = [tetro[4], tetro[5], tetro[6], tetro[8]];
    tetrominoL[2] = [tetro[1], tetro[5], tetro[9], tetro[10]];
    tetrominoL[3] = [tetro[2], tetro[4], tetro[5], tetro[6]];

    // for(var i of tetrominoL){
    //     i.squareColor == "orange";
    // }

    // var tetrominoO = [];

    tetrominoO[0] = [tetro[1], tetro[2], tetro[5], tetro[6]];
    tetrominoO[1] = [tetro[1], tetro[2], tetro[5], tetro[6]];
    tetrominoO[2] = [tetro[1], tetro[2], tetro[5], tetro[6]];
    tetrominoO[3] = [tetro[1], tetro[2], tetro[5], tetro[6]];

    // for(var i of tetrominoO){
    //     i.squareColor == "yellow";
    // }

    // var tetrominoS = [];

    tetrominoS[0] = [tetro[0], tetro[4], tetro[5], tetro[9]];
    tetrominoS[1] = [tetro[5], tetro[6], tetro[8], tetro[9]];
    tetrominoS[2] = [tetro[1], tetro[5], tetro[6], tetro[10]];
    tetrominoS[3] = [tetro[1], tetro[2], tetro[4], tetro[5]];

    // for(var i of tetrominoS){
    //     i.squareColor == "green";
    // }

    // var tetrominoT = [];

    tetrominoT[0] = [tetro[1], tetro[4], tetro[5], tetro[9]];
    tetrominoT[1] = [tetro[4], tetro[5], tetro[6], tetro[9]];
    tetrominoT[2] = [tetro[1], tetro[5], tetro[6], tetro[9]];
    tetrominoT[3] = [tetro[1], tetro[4], tetro[5], tetro[6]];

    // for(var i of tetrominoT){
    //     i.squareColor == "purple";
    // }

    // var tetrominoZ = [];

    tetrominoZ[0] = [tetro[1], tetro[4], tetro[5], tetro[8]];
    tetrominoZ[1] = [tetro[4], tetro[5], tetro[9], tetro[10]];
    tetrominoZ[2] = [tetro[2], tetro[5], tetro[6], tetro[9]];
    tetrominoZ[3] = [tetro[0], tetro[1], tetro[5], tetro[6]];

    // for(var i of tetrominoZ){
    //     i.squareColor == "red";
    // }

    var i = Math.floor(Math.random() * tetrominoZ.length);
    var tetrominoesArr = [tetrominoO[i], tetrominoJ[i], tetrominoS[i], tetrominoZ[i], tetrominoT[i], tetrominoL[i], tetrominoI[i]];

    var x = Math.floor(Math.random() * tetrominoesArr.length);
    tetrominoes = tetrominoesArr[x];
}

And the functions that control them:

function rotateTetromino(){

    for(let i of tetrominoes){
        i.undrawBlock();
    }

    if(tetrominoes == tetrominoI[0]){
        tetrominoes = tetrominoI[1];
    } else if(tetrominoes == tetrominoI[1]){
        tetrominoes = tetrominoI[2];
    } else if(tetrominoes == tetrominoI[2]){
        tetrominoes = tetrominoI[3];
    } else if(tetrominoes == tetrominoI[3]){
        tetrominoes = tetrominoI[0];
    } else if(tetrominoes == tetrominoJ[0]){
        tetrominoes = tetrominoJ[1];
    } else if(tetrominoes == tetrominoJ[1]){
        tetrominoes = tetrominoJ[2];
    } else if(tetrominoes == tetrominoJ[2]){
        tetrominoes = tetrominoJ[3];
    } else if(tetrominoes == tetrominoJ[3]){
        tetrominoes = tetrominoJ[0];
    } else if(tetrominoes == tetrominoL[0]){
        tetrominoes = tetrominoL[1];
    } else if(tetrominoes == tetrominoL[1]){
        tetrominoes = tetrominoL[2];
    } else if(tetrominoes == tetrominoL[2]){
        tetrominoes = tetrominoL[3];
    } else if(tetrominoes == tetrominoL[3]){
        tetrominoes = tetrominoL[0];
    } else if(tetrominoes == tetrominoO[0]){
        tetrominoes = tetrominoO[1];
    } else if(tetrominoes == tetrominoO[1]){
        tetrominoes = tetrominoO[2];
    } else if(tetrominoes == tetrominoO[2]){
        tetrominoes = tetrominoO[3];
    } else if(tetrominoes == tetrominoO[3]){
        tetrominoes = tetrominoO[0];
    } else if(tetrominoes == tetrominoS[0]){
        tetrominoes = tetrominoS[1];
    } else if(tetrominoes == tetrominoS[1]){
        tetrominoes = tetrominoS[2];
    } else if(tetrominoes == tetrominoS[2]){
        tetrominoes = tetrominoS[3];
    } else if(tetrominoes == tetrominoS[3]){
        tetrominoes = tetrominoS[0];
    } else if(tetrominoes == tetrominoT[0]){
        tetrominoes = tetrominoT[1];
    } else if(tetrominoes == tetrominoT[1]){
        tetrominoes = tetrominoT[2];
    } else if(tetrominoes == tetrominoT[2]){
        tetrominoes = tetrominoT[3];
    } else if(tetrominoes == tetrominoT[3]){
        tetrominoes = tetrominoT[0];
    } else if(tetrominoes == tetrominoZ[0]){
        tetrominoes = tetrominoZ[1];
    } else if(tetrominoes == tetrominoZ[1]){
        tetrominoes = tetrominoZ[2];
    } else if(tetrominoes == tetrominoZ[2]){
        tetrominoes = tetrominoZ[3];
    } else if(tetrominoes == tetrominoZ[3]){
        tetrominoes = tetrominoZ[0];
    }
    for(let i of tetrominoes){
        i.drawBlock();
    }
}

function moveTetrominoesLeft(){

    if(tetrominoes.some(k => k.x - 1 < 0) || tetrominoes.some(k => k.squareColor == gameBoardSquared[k.x-1][k.y].squareColor)){
        for(let i of tetrominoes){
            i.drawBlock();
        }
    }
    else{
        for(let i of tetrominoes){
            i.undrawBlock();
        }
        for(let i of tetrominoes){
            i.moveLeft();
            i.drawBlock();
        }
    }
}

function moveTetrominoesRight(){

    if(tetrominoes.some(k => k.x + 1 > gameBoardSquared.length-1) || tetrominoes.some(k => k.squareColor == gameBoardSquared[k.x+1][k.y].squareColor)){
        for(let i of tetrominoes){
            i.drawBlock();
        }
    }
    else{
        for(var i of tetrominoes){
            i.undrawBlock();
        }
        for(var i of tetrominoes){
            i.moveRight();
            i.drawBlock();
        }
    }
}

function tetrominoesSlowFall(){

    for(let i of tetrominoes){
        i.undrawBlock();
    }
    for(let i of tetrominoes){
        i.slowFall();
        i.drawBlock();
    }
}

function collisionDetection(){
    const topBoardBorder = 3;
    for(var i of tetrominoes){
        if(tetrominoes.some(k => k.squareColor == gameBoardSquared[k.x][k.y].squareColor) && tetrominoes.some(k => k.y < topBoardBorder)){
            console.log("Game Over");
            gameOver = true;
        }
        if(tetrominoes.some(k => k.squareColor == gameBoardSquared[k.x][k.y+1].squareColor)){
            for(var i of tetrominoes){
                i.drawBlock();
                gameBoardSquared[i.x][i.y] = i;
            }
            return true;
        }
        else if(tetrominoes.some(k => k.y > playableGameBoardLength-1)){
            for(var i of tetrominoes){
                i.drawBlock();
                gameBoardSquared[i.x][i.y] = i;
            }
            return true;
        }
    }
    return false;
}

function clearRow(){
    for(var rows = 0; rows < gameBoardColumns - 1; rows++){
        while(gameBoardSquared.every(k => k[rows].squareColor == "blue")){
            for(var i = 0; i < gameBoardSquared.length; i++){
                gameBoardSquared[i].splice(rows, 1);
            }
            for(var i = 0; i < gameBoardSquared.length; i++){
                gameBoardSquared[i].unshift(new GridBlock("white", i, rows));
            }
        console.log("clearRow(): ");
        console.log(gameBoardSquared);
        }
    }
}

I think that there is an issue with the center of rotation, that is it positionet in the starting potion of the tetrominoes and not in the current center of the tetrominoe. But maybe you guys could help me better out?

Solution suggested by Tecnogirl: I am moving all of the blocks that will make a tetromino (so the whole tetro array) but am coloring only the ones, that are actually used (the ones, that are under tetrominoes). Removed the collisionDetection() function and placed its part in moveTetrominoesLeft(), moveTetrominoesRight(), tetrominoesSlowFall(). Here is the part, which I changed:

    function moveTetrominoesLeft(){
    if(tetrominoes.some(k => k.x - 1 < 0) || tetrominoes.some(k => k.squareColor !== "white" && gameBoardSquared[k.x-1][k.y].squareColor !== "white")){
        for(let i of tetrominoes){
            i.drawBlock();
        }
    }
    else{
        for(let i of tetro){
            i.undrawBlock();
            i.moveLeft();
        }
        for(let i of tetrominoes){
            i.drawBlock();
        }
    }
}

function moveTetrominoesRight(){
    if(tetrominoes.some(k => k.x + 1 > gameBoardSquared.length-1) || tetrominoes.some(k => k.squareColor !== "white" && gameBoardSquared[k.x+1][k.y].squareColor !== "white")){
        for(let i of tetrominoes){
            i.drawBlock();
        }
    }
    else{
        for(let i of tetro){
            i.undrawBlock();
            i.moveRight();
        }
        for(let i of tetrominoes){
            i.drawBlock();
        }
    }
}

function tetrominoesSlowFall(){
    const topBoardBorder = 3;
    for(var i of tetrominoes){
        if(tetrominoes.some(k => k.squareColor !== "white" && gameBoardSquared[k.x][k.y].squareColor !== "white" && tetrominoes.some(k => k.y < topBoardBorder))){
            console.log("Game Over");
            gameOver = true;
        }
    }
    if(tetrominoes.some(k => k.y > playableGameBoardLength-1)){
        for(var i of tetrominoes){
            i.drawBlock();
            gameBoardSquared[i.x][i.y] = i;
        }
        isCollision = true;
    }
    else if(tetrominoes.some(k => k.squareColor !== "white" && gameBoardSquared[k.x][k.y+1].squareColor !== "white")){
        for(var i of tetrominoes){
            i.drawBlock();
            gameBoardSquared[i.x][i.y] = i;
        }
        isCollision =  true;
    }
    for(let i of tetro){
        i.undrawBlock();
        i.slowFall();
    }
    for(let i of tetrominoes){
        i.drawBlock();
    }
}

and also in the main game loop, there are some changes (since there is no collisionDetection() anymore:

function updateGameBoard(){
    if(!gameOver){
        colourTetromino();
        if(isCollision){
            clearRow();
            drawUpdatedGameBoard();
            makeNewRandomTetromino();
            isCollision = false;
        }
        else{
            tetrominoesSlowFall();
            drawUpdatedGameBoard();
        }
    }
    else{
        clearInterval(myInterval);
        alert("Game Over");
    }

}

function startGame(key){

    if (key === "Enter"){
        myInterval;
        drawSquaredGameBoard();
        makeNewRandomTetromino();
    }

    else if (key==="ArrowUp"){
        rotateTetromino();
    }

    else if (key==="ArrowLeft"){
        moveTetrominoesLeft();
    }

    else if (key==="ArrowRight"){
        moveTetrominoesRight();
    }

    else if (key==="ArrowDown"){
        updateGameBoard();
    }

    else
        console.log("Psst, press 'Enter' to start");
}

Solution

  • You first define your rotating positions as an array of BasicBlocks. This is an array of references to each Basic Block that constitutes a rotated position.

    When you do block.moveLeft(), you change the x value to a different number from the original. This means that the objects saved in the array of each position have changed to have that new x value and so when you try to rotate, the positions don't make sense anymore.

    Example:

    Look at tetrominoS. Its first position is

    tetrominoS[0] = [tetro[0], tetro[4], tetro[5], tetro[9]] // the position you first defined
    

    In memory:

    tetro[0] = reference to the BasicBlock (x = 4, y = 0)

    tetro[4] = reference to the BasicBlock (x = 4, y = 1)

    tetro[5] = reference to the BasicBlock (x = 5, y = 1)

    tetro[9] = reference to the BasicBlock (x = 5, y = 2)

    Then you do: moveLeft() (for example)

    Move left changes all the x values to x-1 so you are doing

    BasicBlock (x = 4, y = 0).x--; tetro[0] now points to BasicBlock (x = 3, y = 0)

    BasicBlock (x = 4, y = 1).x--; tetro[4] now points to BasicBlock (x = 3, y = 1)

    BasicBlock (x = 5, y = 1).x--; tetro[5] now points to BasicBlock (x = 4, y = 1)

    BasicBlock (x = 5, y = 2).x--; tetro[9] now points to BasicBlock (x = 4, y = 2)

    Then you rotate:

    the next position of tetrominoS is tetrominoS[1] which is

    tetrominoS[1] = [tetro[5], tetro[6], tetro[8], tetro[9]];
    

    but remember that tetro[5] and tetro[9] have been changed! So we get:

    tetrominoS[1] = [BasicBlock (x = 4, y = 1), tetro[6] (unchanged), tetro[8] (unchanged), BasicBlock (x = 4, y = 2)];
    

    which is not what you want.

    Solution:

    Instead of changing the X value of the blocks when you want to move the piece to the left, just remove the color of the current block and draw the color on the block next to it.