<!DOCTYPE html><html><head><title></title><style>
html, body {height:100%;margin:0;}
body {background: black;display: flex;align-items: center;justify-content: center;}
canvas {border:1px solid white;}</style></head><body><canvaswidth="320"height="640"id="game"></canvas><script>// https://tetris.fandom.com/wiki/Tetris_Guideline// get a random integer between the range of [min,max]// @see https://stackoverflow.com/a/1527820/2124254function getRandomInt(min, max){
min =Math.ceil(min);
max =Math.floor(max);returnMath.floor(Math.random()*(max - min +1))+ min;}// generate a new tetromino sequence// @see https://tetris.fandom.com/wiki/Random_Generatorfunction generateSequence(){const sequence =['I','J','L','O','S','T','Z'];while(sequence.length){const rand = getRandomInt(0, sequence.length -1);const name = sequence.splice(rand,1)[0];
tetrominoSequence.push(name);}}// get the next tetromino in the sequencefunction getNextTetromino(){if(tetrominoSequence.length ===0){
generateSequence();}const name = tetrominoSequence.pop();const matrix = tetrominos[name];// I and O start centered, all others start in left-middleconst col = playfield[0].length /2-Math.ceil(matrix[0].length /2);// I starts on row 21 (-1), all others start on row 22 (-2)const row = name ==='I'?-1:-2;return{
name: name,// name of the piece (L, O, etc.)
matrix: matrix,// the current rotation matrix
row: row,// current row (starts offscreen)
col: col // current col};}// rotate an NxN matrix 90deg// @see https://codereview.stackexchange.com/a/186834function rotate(matrix){const N = matrix.length -1;const result = matrix.map((row, i)=>
row.map((val, j)=> matrix[N - j][i]));return result;}// check to see if the new matrix/row/col is validfunction isValidMove(matrix, cellRow, cellCol){for(let row =0; row < matrix.length; row++){for(let col =0; col < matrix[row].length; col++){if(matrix[row][col]&&(// outside the game bounds
cellCol + col <0||
cellCol + col >= playfield[0].length ||
cellRow + row >= playfield.length ||// collides with another piece
playfield[cellRow + row][cellCol + col])){returnfalse;}}}returntrue;}// place the tetromino on the playfieldfunction placeTetromino(){for(let row =0; row < tetromino.matrix.length; row++){for(let col =0; col < tetromino.matrix[row].length; col++){if(tetromino.matrix[row][col]){// game over if piece has any part offscreenif(tetromino.row + row <0){return showGameOver();}
playfield[tetromino.row + row][tetromino.col + col]= tetromino.name;}}}// check for line clears starting from the bottom and working our way upfor(let row = playfield.length -1; row >=0;){if(playfield[row].every(cell =>!!cell)){// drop every row above this onefor(let r = row; r >=0; r--){for(let c =0; c < playfield[r].length; c++){
playfield[r][c]= playfield[r-1][c];}}}else{
row--;}}
tetromino = getNextTetromino();}// show the game over screenfunction showGameOver(){
cancelAnimationFrame(rAF);
gameOver =true;
context.fillStyle ='black';
context.globalAlpha =0.75;
context.fillRect(0, canvas.height /2-30, canvas.width,60);
context.globalAlpha =1;
context.fillStyle ='white';
context.font ='36px monospace';
context.textAlign ='center';
context.textBaseline ='middle';
context.fillText('GAME OVER!', canvas.width /2, canvas.height /2);}const canvas = document.getElementById('game');const context = canvas.getContext('2d');const grid =32;const tetrominoSequence =[];// keep track of what is in every cell of the game using a 2d array// tetris playfield is 10x20, with a few rows offscreenconst playfield =[];// populate the empty statefor(let row =-2; row <20; row++){
playfield[row]=[];for(let col =0; col <10; col++){
playfield[row][col]=0;}}// how to draw each tetromino// @see https://tetris.fandom.com/wiki/SRSconst tetrominos ={'I':[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],'J':[[1,0,0],[1,1,1],[0,0,0],],'L':[[0,0,1],[1,1,1],[0,0,0],],'O':[[1,1],[1,1],],'S':[[0,1,1],[1,1,0],[0,0,0],],'Z':[[1,1,0],[0,1,1],[0,0,0],],'T':[[0,1,0],[1,1,1],[0,0,0],]};// color of each tetrominoconst colors ={'I':'cyan','O':'yellow','T':'purple','S':'green','Z':'red','J':'blue','L':'orange'};let count =0;let tetromino = getNextTetromino();let rAF =null;// keep track of the animation frame so we can cancel itlet gameOver =false;// game loopfunction loop(){
rAF = requestAnimationFrame(loop);
context.clearRect(0,0,canvas.width,canvas.height);// draw the playfieldfor(let row =0; row <20; row++){for(let col =0; col <10; col++){if(playfield[row][col]){const name = playfield[row][col];
context.fillStyle = colors[name];// drawing 1 px smaller than the grid creates a grid effect
context.fillRect(col * grid, row * grid, grid-1, grid-1);}}}// draw the active tetrominoif(tetromino){// tetromino falls every 35 framesif(++count >35){
tetromino.row++;
count =0;// place piece if it runs into anythingif(!isValidMove(tetromino.matrix, tetromino.row, tetromino.col)){
tetromino.row--;
placeTetromino();}}
context.fillStyle = colors[tetromino.name];for(let row =0; row < tetromino.matrix.length; row++){for(let col =0; col < tetromino.matrix[row].length; col++){if(tetromino.matrix[row][col]){// drawing 1 px smaller than the grid creates a grid effect
context.fillRect((tetromino.col + col)* grid,(tetromino.row + row)* grid, grid-1, grid-1);}}}}}// listen to keyboard events to move the active tetromino
document.addEventListener('keydown',function(e){if(gameOver)return;// left and right arrow keys (move)if(e.which ===37|| e.which ===39){const col = e.which ===37? tetromino.col -1: tetromino.col +1;if(isValidMove(tetromino.matrix, tetromino.row, col)){
tetromino.col = col;}}// up arrow key (rotate)if(e.which ===38){const matrix = rotate(tetromino.matrix);if(isValidMove(matrix, tetromino.row, tetromino.col)){
tetromino.matrix = matrix;}}// down arrow key (drop)if(e.which ===40){const row = tetromino.row +1;if(!isValidMove(tetromino.matrix, row, tetromino.col)){
tetromino.row = row -1;
placeTetromino();return;}
tetromino.row = row;}});// start the game
rAF = requestAnimationFrame(loop);</script></body></html>