Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tetris 2d array logic

I'm trying to write tetris in JS with matrixes instead of sprites. Basically to be better at visualising 2d arrays.

I rotate a block by transposing its matrix data and then reversing the rows. But because the block's width and height doesn't completely fill this 4x4 matrix the rotating results in the block moving, instead of rotating in place.

I can't see it, i've already spent more than two days with trying to get such a simple game as tetris working, restarting from scratch a couple of times.. I need help, i really want to be able to program games, and the only thing i got working was tic tac toe. Which i spent more time on than i should.

Here's the full js code. Clicking the canvas rotates the piece.

var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
canvas.width = 400;
canvas.height = 600;

// game object
var G = {};
var current = 0;
var x = 0;
var y = 0;

//GRID
G.grid = [];
G.gridColumns = 10;
G.gridRows = 15;
for (var i = 0; i < G.gridColumns; i++) {
  G.grid[i] = [];
  for (var j = 0; j < G.gridRows; j++) {
    G.grid[i][j] = 0;
  }
}

// Array with all different blocks
G.blocks = [];
//block constructor
function block() {};
G.blocks[0] = new block();
G.blocks[0].matrix = [
  [1, 0, 0, 0],
  [1, 1, 0, 0],
  [0, 1, 0, 0],
  [0, 0, 0, 0]
];
G.blocks[0].width = 2;
G.blocks[0].height = 3;

function transpose(m) {
  // dont understand this completely, but works because j<i
  for (var i = 0; i < m.matrix.length; i++) {
    for (var j = 0; j < i; j++) {
      var temp = m.matrix[i][j];
      m.matrix[i][j] = m.matrix[j][i];
      m.matrix[j][i] = temp;
    }
  }
}

function reverseRows(m) {
  for (var i = 0; i < m.matrix.length; i++) {
    m.matrix[i].reverse();
  }
}

function rotate(m) {
  transpose(m);
  reverseRows(m);
}

function add(m1, m2) {
  for (var i = 0; i < m1.matrix.length; i++) {
    for (var j = 0; j < m1.matrix[i].length; j++) {
      m2[i + x][j + y] = m1.matrix[i][j];
    }
  }
}

function draw(matrix) {
  for (var i = 0; i < matrix.length; i++) {
    for (var j = 0; j < matrix[i].length; j++) {
      if (matrix[i][j] === 1) {
        ctx.fillRect(j * 20, i * 20, 19, 19);
      }
    }
  }
  ctx.strokeRect(0, 0, G.gridColumns * 20, G.gridRows * 20);
}


window.addEventListener("click", function(e) {
  rotate(G.blocks[current]);
});

function tick() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  add(G.blocks[current], G.grid);
  draw(G.grid);
}
setInterval(tick, 1000 / 30);
<canvas id="c"></canvas>

Please ignore the little quirks in my code, i learned programming by myself. Thanks in advance :)

like image 876
onlyme0349 Avatar asked Jul 26 '16 15:07

onlyme0349


2 Answers

Rotations

One problem with actual rotations is that some of them are not going to look all that good, even if the width of the matrix is taken into consideration. Let's see what happens with the rotation of the I shape:

. X . .        . . . .        . . X .        . . . .
. X . .   =>   X X X X   =>   . . X .   =>   . . . .
. X . .        . . . .        . . X .        X X X X
. X . .        . . . .        . . X .        . . . .

From a gameplay perspective, you would expect the 3rd and 4th shapes to be identical to the 1st and 2nd ones, respectively. But it's not what's going to happen with the generic rotation algorithm. You might address the above issue by using a non-square matrix (5x4), but the algorithm is going to get more complicated than you would have initially expected.

Actually, I'd be willing to bet that most Tetris implementations do not bother doing the rotation programmatically and simply hardcode all the different possible shapes of the tetrominoes, in a way that makes the rotations look as good and as 'fair' as possible. A nice thing about that is that you don't have to worry about their size anymore. You can just store them all as 4x4.

As we are going to see here, this can be done in a very compact format.

Encoding tetrominoes as bitmasks

Because a tetromino is basically a set of 'big pixels' that can be either on or off, it is quite suitable and efficient to represent it as a bitmask rather than a matrix of integers.

Let's see how we can encode the two distinct rotations of the S shape:

X . . .     1 0 0 0
X X . .  =  1 1 0 0  =  1000110001000000 (in binary)  =  0x8C40 (in hexadecimal)
. X . .     0 1 0 0
. . . .     0 0 0 0

. X X .     0 1 1 0
X X . .  =  1 1 0 0  =  0110110000000000 (in binary)  =  0x6C00 (in hexadecimal)
. . . .     0 0 0 0
. . . .     0 0 0 0

The two other rotations are the same for this one. So, we can fully define our S shape with:

[ 0x8C40, 0x6C00, 0x8C40, 0x6C00 ]

Doing the same thing for each shape and each rotation, we end up with something like:

var shape = [
  [ 0x4640, 0x0E40, 0x4C40, 0x4E00 ], // 'T'
  [ 0x8C40, 0x6C00, 0x8C40, 0x6C00 ], // 'S'
  [ 0x4C80, 0xC600, 0x4C80, 0xC600 ], // 'Z'
  [ 0x4444, 0x0F00, 0x4444, 0x0F00 ], // 'I'
  [ 0x44C0, 0x8E00, 0xC880, 0xE200 ], // 'J'
  [ 0x88C0, 0xE800, 0xC440, 0x2E00 ], // 'L'
  [ 0xCC00, 0xCC00, 0xCC00, 0xCC00 ]  // 'O'
];

Drawing them

Now, how are we going to draw a tetromino with this new format? Rather than accessing a value in a matrix with matrix[y][x], we're going to test the relevant bit in our bitmask:

for (var y = 0; y < 4; y++) {
  for (var x = 0; x < 4; x++) {
    if (shape[s][r] & (0x8000 >> (y * 4 + x))) {
      ctx.fillRect(x * 20, y * 20, 19, 19);
    }
  }
}

Demo

Below is some demonstration code using this method.

var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
canvas.width = 100;
canvas.height = 100;

var shape = [
  [ 0x4640, 0x0E40, 0x4C40, 0x4E00 ], // 'T'
  [ 0x8C40, 0x6C00, 0x8C40, 0x6C00 ], // 'S'
  [ 0x4C80, 0xC600, 0x4C80, 0xC600 ], // 'Z'
  [ 0x4444, 0x0F00, 0x4444, 0x0F00 ], // 'I'
  [ 0x44C0, 0x8E00, 0xC880, 0xE200 ], // 'J'
  [ 0x88C0, 0xE800, 0xC440, 0x2E00 ], // 'L'
  [ 0xCC00, 0xCC00, 0xCC00, 0xCC00 ]  // 'O'
];

var curShape = 0, curRotation = 0;
draw(curShape, curRotation);

function draw(s, r) {
  ctx.fillStyle = 'white';
  ctx.fillRect(0, 0, 100, 100);
  ctx.fillStyle = 'black';

  for (var y = 0; y < 4; y++) {
    for (var x = 0; x < 4; x++) {
      if (shape[s][r] & (0x8000 >> (y * 4 + x))) {
        ctx.fillRect(x * 20, y * 20, 19, 19);
      }
    }
  }
}

function next() {
  curShape = (curShape + 1) % 7;
  draw(curShape, curRotation);
}

function rotate() {
  curRotation = (curRotation + 1) % 4;
  draw(curShape, curRotation);
}
<canvas id="c"></canvas>
<button onclick="rotate()">Rotate</button>
<button onclick="next()">Next shape</button>
like image 119
Arnauld Avatar answered Oct 02 '22 22:10

Arnauld


I think your issue is that you are always assuming your piece is 4 tiles wide. You may want to shrink wrap your matrix to the smallest space that is still a square. For your Z/S blocks, it would be 3x3. Then the center of your rotation would act correctly.

Your issue right now is that the rotation is kind of working correctly, but the center of your brick is at cell (2, 2) rather than (1, 1) (assuming base 0). C is the reference frame around which your rotation is being applied.

[x][ ][ ][ ][ ]      [ ][ ][X][X][ ]
[X][X][ ][ ][ ]      [ ][X][X][ ][ ]
[ ][X][C][ ][ ]  =>  [ ][ ][C][ ][ ]
[ ][ ][ ][ ][ ]      [ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ]      [ ][ ][ ][ ][ ]

If you could shrink wrap your shape, you could apply your rotation and acheive the following:

[x][ ][ ]      [ ][X][X]
[X][C][ ]  =>  [X][C][ ]
[ ][X][ ]      [ ][ ][ ]
like image 37
zero298 Avatar answered Oct 02 '22 22:10

zero298