Делаем игру Wordle на JavaScript
medium

Делаем игру Wordle на JavaScript

Прокачиваем свой английский

Wordle — когда-то популярная игра в сети. Правила такие: есть поле 5 на 6 клеток и загаданное слово, а у игрока есть 6 попыток угадать это слово. При каждой попытке он пишет свой вариант на новой строке поля, а игра подсвечивает буквы:

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

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

Так как изначально эта игра была только на английском, мы сделаем то же самое — будем угадывать английские слова. В следующей версии сделаем её русской, а пока поупражняемся в языке, тем более что программисту полезно знать английский.

В проекте используется AJAX — технология обновления данных на странице без её перезагрузки. Чтобы запустить проект дома, установите LAMP, MAMP или любой другой веб-сервер. Если есть желание, можно сразу отправить всё на свой хостинг — в этом случае тоже всё будет работать.

Особенность проекта

Сегодня мы попробуем несколько новых штук, которые не использовали до этого:

  • подключение модулей и использование переменных оттуда;
  • работа с уведомлениями на странице.

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

Создаём страницу

Как и во многих подобных проектах с динамическим контентом, всё, что от нас нужно, — создать каркас страницы и разместить на ней блок с игровым полем:

<!DOCTYPE html>
<html lang="ru">
<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>Wordle</title>
</head>
<body>
    <h1> Играем в Wordle </h1>
    
    <div id="game-board">

    </div>
</body>
</html>

Чтобы игра выглядела красиво, используем стили Toastr для внутриигровых уведомлений и сразу подключим наш пока пустой файл со стилями style.css. Добавим всё это в раздел <head>, а про сами уведомления поговорим чуть позже:

<link rel="stylesheet" href="style.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css" rel="stylesheet"/>

Последнее, что нам нужно на этом этапе — подключить наш основной скрипт, который возьмёт на себя всю работу, скрипт уведомлений Toastr (именно в нём используется AJAX) и jQuery для управления страницей изнутри. Добавим все скрипты в самый конец страницы перед закрывающим тегом </body>:

<script
src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
<script src="script.js" type="module"></script>

Если открыть эту страницу в браузере, то кроме заголовка мы пока ничего не увидим — у нас ещё нет скрипта, который бы сделал всё остальное. Но раз мы уже видим заголовок, добавим в файл со стилями запись о том, что его нужно выровнять по центру:

/* заголовок страницы */
h1 {
text-align: center;
}

Делаем игру Wordle на JavaScript

Создаём файл со словами

Для игры нам понадобятся все английские слова из 5 букв. Если верить словарю, их всего 5756, и переменная такого размера будет выглядеть катастрофически большой. Чтобы не раздувать наш основной скрипт, будем хранить все слова как элементы массива в отдельном модуле words.js, а в основном скрипте просто импортируем эту переменную из файла.

Мы уже собрали для вас этот файлик, можете поглядеть: mihailmaximov.ru

Пишем скрипт

В самом начале скрипта опишем все глобальные переменные, которые нам понадобятся, причём слова импортируем из файла командой import{}. Загадываем слово так: берём случайное число от 0 до 1, умножаем на количество слов и округляем результат до целого. На всякий случай выведем в консоль загаданное слово — так мы сможем проверить, правильно ли работает наша программа.

// импортируем слова из файла
import { WORDS } from "./words.js";

// количество попыток
const NUMBER_OF_GUESSES = 6;
// сколько попыток осталось
let guessesRemaining = NUMBER_OF_GUESSES;
// текущая попытка
let currentGuess = [];
// следующая буква
let nextLetter = 0;
// загаданное слово
let rightGuessString = WORDS[Math.floor(Math.random() * WORDS.length)]
// на всякий случай выведем в консоль загаданное слово, чтобы проверить, как работает игра
console.log(rightGuessString)

Теперь создадим игровую доску — нам нужно сделать её 5 на 6 клеток и предусмотреть разбиение на строки. Дело в том, что для проверки каждого нового слова нам понадобится содержимое всей строки, а это удобнее делать, когда к ней можно обратиться напрямую.

// создаём игровое поле
function initBoard() {
    // получаем доступ к блоку на странице
    let board = document.getElementById("game-board");

    // создаём строки
    // делаем цикл от 1 до 6, потому что попыток у нас как раз 6
    for (let i = 0; i < NUMBER_OF_GUESSES; i++) {
        // создаём новый блок на странице
        let row = document.createElement("div")
        // добавляем к нему класс, чтобы потом работать со строками напрямую
        row.className = "letter-row"
        
        // создаём отдельные клетки
        // добавляем по 5 клеток в ряд
        for (let j = 0; j < 5; j++) {
            // создаём новый блок на странице
            let box = document.createElement("div")
            // добавляем к нему класс
            box.className = "letter-box"
            // вкладываем новый блок внутрь блока со строкой
            row.appendChild(box)
        }

        // как все 5 клеток готовы, добавляем новую строку на поле
        board.appendChild(row)
    }
}

// рисуем игровое поле
initBoard();

Сразу же добавим оформление в файл со стилями — нам нужны стили для всей доски целиком, для строки и для отдельной клетки:

/* стиль для всей доски */
#game-board {
  /* делаем выравнивание всех элементов по центру */
  display: flex;
  align-items: center;
  /* добавляем выравнивание по вертикали */
  flex-direction: column;
}

/* стиль для строки */
.letter-row {
  /* каждая клетка пусть будет в своём отдельном контейнере */
  display: flex;
}

/* стиль для клетки */
.letter-box {
  /* рисуем границу */
  border: 2px solid gray;
  border-radius: 3px;
  /* добавляем отступы */
  margin: 2px;
  /* размер шрифта */
  font-size: 2.5rem;
  font-weight: 700;
  /* высота и ширина клетки */
  height: 3rem;
  width: 3rem;
  /* выравниваем содержимое по центру */
  display: flex;
  justify-content: center;
  align-items: center;
  /* делаем все буквы большими */
  text-transform: uppercase;
}
Делаем игру Wordle на JavaScript

Вводим слова

Чтобы игрок мог вводить слова, добавим обработчик события "keydown" — оно сработает в момент нажатия клавиши. Работать алгоритм будет так:

  1. Если все попытки угадать слово истрачены, обработчик тут же завершается, потому что вводить новые символы просто некуда.
  2. Если нажата клавиша Backspace — вызывается функция удаления последнего символа.
  3. Если нажат Enter — вызывается функция проверки слова, угадали мы или нет.
  4. А если нажата какая-то другая клавиша, то алгоритм проверит, попадают ли введённые символы в английский алфавит (потому что у нас сейчас версия для английского языка). Если попадают — вызывается функция добавления символа в клетку. Здесь нам пригодятся регулярные выражения, с которыми мы уже работали в других проектах.

Запишем это на JavaScript:

// обработчик нажатия на клавиши
document.addEventListener("keydown", (e) => {

    // если попыток не осталось
    if (guessesRemaining === 0) {
        // выходим из функции
        return
    }

    // получаем код нажатой клавиши
    let pressedKey = String(e.key)
    // если нажат Backspace и в строке есть хоть один символ
    if (pressedKey === "Backspace" && nextLetter !== 0) {
        // то удаляем последнюю введённую букву
        deleteLetter();
        // и выходим из обработчика
        return;
    }

    // если нажат Enter
    if (pressedKey === "Enter") {
        // проверяем введённое слово
        checkGuess();
        // и выходим из обработчика
        return;
    }

    // проверяем, есть ли введённый символ в английском алфавите
    let found = pressedKey.match(/[a-z]/gi)
    // если нет
    if (!found || found.length > 1) {
        // то выходим из обработчика
        return
    // иначе добавляем введённую букву в новую клетку
    } else {
        insertLetter(pressedKey)
    }
})

Если обновить страницу с проектом в браузере, то при попытке ввода получим ошибку — всё потому, что у нас нет ни одной функции, на которые мы здесь ссылаемся. Исправим это и добавим их все по очереди.

Добавляем вывод букв на экран

Нам нужно написать функцию insertLetter(), которая будет рисовать в клетках вводимые буквы. Смысл в том, что нам сначала надо проверить — а есть ли вообще свободные клетки в строке. Если есть — получаем номер клетки, добавляем в неё текст и переходим к следующей. 

Чтобы было красивее, нарисуем жирную обводку везде, где есть буквы. Для этого добавим класс "filled-box" к текущему элементу и не забудем прописать его в стилях.

// выводим букву в клетку
function insertLetter (pressedKey) {
    // если клетки закончились
    if (nextLetter === 5) {
        // выходим из функции
        return;
    }
    // получаем доступ к текущей строке
    let row = document.getElementsByClassName("letter-row")[6 - guessesRemaining]
    // и к текущей клетке, где будет появляться буква
    let box = row.children[nextLetter]
    // меняем текст в блоке с клеткой на нажатый символ
    box.textContent = pressedKey
    // добавляем к клетке жирную обводку
    box.classList.add("filled-box")
    // добавляем введённый символ к массиву, в которой хранится наша текущая попытка угадать слово
    currentGuess.push(pressedKey)
    // помечаем, что дальше будем работать со следующей клеткой
    nextLetter += 1
}

И добавляем короткий класс "filled-box" в файл со стилями:

/* добавляем жирную обводку у заполненных клеток */
.filled-box {
  border: 3px solid black;
}
Делаем игру Wordle на JavaScript
Теперь мы можем вводить слова, но исправить пока ничего нельзя

Удаляем символы

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

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

// удаление символа
function deleteLetter () {
    // получаем доступ к текущей строке
    let row = document.getElementsByClassName("letter-row")[6 - guessesRemaining]
    // и к последнему введённому символу
    let box = row.children[nextLetter - 1]
    // очищаем содержимое клетки
    box.textContent = ""
    // убираем жирную обводку
    box.classList.remove("filled-box")
    // удаляем последний символ из массива с нашей текущей догадкой
    currentGuess.pop()
    // помечаем, что у нас теперь на одну свободную клетку больше
    nextLetter -= 1
}

Проверяем догадку

Когда мы ввели все буквы в строке, то последнее, что нам осталось сделать по нажатию Enter, — проверить нашу догадку и подсветить буквы разными цветами.

Для этого мы сначала собираем строку с догадкой из символов в массиве, а потом начинаем проверять. Если букв меньше 5 — выводим уведомление, причём у нас сразу получится красивое уведомление, потому что будем использовать Toastr. В этой библиотеке уже есть стандартные уведомления об ошибках, поэтому нам достаточно использовать команду toastr.error("Введены не все буквы!"), чтобы сразу получить стильное уведомление в правом верхнем углу экрана.

Точно так же проверяем, есть ли введённое слово в списке, чтобы игрок не вводил что угодно, лишь бы проверить буквы. Если нет — тоже выводим уведомление и выходим из проверки догадки.

Если в догадке 5 букв и слово есть в списке, то начинаем раскрашивать клетки в зависимости от того, угадали мы с позицией буквы или нет. Когда все буквы станут зелёными, то сразу обнуляем количество попыток и с помощью Toastr выводим уведомление о победе. А если все попытки закончились — сообщение о том, что мы проиграли.

// проверка введённого слова
function checkGuess () {
    // получаем доступ к текущей строке
    let row = document.getElementsByClassName("letter-row")[6 - guessesRemaining]
    // переменная, где будет наша догадка
    let guessString = ''
    // делаем из загаданного слова массив символов
    let rightGuess = Array.from(rightGuessString)

    // собираем все введённые в строке буквы в одно слово
    for (const val of currentGuess) {
        guessString += val
    }

    // если в догадке меньше 5 букв — выводим уведомление, что букв не хватает
    if (guessString.length != 5) {
        // error означает, что уведомление будет в формате ошибки
        toastr.error("Введены не все буквы!");
        // и после вывода выходим из проверки догадки
        return;
    }

    // если введённого слова нет в списке возможных слов — выводим уведомление
    if (!WORDS.includes(guessString)) {
        toastr.error("Такого слова нет в списке!")
        // и после вывода выходим из проверки догадки
        return;
    }

    // перебираем все буквы в строке, чтобы подсветить их нужным цветом
    for (let i = 0; i < 5; i++) {
        // убираем текущий цвет, если он был
        let letterColor = ''
        // получаем доступ к текущей клетке
        let box = row.children[i]
        // и к текущей букве в догадке
        let letter = currentGuess[i]
        
        // смотрим, на каком месте в исходном слове стоит текущая буква
        let letterPosition = rightGuess.indexOf(currentGuess[i])
        // если такой буквы нет в исходном слове
        if (letterPosition === -1) {
            // закрашиваем клетку серым
            letterColor = 'grey'
        // иначе, когда мы точно знаем, что буква в слове есть
        } else {
            // если позиция в слове совпадает с текущей позицией 
            if (currentGuess[i] === rightGuess[i]) {
                // закрашиваем клетку зелёным
                letterColor = 'green'
            } else {
                // в противном случае закрашиваем жёлтым
                letterColor = 'yellow'
            }

            // заменяем обработанный символ на знак решётки, чтобы не использовать его на следующем шаге цикла
            rightGuess[letterPosition] = "#"
        }

        // применяем выбранный цвет к фону клетки
        box.style.backgroundColor = letterColor;
    }

    // если мы угадали
    if (guessString === rightGuessString) {
        // выводим сообщение об успехе
        toastr.success("Вы выиграли!")
        // обнуляем количество попыток
        guessesRemaining = 0;
        // выходим из проверки
        return
    // в противном случае
    } else {
        // уменьшаем счётчик попыток
        guessesRemaining -= 1;
        // обнуляем массив с символами текущей попытки
        currentGuess = [];
        // начинаем отсчёт букв заново
        nextLetter = 0;

        // если попытки закончились
        if (guessesRemaining === 0) {
            // выводим сообщение о проигрыше
            toastr.error("У вас не осталось попыток. Вы проиграли!");
            // и выводим загаданное слово
            toastr.info(`Загаданное слово: "${rightGuessString}"`)
        }
    }
}
Делаем игру Wordle на JavaScript

Поиграть на странице проекта

<!DOCTYPE html>
<html lang="ru">
<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>Wordle</title>
    <link rel="stylesheet" href="style.css">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css" rel="stylesheet"/>
</head>
<body>
    <h1> Играем в Wordle </h1>
    
    <div id="game-board">

    </div>

<script
src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
<script src="script.js" type="module"></script>
</body>
</html>

/* заголовок страницы */
h1 {
 text-align: center;
}

/* стиль для всей доски */
#game-board {
  /* делаем выравнивание всех элементов по центру */
  display: flex;
  align-items: center;
  /* добавляем выравнивание по вертикали */
  flex-direction: column;
}

/* стиль для строки */
.letter-row {
  /* каждая клетка пусть будет в своём отдельном контейнере */
  display: flex;
}

/* стиль для клетки */
.letter-box {
  /* рисуем границу */
  border: 2px solid gray;
  border-radius: 3px;
  /* добавляем отступы */
  margin: 2px;
  /* размер шрифта */
  font-size: 2.5rem;
  font-weight: 700;
  /* высота и ширина клетки */
  height: 3rem;
  width: 3rem;
  /* выравниваем содержимое по центру */
  display: flex;
  justify-content: center;
  align-items: center;
  /* делаем все буквы большими */
  text-transform: uppercase;
}

/* добавляем жирную обводку у заполненных клеток */
.filled-box {
  border: 3px solid black;
}

// импортируем слова из файла
import { WORDS } from "./words.js";

// количество попыток
const NUMBER_OF_GUESSES = 6;
// сколько попыток осталось
let guessesRemaining = NUMBER_OF_GUESSES;
// текущая попытка
let currentGuess = [];
// следующая буква
let nextLetter = 0;
// загаданное слово
let rightGuessString = WORDS[Math.floor(Math.random() * WORDS.length)]
// на всякий случай выведем в консоль загаданное слово, чтобы проверить, как работает игра
console.log(rightGuessString)

// создаём игровое поле
function initBoard() {
    // получаем доступ к блоку на странице
    let board = document.getElementById("game-board");

    // создаём строки
    // делаем цикл от 1 до 6, потому что попыток у нас как раз 6
    for (let i = 0; i < NUMBER_OF_GUESSES; i++) {
        // создаём новый блок на странице
        let row = document.createElement("div")
        // добавляем к нему класс, чтобы потом работать со строками напрямую
        row.className = "letter-row"
        
        // создаём отдельные клетки
        // добавляем по 5 клеток в ряд
        for (let j = 0; j < 5; j++) {
            // создаём новый блок на странице
            let box = document.createElement("div")
            // добавляем к нему класс
            box.className = "letter-box"
            // вкладываем новый блок внутрь блока со строкой
            row.appendChild(box)
        }

        // как все 5 клеток готовы, добавляем новую строку на поле
        board.appendChild(row)
    }
}

// удаление символа
function deleteLetter () {
    // получаем доступ к текущей строке
    let row = document.getElementsByClassName("letter-row")[6 - guessesRemaining]
    // и к последнему введённому символу
    let box = row.children[nextLetter - 1]
    // очищаем содержимое клетки
    box.textContent = ""
    // убираем жирную обводку
    box.classList.remove("filled-box")
    // удаляем последний символ из массива с нашей текущей догадкой
    currentGuess.pop()
    // помечаем, что у нас теперь на одну свободную клетку больше
    nextLetter -= 1
}

// проверка введённого слова
function checkGuess () {
    // получаем доступ к текущей строке
    let row = document.getElementsByClassName("letter-row")[6 - guessesRemaining]
    // переменная, где будет наша догадка
    let guessString = ''
    // делаем из загаданного слова массив символов
    let rightGuess = Array.from(rightGuessString)

    // собираем все введённые в строке буквы в одно слово
    for (const val of currentGuess) {
        guessString += val
    }

    // если в догадке меньше 5 букв — выводим уведомление, что букв не хватает
    if (guessString.length != 5) {
        // error означает, что уведомление будет в формате ошибки
        toastr.error("Введены не все буквы!");
        // и после вывода выходим из проверки догадки
        return;
    }

    // если введённого слова нет в списке возможных слов — выводим уведомление
    if (!WORDS.includes(guessString)) {
        toastr.error("Такого слова нет в списке!")
        // и после вывода выходим из проверки догадки
        return;
    }

    // перебираем все буквы в строке, чтобы подсветить их нужным цветом
    for (let i = 0; i < 5; i++) {
        // убираем текущий цвет, если он был
        let letterColor = ''
        // получаем доступ к текущей клетке
        let box = row.children[i]
        // и к текущей букве в догадке
        let letter = currentGuess[i]
        
        // смотрим, на каком месте в исходном слове стоит текущая буква
        let letterPosition = rightGuess.indexOf(currentGuess[i])
        // если такой буквы нет в исходном слове
        if (letterPosition === -1) {
            // закрашиваем клетку серым
            letterColor = 'grey'
        // иначе, когда мы точно знаем, что буква в слове есть
        } else {
            // если позиция в слове совпадает с текущей позицией 
            if (currentGuess[i] === rightGuess[i]) {
                // закрашиваем клетку зелёным
                letterColor = 'green'
            } else {
                // в противном случае закрашиваем жёлтым
                letterColor = 'yellow'
            }

            // заменяем обработанный символ на знак решётки, чтобы не использовать его на следующем шаге цикла
            rightGuess[letterPosition] = "#"
        }

        // применяем выбранный цвет к фону клетки
        box.style.backgroundColor = letterColor;
    }

    // если мы угадали
    if (guessString === rightGuessString) {
        // выводим сообщение об успехе
        toastr.success("Вы выиграли!")
        // обнуляем количество попыток
        guessesRemaining = 0;
        // выходим из проверки
        return
    // в противном случае
    } else {
        // уменьшаем счётчик попыток
        guessesRemaining -= 1;
        // обнуляем массив с символами текущей попытки
        currentGuess = [];
        // начинаем отсчёт букв заново
        nextLetter = 0;

        // если попытки закончились
        if (guessesRemaining === 0) {
            // выводим сообщение о проигрыше
            toastr.error("У вас не осталось попыток. Вы проиграли!");
            // и выводим загаданное слово
            toastr.info(`Загаданное слово: "${rightGuessString}"`)
        }
    }
}

// выводим букву в клетку
function insertLetter (pressedKey) {
    // если клетки закончились
    if (nextLetter === 5) {
        // выходим из функции
        return;
    }
    // получаем доступ к текущей строке
    let row = document.getElementsByClassName("letter-row")[6 - guessesRemaining]
    // и к текущей клетке, где будет появляться буква
    let box = row.children[nextLetter]
    // меняем текст в блоке с клеткой на нажатый символ
    box.textContent = pressedKey
    // добавляем к клетке жирную обводку
    box.classList.add("filled-box")
    // добавляем введённый символ к массиву, в которой хранится наша текущая попытка угадать слово
    currentGuess.push(pressedKey)
    // помечаем, что дальше будем работать со следующей клеткой
    nextLetter += 1
}

// обработчик нажатия на клавиши
document.addEventListener("keydown", (e) => {

    // если попыток не осталось
    if (guessesRemaining === 0) {
        // выходим из функции
        return
    }

    // получаем код нажатой клавиши
    let pressedKey = String(e.key)
    // если нажат Backspace и в строке есть хоть один символ
    if (pressedKey === "Backspace" && nextLetter !== 0) {
        // то удаляем последнюю введённую букву
        deleteLetter();
        // и выходим из обработчика
        return;
    }

    // если нажат Enter
    if (pressedKey === "Enter") {
        // проверяем введённое слово
        checkGuess();
        // и выходим из обработчика
        return;
    }

    // проверяем, есть ли введённый символ в английском алфавите
    let found = pressedKey.match(/[a-z]/gi)
    // если нет
    if (!found || found.length > 1) {
        // то выходим из обработчика
        return
    // иначе добавляем введённую букву в новую клетку
    } else {
        insertLetter(pressedKey)
    }
})


initBoard();

Что дальше

Сейчас уже можно полноценно играть и угадывать слова, но можно сделать следующий шаг:

  • добавить наэкранную клавиатуру, чтобы можно было играть на устройствах без клавиатуры;
  • добавить красивую анимацию, чтобы игра выглядела эффектнее;
  • сделать русскую версию.

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

Исходный код:

Пол Акиньеми

Художник:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Кирилл Климентьев

Соцсети:

Виталий Вебер

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Вам может быть интересно
medium