Как стать автором
Обновить

Игра Жизнь — клеточный автомат на HTML, JS

Уровень сложности Простой
Время на прочтение 11 мин
Количество просмотров 8.8K

Игра Жизнь - это клеточный автомат созданный в 1970 году Джоном Конвеем

Это не совсем игра, а просто симуляция клеток по определенным правилам.От игрока лишь требуется размещать эти клетки.

В этом посте мы сделаем "Игру Жизнь" на HTML странице при помощи CSS & JS

Правила

Клетка - она может быть либо живой (темной), либо мертвой (белой).

У клетки есть 8 соседей вокруг и могут влиять на нее.

Если клетка мертва и у нее есть 3 живых соседа, тогда она рождается.

Если клетка жива и у нее есть 2 или 3 соседа, то она остается живой.В противном случае умирает от перенаселения или одиночества

Пишем главную страницу:

Создаем базовый код HTML и подключаем к нему .css и .js

Обозначим главный блок div в котором будут находится все клетки:

<div class="mainBox"></div>
.mainBox {
    border: solid 3px #000000;
    display: flex;
    flex-wrap: wrap;
    width: 45%;
    height: 83%;
    margin: 4% 26%;
    position: fixed;
    justify-content: flex-start;
}

Внутрь mainBox создаем:

<div class="cellBox"></div>

Также сразу же даем класс deathCell и функцию onclick="aliveButtons(this)" для того чтобы в будущем оживлять клетки на которые мы нажимаем

Должно выйти так:

<div class="cellBox deathCell" onclick="aliveButtons(this)"></div>

Даем стили:

.cellBox { /* Стили клеток */
    display: block;
    width: 5.77%;
    height: 5.75%;
    border: solid 2px #808080;
    background-color: #ffffff;
}

.deathCell { /* Класс мертвой клетки */
    background-color: #ffffff;
}

.aliveCell { /* Класс живой клетки */
    background-color: #2c2c2c;
}

Делаем возможность оживлять/убивать кнопки:

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

Для этого воспользуемся :hover

.cellBox:hover {
    background-color: #000000;
    cursor: pointer;
}

Теперь перейдем на JS и воспользуемся нашими функциями aliveButtons(this)

function aliveButtons(obj) { // получаем через (this) нажатую клетку
    if (obj.classList.contains("deathCell")) { // проверяем мертва ли клетка
        // оживляем клетку
        obj.classList.add("aliveCell") // добавляем класс aliveCell 
        obj.classList.remove("deathCell") // убираем класс deathCell
    } else if (obj.classList.contains("aliveCell")) {
        // тоже самое, только наоборот
        // убиваем клетку 
        obj.classList.add("deathCell")
        obj.classList.remove("aliveCell")
    }
}

Теперь мы можем оживлять и убивать клетки

Пишем JS скрипт:

Для начала стоит создать и получить все наши клетки:

function summonCells() { // делаем функцию генератора
    mainBoxId = document.getElementById("mainBoxId") // получаем главный блок через id
    appendingElement = '<div class="cellBox deathCell" onclick="aliveButtons(this)"></div>' // элементы которые будем добавлять
    for (var i = 0; i < 255; i++) {
        mainBoxId.innerHTML += appendingElement // редактируем гл. блок
    }
}

summonCells() // активируем функцию
cellBoxes = document.querySelectorAll(".cellBox") // получаем всех через класс

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

function framesUpdate() {
    setTimeout(function () { // цикличная функция
        framesUpdate() // вызываем ее вновь
    }, 1000) // задержка 1-секунда
}
framesUpdate() // Вызов нашей функции при запуске. Вставляем в конце кода!

Теперь самое интересное.Создаем функцию для определения соседских клеток и заодно в нее засунем правила

function neighborCheck() {}

Задаем универсальный цикл для проверки всех клеток

function neighborCheck() {
    for (var i = 0; i < Object.keys(cellBoxes).length; i++) { // сравниваем i с общим кол-вом наших клеток
      
    }
}

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

Сейчас мы зададим еще одну переменную в каждой клетке используя цикл

cellBoxes[i].alivedNeighbor = 0
// вначале мы задаем ей значение 0, но в следующие разы просто обновляем ее

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

if (i - 17 >= 0) { //Проверка, чтобы мы не проверяли если у блока нет соседей выше
    if (cellBoxes[i - 17].classList.contains("aliveCell")) // проверяем жива ли клетка {
        cellBoxes[i].alivedNeighbor += 1 // добавляем +1 к кол-ву живых соседей нашей клетки
    }
}
if (i - 16 >= 0) { // у нас клетки 16х16 размером, поэтому чтобы проверить верхнюю левую, надо вычислить 17 клеток назад, чтобы верхнюю 16
    if (cellBoxes[i - 16].classList.contains("aliveCell")) {
        cellBoxes[i].alivedNeighbor += 1
    }
} 
if (i - 15 >= 0) {// тоже самое тут, но уже 15 и т.д.
    if (cellBoxes[i - 15].classList.contains("aliveCell")) {
        cellBoxes[i].alivedNeighbor += 1
    }
}
if (i - 1 >= 0) { // чтобы полностью понять как это работает, попробуйте посмотреть соседей клетки через консоль введя cellBoxes[номер клетки - 17]
    if (cellBoxes[i - 1].classList.contains("aliveCell")) {
        cellBoxes[i].alivedNeighbor += 1
    }
}
if (i + 1 <= 255) { //тут уже мы смотрим на соседей после самой клетки
    if (cellBoxes[i + 1].classList.contains("aliveCell")) {
        cellBoxes[i].alivedNeighbor += 1
    }
}
if (i + 15 <= 255) {
    if (cellBoxes[i + 15].classList.contains("aliveCell")) {
        cellBoxes[i].alivedNeighbor += 1
    }
}
if (i + 16 <= 255) {
    if (cellBoxes[i + 16].classList.contains("aliveCell")) {
        cellBoxes[i].alivedNeighbor += 1
    }
}
if (i + 17 <= 255) {
    if (cellBoxes[i + 17].classList.contains("aliveCell")) {
        cellBoxes[i].alivedNeighbor += 1
    }
}

Проверку живых соседей мы сделали, теперь зададим правила

Задаем еще один такой же цикл, чтобы правила работали только после обнаружения у всех клеток, их соседей

for (i = 0; i < Object.keys(cellBoxes).length; i++) { // наш цикл, такой же как и прошлый
    if (cellBoxes[i].alivedNeighbor === 3) { // проверяем, если живы 3 соседа
        if (cellBoxes[i].classList.contains("deathCell")) { // если клетка уже мертва
            cellBoxes[i].classList.add("aliveCell") // оживляем ее
            cellBoxes[i].classList.remove("deathCell")
        }
    } else if (cellBoxes[i].classList.contains("aliveCell")) { // или же если клетка уже жива
        if (cellBoxes[i].alivedNeighbor === 2) { // если у нее есть 2 живых соседа
            cellBoxes[i].classList.add("aliveCell") // оставляем ее живой
            cellBoxes[i].classList.remove("deathCell")
        } else { // если у нее не 2 соседа, тогда она умирает
            cellBoxes[i].classList.add("deathCell")
            cellBoxes[i].classList.remove("aliveCell")
        }
    } else { // в любых других случаях
        if (cellBoxes[i].classList.contains("aliveCell")) { // если клетка жива
            cellBoxes[i].classList.add("deathCell") // убиваем ее
            cellBoxes[i].classList.remove("aliveCell")
        }
    }
}

В нашу функцию framesUpdate() добавляем нашу проверку соседей

function framesUpdate() {
    setTimeout(function () {
        neighborCheck() // вот и проверка соседей
        framesUpdate()
    }, 1000)
}

Весь код должен выглядить примерно таким:

function summonCells() {
    mainBoxId = document.getElementById("mainBoxId")
    appendingElement = '<div class="cellBox deathCell" onclick="aliveButtons(this)"></div>'
    for (var i = 0; i < 255; i++) {
        mainBoxId.innerHTML += appendingElement
    }
}

function aliveButtons(obj) {
    if (obj.classList.contains("deathCell")) {
        obj.classList.add("aliveCell")
        obj.classList.remove("deathCell")
    } else if (obj.classList.contains("aliveCell")) {
        obj.classList.add("deathCell")
        obj.classList.remove("aliveCell")
    }
}

function neighborCheck() {
    for (var i = 0; i < Object.keys(cellBoxes).length; i++) {
        cellBoxes[i].alivedNeighbor = 0
        if (i - 17 >= 0) {
            if (cellBoxes[i - 17].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i - 16 >= 0) {
            if (cellBoxes[i - 16].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i - 15 >= 0) {
            if (cellBoxes[i - 15].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i - 1 >= 0) {
            if (cellBoxes[i - 1].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i + 1 <= 255) {
            if (cellBoxes[i + 1].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i + 15 <= 255) {
            if (cellBoxes[i + 15].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i + 16 <= 255) {
            if (cellBoxes[i + 16].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i + 17 <= 255) {
            if (cellBoxes[i + 17].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
    }
    for (i = 0; i < Object.keys(cellBoxes).length; i++) {
        if (cellBoxes[i].alivedNeighbor === 3) {
            if (cellBoxes[i].classList.contains("deathCell")) {
                cellBoxes[i].classList.add("aliveCell")
                cellBoxes[i].classList.remove("deathCell")
            }
        } else if (cellBoxes[i].classList.contains("aliveCell")) {
            if (cellBoxes[i].alivedNeighbor === 2) {
                cellBoxes[i].classList.add("aliveCell")
                cellBoxes[i].classList.remove("deathCell")
            } else {
                cellBoxes[i].classList.add("deathCell")
                cellBoxes[i].classList.remove("aliveCell")
            }
        } else {
            if (cellBoxes[i].classList.contains("aliveCell")) {
                cellBoxes[i].classList.add("deathCell")
                cellBoxes[i].classList.remove("aliveCell")
            }
        }
    }
}

function framesUpdate() {
    setTimeout(function () {
        neighborCheck()
        framesUpdate()
    }, 1000)
}
summonCells()
cellBoxes = document.querySelectorAll(".cellBox")

Основной код мы написали, но без интерфейса мы не можем им управлять

В HTML добавляем следующее (выше mainBox):

<div class="mainInterface"> <!-- главный блок интерфейса -->
    <button onclick="startAnim()" class="interfaceBtn">Start</button> <!-- кнопка старта -->
    <button onclick="resetToDefault()" class="interfaceBtn">Reset</button> <!-- кнопка для убийства всех блоков -->
    <input type="range" min="1" max="255" step="1" value="128" class="interfaceRange" id="genSpeedInput" oninput="setSpeedInput()"> <!-- Ползунок для управления скоростью --> 
    <p class="interfaceTxt" id="genBySec">Поколений в секунду: 128</p> <!-- текст с нынешней скоростью -->
    <p class="interfaceTxt" id="genereationNumber">Поколение: 0</p> <!-- нынешнее поколение -->
</div>

Стили для них:

.mainInterface { /* главный блок интерфейса */
    position: fixed; /* Фиксация в левой верхнем углу */
    left: 0;
    top: 0;
    background-color: rgba(73, 73, 73, 0.26);
    width: 11%;
    height: 25%;
    border-radius: 10px;
    padding: 20px; /* Чтобы блоки держались чуть дальше от границ */
}

.interfaceBtn { /* Кнопки интерфейса */
    width: 90%;
    margin: 10px auto; /* центрирование */
    display: block;
}

.interfaceRange { /* Ползунок */
    margin: 0 auto; /* центрирование */
    display: block;
}

Теперь у нас есть интерфейс в левом верхнем углу, но он не рабочий.

Переходим в JS

// просто framesUpdate(), заменяем на:
function startAnim() {
    framesUpdate()
}

Теперь у нас рабочий старт по кнопке

Добавляем следующие переменные вначале кода:

genNumberId = document.getElementById("genereationNumber") // По айди получаем текст с номером поколения
genNumber = 0 // сам номер который будет изменяться
genSpeedId = document.getElementById("genBySec") // получаем также по айди текст с текующей скоростью
genBySec = 0 // значение скорости которое будет изменяться

Добавим ресет чтобы убить всех клеток:

function resetToDefault() {
    for (var i = 0; i < Object.keys(cellBoxes).length; i++) { // цикл обращается ко всем клеткам
        if (cellBoxes[i].classList.contains("aliveCell")) { // если клетка жива, то убивает ее
            cellBoxes[i].classList.add("deathCell")
            cellBoxes[i].classList.remove("aliveCell")
        }
    }
}

Добавим управление скоростью:

function setSpeedInput() {
    genBySec = document.getElementById("genSpeedInput").value // Получаем значение с ползунка
    genSpeedId.innerHTML = "Поколений в секунду: " + genBySec // изменяем текст текущей скорости в интерфейсе
}

Теперь нужно чтобы скорость изменялась, поэтому редактируем функцию framesUpdate()

function framesUpdate() {
    setTimeout(function () {
        neighborCheck()
        framesUpdate()
    }, 1000 / genBySec) // Делим секунду на выбранную скорость.Тоесть делим секунду на выбранное кол-во поколений
}

Добавляем подсчет поколений в интерфейсе

В конце функции neighborCheck (вне циклов) добавляем следующее:

genNumber += 1 // изменяем значение текущего поколения
genNumberId.innerHTML = "Поколение: " + genNumber // заменяем текст в интерфейсе

Мы закончили работать с интерфейсом

Весь код:

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Game Of Life</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <main>
        <div class="mainInterface">
            <button onclick="startAnim()" class="interfaceBtn">Start</button>
            <button onclick="resetToDefault()" class="interfaceBtn">Reset</button>
            <input type="range" min="1" max="255" step="1" value="128" class="interfaceRange" id="genSpeedInput" oninput="setSpeedInput()">
            <p class="interfaceTxt" id="genBySec">Поколений в секунду: 128</p>
            <p class="interfaceTxt" id="genereationNumber">Поколение: 0</p>
        </div>
        <div class="mainBox">
            <div class="cellBox deathCell" onclick="aliveButtons(this)"></div>
        </div>
    </main>

    <script src="script.js"></script>
</body>
</html>

CSS

* {
    font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
body {
    margin: 0;

}
.mainInterface {
    position: fixed;
    background-color: rgba(73, 73, 73, 0.26);
    width: 11%;
    height: 25%;
    border-radius: 10px;
    padding: 20px;
}
.interfaceBtn {
    width: 90%;
    margin: 10px auto;
    display: block;
}
.interfaceRange {
    margin: 0 auto;
    display: block;
}
.mainBox {
    border: solid 3px #000000;
    display: flex;
    flex-wrap: wrap;
    width: 45%;
    height: 83%;
    margin: 4% 26%;
    position: fixed;
    justify-content: flex-start;
}
.cellBox {
    display: block;
    width: 5.77%;
    height: 5.75%;
    border: solid 2px #808080;
    background-color: #ffffff;
}
.cellBox:hover {
    background-color: #000000;
    cursor: pointer;
}
.deathCell {
    background-color: #ffffff;
}
.aliveCell {
    background-color: #2c2c2c;
}

JS

genNumberId = document.getElementById("genereationNumber")
genNumber = 0
genSpeedId = document.getElementById("genBySec")
genBySec = 0

function summonCells() {
    mainBoxId = document.getElementById("mainBoxId")
    appendingElement = '<div class="cellBox deathCell" onclick="aliveButtons(this)"></div>'
    for (var i = 0; i < 255; i++) {
        mainBoxId.innerHTML += appendingElement
    }
}
function aliveButtons(obj) {
    if (obj.classList.contains("deathCell")) {
        obj.classList.add("aliveCell")
        obj.classList.remove("deathCell")
    } else if (obj.classList.contains("aliveCell")) {
        obj.classList.add("deathCell")
        obj.classList.remove("aliveCell")
    }
}
function resetToDefault() {
    for (var i = 0; i < Object.keys(cellBoxes).length; i++) {
        if (cellBoxes[i].classList.contains("aliveCell")) {
            cellBoxes[i].classList.add("deathCell")
            cellBoxes[i].classList.remove("aliveCell")
        }
    }
}
function setSpeedInput() {
    genBySec = document.getElementById("genSpeedInput").value
    genSpeedId.innerHTML = "Поколений в секунду: " + genBySec
}
function neighborCheck() {
    for (var i = 0; i < Object.keys(cellBoxes).length; i++) {
        cellBoxes[i].alivedNeighbor = 0
        if (i - 17 >= 0) {
            if (cellBoxes[i - 17].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i - 16 >= 0) {
            if (cellBoxes[i - 16].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i - 15 >= 0) {
            if (cellBoxes[i - 15].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i - 1 >= 0) {
            if (cellBoxes[i - 1].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i + 1 <= 255) {
            if (cellBoxes[i + 1].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i + 15 <= 255) {
            if (cellBoxes[i + 15].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i + 16 <= 255) {
            if (cellBoxes[i + 16].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
        if (i + 17 <= 255) {
            if (cellBoxes[i + 17].classList.contains("aliveCell")) {
                cellBoxes[i].alivedNeighbor += 1
            }
        }
    }
    for (i = 0; i < Object.keys(cellBoxes).length; i++) {
        if (cellBoxes[i].alivedNeighbor === 3) {
            if (cellBoxes[i].classList.contains("deathCell")) {
                cellBoxes[i].classList.add("aliveCell")
                cellBoxes[i].classList.remove("deathCell")
            }
        } else if (cellBoxes[i].classList.contains("aliveCell")) {
            if (cellBoxes[i].alivedNeighbor === 2) {
                cellBoxes[i].classList.add("aliveCell")
                cellBoxes[i].classList.remove("deathCell")
            } else {
                cellBoxes[i].classList.add("deathCell")
                cellBoxes[i].classList.remove("aliveCell")
            }
        } else {
            if (cellBoxes[i].classList.contains("aliveCell")) {
                cellBoxes[i].classList.add("deathCell")
                cellBoxes[i].classList.remove("aliveCell")
            }
        }
    }
    genNumber += 1
    genNumberId.innerHTML = "Поколение: " + genNumber
}

function framesUpdate() {
    setTimeout(function () {
        neighborCheck()
        framesUpdate()
    }, 1000 / genBySec)
}
summonCells()
cellBoxes = document.querySelectorAll(".cellBox")
function startAnim() {
    framesUpdate()
}

Спасибо за прочтение данной статьи.

Буду очень благодарен если заглянете на мой телеграм канал: https://t.me/blg_projects

Теги:
Хабы:
-7
Комментарии 16
Комментарии Комментарии 16

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн