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;
}
Создаём файл со словами
Для игры нам понадобятся все английские слова из 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;
}
Вводим слова
Чтобы игрок мог вводить слова, добавим обработчик события "keydown" — оно сработает в момент нажатия клавиши. Работать алгоритм будет так:
- Если все попытки угадать слово истрачены, обработчик тут же завершается, потому что вводить новые символы просто некуда.
- Если нажата клавиша Backspace — вызывается функция удаления последнего символа.
- Если нажат Enter — вызывается функция проверки слова, угадали мы или нет.
- А если нажата какая-то другая клавиша, то алгоритм проверит, попадают ли введённые символы в английский алфавит (потому что у нас сейчас версия для английского языка). Если попадают — вызывается функция добавления символа в клетку. Здесь нам пригодятся регулярные выражения, с которыми мы уже работали в других проектах.
Запишем это на 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;
}
Удаляем символы
Когда мы разобрались с механизмом добавления символа, то удалить его совсем просто: достаточно отменить все изменения в текущей клетке. Это значит, что нам нужно:
- очистить текст в текущей клетке;
- убрать жирную обводку;
- удалить последний символ из нашей строки с текущей догадкой;
- пометить, что теперь у нас на одну клетку больше, чем было.
// удаление символа
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}"`)
}
}
}
<!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();
Что дальше
Сейчас уже можно полноценно играть и угадывать слова, но можно сделать следующий шаг:
- добавить наэкранную клавиатуру, чтобы можно было играть на устройствах без клавиатуры;
- добавить красивую анимацию, чтобы игра выглядела эффектнее;
- сделать русскую версию.
Попробуем реализовать это в следующей версии. Подпишитесь, чтобы не пропустить продолжение проекта.