В этот раз я решил писать тетрис самым простым способом — не делать из него парад архитектурных решений, а реализовать его максимально стройно и легко.
единственная структура данных, которую я решил использовать - это точка:
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>
Объектная
Спасибо за подробное кодирование, пояснения и прекрасный результат — отлично работающий код. Для внука искала, теперь будем осваивать потихоньку, время у нас еще есть!