В этот раз я решил писать тетрис самым простым способом — не делать из него парад архитектурных решений, а реализовать его максимально стройно и легко.
единственная структура данных, которую я решил использовать - это точка: function Point(x, y){ this.x = x; this.y = y; } Point.prototype = { x : 0, y : 0, constructor : Point}; Для фигур нужен минимум: - ассортимент названий фигур var MODELS = ['L', 'J', 'O', 'I', 'Z', 'S', 'T']; - их конфигурации (относительные координаты точек, составляющих фигуру) var CONFIGS = { 'L' : [new Point(0, -2), new Point(0, -1), new Point(0, 0), new Point(1, 0)], 'J' : [new Point(0, -2), new Point(0, -1), new Point(0, 0), new Point( -1, 0)], 'O' : [new Point(0, -1), new Point(1, -1), new Point(1, 0), new Point(0, 0)], 'I' : [new Point(0, -2), new Point(0, -1), new Point(0, 0), new Point(0, 1)], 'Z' : [new Point(-1, -1), new Point(0, -1), new Point(0, 0), new Point(1, 0)], 'S' : [new Point(1, -1), new Point(0, -1), new Point(0, 0), new Point(-1, 0)], 'T' : [new Point(-1, 0), new Point(0, 0), new Point(1, 0), new Point(0, 1)] }; - чтобы это все выглядело не так уныло - цвета var COLORS = ['#ff00ff', '#ff0000', '#00ff00', '#0000ff', '#ffff00']; function randomColor(){ return COLORS[Math.floor(Math.random() * COLORS.length)]; } - и, собственно, генератор новой фигуры (единственный момент, где известна ее конфигурация) function getRandomTetr(){ return CONFIGS[MODELS[Math.floor(Math.random() * MODELS.length)]]; } Для стакана (10x22) нужно: - цвета пустых и сгорающих клеток var EMPTY = '#c0c0c0'; var DEAD = '#ffffff'; - генератор пустой строки function getEmptyLine(){//10 empty (white) cells return [EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY]; } - генератор сгорающей строки function paintFullLine(arr){ for(var i = 0; i < arr.length; i++){ arr[i] = DEAD; } } - узнать про клетку, занята она или свободна, можно по цвету (и исходя из этого определить, заполнена ли линия) function isLineFull(arr){ for(var i = 0; i < arr.length; i++){ if(arr[i] == EMPTY){ return false; } } return true; } - само поле - просто массив строк (которые тоже массивы): var fieldPoints = []; - инициализация поля пустыми клетками function initFieldPoints(){ fieldPoints = []; for(var i = 0; i < 22; i++){ fieldPoints.push(getEmptyLine()); } } - определение, свободна ли клетка (и находится ли она в пределах поля) function isPointFree(x, y){ if(!fieldPoints[y]){ return false; } if(!fieldPoints[y][x]){ return false; } return fieldPoints[y][x] == EMPTY; } - проверка, легально ли положение фигуры (arr - 4 точки, составляющие фигуру, в абсолютных координатах) function allowTetr(arr){ for(var i = 0; i < arr.length; i++){ if(!isPointFree(arr[i].x, arr[i].y)){ return false; } } return true; } - добавление точки на поле (происходит когда фигура упала, для "вмораживания" в поле) function addPoint(x, y, color){ fieldPoints[y][x] = color; } Данные и методы для текущей фигуры: - угол, цвет, координаты, массив точек (4 клетки, составляющие фигуру, в координатах относительно центра вращения фигуры, как в CONFIGS) var angle = 0; var x = 4; var y = 2; var points = []; var pColor = '#0000ff'; - перевод координат клеток в глобальную систему (опционально - со смещением по осям - для проверки легальности смещений) function globalTetrPosition(arr, dx, dy){ dx = dx || 0; dy = dy || 0; var res = []; for(var i = 0; i < arr.length; i++){ res[i] = new Point(arr[i].x + x + dx, arr[i].y + y + dy); } return res; } - "смерть" упавшей фигуры, она становится частью поля function die(){ var arr = globalTetrPosition(points); for(var i = 0; i < arr.length; i++){ addPoint(arr[i].x, arr[i].y, pColor); } } - смещение влево/вправо (d - дельта по оси x) function move(d){ if(allowTetr(globalTetrPosition(points, d))){ x += d; } } - поворот function rotate(){ var a = angle + 90 > 270 ? 0 : angle + 90; var arr = []; if(a == 90 || a == 270){ for(var i = 0; i < points.length; i++){ arr[i] = new Point(-points[i].y, points[i].x); } }else{ for(var i = 0; i < points.length; i++){ arr[i] = new Point(-points[i].y, points[i].x); } } //check if(allowTetr(globalTetrPosition(arr))){ angle = a; points = arr; } } - смещение на одну клетку вниз (возвращает легальность попытки), если ниже некуда - убивает фигуру, проверяте заполненность линий и генерит новую function down(){ //check if(allowTetr(globalTetrPosition(points, 0, 1))){ ++y; return true; } die(); checkFullLines(); createTetr(); return false; } - сброс фигуры вниз function drop(){ while(down()){ } } Управление - постановка на паузу var paused = false; function togglePaused(){ paused = !paused; document.getElementById('status').innerHTML = paused ? 'paused' : 'playing'; } - обработка клавиатурного ввода function handleKey(evt){ switch(evt.keyCode){ case 37://left if(!paused)move(-1); break; case 39://right if(!paused)move(1); break; case 38://up if(!paused)rotate(); break; case 40://down if(!paused)down(); break; case 32://space if(!paused)drop(); break; case 27://escape togglePaused(); break; } evt.preventDefault();//нужно чтобы, например, не листалась страница по пробелу render();//отрисовка } Игровая логика - счет var score = 0; - создание новой фигуры function createTetr(){ points = getRandomTetr(); angle = 0; x = 4 + Math.round(Math.random()); y = 2; pColor = randomColor(); if(!allowTetr(globalTetrPosition(points))){//проверка, влезает ли фигура в момент создания, если нет - увы, гейм овер paused = true; document.getElementById('status').innerHTML = 'GAME OVER'; render(); } } - метод, проверяющий, есть ли сгорающие строки function checkFullLines(){ //набивает массив строк, которые сгорят var toKill = []; for(var i = fieldPoints.length - 1; i >= 0; i--){ if(isLineFull(fieldPoints[i])){ toKill.push(i); paintFullLine(fieldPoints[i]); } } //рисует render(); if(toKill.length){ //вычисляет прирост счета (больше строк - больше бонус) var deltaScore = 100 * toKill.length + (toKill.length - 1) * (toKill.length - 1) * 100; addScore(deltaScore); document.getElementById('dscore').innerHTML = '+ ' + deltaScore; //ставит паузу... paused = true; //и через 150 миллисекунд снимает с нее, удаляет сгоревшие строки и набивает новые //это нужно, чтобы можно было посмотреть, какие строчки горят setTimeout(function(){ for(var k = 0; k < toKill.length; k++){ fieldPoints.splice(toKill[k], 1); } for(var k = 0; k < toKill.length; k++){ fieldPoints.unshift(getEmptyLine()); } toKill = []; render(); document.getElementById('dscore').innerHTML = ''; paused = false; }, 150); } } - (ре)старт игры: (пере)запуск таймера, сброс счета и так далее var intervalID = ''; function start(re){ window.clearInterval(intervalID); initFieldPoints(); paused = false; document.getElementById('status').innerHTML = 'playing'; addScore(-score); createTetr(); intervalID = window.setInterval(step, 600); render(); } - шаг симуляции (падение на одну клетку) function step(){ if(paused){ return; } down(); render(); } Визуальная часть (рендер на canvas, текст просто в HTML) - счет function addScore(d){ score += d; document.getElementById('score').innerHTML = score; } - сторона клетки var CELL_SIZE = 20; - создание пути квадрата function pathRect(gr, x, y, w, h){ gr.moveTo(x, y); gr.lineTo(x + w, y); gr.lineTo(x + w, y + h); gr.lineTo(x, y + h); gr.lineTo(x, y); } - заливка и рисование пути function graphicsRender(gr){ gr.fill(); gr.stroke(); } - отрисовка function render(){ //PRE - RENDER //фигура var currentPos = globalTetrPosition(points); //цвета, которыми надо рисовать var colors = {}; //для каждой клетки поля for(var i = 0; i < 22; i++){//y for(var k = 0; k < 10; k++){//x //если нет такого цвета - создаем массив if(!colors[fieldPoints[i][k]]){ colors[fieldPoints[i][k]] = []; } //и пишем туда клетку, которую надо залит этим цветом colors[fieldPoints[i][k]].push(new Point(k, i)); } } //это же для текущей фигуры for(var c = 0; c < currentPos.length; c++){ if(!colors[pColor]){ colors[pColor] = []; } colors[pColor].push(new Point(currentPos[c].x, currentPos[c].y)); } //RENDER //контекст var can = document.getElementById('canvas'); var graphics = can.getContext('2d'); //очистка graphics.clearRect(0,0,10 * CELL_SIZE, 22 * CELL_SIZE); //цвет границ между клетками и толщина graphics.strokeStyle = "#000000"; graphics.lineWidth = 1; //для каждого цвета for(var k in colors){ graphics.beginPath(); graphics.fillStyle = k; //добавляем в путь каждую клетку for(var i = 0; i < colors[k].length; i++){ pathRect(graphics, colors[k][i].x * CELL_SIZE, colors[k][i].y * CELL_SIZE, CELL_SIZE, CELL_SIZE); } //и рисуем полученный путь graphicsRender(graphics); } }
Ну и вариант целиком, чтобы сразу запустить:
<html> <head> <title>TETRIS - FIRST PLAYABLE</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> .button { border: 1px solid black; padding: 15px; border-radius:10px; font-size: 16px; cursor: pointer; } </style> <script type="text/javascript"> //point class function Point(x, y){ this.x = x; this.y = y; } Point.prototype = { x : 0, y : 0, constructor : Point}; //tetrs theory var MODELS = ['L', 'J', 'O', 'I', 'Z', 'S', 'T']; var CONFIGS = { 'L' : [new Point(0, -2), new Point(0, -1), new Point(0, 0), new Point(1, 0)], 'J' : [new Point(0, -2), new Point(0, -1), new Point(0, 0), new Point( -1, 0)], 'O' : [new Point(0, -1), new Point(1, -1), new Point(1, 0), new Point(0, 0)], 'I' : [new Point(0, -2), new Point(0, -1), new Point(0, 0), new Point(0, 1)], 'Z' : [new Point(-1, -1), new Point(0, -1), new Point(0, 0), new Point(1, 0)], 'S' : [new Point(1, -1), new Point(0, -1), new Point(0, 0), new Point(-1, 0)], 'T' : [new Point(-1, 0), new Point(0, 0), new Point(1, 0), new Point(0, 1)] }; var COLORS = ['#ff00ff', '#ff0000', '#00ff00', '#0000ff', '#ffff00']; function randomColor(){ return COLORS[Math.floor(Math.random() * COLORS.length)]; } function getRandomTetr(){ return CONFIGS[MODELS[Math.floor(Math.random() * MODELS.length)]]; } //field statics //field 10x20 //access fieldPoints[y][x] var EMPTY = '#c0c0c0'; var DEAD = '#ffffff'; var CELL_SIZE = 20; function getEmptyLine(){//10 empty (white) cells return [EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY]; } function isLineFull(arr){ for(var i = 0; i < arr.length; i++){ if(arr[i] == EMPTY){ return false; } } return true; } function paintFullLine(arr){ for(var i = 0; i < arr.length; i++){ arr[i] = DEAD; } } var score = 0; function checkFullLines(){ var toKill = []; for(var i = fieldPoints.length - 1; i >= 0; i--){ if(isLineFull(fieldPoints[i])){ toKill.push(i); paintFullLine(fieldPoints[i]); } } render(); if(toKill.length){ var deltaScore = 100 * toKill.length + (toKill.length - 1) * (toKill.length - 1) * 100; addScore(deltaScore); document.getElementById('dscore').innerHTML = '+ ' + deltaScore; console.log('+ ' + deltaScore); paused = true; setTimeout(function(){ for(var k = 0; k < toKill.length; k++){ fieldPoints.splice(toKill[k], 1); } for(var k = 0; k < toKill.length; k++){ fieldPoints.unshift(getEmptyLine()); } toKill = []; render(); document.getElementById('dscore').innerHTML = ''; paused = false; }, 150); } } var fieldPoints = []; function initFieldPoints(){ fieldPoints = []; for(var i = 0; i < 22; i++){ fieldPoints.push(getEmptyLine()); } } function isPointFree(x, y){ if(!fieldPoints[y]){ return false; } if(!fieldPoints[y][x]){ return false; } return fieldPoints[y][x] == EMPTY; } function allowTetr(arr){ for(var i = 0; i < arr.length; i++){ if(!isPointFree(arr[i].x, arr[i].y)){ return false; } } return true; } function addPoint(x, y, color){ fieldPoints[y][x] = color; } //current tetr var angle = 0; var x = 4; var y = 2; var points = []; var pColor = '#0000ff'; function createTetr(){ points = getRandomTetr(); angle = 0; x = 4 + Math.round(Math.random()); y = 2; pColor = randomColor(); if(!allowTetr(globalTetrPosition(points))){ paused = true; document.getElementById('status').innerHTML = 'GAME OVER'; render(); } } function rotate(){ var a = angle + 90 > 270 ? 0 : angle + 90; var arr = []; if(a == 90 || a == 270){ for(var i = 0; i < points.length; i++){ arr[i] = new Point(-points[i].y, points[i].x); } }else{ for(var i = 0; i < points.length; i++){ arr[i] = new Point(-points[i].y, points[i].x); } } //check if(allowTetr(globalTetrPosition(arr))){ angle = a; points = arr; } } function move(d){ //check if(allowTetr(globalTetrPosition(points, d))){ x += d; } } /** * * @returns {boolean - can move further down} */ function down(){ //check if(allowTetr(globalTetrPosition(points, 0, 1))){ ++y; return true; } die(); checkFullLines(); createTetr(); return false; } function drop(){ while(down()){ } } function die(){ var arr = globalTetrPosition(points); for(var i = 0; i < arr.length; i++){ addPoint(arr[i].x, arr[i].y, pColor); } } function globalTetrPosition(arr, dx, dy){ dx = dx || 0; dy = dy || 0; var res = []; for(var i = 0; i < arr.length; i++){ res[i] = new Point(arr[i].x + x + dx, arr[i].y + y + dy); } return res; } /** * GAMEPLAY */ //var glass; var intervalID = ''; function start(re){ window.clearInterval(intervalID); initFieldPoints(); paused = false; document.getElementById('status').innerHTML = 'playing'; addScore(-score); createTetr(); intervalID = window.setInterval(step, 600); render(); } var paused = false; function togglePaused(){ paused = !paused; document.getElementById('status').innerHTML = paused ? 'paused' : 'playing'; } function step(){ if(paused){ return; } down(); render(); } /** * INPUT */ var action = ''; function handleKey(evt){ //console.log(evt); switch(evt.keyCode){ case 37://left if(!paused)move(-1); break; case 39://right if(!paused)move(1); break; case 38://up if(!paused)rotate(); break; case 40://down if(!paused)down(); break; case 32://space if(!paused)drop(); break; case 27://escape togglePaused(); break; } evt.preventDefault(); render(); } /** * RENDER */ function drawRect(gr, x, y, w, h){ gr.fillRect(x, y, w, h); gr.strokeRect(x, y, w, h); } function pathRect(gr, x, y, w, h){ gr.moveTo(x, y); gr.lineTo(x + w, y); gr.lineTo(x + w, y + h); gr.lineTo(x, y + h); gr.lineTo(x, y); } function graphicsRender(gr){ gr.fill(); gr.stroke(); } function render(){ //PRE - RENDER var currentPos = globalTetrPosition(points); var colors = {}; for(var i = 0; i < 22; i++){//y for(var k = 0; k < 10; k++){//x if(!colors[fieldPoints[i][k]]){ colors[fieldPoints[i][k]] = []; } colors[fieldPoints[i][k]].push(new Point(k, i)); } } for(var c = 0; c < currentPos.length; c++){ if(!colors[pColor]){ colors[pColor] = []; } colors[pColor].push(new Point(currentPos[c].x, currentPos[c].y)); } //RENDER var can = document.getElementById('canvas'); var graphics = can.getContext('2d'); graphics.clearRect(0,0,10 * CELL_SIZE, 22 * CELL_SIZE); graphics.strokeStyle = "#000000"; graphics.lineWidth = 1; for(var k in colors){ graphics.beginPath(); graphics.fillStyle = k; for(var i = 0; i < colors[k].length; i++){ pathRect(graphics, colors[k][i].x * CELL_SIZE, colors[k][i].y * CELL_SIZE, CELL_SIZE, CELL_SIZE); } graphicsRender(graphics); } } function addScore(d){ score += d; document.getElementById('score').innerHTML = score; } </script> </head> <body onload="start()" onkeydown="handleKey(event)"> <button onclick="start(true)">reset</button> <span id="status">playing</span> | score = <span id="score">0</span><span id="dscore"></span> <button onclick="togglePaused()">ESC (pause)</button> <canvas width="200" height="440" id="canvas"></canvas> <div style="clear:both;"> <button class="button" onclick="rotate()">↑ (rotate)</button><button class="button" onclick="drop()">SPACE (drop)</button> <button class="button" onclick="move(-1)">←</button> <button class="button" onclick="down()">↓</button> <button class="button" onclick="move(1)">→</button> </div> </body> </html>
Спасибо за подробное кодирование, пояснения и прекрасный результат — отлично работающий код. Для внука искала, теперь будем осваивать потихоньку, время у нас еще есть!