Наверх

Ещё один вариант тетриса от www0z0k

Вот пример создания тетриса от другого программиста — www0z0k. Тут гораздо больше используется преимуществ ООП, да и работает, по-моему, шустрее.


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


2 комментария

  1. Елена 30 ноября 2018, 02:55 # 0
    Спасибо огромное за уроки и за коды для Тетрис, из всего интернета только у вас нашла то, что нам было нужно!
    Спасибо за подробное кодирование, пояснения и прекрасный результат — отлично работающий код. Для внука искала, теперь будем осваивать потихоньку, время у нас еще есть!
    1. Гость 03 декабря 2018, 15:07 # 0
      Спасибо, отличные уроки, будем вникать

      Авторизация

      через сервис Loginza:


      Шаблоны MODX

      1 2 Дальше »

      Объектная
      модель
      MODX