Игра жизни - это эволюционная игра для нулевых игроков, в которой развитие игры будет зависеть от ее начального состояния. В игре есть четыре основных правила:

  • если у живой клетки более трех соседей, она умирает
  • если у живой клетки меньше трех соседей, она умирает
  • если у мертвой клетки ровно три соседа - оживает
  • в других случаях клетка остается в своем состоянии

Полную информацию можно найти здесь:

И вы сможете насладиться эпичностью всего игрового процесса здесь:

Применяя эти правила, наша цель - реализовать этот симулятор жизни в нашем проекте. Результат должен быть примерно таким.

Подготовка среды разработки.

Наш HTML-файл предельно прост: название проекта, две кнопки для запуска и остановки игры и наш холст с шириной и высотой, на котором будет происходить вся магия.

Обратите внимание, что порядок скриптов важен. GameOfLife.js (наш файл класса) должен идти до index.js

<body>
    <h1>Game of Life</h1>
    <div class="buttons-div">
        <button id="start-random">start Random</button>
        <button id="stop">stop</button>
     </div>
     <canvas id="gamefield" width="1400" height="500"></canvas>
</body>
<script src="GameOfLife.js"></script>
<script src="index.js"></script>

Код CSS:

*{
    text-align: center;
    background-color: #181818;
}
h1{
    font-size: 40px;
    color: White;
    margin: 0px;
    margin-top: 15px;
    font-family: 'Roboto', sans-serif;
    font-weight: 300;
}
.buttons-div > button{
    background-color: rgba(255, 255, 255, 0.6);
    font-family: 'Roboto', sans-serif;
    font-size: 18px;
    padding: 10px;
    border: none;
    border-radius: 10px;
    margin: 20px;
    font-weight: 300;
    outline: none;
}

В index.js мы просто определяем холст и контекст холста:

const canvas = document.querySelector("#gamefield")
const ctx = canvas.getContext("2d")

В GameOfLife.js мы создадим только один класс, содержащий основную логику игры.

Подумав, какие функции и переменные нам нужны, я пришел к такому:

Переменные:

Функции:

В функции arrayInitialization, создающей 2 2-мерных массива с нулями:

for (let i = 0; i < this.cells_in_rows; i++) {
   this.active_array[i] = [];
   for (let j = 0; j < this.cells_in_column; j++) {
      this.active_array[i][j] = 0;
   }
}
this.inactive_array = this.active_array;

В функции arrayRandomize мы перебираем active_array и случайным образом присваиваем значения один и ноль каждому блоку:

for (let i = 0; i < this.cells_in_rows; i++) {
    for (let j = 0; j < this.cells_in_column; j++) {
         this.active_array[i][j] = (Math.random() > 0.5) ? 1 : 0;
    }
}

Цель функции fillArray - указать цвет и местоположение для каждой ячейки в зависимости от ее состояния.

Мы определяем цвет и присваиваем ему значение alive_color, если ячейка имеет значение 1, и dead_color, если ячейка имеет значение 0.

Ctx - это контекст нашего холста, который мы определили ранее в index.js.

Функция fillRect рисует заполненный прямоугольник и в качестве свойств принимает:

  1. Position x (верхний левый угол прямоугольника) - положение итератора j умножается на размер ячеек
  2. Позиция y (верхний левый угол прямоугольника) - позиция итератора i умножается на размер ячеек
  3. Ширина - наш квадрат имеет ширину this.cell_size - 5 пикселей
  4. Высота - наш квадрат имеет высоту this.cell_size - 5 пикселей
for (let i = 0; i < this.cells_in_rows; i++) {
    for (let j = 0; j < this.cells_in_column; j++) {
         let color;
         if (this.active_array[i][j] == 1)
             color = this.alive_color;
         else
             color = this.dead_color;
         ctx.fillStyle = color;
         ctx.fillRect(j * this.cell_size, i * this.cell_size,    this.cell_size, this.cell_size);
    }
}

Мы почти у цели! Теперь нам нужно обновить состояние в соответствии с правилами игры.

В функции updateLifeCycle мы перебираем все ячейки, возвращая новое состояние для конкретной ячейки и присваивая его значение inactive_array. После завершения цикла мы назначаем inactive_array нашим active_array.

for (let i = 0; i < this.cells_in_rows; i++) {
    for (let j = 0; j < this.cells_in_column; j++) {
        let new_state = this.updateCellValue(i, j);
        this.inactive_array[i][j] = new_state;
    }
}
this.active_array = this.inactive_array

В функции updateCellValue логика довольно проста. Он принимает позицию столбца и позицию строки ячейки и возвращает единицу или ноль.

const total = this.countNeighbours(row, col);
// cell with more than 4 or less then 3 neighbours dies. 1 => 0; 0 => 0
if (total > 4 || total < 3) {
    return 0;
}
// dead cell with 3 neighbours becomes alive. 0 => 1
else if (this.active_array[row][col] === 0 && total === 3) {
    return 1;
}
// or returning its status back. 0 => 0; 1 => 1
else {
    return this.active_array[row][col];
}

Функция countNeighbours - это вспомогательная функция для подсчета соседей.

totalNeighbours = 0

Для одной ячейки нам нужно пройти:

  1. На один ряд вверх и сосчитайте соседей:
  • totalNeighbours + = this.active_array [строка -1] [столбец - 1]
  • totalNeighbours + = this.active_array [строка -1] [столбец]
  • totalNeighbours + = this.active_array [строка -1] [столбец + 1]

2. На одну строку вниз:

  • totalNeighbours + = this.active_array [строка + 1] [столбец - 1]
  • totalNeighbours + = this.active_array [строка +] [столбец]
  • totalNeighbours + = this.active_array [строка + 1] [столбец + 1]

3. В той же строке:

  • totalNeighbours + = this.active_array [строка] [столбец - 1]
  • totalNeighbours + = this.active_array [строка] [столбец + 1]

Чтобы обрабатывать отрицательные индексы и индексы, превышающие длину массива, мы должны создать другую вспомогательную функцию, которая обрабатывает такое исключение. Я сделал это с помощью блока try-catch.

Попробуйте получить значение this.active_array [-1] [- 1]. Вы можете? Прохладный! Вы не можете? Вместо этого дай мне ноль.

this.setCellValueHelper = (row, col) => {
    try {
        return this.active_array[row][col];
    }
    catch {
        return 0;
     }
};
this.countNeighbours = (row, col) => {
    let total_neighbours = 0;
    total_neighbours += this.setCellValueHelper(row - 1, col - 1);
    total_neighbours += this.setCellValueHelper(row - 1, col);
    total_neighbours += this.setCellValueHelper(row - 1, col + 1);
    total_neighbours += this.setCellValueHelper(row, col - 1);
    total_neighbours += this.setCellValueHelper(row, col + 1);
    total_neighbours += this.setCellValueHelper(row + 1, col - 1);
    total_neighbours += this.setCellValueHelper(row + 1, col);
    total_neighbours += this.setCellValueHelper(row + 1, col + 1);
    return total_neighbours;
};

Две дополнительные функции для настройки нашей игры, и мы готовы к работе!

this.gameSetUp = () => {
    this.arrayInitialization();
};
this.runGame = () => {
    this.updateLifeCycle();
    this.fillArray();
};

В index.js создается экземпляр нашего класса GameOfLife. А после загрузки окна вы можете назначить eventListeners нашим кнопкам aaaa и наслаждаться :)

const game = new GameOfLife()
game.gameSetUp()
window.onload = () => {
   document.querySelector("#start-random").addEventListener("click", () => {
       game.arrayRandomize();
       game.fillArray();
       window.setInterval(() => {
           game.runGame();
       }, 300)
    })
  document.querySelector("#stop").addEventListener("click", () => {
       game.gameSetUp();
  })
}

Спасибо за прочтение! :)

Репозиторий Github: https://github.com/fainapahanko/Conway-s-Game-Of-Life