Игра жизни - это эволюционная игра для нулевых игроков, в которой развитие игры будет зависеть от ее начального состояния. В игре есть четыре основных правила:
- если у живой клетки более трех соседей, она умирает
- если у живой клетки меньше трех соседей, она умирает
- если у мертвой клетки ровно три соседа - оживает
- в других случаях клетка остается в своем состоянии
Полную информацию можно найти здесь:
И вы сможете насладиться эпичностью всего игрового процесса здесь:
Применяя эти правила, наша цель - реализовать этот симулятор жизни в нашем проекте. Результат должен быть примерно таким.
Подготовка среды разработки.
Наш 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 рисует заполненный прямоугольник и в качестве свойств принимает:
- Position x (верхний левый угол прямоугольника) - положение итератора j умножается на размер ячеек
- Позиция y (верхний левый угол прямоугольника) - позиция итератора i умножается на размер ячеек
- Ширина - наш квадрат имеет ширину this.cell_size - 5 пикселей
- Высота - наш квадрат имеет высоту 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
Для одной ячейки нам нужно пройти:
- На один ряд вверх и сосчитайте соседей:
- 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