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

  • Зеленая буква — если она правильная и находится в правильном месте фактического слова.
  • Желтая буква — если она правильная, но не в правильном месте фактического слова.
  • Серая буква — если ее нет в реальном слове.

По сути, это то, что вы научитесь создавать в этом руководстве: мы создадим клон Wordle в React. Все анимации будут напоминать оригинальную игру Wordle. При каждом обновлении страницы вы будете угадывать новое слово. Чтобы создать эту игру, вам нужно иметь базовые знания о React.

Настраивать

Чтобы настроить проект React:
выполните команду npx create-react-app wordle, а затем npm start, чтобы запустить проект.
В папке src у вас будет только три файла ( App.js, index.css и index.js). Удалите все остальные файлы, чтобы сохранить папку src в чистоте.
Добавьте следующий код в остальные файлы папки src.

//   src/App.js
function App() {
  return (
    <div className="App">
      <h1>WORDLE</h1>
    </div>
  );
}

export default App
/* src/index.css*/
body {
  text-align: center;
  font-size: 1em;
  font-family: verdana;
  margin: 0;
}
h1 {
  font-size: 1.2em;
  padding: 20px 0;
  border-bottom: 1px solid #eee;
  margin: 0 0 30px 0;
  color: #333;
}
//   src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Использовать JSON-сервер

Вам нужен список слов, чтобы начать создание игры Wordle. Вы случайным образом выберете одно из этих слов, чтобы начать игру. Мы можем получить список слов из внешнего источника. Для хранения наших данных требуется файл JSON. Просто создайте файл db.json внутри data folder и поместите в него следующие данные.

{
  "solutions": [
    {"id": 1, "word": "ninja"},
    {"id": 2, "word": "spade"},
    {"id": 3, "word": "pools"},
    {"id": 4, "word": "drive"},
    {"id": 5, "word": "relax"},
    {"id": 6, "word": "times"},
    {"id": 7, "word": "train"},
    {"id": 8, "word": "cores"},
    {"id": 9, "word": "pours"},
    {"id": 10, "word": "blame"},
    {"id": 11, "word": "banks"},
    {"id": 12, "word": "phone"},
    {"id": 13, "word": "bling"},
    {"id": 14, "word": "coins"},
    {"id": 15, "word": "hello"}
  ]
}

Установите json-сервер, используя npm i json-server для получения данных внутри компонента React. Json-сервер превращает данные JSON в конечные точки API. Запустите json-сервер с помощью команды json-server ./data/db.json --port 3001. Он предоставит конечную точку https://localhost:3001/solutions для доступа к вышеуказанным данным. Мы будем использовать хук useEffect() для получения данных с json-сервера. Также нам нужен хук useState() для хранения данных с json-сервера. Теперь файл App.js будет выглядеть так:

//   src/App.js

import { useEffect, useState } from 'react'

function App() {
  const [solution, setSolution] = useState(null)

  useEffect(() => {
    fetch('https://localhost:3001/solutions')
      .then(res => res.json())
      .then(json => {
        // random int between 0 & 14
        const randomSolution = json[Math.floor(Math.random() * json.length)]
        setSolution(randomSolution.word)
      })
  }, [setSolution])

  return (
    <div className="App">
      <h1>Wordle</h1>
      {solution && <div>Solution is: {solution}</div>}
    </div>
  )
}
export default App

Сделать словесный крючок

Теперь вам нужно написать логику игры для отслеживания предположений пользователей. Вы будете использовать раскрашивание букв, чтобы проверить правильность догадки. Вам нужно сделать собственный крючок Wordle для обработки игровой логики. Мы будем использовать этот хук для реализации функциональности. Таким образом, мы будем отделять пользовательский интерфейс игры от логики. Создайте файл useWordle.js в папке src/hooks. Давайте добавим скелетную логику в файл useWordle.js.

// src/hooks/useWordle.js

import { useState } from 'react'

const useWordle = (solution) => {
  const [turn, setTurn] = useState(0) 
  const [currentGuess, setCurrentGuess] = useState('')
  const [guesses, setGuesses] = useState([]) // each guess is an array
  const [history, setHistory] = useState([]) // each guess is a string
  const [isCorrect, setIsCorrect] = useState(false)

  // format a guess into an array of letter objects 
  // e.g. [{key: 'a', color: 'yellow'}]
  const formatGuess = () => {

  }

  // add a new guess to the guesses state
  // update the isCorrect state if the guess is correct
  // add one to the turn state
  const addNewGuess = () => {

  }

  // handle keyup event & track current guess
  // if user presses enter, add the new guess
  const handleKeyup = () => {

  }

  return {turn, currentGuess, guesses, isCorrect, handleKeyup}
}

export default useWordle

Отслеживание текущего предположения

Вам нужно отслеживать догадку, пока пользователь отправляет слово. Для этого мы будем использовать прослушиватель событий для каждого нажатия клавиши. Мы создадим новый компонент Wordle React для настройки этого слушателя. Мы также получим доступ к хуку useWordle() внутри компонента Wordle.

// src/components/Wordle.js

import React, { useEffect } from 'react'
import useWordle from '../hooks/useWordle'

export default function Wordle({ solution }) {
  const { currentGuess, handleKeyup } = useWordle(solution)

  useEffect(() => {
    window.addEventListener('keyup', handleKeyup)

    return () => window.removeEventListener('keyup', handleKeyup)
  }, [handleKeyup])

  return (
    <div>
      <div>Current Guess - {currentGuess}</div>
    </div>
  )
}

При каждом нажатии клавиши будет запускаться функция handleKeyup. Итак, вам нужно убедиться, что отслеживаются только английские буквы. Кроме того, Backspace удалит последнюю букву из текущего предположения.

const handleKeyup = ({ key }) => {
    if (key === 'Backspace') {
      setCurrentGuess(prev => prev.slice(0, -1))
      return
    }
    if (/^[A-Za-z]$/.test(key)) {
      if (currentGuess.length < 5) {
        setCurrentGuess(prev => prev + key)
      }
    }
  }

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

import Wordle from './components/Wordle'

return (
    <div className="App">
      <h1>Wordle (Lingo)</h1>
      {solution && <Wordle solution={solution} />}
    </div>
)

Отправить и отформатировать догадки

Когда пользователь отправляет слово, нажимая Enter, нам нужно обработать это слово для дальнейшей игровой логики. Мы построим следующую логику:

  • Добавляйте предположение только в том случае, если ход меньше пяти.
  • Не допускайте дублирования слов.
  • Контрольное слово состоит из пяти символов.

Функция угадывания формата сравнивает каждую букву со словом решения и соответствующим образом применяет цвета.

const formatGuess = () => {
    let solutionArray = [...solution]
    let formattedGuess = [...currentGuess].map((l) => {
      return {key: l, color: 'grey'}
    })

    // find any green letters
    formattedGuess.forEach((l, i) => {
      if (solution[i] === l.key) {
        formattedGuess[i].color = 'green'
        solutionArray[i] = null
      }
    })

    // find any yellow letters
    formattedGuess.forEach((l, i) => {
      if (solutionArray.includes(l.key) && l.color !== 'green') {
        formattedGuess[i].color = 'yellow'
        solutionArray[solutionArray.indexOf(l.key)] = null
      }
    })

    return formattedGuess
}

Кроме того, вам нужно вызывать formatGuess() внутри handleKeyup(), когда пользователь нажимает Enter.

const handleKeyup = ({ key }) => {
    if (key === 'Enter') {
      // only add guess if turn is less than 5
      if (turn > 5) {
        console.log('you used all your guesses!')
        return
      }
      // do not allow duplicate words
      if (history.includes(currentGuess)) {
        console.log('you already tried that word.')
        return
      }
      // check word is 5 chars
      if (currentGuess.length !== 5) {
        console.log('word must be 5 chars.')
        return
      }
      const formatted = formatGuess()
      console.log(formatted)
    }
    if (key === 'Backspace') {
      setCurrentGuess(prev => prev.slice(0, -1))
      return
    }
    if (/^[A-Za-z]$/.test(key)) {
      if (currentGuess.length < 5) {
        setCurrentGuess(prev => prev + key)
      }
    }
}

return {turn, currentGuess, guesses, isCorrect, handleKeyup}

Добавьте новые догадки

Теперь вы выполнили отслеживание догадок, отправку догадок и форматирование догадок. Вы должны добавить отформатированное предположение в массив предположения хука useState(). После этого мы распечатаем эти догадки на сетке Wordle. В методе handleKeyup нам нужно вызвать addNewGuess() после formatGuess().

  const formatted = formatGuess()
  addNewGuess(formatted)

Нам нужен список из шести догадок для печати на игровой сетке. Итак, мы должны установить длину догадок на шесть в useWordle.js.

const [guesses, setGuesses] = useState([...Array(6)])

Давайте реализуем addNewGuess(), чтобы обновить список догадок для сетки Wordle.

const addNewGuess = (formattedGuess) => {
    if (currentGuess === solution) {
      setIsCorrect(true)
    }
    setGuesses(prevGuesses => {
      let newGuesses = [...prevGuesses]
      newGuesses[turn] = formattedGuess
      return newGuesses
    })
    setHistory(prevHistory => {
      return [...prevHistory, currentGuess]
    })
    setTurn(prevTurn => {
      return prevTurn + 1
    })
    setCurrentGuess('')
}

Также нам нужны все эти значения внутри компонента Wordle.

const { currentGuess, guesses, turn, isCorrect, handleKeyup } = useWordle(solution)

Создайте игровую сетку

Теперь нам нужно отобразить догадки в сетке Wordle. Мы создадим сетку из шести рядов. Каждый ряд будет состоять из пяти квадратов для пяти букв. Итак, мы создадим два компонента, Grid и Row. Давайте сначала создадим компонент Grid.

// src/components/Grid.js

import React from 'react'
import Row from './Row'

export default function Grid({ guesses, currentGuess, turn }) {
  return (
    <div>
      {guesses.map((g, i) => {
        return <Row key={i} /> 
      })}
    </div>
  )
}

Мы должны создать компонент Row для компонента Grid.

// src/components/Row.js

import React from 'react'

export default function Row() {

  return (
    <div className="row">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>
  )

}

Давайте добавим стили в файл index.css для компонента Row.

.row {
  text-align: center;
  display: flex;
  justify-content: center;
}
.row > div {
  display: block;
  width: 60px;
  height: 60px;
  border: 1px solid #bbb;
  margin: 4px;
  text-align: center;
  line-height: 60px;
  text-transform: uppercase;
  font-weight: bold;
  font-size: 2.5em;
}

Наконец, добавьте компонент Grid к компоненту Wordle.

... 
import Grid from './Grid'
...
return (
    <div>
      ...
      <Grid guesses={guesses} currentGuess={currentGuess} turn={turn} />
    </div>
  )

Показать прошлые и текущие предположения

На данный момент в сетке не отображается ни одна буква. Мы отобразим список догадок в этой сетке. Во-первых, мы будем передавать каждое предположение Row. Все буквы догадки будут отображаться на квадратах строки. Каждый квадрат будет иметь цвет фона в соответствии с догадкой. Давайте угадаем каждую строку внутри компонента Grid.

// src/components/Grid.js

export default function Grid({ guesses, currentGuess, turn }) {
  return (
    <div>
      {guesses.map((g, i) => {
        return <Row key={i} guess={g} /> 
      })}
    </div>
  )
}

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

// src/components/Row.js

import React from 'react'

export default function Row({ guess }) {

  if (guess) {
    return (
      <div className="row past">
        {guess.map((l, i) => (
          <div key={i} className={l.color}>{l.key}</div>
        ))}
      </div>
    )
  }

  return (
    <div className="row">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>
  )

}

Добавьте следующие стили для квадратов в файл index.css.

.row > div.green {
  background: #5ac85a;
  border-color: #5ac85a;
}
.row > div.grey {
  background: #a1a1a1;
  border-color: #a1a1a1;
}
.row > div.yellow {
  background: #e2cc68;
  border-color: #e2cc68;
}

Мы отображаем все прошлые предположения в сетке. Нам также нужно отображать текущее предположение при наборе текста. В компоненте Grid мы хотим передать текущее предположение компоненту Row. Таким образом, строка с текущим ходом будет отображать текущую догадку.

// src/components/Grid.js

return (
    <div>
      {guesses.map((g, i) => {
        if (turn === i) {
          return <Row key={i} currentGuess={currentGuess} />
        }
        return <Row key={i} guess={g} /> 
      })}
    </div>
)

Теперь обновите логику компонента Row для текущего предположения. Кроме того, извлеките текущее предположение в качестве реквизита в компоненте Row.

// src/components/Row.js
if (currentGuess) {
    let letters = currentGuess.split('')

    return (
      <div className="row current">
        {letters.map((letter, i) => (
          <div key={i} className="filled">{letter}</div>
        ))}
        {[...Array(5 - letters.length)].map((_,i) => (
          <div key={i}></div>
        ))}
      </div>
    )
}

Теперь мы можем отобразить текущее предположение и прошлые предположения.

Повтор сеанса с открытым исходным кодом

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

Начните получать удовольствие от отладки — начните использовать OpenReplay бесплатно.

Мозаичная анимация

Нам нужно сделать еще несколько вещей. Прежде всего, мы займемся анимацией букв и слов. Мы сделаем это с помощью анимации ключевых кадров. Мы не будем подробно обсуждать анимацию, так как мы сосредоточены на создании игры Wordle с использованием React. Давайте добавим анимацию в файл index.css.

.row > div.green {
  --background: #5ac85a;
  --border-color: #5ac85a;
  animation: flip 0.5s ease forwards;
}
.row > div.grey {
  --background: #a1a1a1;
  --border-color: #a1a1a1;
  animation: flip 0.6s ease forwards;
}
.row > div.yellow {
  --background: #e2cc68;
  --border-color: #e2cc68;
  animation: flip 0.5s ease forwards;
}
.row > div:nth-child(2) {
  animation-delay: 0.2s;
}
.row > div:nth-child(3) {
  animation-delay: 0.4s;
}
.row > div:nth-child(4) {
  animation-delay: 0.6s;
}
.row > div:nth-child(5) {
  animation-delay: 0.8s;
}

/* keyframe animations */
@keyframes flip {
  0% {
    transform: rotateX(0);
    background: #fff;
    border-color: #333;
  }
  45% {
    transform: rotateX(90deg);
    background: white;
    border-color: #333;
  }
  55% {
    transform: rotateX(90deg);
    background: var(--background);
    border-color: var(--border-color);
  }
  100% {
    transform: rotateX(0deg);
    background: var(--background);
    border-color: var(--border-color);
    color: #eee;
  }
}

Нам также нужно добавить эффект отскока к квадратам текущего предположения. Давайте добавим эту анимацию отскока в файл index.css.

.row.current > div.filled {
  animation: bounce 0.2s ease-in-out forwards;
}

@keyframes bounce {
  0% { 
    transform: scale(1);
    border-color: #ddd;
  }
  50% { 
    transform: scale(1.2);
  }
  100% {
    transform: scale(1);
    border-color: #333;
  }
}

Теперь мы закончили с частью анимации.

Компонент клавиатуры

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

//  src/constants/keys.js

  const keys = [
    {key: "a"}, {key: "b"}, {key: "c"}, {key: "d"}, {key: "e"},
    {key: "f"}, {key: "g"}, {key: "h"}, {key: "i"}, {key: "j"},
    {key: "k"}, {key: "l"}, {key: "m"}, {key: "n"}, {key: "o"},
    {key: "p"}, {key: "q"}, {key: "r"}, {key: "s"}, {key: "t"},
    {key: "u"}, {key: "v"}, {key: "w"}, {key: "x"}, {key: "y"},
    {key: "z"},
  ];

export default keys;

Теперь нам нужно создать компонент Keypad.

//  src/components/Keypad.js

import React, { useState, useEffect } from 'react'

export default function Keypad({keys}) {
  const [letters, setLetters] = useState(null)

  useEffect(() => {
    setLetters(keys)
  }, [keys])

  return (
    <div className="keypad">
      {letters && letters.map(l => {
        return (
          <div key={l.key}>{l.key}</div>
        )
      })}
    </div>
  )
}

Давайте добавим стили в index.css для компонента Keypad.

.keypad {
  max-width: 500px;
  margin: 20px auto;
}
.keypad > div {
  margin: 5px;
  width: 40px;
  height: 50px;
  background: #eee;
  display: inline-block;
  border-radius: 6px;
  line-height: 50px;
}

Теперь вам нужно добавить компонент Keypad к компоненту Wordle.

import Keypad from './Keypad'
import keys from '../constants/keys'

return (
    <div>
      ...
      <Keypad keys={keys}/>
    </div>
)

На данный момент клавиатура не отображает используемые клавиши. Мы должны обновить его внутри useWordle.js.

// src/hooks/useWordle.js
const [usedKeys, setUsedKeys] = useState({}) 

const addNewGuess = (formattedGuess) => {
    if (currentGuess === solution) {
      setIsCorrect(true)
    }
    setGuesses(prevGuesses => {
      let newGuesses = [...prevGuesses]
      newGuesses[turn] = formattedGuess
      return newGuesses
    })
    setHistory(prevHistory => {
      return [...prevHistory, currentGuess]
    })
    setTurn(prevTurn => {
      return prevTurn + 1
    })
    setUsedKeys(prevUsedKeys => {
      formattedGuess.forEach(l => {
        const currentColor = prevUsedKeys[l.key]

        if (l.color === 'green') {
          prevUsedKeys[l.key] = 'green'
          return
        }
        if (l.color === 'yellow' && currentColor !== 'green') {
          prevUsedKeys[l.key] = 'yellow'
          return
        }
        if (l.color === 'grey' && currentColor !== ('green' || 'yellow')) {
          prevUsedKeys[l.key] = 'grey'
          return
        }
      })

      return prevUsedKeys
    })
    setCurrentGuess('')
}

  return {turn, currentGuess, guesses, isCorrect, usedKeys, handleKeyup}

Также обновите компонент Wordle.

const { currentGuess, guesses, turn, isCorrect, usedKeys, handleKeyup } = useWordle(solution)

<Keypad keys={keys} usedKeys={usedKeys}/>

Теперь обработайте реквизит usedKeys внутри компонента Keypad и отразите используемые цвета клавиш на клавиатуре.

export default function Keypad({ keys, usedKeys }) {
  return (
    <div className="keypad">
      {letters && letters.map(l => {
        const color = usedKeys[l.key]
        return (
          <div key={l.key} className={color}>{l.key}</div>
        )
      })}
    </div>
  )
}

Мы должны добавить стили в index.css для компонента Keypad.

.keypad > div.green {
  background: #5ac85a;
  color: #fff;
  transition: all 0.3s ease-in;
}
.keypad > div.yellow {
  background: #e2cc68;
  color: #fff;
  transition: all 0.3s ease-in;
}
.keypad > div.grey {
  background: #a1a1a1;
  color: #fff;
  transition: all 0.3s ease-in;
}

Теперь мы закончили с компонентом Keypad.

Отображение модального окна

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

  • Когда пользователь угадывает правильно.
  • Когда у пользователя заканчиваются ходы.

Для этого мы добавим прослушиватель событий внутри хука useEffect() компонента Wordle.

useEffect(() => {
    window.addEventListener('keyup', handleKeyup)

    if (isCorrect) {
      window.removeEventListener('keyup', handleKeyup)
    }
    if (turn > 5) {
      window.removeEventListener('keyup', handleKeyup)
    }

    return () => window.removeEventListener('keyup', handleKeyup)
  }, [handleKeyup, isCorrect, turn])

Теперь мы должны сделать так, чтобы модальное окно появлялось на экране, когда игра заканчивается. Давайте создадим компонент Modal.

// src/components/Modal.js

import React from 'react'

export default function Modal({ isCorrect, solution, turn }) {
  return (
    <div className="modal">
      {isCorrect && (
        <div>
          <h1>You Win!</h1>
          <p className="solution">{solution}</p>
          <p>You found the word in {turn} guesses</p>
        </div>
      )}
      {!isCorrect && (
        <div>
          <h1>Unlucky!</h1>
          <p className="solution">{solution}</p>
          <p>Better luck next time</p>
        </div>
      )}
    </div>
  )
}

Также добавьте стили в index.css для компонента Modal.

.modal {
  background: rgba(255,255,255,0.7);
  position: fixed;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
}
.modal div {
  max-width: 480px;
  background: #fff;
  padding: 40px;
  border-radius: 10px;
  margin: 10% auto;
  box-shadow: 2px 2px 10px rgba(0,0,0,0.3);
}
.modal .solution {
  border: 1px solid MediumSeaGreen;
  color: #fff;
  background-color: MediumSeaGreen;
  font-weight: bold;
  font-size: 2.5rem;
  text-transform: uppercase;
  letter-spacing: 1px;
}

Нам нужно добавить компонент Modal внутрь компонента Wordle.

import React, { useState, useEffect } from 'react'

import Modal from './Modal'

const [showModal, setShowModal] = useState(false)

useEffect(() => {
    window.addEventListener('keyup', handleKeyup)

    if (isCorrect) {
      setTimeout(() => setShowModal(true), 2000)
      window.removeEventListener('keyup', handleKeyup)
    }
    if (turn > 5) {
      setTimeout(() => setShowModal(true), 2000)
      window.removeEventListener('keyup', handleKeyup)
    }

    return () => window.removeEventListener('keyup', handleKeyup)
  }, [handleKeyup, isCorrect, turn])

  {showModal && <Modal isCorrect={isCorrect} turn={turn} solution={solution} />}

Когда игра закончится, на экране появится модальное окно.

Вы закончили создание клона Wordle с помощью React!

Первоначально опубликовано в Блоге Openreplay 30 декабря 2022 г.