Перейти к содержанию
С 1 января 2024 года клиент Steam будет поддерживать только Windows 10 и более поздние версии.

Simple Tetris JS


Рекомендуемые сообщения

  • Администратор

Simple Tetris JS


Это самый простой тетрис на JavaScript. Его изменениями, модификацией и доработкой занимаются все желающие в свое свободное время. За основу был взят код с Github, я честно уже не помню чей, я знаю, что набрал в поиске Тетрис, взял первый же JavaScript код, и начал его модифицировать. По итогу, у этого тетриса присутствуют следующие особенности:

  • Стандартное поведение браузера блокируется, что бы не было такого, если вы нажимаете стрелочку вверх или вниз, страница не уезжала и не мешала вам играть. Но при этом, если на странице открыто какое либо поле ввода для текста, например форма логин и пароля для входа на сайт, то блокировка на время активности доп. окна не работает. За репорт спасибо @Ed MSL @Valsorya
  • Указана ссылка на тему поддержки. Само собой вы ее можете заменить.
  • Добавлен счет, который отображает количество уничтоженных линий и текущую скорость тетромино.
  • Добавлена возможность установить свои звуки в игру
  • При проигрыше фоновая музыка останавливается
  • Присутствует контейнер, который показывает следующий тетромино, за доработку спасибо @Ed MSL
  • Присутствует функционал паузы игры
  • @Ed MSL добавил кнопку отвечающую за старт игры 

 

А ну ка пыль сдуй отсюда!

Ссылка на комментарий
Поделиться на другие сайты

  • Администратор

Добавил ссылку ведущую в эту тему, и чуть чуть украсил страницу.

А ну ка пыль сдуй отсюда!

Ссылка на комментарий
Поделиться на другие сайты

  • Администратор
1 час назад, Valsorya сказал:

108??

Это подсказка.

Добавил счет на страницу с тетрисом. Который отображает количество уничтоженных линий и текущую скорость тетромино. Я не стал даже пытаться делать таблицу лидеров, ибо это надо таблицу создавать в базе данных, а это сложно для меня. Поэтому я посчитал, что с текущим трафиком таблица лидеров не нужна. А вот счет, для своего личного анализа, подойдет.

А ну ка пыль сдуй отсюда!

Ссылка на комментарий
Поделиться на другие сайты

  • Администратор

Начал заниматься музыкальным оформлением. Пока добавил только фоновый музончик. Я скомпилировал два ремикса коробейников, пока думаю этого хватит.

Не думаю, что кто то сможет играть дольше, чем длина этих двух ремиксов.

А ну ка пыль сдуй отсюда!

Ссылка на комментарий
Поделиться на другие сайты

  • Администратор

Фоновая музыка теперь при проигрыше будет останавливаться.

А ну ка пыль сдуй отсюда!

Ссылка на комментарий
Поделиться на другие сайты

  • Администратор

Добавил контейнер, который показывает следующий блок. Это поможет набирать больший счет.

Дизайном пока не занимался. Я думаю это надо делать в конце, после реализации всех своих идей.

Добавил возможность включить или отключить музыку:

document.addEventListener('keydown', function(e) {
  if (e.which === 77) { // Код клавиши "M"
    toggleSound();
  }
});

function toggleSound() {
  isSoundEnabled = !isSoundEnabled;

  if (isSoundEnabled) {
    bgMusic.play();
  } else {
    bgMusic.pause();
  }
}

let isSoundEnabled = true;

 

А ну ка пыль сдуй отсюда!

Ссылка на комментарий
Поделиться на другие сайты

  • Администратор

Так же реализовал паузу игры, для этого я добавил переменную для отслеживания паузы.

let isPaused = false;

Чуть чуть изменил функцию loop(), а именно в самом начале добавил следующее:

function loop() {
  if (isPaused) {
    return; // Остановить выполнение функции при паузе
  }

  rAF = requestAnimationFrame(loop);

Ну и добавил обработку события после нажатия клавиши:

document.addEventListener('keydown', function(e) {
  if (e.which === 80) { // Код клавиши "P"
    togglePause();
  }
});

function togglePause() {
  isPaused = !isPaused;

  if (isPaused) {
    cancelAnimationFrame(rAF); // Останавливаем анимацию и игровой цикл при паузе
    bgMusic.pause();
  } else {
    rAF = requestAnimationFrame(loop); // Возобновляем анимацию и игровой цикл после паузы
    bgMusic.play();
  }
}

 

Так же я попытался реализовать переворот тетромино у края игрового поля. Для добавления этой функциональности я использовал следующий подход:

В функции isValidMove(matrix, cellRow, cellCol) добавил проверку на возможность переворота тетромино у края игрового поля. Это можно сделать путем проверки координаты cellCol + col внутри цикла на выход за границы игрового поля.

function 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] && (
          cellCol + col < 0 ||
          cellCol + col >= playfield[0].length ||
          cellRow + row >= playfield.length ||
          playfield[cellRow + row][cellCol + col]
        )
      ) {
        return false;
      }
    }
  }
  return true;
}

В обработчике события keydown (Это как раз таки тот обработчик, который я добавлял первым, на блокировка прокрутки браузера) я добавил проверку на код клавиши "38" (стрелка вверх) и реализовал переворот тетромино, только если это возможно:

if (e.which === 38) {
  const matrix = rotate(tetromino.matrix);
  const col = tetromino.col;

  if (isValidMove(matrix, tetromino.row, col)) {
    tetromino.matrix = matrix;
  }
}

Таким образом, после внесения этих изменений, тетромино стал переворачиваться при нажатии клавиши "стрелка вверх", даже если оно находится у края игрового поля.

А ну ка пыль сдуй отсюда!

Ссылка на комментарий
Поделиться на другие сайты

  • Администратор

Я считаю игру оконченной.

image.png

Остальное зависит все от этой темы поддержки.

Кстати. Первый зафиксированный рекорд, это 70 уничтоженных линий.

 

А ну ка пыль сдуй отсюда!

Ссылка на комментарий
Поделиться на другие сайты

  • Администратор
6 часов назад, Ryancoolround сказал:

Кстати. Первый зафиксированный рекорд, это 70 уничтоженных линий.

Уже не актуально. Я уничтожил 166 линий.

 

А ну ка пыль сдуй отсюда!

Ссылка на комментарий
Поделиться на другие сайты

  • Администратор

Ладно. Учитывая то, что IP-Gamers.NET стал форумом поддержки, а версия тетриса у нас на сайте, потихоньку, полегоньку собрана мной, думаю самое время предоставить коды для встраивания игры на свои площадки. Ниже ядро игры, оно же JavaScript.

document.addEventListener('keydown', function(e) {
  if (e.which === 80) { // Код клавиши "P"
    togglePause();
  }
});

function togglePause() {
  isPaused = !isPaused;

  if (isPaused) {
    cancelAnimationFrame(rAF); // Останавливаем анимацию и игровой цикл при паузе
    bgMusic.pause();
  } else {
    rAF = requestAnimationFrame(loop); // Возобновляем анимацию и игровой цикл после паузы
    bgMusic.play();
  }
}


document.addEventListener('keydown', function(e) {
  if (e.which === 77) { // Код клавиши "M"
    toggleSound();
  }
});

function toggleSound() {
  isSoundEnabled = !isSoundEnabled;

  if (isSoundEnabled) {
    bgMusic.play();
  } else {
    bgMusic.pause();
  }
}

let isPaused = false;
let isSoundEnabled = true;

// Определение нового элемента <canvas> для отображения следующего тетромино
const nextPieceCanvas = document.getElementById('nextPiece');
const nextPieceContext = nextPieceCanvas.getContext('2d');
const nextPieceGrid = 20;

// Функция для отображения следующего тетромино
function drawNextPiece() {
  nextPieceContext.clearRect(0, 0, nextPieceCanvas.width, nextPieceCanvas.height);
  
  if (tetrominoSequence.length > 0) {
    const nextPieceName = tetrominoSequence[tetrominoSequence.length - 1];
    const nextPieceMatrix = tetrominos[nextPieceName];
  
    for (let row = 0; row < nextPieceMatrix.length; row++) {
      for (let col = 0; col < nextPieceMatrix[row].length; col++) {
        if (nextPieceMatrix[row][col]) {
          const x = col * nextPieceGrid;
          const y = row * nextPieceGrid;
          nextPieceContext.fillStyle = colors[nextPieceName];
          nextPieceContext.fillRect(x, y, nextPieceGrid - 1, nextPieceGrid - 1);
        }
      }
    }
  }
}

// Вызов функции отображения следующего тетромино при загрузке страницы
window.addEventListener('load', drawNextPiece);

// Генерация следующего тетромино
function getNextTetromino() {
  if (tetrominoSequence.length === 0) {
    generateSequence();
  }

  const name = tetrominoSequence.pop();
  const matrix = tetrominos[name];

  const col = playfield[0].length / 2 - Math.ceil(matrix[0].length / 2);

  const row = name === 'I' ? -1 : -2;

  // Отображение следующего тетромино
  drawNextPiece();

  return {
    name: name,
    matrix: matrix,
    row: row,
    col: col
  };
}

const bgMusic = document.getElementById('bgMusic');
const lineClearSound = document.getElementById('lineClearSound');
const gameOverSound = document.getElementById('gameOverSound');
bgMusic.volume = 0.6;
bgMusic.play();

// Счет Игры
function updateScore(clearedLines) {
  const lineScores = [0, 1]; 
  score += lineScores[clearedLines]; 
  level = Math.floor(score / 5) + 1; 
  updateSpeed(level);
  const scoreElement = document.getElementById('score');
  scoreElement.textContent = `Счет: ${score}`;

  // Сохраняем счет игрока в localStorage
  localStorage.setItem('playerScore', score.toString());
}

window.addEventListener('load', function() {
  const playerScore = localStorage.getItem('playerScore');
  if (playerScore) {
    score = parseInt(playerScore);
    const scoreElement = document.getElementById('score');
    scoreElement.textContent = `Счет: ${score}`;
  }
});

// Окончание Счета Игры


function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);

  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function 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);
  }
}

function getNextTetromino() {
  if (tetrominoSequence.length === 0) {
    generateSequence();
  }

  const name = tetrominoSequence.pop();
  const matrix = tetrominos[name];

  const col = playfield[0].length / 2 - Math.ceil(matrix[0].length / 2);

  const row = name === 'I' ? -1 : -2;

  return {
    name: name,      
    matrix: matrix,  
    row: row,        
    col: col         
  };
}

function rotate(matrix) {
  const N = matrix.length - 1;
  const result = matrix.map((row, i) =>
    row.map((val, j) => matrix[N - j][i])
  );

  return result;
}

function 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] && (
          cellCol + col < 0 ||
          cellCol + col >= playfield[0].length ||
          cellRow + row >= playfield.length ||
          playfield[cellRow + row][cellCol + col]
        )
      ) {
        return false;
      }
    }
  }
  return true;
}

function 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]) {
        if (tetromino.row + row < 0) {
          return showGameOver();
        }
        playfield[tetromino.row + row][tetromino.col + col] = tetromino.name;
      }
    }
    lineClearSound.play(); // Воспроизводим звук уничтожения линии
  }

  // Обновление следующего тетромино здесь
  tetromino = getNextTetromino();

  for (let row = playfield.length - 1; row >= 0;) {
    if (playfield[row].every(cell => !!cell)) {
      for (let r = row; r >= 0; r--) {
        for (let c = 0; c < playfield[r].length; c++) {
          playfield[r][c] = playfield[r-1][c];
        }
      }
      updateScore(1);
    }
    else {
      row--;
    }
  }
}

function showGameOver() {
  cancelAnimationFrame(rAF);
  bgMusic.pause();
  gameOverSound.play();
  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('108??', canvas.width / 2, canvas.height / 2);
}

const canvas = document.querySelector('canvas#game');
const context = canvas.getContext('2d');
const grid = 32;
const tetrominoSequence = [];

const playfield = [];

for (let row = -2; row < 20; row++) {
  playfield[row] = [];
  for (let col = 0; col < 10; col++) {
    playfield[row][col] = 0;
  }
}

const 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],
  ]
};

const colors = {
  'I': 'cyan',
  'O': 'yellow',
  'T': 'purple',
  'S': 'green',
  'Z': 'red',
  'J': 'blue',
  'L': 'orange'
};

let score = 0;
let count = 0;
let level = 1;
let speed = 35;
let tetromino = getNextTetromino();
let rAF = null;  
let gameOver = false;

function updateScore(clearedLines) {
  const lineScores = [0, 1]; 
  score += lineScores[clearedLines]; 
  level = Math.floor(score / 5) + 1; 
  updateSpeed(level);
  const scoreElement = document.getElementById('score');
  scoreElement.textContent = `Счет: ${score}`;
  
}

function drawScore() {
  const rowsElement = document.getElementById('rows');
  rowsElement.textContent = `Уничтожено линий: ${score}`;

  const levelElement = document.getElementById('level');
  levelElement.textContent = `Скорость: ${level}`;
}

function drawGrid() {
  context.strokeStyle = 'rgba(255, 255, 255, 0.2)';
  context.lineWidth = 0.5;

  for (let row = 0; row < 20; row++) {
    for (let col = 0; col < 10; col++) {
      context.strokeRect(col * grid, row * grid, grid, grid);
    }
  }
}

function loop() {
  if (isPaused) {
    return; // Остановить выполнение функции при паузе
  }

  rAF = requestAnimationFrame(loop);
  context.clearRect(0,0,canvas.width,canvas.height);
  drawGrid();
  for (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];

        context.fillRect(col * grid, row * grid, grid-1, grid-1);
      }
    }
  }

  if (tetromino) {

    if (++count > speed) {
      tetromino.row++;
      count = 0;

      if (!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]) {

          context.fillRect((tetromino.col + col) * grid, (tetromino.row + row) * grid, grid-1, grid-1);
        }
      }
    }
  }
  drawScore();
  drawNextPiece();
}

function updateSpeed(level) {
  speed = Math.max(10, 35 - (level * 5));
}

updateSpeed(1);

document.addEventListener('keydown', function(e) {
  if (gameOver) return;

  e.preventDefault(); // Отменяем стандартное поведение браузера

  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;
    }
  }
  if (e.which === 32) {
      let row = tetromino.row;
      while (isValidMove(tetromino.matrix, row + 1, tetromino.col)) {
        row++;
      }
      tetromino.row = row;
      placeTetromino();
      return;
    }

  if (e.which === 38) {
  const matrix = rotate(tetromino.matrix);
  const col = tetromino.col;

  if (isValidMove(matrix, tetromino.row, col)) {
    tetromino.matrix = matrix;
  }
}

  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;
  }
});

rAF = requestAnimationFrame(loop);

Далее встраиваем необходимые контейнеры на ту страницу, где должен отображаться тетрис. Обратите внимание, в данном коде вам надо заменить существующие ссылки на тему поддержки. И соответственно ссылки на звуки.

<center>
  <div class="centered-content">
    <canvas width="320" height="640" id="game"></canvas>
    <div class="sidebar">
      <canvas width="75" height="50" id="nextPiece"></canvas>
      <hr>
      <span id="score" style="font-size: 18px;"></span><br>
      <span id="rows" style="font-size: 14px;"></span><br>
      <span id="level" style="font-size: 14px;"></span>
      <hr>
      <a href="https://ip-gamers.net/topic/3218-podderzhka-igry-tetris-onlajn/">Перейти к основной теме поддержки</a>
      <hr>
      Включить / Отключить музыку = M<br>Пауза = P
    </div>
  </div>
  <audio id="gameOverSound">
    <source src="Здесь ссылка на звук, который воспроизводится при проигрыше" type="audio/mpeg">
  </audio>
  <audio id="lineClearSound">
    <source src="Здесь ссылка на звук, который воспроизводится при приземлении тетромино" type="audio/mpeg">
  </audio>
  <audio id="bgMusic" loop>
    <source src="Здесь ссылка на музыку, которая играет в фоне" type="audio/mpeg">
  </audio>
</center>

Ну и придаем всему этому небольшую стилизацию:

/* Общие стили */
  body {
    font-family: Arial, sans-serif;
    background-color: #f2f2f2;
  }

  /* Центрирование содержимого */
  .centered-content {
    display: flex;
    justify-content: center;
    align-items: start;
  }

  /* Стили для области игры */
  #game {
    margin-right: 10px;
  }

  /* Стили для боковой панели */
  .sidebar {
    position: relative;
    top: 0;
    right: 0;
    border: 2px solid #57595d;
    padding: 15px;
    text-align: center;
  }

  .sidebar hr {
    margin-top: 10px;
    margin-bottom: 10px;
    border: none;
    border-top: 1px solid #ccc;
  }

  .sidebar a {
    text-decoration: none;
  }

Вот в принципе и все.

А ну ка пыль сдуй отсюда!

Ссылка на комментарий
Поделиться на другие сайты

  • Администратор

@Ed MSL написал на IPBMafia:

image.png

Текста много, и из-за индексации отвечаю тут. Надеюсь вы понимаете...

А вот эту проблему я исправить почему то не могу. А я пытался сделать многое.

Во первых в функции placeTetromino() я так же добавлял вызов drawNextPiece(), это должно было гарантировать отображение следующего тетромино в окне справа после каждого размещения на игровом поле. Но этот путь не сработал.

Соответственно, дальше я выяснил, что функция getNextTetromino() была продублирована. Удаление лишней функции и использование единственной версии функции так же должно было исправить проблему, когда следующая фигура не отображается в окне справа. Но и это не сработало.

Позже я попытался добавить проверку в функцию generateSequence(), чтобы каждое тетромино появлялось в последовательности не более одного раза до тех пор, пока все тетромино не будут использованы. Это так же гарантировало бы, что каждое тетромино будет отображаться в окне следующего тетромино. Угадай что? Это не сработало!

Ну ладно, пошли ультимативным путем. Добавляем в функцию generateSequence() проверку на пустоту массива tetrominoSequence и генерируем новую последовательность, если массив пустой. В связи с этим, в функции getNextTetromino() добавляем вызов функции generateSequence(), если массив tetrominoSequence пустой.

Это уже должно было 100% гарантировать, что следующий тетромино всегда будет показан. Но коду плевать на гарантии, так же как работодателям на трудовой кодекс.

Когда я решил использовать уже две последовательности: одну для выбора текущего тетромино и другую для отображения следующего тетромино. Я вообще сломал свой тетрис, и пришлось делать откат.

Так что пусть будет момент, когда периодически не отображается следующий тетромино. Ибо я это пофиксить не могу.

А ну ка пыль сдуй отсюда!

Ссылка на комментарий
Поделиться на другие сайты

Привет.

Посидел немного с твоим кодом, из принципа решил проблему пофиксить. И решил.

Во-первых, у тебя две функции getNextTetromino в коде, и нижняя, соответственно, переопределяет верхнюю. Но там отличия в одной строке кода, drawNextPiece();, и проблема не в ней, однако, это подсказка к поиску причины🙂

В общем, чтобы пофиксить проблему выше, надо добавить одну строчку кода:

function getNextTetromino() {
  if (tetrominoSequence.length === 0) {
    generateSequence();
  }

  const name = tetrominoSequence.pop();
  
  /* Вот здесь еще одна проверка. */
  if (tetrominoSequence.length === 0) generateSequence();
  
  const matrix = tetrominos[name];

  const col = playfield[0].length / 2 - Math.ceil(matrix[0].length / 2);

  const row = name === 'I' ? -1 : -2;
  
  // Отображение следующего тетромино
  drawNextPiece();

  return {
    name: name,      
    matrix: matrix,  
    row: row,        
    col: col         
  };
}

Если взглянуть в функцию drawNextPiece, которая отрисовывает след. фигуру, то там есть условие:

if (tetrominoSequence.length > 0).

Теперь вернемся в функцию getNextTetromino и взглянем сюда:

const name = tetrominoSequence.pop();

А здесь у нас мутирующий метод pop, который изменяет исходный массив. И когда длина tetrominoSequence = 1, после этого метода она становится 0, т.е. массив пустой. И уже пустой массив попадает в drawNextPiece, и не проходит проверку if (tetrominoSequence.length > 0). Вот и все. Поэтому проверку надо делать после метода pop, как во фрагменте кода выше.

Это быстрое решение. По хорошему, надо немного зарефакторить код. Но это уже по желанию🙂

Ссылка на комментарий
Поделиться на другие сайты

Для публикации сообщений создайте учётную запись или авторизуйтесь

Вы должны быть пользователем, чтобы оставить комментарий

Создать учетную запись

Зарегистрируйте новую учётную запись в нашем сообществе. Это очень просто!

Регистрация нового пользователя

Войти

Уже есть аккаунт? Войти в систему.

Войти
  • Последние посетители   0 пользователей онлайн

    • Ни одного зарегистрированного пользователя не просматривает данную страницу



×
×
  • Создать...