Создание игры "Сапёр" с использованием JavaScript

JavaScriptJavaScriptBeginner
Практиковаться сейчас

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Я полагаю, что каждый из вас когда-то играл в классическую игру "Сапёр". Она имеет простые правила, но крайне увлекательна. Вы когда-нибудь думали о том, чтобы создать её самостоятельно? Сегодня мы создадим веб-версию игры "Сапёр". Давайте сначала взглянем на скриншот интерфейса.

👀 Предпросмотр

Предпросмотр интерфейса игры "Сапёр"

🎯 Задачи

В этом проекте вы научитесь:

  • Как проектировать алгоритм игры для игры "Сапёр"
  • Как создавать файловую структуру для проекта
  • Как реализовать макет страницы с использованием HTML и CSS
  • Как рисовать сетку с использованием JavaScript
  • Как добавлять события нажатия на клетки для обработки игрового процесса
  • Как реализовать функции управления игрой, такие как начало и конец игры

🏆 Достижения

После завершения этого проекта вы сможете:

  • Проектировать и реализовывать алгоритмы игр
  • Создавать файловую структуру для веб-проекта
  • Использовать HTML и CSS для создания макетов страниц
  • Использовать JavaScript для рисования сеток и обработки событий
  • Реализовывать функции управления игрой

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL javascript(("JavaScript")) -.-> javascript/DOMManipulationGroup(["DOM Manipulation"]) javascript(("JavaScript")) -.-> javascript/BasicConceptsGroup(["Basic Concepts"]) javascript/BasicConceptsGroup -.-> javascript/cond_stmts("Conditional Statements") javascript/BasicConceptsGroup -.-> javascript/loops("Loops") javascript/BasicConceptsGroup -.-> javascript/functions("Functions") javascript/BasicConceptsGroup -.-> javascript/array_methods("Array Methods") javascript/BasicConceptsGroup -.-> javascript/obj_manip("Object Manipulation") javascript/DOMManipulationGroup -.-> javascript/dom_select("DOM Selection") javascript/DOMManipulationGroup -.-> javascript/dom_manip("DOM Manipulation") javascript/DOMManipulationGroup -.-> javascript/event_handle("Event Handling") javascript/DOMManipulationGroup -.-> javascript/dom_traverse("DOM Traversal") subgraph Lab Skills javascript/cond_stmts -.-> lab-445713{{"Создание игры #quot;Сапёр#quot; с использованием JavaScript"}} javascript/loops -.-> lab-445713{{"Создание игры #quot;Сапёр#quot; с использованием JavaScript"}} javascript/functions -.-> lab-445713{{"Создание игры #quot;Сапёр#quot; с использованием JavaScript"}} javascript/array_methods -.-> lab-445713{{"Создание игры #quot;Сапёр#quot; с использованием JavaScript"}} javascript/obj_manip -.-> lab-445713{{"Создание игры #quot;Сапёр#quot; с использованием JavaScript"}} javascript/dom_select -.-> lab-445713{{"Создание игры #quot;Сапёр#quot; с использованием JavaScript"}} javascript/dom_manip -.-> lab-445713{{"Создание игры #quot;Сапёр#quot; с использованием JavaScript"}} javascript/event_handle -.-> lab-445713{{"Создание игры #quot;Сапёр#quot; с использованием JavaScript"}} javascript/dom_traverse -.-> lab-445713{{"Создание игры #quot;Сапёр#quot; с использованием JavaScript"}} end

Подготовка к разработке

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

Правила игры "Сапёр" просты:

  • На игровом поле есть несколько квадратов, каждый квадрат содержит число (пустое означает, что число равно 0) или бомбу. Число в квадрате обозначает количество бомб в соседних квадратах. Задача игрока - найти квадраты с числами за как можно меньшее время.
  • За исключением квадратов на краях, каждый квадрат имеет 8 соседних квадратов: вверх, вниз, влево, вправо и 4 диагональных квадрата. Поэтому диапазон чисел от 0 до 8.

Итак, наш алгоритм следующий:

  • В зависимости от выбранного пользователем уровня сложности (есть три уровня: начинающий, средний и продвинутый, при этом количество бомб и квадратов увеличивается с повышением уровня), случайным образом генерируется определенное количество бомб и расставляется их случайным образом по квадратам. Затем просматриваются квадраты, вычисляется число в каждом квадрате и помечается на квадрате. Когда игрок щелкает левой кнопкой мыши по квадрату, содержимое квадрата отображается (если в квадрате есть бомба, вызов не удался и игра заканчивается), а когда игрок щелкает правой кнопкой мыши по квадрату, квадрат помечается как бомба. Победа достигается только в том случае, если все бомбы правильно помечены и все квадраты без бомб открыты, и игра заканчивается.

Полезный совет: Поскольку диапазон чисел в квадратах от 0 до 8, мы можем рассмотреть, что числа в квадратах, где находятся бомбы, помечаются как 9, чтобы облегчить вычисления.

Структура файлов проекта

Сначала нам нужно создать следующую структуру файлов в пути ~/project:

~/project
 |__ index.html
 |__ index.css
 |__ index.js
 |__ jms.js
 |__ mine.png
 |__ flag.png

mine.png и flag.png в директории проекта - это изображения для мины и флага соответственно.

Макет страницы

Сначала нам нужна панель для отображения игровой информации, включая количество оставшихся мин, пройденное время, уровень сложности и т.д. Поскольку количество квадратов не фиксировано, мы не будем сейчас рисовать квадраты, а вместо этого нарисуем их в коде JS.

Создайте файл index.html и добавьте следующий код:

<!-- index.html -->
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Minesweeper with JavaScript</title>
    <link
      href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css"
      rel="stylesheet"
    />
    <link rel="stylesheet" type="text/css" href="index.css" />
  </head>
  <body class="bg-gray-100">
    <div
      id="JMS_main"
      class="container mx-auto p-4 bg-white shadow-lg rounded-lg"
    >
      <table id="landmine" class="mx-auto"></table>
      <div id="operation" class="text-center">
        <div class="text-lg text-red-600">
          Количество оставшихся мин:
          <span class="font-semibold" id="landMineCount">0</span>
        </div>
        <div class="text-lg text-orange-500">
          Продолжительность времени:
          <span class="font-semibold" id="costTime">0</span> с
        </div>
        <fieldset class="my-4">
          <legend class="text-lg font-semibold">Выбор уровня сложности:</legend>
          <input
            type="radio"
            name="level"
            id="llevel"
            checked="checked"
            value="10"
            class="mr-2"
          />
          <label for="llevel">Начальный (10*10) </label><br />
          <input
            type="radio"
            name="level"
            id="mlevel"
            value="15"
            class="mr-2"
          />
          <label for="mlevel">Средний (15*15) </label><br />
          <input
            type="radio"
            name="level"
            id="hlevel"
            value="20"
            class="mr-2"
          />
          <label for="hlevel">Продвинутый (20*20) </label><br />
        </fieldset>
        <button
          id="begin"
          class="px-4 py-2 bg-blue-500 text-white font-semibold rounded-lg hover:bg-blue-700"
        >
          Начать игру</button
        ><br />
        <div class="text-left mt-4">
          <div class="font-semibold">Совет:</div>
          <ul class="list-disc ml-6">
            <li>1. Нажмите "Начать игру", чтобы запустить игровой таймер.</li>
            <li>2. Нажатие "Начать игру" во время игры запустит новую игру.</li>
          </ul>
        </div>
      </div>
    </div>
    <script src="jms.js"></script>
    <script src="index.js"></script>
  </body>
</html>
✨ Проверить решение и практиковаться

Стили макета страницы

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

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

.container {
  max-width: 600px;
}
table {
  background: #ccc;
  margin: 0 auto;
}
td {
  border: 2px outset #eee;
  font-size: 20px;
  width: 32px;
  height: 32px;
  text-align: center;
  cursor: pointer;
}
button {
  padding: 2px 10px;
  margin: 5px;
  font-size: 20px;
  cursor: pointer;
}

.clicked-cell {
  background-color: #a0aec0;
  color: #fff;
  font-weight: bold;
}

.landMine {
  background-image: url(mine.png);
  background-position: center;
  background-repeat: no-repeat;
}

.flag {
  background-image: url(flag.png);
  background-position: center;
  background-repeat: no-repeat;
}

После завершения этого шага откройте index.html в вашем браузере.

Интерфейс игры "Сапёр"
Интерфейс игры "Сапёр"
✨ Проверить решение и практиковаться

Отрисовка сетки

После завершения предыдущих шагов нам нужно нарисовать сетку. Чтобы сделать код более понятным, мы разделяем часть реализации игры и часть вызова. Часть реализации игры размещается в jms.js, который находится в том же каталоге, что и index.html, а часть вызова игры размещается в index.js, также в том же каталоге.

Для отрисовки сетки нам нужно передать некоторые параметры, такие как id таблицы, в которую будет нарисована сетка, и количество ячеек (представляется количеством строк и столбцов). Кроме того, другие данные, связанные с игрой, нужно инициализировать.

Часть jms.js

Добавьте следующий код в jms.js и сохраните его:

(function () {
  // Инициализируем объект "Сапёр" и инициализируем данные
  var JMS = function (
    id,
    rowCount,
    colCount,
    minLandMineCount,
    maxLandMineCount
  ) {
    if (!(this instanceof JMS))
      return new JMS(
        id,
        rowCount,
        colCount,
        minLandMineCount,
        maxLandMineCount
      );
    this.doc = document;
    this.table = this.doc.getElementById(id); // Таблица для рисования сетки
    this.cells = this.table.getElementsByTagName("td"); // Ячейки
    this.rowCount = rowCount || 10; // Количество строк в сетке
    this.colCount = colCount || 10; // Количество столбцов в сетке
    this.landMineCount = 0; // Количество мин
    this.markLandMineCount = 0; // Количество отмеченных мин
    this.minLandMineCount = minLandMineCount || 10; // Минимальное количество мин
    this.maxLandMineCount = maxLandMineCount || 20; // Максимальное количество мин
    this.arrs = []; // Массив, соответствующий ячейкам
    this.beginTime = null; // Время начала игры
    this.endTime = null; // Время окончания игры
    this.currentSetpCount = 0; // Количество сделанных шагов
    this.endCallBack = null; // Коллбэк-функция при окончании игры
    this.landMineCallBack = null; // Коллбэк-функция для обновления оставшегося количества мин при пометке мины
    this.doc.oncontextmenu = function () {
      // Отключаем контекстное меню
      return false;
    };
    this.drawMap();
  };

  // Создаем ячейки в прототипе JMS
  JMS.prototype = {
    // Рисуем сетку
    drawMap: function () {
      var tds = [];
      // Для совместимости с браузерами
      if (
        window.ActiveXObject &&
        parseInt(navigator.userAgent.match(/msie ([\d.]+)/i)[1]) < 8
      ) {
        // Создаем новый CSS-стильный файл
        var css = "#JMS_main table td{background-color:#888;}",
          // Получаем тег head
          head = this.doc.getElementsByTagName("head")[0],
          // Создаем тег style
          style = this.doc.createElement("style");
        style.type = "text/css";
        if (style.styleSheet) {
          // Назначаем CSS-стили к тегу style
          style.styleSheet.cssText = css;
        } else {
          // Создаем узел в теге style
          style.appendChild(this.doc.createTextNode(css));
        }
        // Добавляем тег style в качестве дочернего тега к тегу head
        head.appendChild(style);
      }
      // Цикл для создания таблицы
      for (var i = 0; i < this.rowCount; i++) {
        tds.push("<tr>");
        for (var j = 0; j < this.colCount; j++) {
          tds.push("<td id='m_" + i + "_" + j + "'></td>");
        }
        tds.push("</tr>");
      }
      this.setTableInnerHTML(this.table, tds.join(""));
    },
    // Добавляем HTML в таблицу
    setTableInnerHTML: function (table, html) {
      if (navigator && navigator.userAgent.match(/msie/i)) {
        // Создаем div внутри владельца документа таблицы
        var temp = table.ownerDocument.createElement("div");
        // Создаем содержимое tbody таблицы
        temp.innerHTML = "<table><tbody>" + html + "</tbody></table>";
        if (table.tBodies.length == 0) {
          var tbody = document.createElement("tbody");
          table.appendChild(tbody);
        }
        table.replaceChild(temp.firstChild.firstChild, table.tBodies[0]);
      } else {
        table.innerHTML = html;
      }
    }
  };

  window.JMS = JMS;
})();

Вышеприведенный код содержит некоторый код для совместимости с браузерами IE, который можно игнорировать.

Часть index.js

В коде вызова в index.js нам нужно привязать событие кнопок выбора уровня сложности, а затем вызвать JMS, который мы определили выше, чтобы начать рисовать сетку.

Добавьте следующий код в index.js и сохраните его:

var jms = null,
  timeHandle = null;
window.onload = function () {
  var radios = document.getElementsByName("level");
  for (var i = 0, j = radios.length; i < j; i++) {
    radios[i].onclick = function () {
      if (jms != null)
        if (jms.landMineCount > 0)
          if (!confirm("Вы уверены, что хотите закончить текущую игру?"))
            return false;
      var value = this.value;
      init(value, value, (value * value) / 5 - value, (value * value) / 5);
      document.getElementById("JMS_main").style.width =
        value * 40 + 180 + 60 + "px";
    };
  }
  init(10, 10);
};

function init(rowCount, colCount, minLandMineCount, maxLandMineCount) {
  var doc = document,
    landMineCountElement = doc.getElementById("landMineCount"),
    timeShow = doc.getElementById("costTime"),
    beginButton = doc.getElementById("begin");
  if (jms != null) {
    clearInterval(timeHandle);
    timeShow.innerHTML = 0;
    landMineCountElement.innerHTML = 0;
  }
  jms = JMS("landmine", rowCount, colCount, minLandMineCount, maxLandMineCount);
}

Затем откройте index.html в браузере, и сетка должна быть отображена. Эффект выглядит так:

Отображение сетки игры "Сапёр"

Нажмите на выбор уровня сложности справа, чтобы увидеть изменение количества ячеек.

✨ Проверить решение и практиковаться

Инициализация игры

Теперь давайте начнем инициализировать игру, которая主要包括三个步骤:

  1. 将所有单元格(在代码中由数组表示)初始化为 0。
  2. 生成随机数量的地雷,并将它们随机放置在数组中,将数组项的值设置为 9。
  3. 计算其他单元格中的数字,并将值存储在数组中。

jms.js中的JMS.prototype内部添加以下代码:

// Инициализация: установить значение по умолчанию массива равным 0 и определить количество мин
init: function () {
    for (var i = 0; i < this.rowCount; i++) {
        this.arrs[i] = [];
        for (var j = 0; j < this.colCount; j++) {
            this.arrs[i][j] = 0;
        }
    }
    this.landMineCount = this.selectFrom(this.minLandMineCount, this.maxLandMineCount);
    this.markLandMineCount = 0;
    this.beginTime = null;
    this.endTime = null;
    this.currentSetpCount = 0;
},
// Установить значение элементов массива, которые содержат мины, равным 9
landMine: function () {
    var allCount = this.rowCount * this.colCount - 1,
        tempArr = {};
    for (var i = 0; i < this.landMineCount; i++) {
        var randomNum = this.selectFrom(0, allCount),
            rowCol = this.getRowCol(randomNum);
        if (randomNum in tempArr) {
            i--;
            continue;
        }
        this.arrs[rowCol.row][rowCol.col] = 9;
        tempArr[randomNum] = randomNum;
    }
},
// Вычислить числа в других ячейках
calculateNoLandMineCount: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            if (this.arrs[i][j] == 9)
                continue;
            if (i > 0 && j > 0) {
                if (this.arrs[i - 1][j - 1] == 9)
                    this.arrs[i][j]++;
            }
            if (i > 0) {
                if (this.arrs[i - 1][j] == 9)
                    this.arrs[i][j]++;
            }
            if (i > 0 && j < this.colCount - 1) {
                if (this.arrs[i - 1][j + 1] == 9)
                    this.arrs[i][j]++;
            }
            if (j > 0) {
                if (this.arrs[i][j - 1] == 9)
                    this.arrs[i][j]++;
            }
            if (j < this.colCount - 1) {
                if (this.arrs[i][j + 1] == 9)
                    this.arrs[i][j]++;
            }
            if (i < this.rowCount - 1 && j > 0) {
                if (this.arrs[i + 1][j - 1] == 9)
                    this.arrs[i][j]++;
            }
            if (i < this.rowCount - 1) {
                if (this.arrs[i + 1][j] == 9)
                    this.arrs[i][j]++;
            }
            if (i < this.rowCount - 1 && j < this.colCount - 1) {
                if (this.arrs[i + 1][j + 1] == 9)
                    this.arrs[i][j]++;
            }
        }
    }
},
// Получить случайное число
selectFrom: function (iFirstValue, iLastValue) {
    var iChoices = iLastValue - iFirstValue + 1;
    return Math.floor(Math.random() * iChoices + iFirstValue);
},
// Найти номер строки и столбца по значению
getRowCol: function (val) {
    return {
        row: parseInt(val / this.colCount),
        col: val % this.colCount
    };
},
✨ Проверить решение и практиковаться

Добавление событий клика к ячейкам

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

此外,当单元格首次被点击时(通常涉及运气),如果其周围有空白区域,应直接展开。

Добавьте следующий код в JMS.prototype в jms.js:

// Получить элемент
$: function (id) {
    return this.doc.getElementById(id);
},
// Привязать события клика (левый и правый) к каждой ячейке
bindCells: function () {
    var self = this;
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            (function (row, col) {
                self.$("m_" + i + "_" + j).onmousedown = function (e) {
                    e = e || window.event;
                    var mouseNum = e.button;
                    var className = this.className;
                    if (mouseNum == 2) {
                        if (className == "flag") {
                            this.className = "";
                            self.markLandMineCount--;
                        } else {
                            this.className = "flag";
                            self.markLandMineCount++;
                        }
                        if (self.landMineCallBack) {
                            self.landMineCallBack(self.landMineCount - self.markLandMineCount);
                        }
                    } else if (className!= "flag") {
                        self.openBlock.call(self, this, row, col);
                    }
                };
            })(i,j);
        }
    }
},
// Раскрыть область без мин
showNoLandMine: function (x, y) {
    for (var i = x - 1; i < x + 2; i++)
        for (var j = y - 1; j < y + 2; j++) {
            if (!(i == x && j == y)) {
                var ele = this.$("m_" + i + "_" + j);
                if (ele && ele.className == "") {
                    this.openBlock.call(this, ele, i, j);
                }
            }
        }
},
// Отобразить
openBlock: function (obj, x, y) {
    if (this.arrs[x][y]!= 9) {
        this.currentSetpCount++;
        if (this.arrs[x][y]!= 0) {
            obj.innerHTML = this.arrs[x][y];
        }
        obj.className = "clicked-cell";
        if (this.currentSetpCount + this.landMineCount == this.rowCount * this.colCount) {
            this.success();
        }
        obj.onmousedown = null;
        if (this.arrs[x][y] == 0) {
            this.showNoLandMine.call(this, x, y);
        }
    } else {
        this.failed();
    }
},
// Отобразить мины
showLandMine: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            if (this.arrs[i][j] == 9) {
                this.$("m_" + i + "_" + j).className = "landMine";
            }
        }
    }
},
// Показать информацию о всех ячейках
showAll: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            if (this.arrs[i][j] == 9) {
                this.$("m_" + i + "_" + j).className = "landMine";
            } else {
                var ele=this.$("m_" + i + "_" + j);
                if (this.arrs[i][j]!= 0)
                    ele.innerHTML = this.arrs[i][j];
                ele.className = "normal";
            }
        }
    }
},
// Удалить отображенную информацию о ячейках
hideAll: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            var tdCell = this.$("m_" + i + "_" + j);
            tdCell.className = "";
            tdCell.innerHTML = "";
        }
    }
},
// Удалить события, привязанные к ячейкам
disableAll: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            var tdCell = this.$("m_" + i + "_" + j);
            tdCell.onmousedown = null;
        }
    }
},
✨ Проверить решение и практиковаться

Добавление функций управления игрой

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

  1. Добавить слушатель события клика на кнопку "Старт" для сброса параметров игры.
  2. Начинать отсчитывать время при запуске игры.
  3. Остановить отсчет времени и показать сообщение при окончании игры.

Секция jms.js

Добавьте функцию входа в игру и запуска в JMS.prototype в jms.js:

// Запуск игры
begin: function() {
    this.currentSetpCount = 0; // Сбросить счетчик шагов до нуля
    this.markLandMineCount = 0;
    this.beginTime = new Date(); // Время начала игры
    this.hideAll();
    this.bindCells();
},
// Конец игры
end: function() {
    this.endTime = new Date(); // Время окончания игры
    if (this.endCallBack) { // Вызвать коллбэк-функцию, если она существует
        this.endCallBack();
    }
},
// Победа в игре
success: function() {
    this.end();
    this.showAll();
    this.disableAll();
    alert("Поздравляем!");
},
// Неудача в игре
failed: function() {
    this.end();
    this.showAll();
    this.disableAll();
    alert("ИГРА ОКОНЧЕНА!");
},
// Точка входа
play: function() {
    this.init();
    this.landMine();
    this.calculateNoLandMineCount();
},

Секция index.js

В index.js добавьте слушатель события на кнопку "Старт", чтобы запустить игру, и отобразить время игры и оставшееся количество мин (после jms = JMS("landmine", rowCount, colCount, minLandMineCount, maxLandMineCount); в функции init в index.js):

jms.endCallBack = function () {
  clearInterval(timeHandle);
};
jms.landMineCallBack = function (count) {
  landMineCountElement.innerHTML = count;
};

// Привязать событие к кнопке "Начать игру"
beginButton.onclick = function () {
  jms.play(); // Инициализировать игру

  // Отобразить количество мин
  landMineCountElement.innerHTML = jms.landMineCount;

  // Запустить игру
  jms.begin();

  // Обновить прошедшее время
  timeHandle = setInterval(function () {
    timeShow.innerHTML = parseInt((new Date() - jms.beginTime) / 1000);
  }, 1000);
};
✨ Проверить решение и практиковаться

Запуск и тестирование

В этом моменте наша веб-версия игры "Сапёр" готова. Откройте файл index.html в вашем браузере, чтобы начать играть.

Нажмите на кнопку Go Live в нижнем правом углу WebIDE и переключитесь на вкладку Web 8080.

Демонстрация игрового процесса в игре "Сапёр"
✨ Проверить решение и практиковаться

Обзор

В этом эксперименте в основном используется JavaScript для реализации веб-версии классической игры "Сапёр". Предполагается, что в результате этого эксперимента пользователи смогут повысить свои знания и навыки в использовании JavaScript. Кроме того, пользователи также могут научиться использовать язык JavaScript для абстрагирования и инкапсуляции объектов в игре.