DEV Community

Cover image for Creando un Tetris con JavaScript III: el tablero

Creando un Tetris con JavaScript III: el tablero

En nuestro Tetris necesitamos un tablero donde poner las piezas, que el usuario pueda moverlas o rotarlas, etc.

De la misma forma que utilizamos una matriz para representar las piezas, utilizaremos una matriz para representar la totalidad del tablero. Al principio, todo el tablero estará vacío, pero a medida que vayan bajando piezas, estas se irán añadiendo a la parte inferior del mismo. Es decir, cuando una pieza no pueda bajar más (bien porque ha alcanzado la parte inferior del tablero, o bien porque se ha ido a posar en antiguas piezas o partes de piezas que ahora lo conforman), portaremos esa pieza al tablero como una parte más.

Mientras una pieza esté bajando, tendremos que pintarla encima del tablero, pero no pasará a formar parte de él (de otra forma no podríamos detectar colisiones, al no saber qué puntos forman parte de la pieza y cuáles del tablero).

Tablero de Tetris, vacío, con una barra bajando

Así, a continuación podemos ver un tablero de juego de juego de 10 filas y 5 columnas, y su representación, además de cómo podemos crearlo con JavaScript.

Tablero de Tetris, vacío.

let board = [ /* Fila 0 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 1 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 2 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 3 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 4 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 5 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 6 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 7 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 8 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 9 */ [ 0, 0, 0, 0, 0 ] ];
Enter fullscreen mode Exit fullscreen mode

Al final, lo que tenemos es una matriz creada a partir de un vector de vectores. Igual que con las piezas, el vector principal representa a las filas, mientras que cada uno de los vectores que lo forman son las columnas de esa fila.

El código que tenemos ahí arriba es perfectamente válido para crear una matriz 10x5, pero... ¿y si quisiérsmo crear una matriz de forma que no supiéramos el número de filas y columnas previamente? Pues podemos crear cada vector como un objeto Array. Le pasaremos el número de elementos necesario. Además, el método fill() lo podemos emplear para inicializar cada elemento (a partir de ahora, lo llamaremos celda), a cero.

Al final, si el tablero se llama board, accederemos a cada celda con un número entre corchetes que representará la fila, y otro número entre corchetes que representará la columna. El primer acceso con corchetes nos devolverá el vector de esa fila, mientras que el segundo número nos devolverá la celda específica. Solo hay que recordar que en JavaScript todos los Array's empiezan en cero.

Así, para crear nuestro tablero, podríamos crear una función como la siguiente:

function createBoard(rows, cols)
{
    let toret = new Array(rows);

    for(let i = 0; i < rows; ++i) {
        toret[ i ] = new Array( cols ).fill( 0 );
    }

    return toret;
}
Enter fullscreen mode Exit fullscreen mode

Para acceder a una celda en concreto, podríamos utilizar una función como la siguiente:

function cellFrom(board, row, col)
{
    const ROWS = board.length;
    const COLS = board[ 0 ].length;

    if ( row < 0
      || row >= ROWS )
    {
        throw new RangeError( "row: 0 - " + ROWS + ": " + row + "??" );
    }

    if ( col < 0
      || col >= COLS )
    {
        throw new RangeError( "col: 0 - " + COLS + ": " + col + "??" );
     }

     return board[ row ][ col ];
}
Enter fullscreen mode Exit fullscreen mode

Es cierto que ern definitiva vamos a utilizar una clase, pero probablemente ver estas funciones individuales nos ayude a entender el concepto.

Así, para acceder a la tercera columna de la primera fila, llamaríamos a:

console.log( cellFrom( board, 0, 2 ) );
Enter fullscreen mode Exit fullscreen mode

Utilizamos excepciones para detectar los casos en los que pedimos valores de fila o columna fuera de rango. La excepción estándar RangeError es la adecuada; tendremos que pasarle un mensaje de error, como a cualquier otra.

Tablero de Tetris con restos de piezas y una pieza bajando.

La situación en la imagen de arriba se representaría en el tablero de la siguiente forma:

let board = [ /* Fila 0 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 1 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 2 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 3 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 4 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 5 */ [ 0, 0, 0, 0, 0 ],
              /* Fila 6 */ [ 0, 0, 0, 1, 1 ],
              /* Fila 7 */ [ 1, 1, 0, 1, 1 ],
              /* Fila 8 */ [ 0, 1, 1, 0, 1 ],
              /* Fila 9 */ [ 1, 1, 0, 1, 0 ] ];
Enter fullscreen mode Exit fullscreen mode

¿Dudas? ¡No dudes en ponérmelas en los comentarios!

De acuerdo, entonces podemos crear una nueva clase Board que cree y mantenga el tablero con las celdas que necesitamos.

class Board {
    static PIXEL_SIZE = 24;
    #_color = "darkgreen";
    #_rows;
    #_cols;
    #_board;

    constructor(rows, cols, color)
    {
        this._rows = rows;
        this._cols = cols;

        if ( color != null ) {
            this._color = color;
        }

        this._board = new Array( rows );
        for(let i = 0; i < rows; ++i) {
            this._board[ i ] = new Array( cols ).fill( 0 );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Con la clase anterior, tenemos claro cómo se crean el tablero, de hecho en el constructor de la clase.

Necesitamos también getters que nos devuelvan, por ejemplo, el número de filas (rows) y columnas (cols).

Pero aparte de todo eso, necesitaremos sendos métodos, además, que nos devuelva, y nos permita cambiar, una celda determinada.

class Board {
    // más cosas...

    cell(row, col)
    {
        if ( row < 0
          || row >= this.rows)
        {
            throw new RangeError( "row: 0 - " + this.rows + ": " + row + "??" );
        }

        if ( col < 0
          || col >= this.cols)
        {
            throw new RangeError( "col: 0 - " + this.cols + ": " + col + "??" );
        }

        return this._board[ row ][ col ];
    }

    setCell(row, col, val)
    {
        if ( val == null ) {
            val = 1;
        }

        if ( row < 0
          || row >= this.rows)
        {
            throw new RangeError( "row: 0 - " + this.rows + ": " + row + "??" );
        }

        if ( col < 0
          || col >= this.cols)
        {
            throw new RangeError( "col: 0 - " + this.cols + ": " + col + "??" );
        }


        this._board[ row ][ col ] = val;
    }
}
Enter fullscreen mode Exit fullscreen mode

Además, a medida que avance el juego, tendremos que eliminar filas del tablero (porque se han rellenado de celdas con '1', es decir, están llenas) e insertar filas al principio del tablero (para compensar las filas que hemos eliminado).

class Board {
    // Más cosas...

    insertEmptyRows(numRows)
    {
        for(let i = 0; i < numRows; ++i) {
            this._board.unshift( new Array( this.cols ).fill( 0 ) );
        }
    }

    removeRows(listRows)
    {
        let newBoard = [];

        for(let numRow = 0; numRow < this._board.length; ++numRow) {
            if ( !listRows.includes( numRow ) ) {
                newBoard.push( this._board[ numRow ] );
            }
        }

        this._board = newBoard;
    }
}
Enter fullscreen mode Exit fullscreen mode

Bueno, tenemos el core del juego creado. Nos falta la vista para representar tablero y piezas bajando, y el gestor del juego, que lo arranque, lo pare, haga descender piezas, y preste atención a las teclas pulsadas por el usuario.

En la próxima entrega veremos la clase Canvas, que se emparejará con un elemento Canvas de HTML, y se encargará de pintar el tablero.

Top comments (0)