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");
}
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.