Создание клона Wordle с помощью React Native

Этот пост был первоначально опубликован на сайте React Native School.

Wordle становится все более популярной игрой, и ее можно создать с помощью React Native… в одном файле!

Вот демонстрация того, что мы будем создавать сегодня.

Давайте приступим, не так ли?

В этом руководстве мы будем использовать Expo и TypeScript. Не стесняйтесь использовать то, что вам нравится — ничего специфичного для Expo нет.

Чтобы создать новый проект Expo с TypeScript, выполните следующие команды

expo init WordleClone
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем выберите «blank (TypeScript)» в разделе Managed Workflow.

Построение пользовательского интерфейса Wordle с помощью React Native

Сначала мы создадим пользовательский интерфейс, а затем добавим функциональность. Мы будем использовать SafeAreaView, чтобы весь наш контент был виден и не скрывался никакими вырезами, которые потенциально могут быть на устройстве.

import React from 'react';
import { StyleSheet, View, SafeAreaView } from 'react-native';

export default function App() {
  return (
    <SafeAreaView>
      <View></View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({});
Вход в полноэкранный режим Выход из полноэкранного режима

Угадайте блоки

В Wordle у вас есть 6 шансов угадать слово из 5 букв. Сначала мы построим сетку для отображения угаданных слов.

import React from 'react';
import { StyleSheet, View, SafeAreaView } from 'react-native';

const GuessRow = () => (
  <View style={styles.guessRow}>
    <View style={styles.guessSquare}></View>
    <View style={styles.guessSquare}></View>
    <View style={styles.guessSquare}></View>
    <View style={styles.guessSquare}></View>
    <View style={styles.guessSquare}></View>
  </View>
);

export default function App() {
  return (
    <SafeAreaView>
      <View>
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  guessRow: {
    flexDirection: 'row',
    justifyContent: 'center',
  },
  guessSquare: {
    borderColor: '#d3d6da',
    borderWidth: 2,
    width: 50,
    height: 50,
    alignItems: 'center',
    justifyContent: 'center',
    margin: 5,
  },
});
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем нам нужно отобразить буквы в каждом «блоке». Поскольку существует приличное количество дубликатов (и оно будет только увеличиваться), я собираюсь разбить блок на компоненты.

import React from 'react';
import { StyleSheet, View, SafeAreaView, Text } from 'react-native';

const Block = ({ letter }: { letter: string }) => (
  <View style={styles.guessSquare}>
    <Text style={styles.guessLetter}>{letter}</Text>
  </View>
);

const GuessRow = () => (
  <View style={styles.guessRow}>
    <Block letter="A" />
    <Block letter="E" />
    <Block letter="I" />
    <Block letter="O" />
    <Block letter="" />
  </View>
);

// ...

const styles = StyleSheet.create({
  // ...
  guessLetter: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#878a8c',
  },
});
Вход в полноэкранный режим Выход из полноэкранного режима

Клавиатура

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

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

import React from 'react';
import {
  StyleSheet,
  View,
  SafeAreaView,
  Text,
  TouchableOpacity,
} from 'react-native';

// ...

const Keyboard = () => {
  const row1 = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'];

  return (
    <View style={styles.keyboard}>
      <View style={styles.keyboardRow}>
        {row1.map((letter) => (
          <TouchableOpacity>
            <View style={styles.key}>
              <Text style={styles.keyLetter}>{letter}</Text>
            </View>
          </TouchableOpacity>
        ))}
      </View>
    </View>
  );
};

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
      </View>
      <Keyboard />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  // ...

  container: {
    justifyContent: 'space-between',
    flex: 1,
  },

  // keyboard
  keyboard: { flexDirection: 'column' },
  keyboardRow: {
    flexDirection: 'row',
    justifyContent: 'center',
    marginBottom: 10,
  },
  key: {
    backgroundColor: '#d3d6da',
    padding: 10,
    margin: 3,
    borderRadius: 5,
  },
  keyLetter: {
    fontWeight: '500',
    fontSize: 15,
  },
});
Вход в полноэкранный режим Выход из полноэкранного режима

Затем мы можем использовать наш компонент KeyboardRow для заполнения следующих двух уровней клавиатуры. В третьем ряду я использую для представления клавиши удаления.

// ...

const KeyboardRow = ({ letters }: { letters: string[] }) => (
  <View style={styles.keyboardRow}>
    {letters.map((letter) => (
      <TouchableOpacity>
        <View style={styles.key}>
          <Text style={styles.keyLetter}>{letter}</Text>
        </View>
      </TouchableOpacity>
    ))}
  </View>
);

const Keyboard = () => {
  const row1 = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'];
  const row2 = ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'];
  const row3 = ['Z', 'X', 'C', 'V', 'B', 'N', 'M', '⌫'];

  return (
    <View style={styles.keyboard}>
      <KeyboardRow letters={row1} />
      <KeyboardRow letters={row2} />
      <KeyboardRow letters={row3} />
    </View>
  );
};

// ...
Вход в полноэкранный режим Выход из полноэкранного режима

И, наконец, мы добавим клавишу «ENTER», чтобы пользователь мог отправить свою догадку.

// ...

const Keyboard = () => {
  const row1 = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'];
  const row2 = ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'];
  const row3 = ['Z', 'X', 'C', 'V', 'B', 'N', 'M', '⌫'];

  return (
    <View style={styles.keyboard}>
      <KeyboardRow letters={row1} />
      <KeyboardRow letters={row2} />
      <KeyboardRow letters={row3} />
      <View style={styles.keyboardRow}>
        <TouchableOpacity>
          <View style={styles.key}>
            <Text style={styles.keyLetter}>ENTER</Text>
          </View>
        </TouchableOpacity>
      </View>
    </View>
  );
};

// ...
Вход в полноэкранный режим Выход из полноэкранного режима

Захват пользовательского ввода

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

// ...

const KeyboardRow = ({
  letters,
  onKeyPress,
}: {
  letters: string[],
  onKeyPress: (letter: string) => void,
}) => (
  <View style={styles.keyboardRow}>
    {letters.map((letter) => (
      <TouchableOpacity onPress={() => onKeyPress(letter)}>
        <View style={styles.key}>
          <Text style={styles.keyLetter}>{letter}</Text>
        </View>
      </TouchableOpacity>
    ))}
  </View>
);

const Keyboard = ({ onKeyPress }: { onKeyPress: (letter: string) => void }) => {
  const row1 = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'];
  const row2 = ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'];
  const row3 = ['Z', 'X', 'C', 'V', 'B', 'N', 'M', '⌫'];

  return (
    <View style={styles.keyboard}>
      <KeyboardRow letters={row1} onKeyPress={onKeyPress} />
      <KeyboardRow letters={row2} onKeyPress={onKeyPress} />
      <KeyboardRow letters={row3} onKeyPress={onKeyPress} />
      <View style={styles.keyboardRow}>
        <TouchableOpacity onPress={() => onKeyPress('ENTER')}>
          <View style={styles.key}>
            <Text style={styles.keyLetter}>ENTER</Text>
          </View>
        </TouchableOpacity>
      </View>
    </View>
  );
};

export default function App() {
  const handleKeyPress = (letter: string) => {
    alert(letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Вход в полноэкранный режим Выход из полноэкранного режима

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

// ...

const GuessRow = () => (
  <View style={styles.guessRow}>
    <Block letter="" />
    <Block letter="" />
    <Block letter="" />
    <Block letter="" />
    <Block letter="" />
  </View>
);

// ...
Вход в полноэкранный режим Выход из полноэкранного режима

Захват и сохранение нажатия букв

Теперь мы можем перехватывать фактические нажатия клавиш пользователем. Мы сохраним угаданную пользователем букву в состоянии React, а затем передадим это состояние компоненту GuessRow. Мы обработаем несколько угадываний позже.

// ...

const GuessRow = ({ guess }: { guess: string }) => {
  const letters = guess.split('');

  return (
    <View style={styles.guessRow}>
      <Block letter={letters[0]} />
      <Block letter={letters[1]} />
      <Block letter={letters[2]} />
      <Block letter={letters[3]} />
      <Block letter={letters[4]} />
    </View>
  );
};

// ...

export default function App() {
  const [guess, setGuess] = React.useState('');

  const handleKeyPress = (letter: string) => {
    setGuess(guess + letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guess} />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Вход в полноэкранный режим Выход из полноэкранного режима

Обработка завершенного угадывания

Отгадка считается завершенной, если ее длина составляет 5 символов. Мы не хотим продолжать добавлять символы к строке из 5 символов.

// ...

export default function App() {
  const [guess, setGuess] = React.useState('');

  const handleKeyPress = (letter: string) => {
    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuess(guess + letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guess} />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Войти в полноэкранный режим Выйти из полноэкранного режима

Обработка нажатия кнопки удаления

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

// ...

export default function App() {
  const [guess, setGuess] = React.useState('');

  const handleKeyPress = (letter: string) => {
    if (letter === '⌫') {
      setGuess(guess.slice(0, -1));
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuess(guess + letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guess} />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Вход в полноэкранный режим Выход из полноэкранного режима

Обработка нажатия Enter

Когда пользователь нажимает кнопку «Enter», нам нужно учесть несколько моментов. Первое — если слово слишком короткое (длина меньше 5).

// ...

export default function App() {
  const [guess, setGuess] = React.useState('');

  const handleKeyPress = (letter: string) => {
    if (letter === 'ENTER') {
      if (guess.length !== 5) {
        alert('Word too short.');
        return;
      }
    }

    if (letter === '⌫') {
      setGuess(guess.slice(0, -1));
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuess(guess + letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guess} />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Вход в полноэкранный режим Выход из полноэкранного режима

Нам также необходимо проверить, существует ли слово, которое пользователь ввел, в нашем словаре. Это предотвращает поиск слов с помощью слов типа «aeiou» в поисках гласных.

// ...

const words = ['LIGHT', 'WRUNG', 'COULD', 'PERKY', 'MOUNT', 'WHACK', 'SUGAR'];

export default function App() {
  const [guess, setGuess] = React.useState('');

  const handleKeyPress = (letter: string) => {
    if (letter === 'ENTER') {
      if (guess.length !== 5) {
        alert('Word too short.');
        return;
      }

      if (!words.includes(guess)) {
        alert('Not a valid word.');
        return;
      }
    }

    if (letter === '⌫') {
      setGuess(guess.slice(0, -1));
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuess(guess + letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guess} />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Войти в полноэкранный режим Выход из полноэкранного режима

Наконец, проверим, равно ли представленное слово активному слову.

// ...

export default function App() {
  const [activeWord] = React.useState(words[0]);
  const [guess, setGuess] = React.useState('');

  const handleKeyPress = (letter: string) => {
    if (letter === 'ENTER') {
      if (guess.length !== 5) {
        alert('Word too short.');
        return;
      }

      if (!words.includes(guess)) {
        alert('Not a valid word.');
        return;
      }

      if (guess === activeWord) {
        alert('You win!');
        return;
      }
    }

    if (letter === '⌫') {
      setGuess(guess.slice(0, -1));
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuess(guess + letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guess} />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Войти в полноэкранный режим Выход из полноэкранного режима

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

Теперь нам нужно сделать более сложную часть — обработать (до) 6 угадываний, которые может сделать пользователь.

Для этого мы создадим объект guesses и индекс угадывания. Когда пользователь отправляет правильную отгадку, мы увеличиваем наш индекс отгадок на единицу.

Мы можем передать каждую догадку в соответствующую строку GuessRow для отображения.

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

// ...

interface IGuess {
  [key: number]: string;
}

const defaultGuess: IGuess = {
  0: '',
  1: '',
  2: '',
  3: '',
  4: '',
  5: '',
};

export default function App() {
  const [activeWord] = React.useState(words[0]);
  const [guessIndex, setGuessIndex] = React.useState(0);
  const [guesses, setGuesses] = React.useState < IGuess > defaultGuess;

  const handleKeyPress = (letter: string) => {
    const guess: string = guesses[guessIndex];

    if (letter === 'ENTER') {
      if (guess.length !== 5) {
        alert('Word too short.');
        return;
      }

      if (!words.includes(guess)) {
        alert('Not a valid word.');
        return;
      }

      if (guess === activeWord) {
        alert('You win!');
        return;
      }

      if (guessIndex < 5) {
        setGuessIndex(guessIndex + 1);
      } else {
        alert('You lose!');
      }
    }

    if (letter === '⌫') {
      setGuesses({ ...guesses, [guessIndex]: guess.slice(0, -1) });
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuesses({ ...guesses, [guessIndex]: guess + letter });
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guesses[0]} />
        <GuessRow guess={guesses[1]} />
        <GuessRow guess={guesses[2]} />
        <GuessRow guess={guesses[3]} />
        <GuessRow guess={guesses[4]} />
        <GuessRow guess={guesses[5]} />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Вход в полноэкранный режим Выход из полноэкранного режима

Индикация точности в блоке угадывания

До сих пор у нас не было визуальной индикации того, правильно ли угадавший разместил буквы и находятся ли они в слове.

Чтобы определить, находится ли буква в слове или в правильном месте, нам нужно знать:

  1. Отправил ли пользователь свою догадку (мы не хотим показывать ему правильность расположения буквы до того, как он нажмет «ENTER»).
  2. Какое слово пытается отгадать пользователь?
  3. Что угадал пользователь?

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

Если да, то мы должны сделать текст белым, а фон зеленым.

На следующем изображении слово, которое пытается угадать пользователь, — «LIGHT», а его отгадка — «TIGHT».

// ...

const Block = ({
  index,
  guess,
  word,
  guessed,
}: {
  index: number,
  guess: string,
  word: string,
  guessed: boolean,
}) => {
  const letter = guess[index];
  const wordLetter = word[index];

  const blockStyles: any[] = [styles.guessSquare];
  const textStyles: any[] = [styles.guessLetter];

  if (letter === wordLetter && guessed) {
    blockStyles.push(styles.guessCorrect);
    textStyles.push(styles.guessedLetter);
  }

  return (
    <View style={blockStyles}>
      <Text style={textStyles}>{letter}</Text>
    </View>
  );
};

const GuessRow = ({
  guess,
  word,
  guessed,
}: {
  guess: string,
  word: string,
  guessed: boolean,
}) => {
  return (
    <View style={styles.guessRow}>
      <Block index={0} guess={guess} word={word} guessed={guessed} />
      <Block index={1} guess={guess} word={word} guessed={guessed} />
      <Block index={2} guess={guess} word={word} guessed={guessed} />
      <Block index={3} guess={guess} word={word} guessed={guessed} />
      <Block index={4} guess={guess} word={word} guessed={guessed} />
    </View>
  );
};

// ...

const words = [
  'LIGHT',
  'TIGHT',
  'WRUNG',
  'COULD',
  'PERKY',
  'MOUNT',
  'WHACK',
  'SUGAR',
];

interface IGuess {
  [key: number]: string;
}

const defaultGuess: IGuess = {
  0: '',
  1: '',
  2: '',
  3: '',
  4: '',
  5: '',
};

export default function App() {
  const [activeWord] = React.useState(words[0]);
  const [guessIndex, setGuessIndex] = React.useState(0);
  const [guesses, setGuesses] = React.useState < IGuess > defaultGuess;

  const handleKeyPress = (letter: string) => {
    // ...
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow
          guess={guesses[0]}
          word={activeWord}
          guessed={guessIndex > 0}
        />
        <GuessRow
          guess={guesses[1]}
          word={activeWord}
          guessed={guessIndex > 1}
        />
        <GuessRow
          guess={guesses[2]}
          word={activeWord}
          guessed={guessIndex > 2}
        />
        <GuessRow
          guess={guesses[3]}
          word={activeWord}
          guessed={guessIndex > 3}
        />
        <GuessRow
          guess={guesses[4]}
          word={activeWord}
          guessed={guessIndex > 4}
        />
        <GuessRow
          guess={guesses[5]}
          word={activeWord}
          guessed={guessIndex > 5}
        />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  // ...
  guessedLetter: {
    color: '#fff',
  },
  guessCorrect: {
    backgroundColor: '#6aaa64',
    borderColor: '#6aaa64',
  },

  container: {
    justifyContent: 'space-between',
    flex: 1,
  },

  // ...
});
Вход в полноэкранный режим Выход из полноэкранного режима

Следующий случай, который необходимо рассмотреть, это если буква находится в слове, но под другим индексом. Проверять это следует только в том случае, если символ в отгадке и символ в слове не совпадают.

Опять же, правильное слово — «СВЕТ», но пользователь угадал «ИДЕТ».

// ...

const Block = ({
  index,
  guess,
  word,
  guessed,
}: {
  index: number,
  guess: string,
  word: string,
  guessed: boolean,
}) => {
  const letter = guess[index];
  const wordLetter = word[index];

  const blockStyles: any[] = [styles.guessSquare];
  const textStyles: any[] = [styles.guessLetter];

  if (letter === wordLetter && guessed) {
    blockStyles.push(styles.guessCorrect);
    textStyles.push(styles.guessedLetter);
  } else if (word.includes(letter) && guessed) {
    blockStyles.push(styles.guessInWord);
    textStyles.push(styles.guessedLetter);
  }

  return (
    <View style={blockStyles}>
      <Text style={textStyles}>{letter}</Text>
    </View>
  );
};

// ...
Вход в полноэкранный режим Выход из полноэкранного режима

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

И снова правильное слово — «СВЕТ», а угаданная пользователем буква — «ИДЕТ».

// ...

const Block = ({
  index,
  guess,
  word,
  guessed,
}: {
  index: number,
  guess: string,
  word: string,
  guessed: boolean,
}) => {
  const letter = guess[index];
  const wordLetter = word[index];

  const blockStyles: any[] = [styles.guessSquare];
  const textStyles: any[] = [styles.guessLetter];

  if (letter === wordLetter && guessed) {
    blockStyles.push(styles.guessCorrect);
    textStyles.push(styles.guessedLetter);
  } else if (word.includes(letter) && guessed) {
    blockStyles.push(styles.guessInWord);
    textStyles.push(styles.guessedLetter);
  } else if (guessed) {
    blockStyles.push(styles.guessNotInWord);
    textStyles.push(styles.guessedLetter);
  }

  return (
    <View style={blockStyles}>
      <Text style={textStyles}>{letter}</Text>
    </View>
  );
};

// ...
Вход в полноэкранный режим Выход из полноэкранного режима

Сброс игры

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

Когда игра будет сброшена, мы сбросим все значения, слушая изменение состояния с помощью useEffect.

import React from 'react';
import {
  StyleSheet,
  View,
  SafeAreaView,
  Text,
  TouchableOpacity,
  Button,
} from 'react-native';

// ...

export default function App() {
  const [activeWord, setActiveWord] = React.useState(words[0]);
  const [guessIndex, setGuessIndex] = React.useState(0);
  const [guesses, setGuesses] = React.useState < IGuess > defaultGuess;
  const [gameComplete, setGameComplete] = React.useState(false);

  const handleKeyPress = (letter: string) => {
    const guess: string = guesses[guessIndex];

    if (letter === 'ENTER') {
      if (guess.length !== 5) {
        alert('Word too short.');
        return;
      }

      if (!words.includes(guess)) {
        alert('Not a valid word.');
        return;
      }

      if (guess === activeWord) {
        setGuessIndex(guessIndex + 1);
        setGameComplete(true);
        alert('You win!');
        return;
      }

      if (guessIndex < 5) {
        setGuessIndex(guessIndex + 1);
      } else {
        setGameComplete(true);
        alert('You lose!');
        return;
      }
    }

    if (letter === '⌫') {
      setGuesses({ ...guesses, [guessIndex]: guess.slice(0, -1) });
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuesses({ ...guesses, [guessIndex]: guess + letter });
  };

  React.useEffect(() => {
    if (!gameComplete) {
      setActiveWord(words[Math.floor(Math.random() * words.length)]);
      setGuesses(defaultGuess);
      setGuessIndex(0);
    }
  }, [gameComplete]);

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow
          guess={guesses[0]}
          word={activeWord}
          guessed={guessIndex > 0}
        />
        <GuessRow
          guess={guesses[1]}
          word={activeWord}
          guessed={guessIndex > 1}
        />
        <GuessRow
          guess={guesses[2]}
          word={activeWord}
          guessed={guessIndex > 2}
        />
        <GuessRow
          guess={guesses[3]}
          word={activeWord}
          guessed={guessIndex > 3}
        />
        <GuessRow
          guess={guesses[4]}
          word={activeWord}
          guessed={guessIndex > 4}
        />
        <GuessRow
          guess={guesses[5]}
          word={activeWord}
          guessed={guessIndex > 5}
        />
      </View>
      <View>
        {gameComplete ? (
          <View style={styles.gameCompleteWrapper}>
            <Text>
              <Text style={styles.bold}>Correct Word:</Text> {activeWord}
            </Text>
            <View>
              <Button
                title="Reset"
                onPress={() => {
                  setGameComplete(false);
                }}
              />
            </View>
          </View>
        ) : null}
        <Keyboard onKeyPress={handleKeyPress} />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  // ...
  keyLetter: {
    fontWeight: '500',
    fontSize: 15,
  },

  // Game complete
  gameCompleteWrapper: {
    alignItems: 'center',
  },
  bold: {
    fontWeight: 'bold',
  },
});
Вход в полноэкранный режим Выход из полноэкранного режима

Это все, что мы рассмотрим сегодня. У вас получилась довольно функциональная версия Wordle в одном файле, использующая React Native.

Дополнительные функции, которых не хватает в нашей версии, включают:

  • Указывать, правильно ли расположена буква, в слове или не в слове на клавиатуре.
  • Создать эмодзи, которым можно поделиться, чтобы показать путь отгадки пользователя, когда игра завершена.

Финальный код

Финальный файл для нашего клона React Native Wordle таков:

import React from 'react';
import {
  StyleSheet,
  View,
  SafeAreaView,
  Text,
  TouchableOpacity,
  Button,
} from 'react-native';

const Block = ({
  index,
  guess,
  word,
  guessed,
}: {
  index: number,
  guess: string,
  word: string,
  guessed: boolean,
}) => {
  const letter = guess[index];
  const wordLetter = word[index];

  const blockStyles: any[] = [styles.guessSquare];
  const textStyles: any[] = [styles.guessLetter];

  if (letter === wordLetter && guessed) {
    blockStyles.push(styles.guessCorrect);
    textStyles.push(styles.guessedLetter);
  } else if (word.includes(letter) && guessed) {
    blockStyles.push(styles.guessInWord);
    textStyles.push(styles.guessedLetter);
  } else if (guessed) {
    blockStyles.push(styles.guessNotInWord);
    textStyles.push(styles.guessedLetter);
  }

  return (
    <View style={blockStyles}>
      <Text style={textStyles}>{letter}</Text>
    </View>
  );
};

const GuessRow = ({
  guess,
  word,
  guessed,
}: {
  guess: string,
  word: string,
  guessed: boolean,
}) => {
  return (
    <View style={styles.guessRow}>
      <Block index={0} guess={guess} word={word} guessed={guessed} />
      <Block index={1} guess={guess} word={word} guessed={guessed} />
      <Block index={2} guess={guess} word={word} guessed={guessed} />
      <Block index={3} guess={guess} word={word} guessed={guessed} />
      <Block index={4} guess={guess} word={word} guessed={guessed} />
    </View>
  );
};

const KeyboardRow = ({
  letters,
  onKeyPress,
}: {
  letters: string[],
  onKeyPress: (letter: string) => void,
}) => (
  <View style={styles.keyboardRow}>
    {letters.map((letter) => (
      <TouchableOpacity onPress={() => onKeyPress(letter)} key={letter}>
        <View style={styles.key}>
          <Text style={styles.keyLetter}>{letter}</Text>
        </View>
      </TouchableOpacity>
    ))}
  </View>
);

const Keyboard = ({ onKeyPress }: { onKeyPress: (letter: string) => void }) => {
  const row1 = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'];
  const row2 = ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'];
  const row3 = ['Z', 'X', 'C', 'V', 'B', 'N', 'M', '⌫'];

  return (
    <View style={styles.keyboard}>
      <KeyboardRow letters={row1} onKeyPress={onKeyPress} />
      <KeyboardRow letters={row2} onKeyPress={onKeyPress} />
      <KeyboardRow letters={row3} onKeyPress={onKeyPress} />
      <View style={styles.keyboardRow}>
        <TouchableOpacity onPress={() => onKeyPress('ENTER')}>
          <View style={styles.key}>
            <Text style={styles.keyLetter}>ENTER</Text>
          </View>
        </TouchableOpacity>
      </View>
    </View>
  );
};

const words = [
  'LIGHT',
  'TIGHT',
  'GOING',
  'WRUNG',
  'COULD',
  'PERKY',
  'MOUNT',
  'WHACK',
  'SUGAR',
];

interface IGuess {
  [key: number]: string;
}

const defaultGuess: IGuess = {
  0: '',
  1: '',
  2: '',
  3: '',
  4: '',
  5: '',
};

export default function App() {
  const [activeWord, setActiveWord] = React.useState(words[0]);
  const [guessIndex, setGuessIndex] = React.useState(0);
  const [guesses, setGuesses] = React.useState < IGuess > defaultGuess;
  const [gameComplete, setGameComplete] = React.useState(false);

  const handleKeyPress = (letter: string) => {
    const guess: string = guesses[guessIndex];

    if (letter === 'ENTER') {
      if (guess.length !== 5) {
        alert('Word too short.');
        return;
      }

      if (!words.includes(guess)) {
        alert('Not a valid word.');
        return;
      }

      if (guess === activeWord) {
        setGuessIndex(guessIndex + 1);
        setGameComplete(true);
        alert('You win!');
        return;
      }

      if (guessIndex < 5) {
        setGuessIndex(guessIndex + 1);
      } else {
        setGameComplete(true);
        alert('You lose!');
        return;
      }
    }

    if (letter === '⌫') {
      setGuesses({ ...guesses, [guessIndex]: guess.slice(0, -1) });
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuesses({ ...guesses, [guessIndex]: guess + letter });
  };

  React.useEffect(() => {
    if (!gameComplete) {
      setActiveWord(words[Math.floor(Math.random() * words.length)]);
      setGuesses(defaultGuess);
      setGuessIndex(0);
    }
  }, [gameComplete]);

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow
          guess={guesses[0]}
          word={activeWord}
          guessed={guessIndex > 0}
        />
        <GuessRow
          guess={guesses[1]}
          word={activeWord}
          guessed={guessIndex > 1}
        />
        <GuessRow
          guess={guesses[2]}
          word={activeWord}
          guessed={guessIndex > 2}
        />
        <GuessRow
          guess={guesses[3]}
          word={activeWord}
          guessed={guessIndex > 3}
        />
        <GuessRow
          guess={guesses[4]}
          word={activeWord}
          guessed={guessIndex > 4}
        />
        <GuessRow
          guess={guesses[5]}
          word={activeWord}
          guessed={guessIndex > 5}
        />
      </View>
      <View>
        {gameComplete ? (
          <View style={styles.gameCompleteWrapper}>
            <Text>
              <Text style={styles.bold}>Correct Word:</Text> {activeWord}
            </Text>
            <View>
              <Button
                title="Reset"
                onPress={() => {
                  setGameComplete(false);
                }}
              />
            </View>
          </View>
        ) : null}
        <Keyboard onKeyPress={handleKeyPress} />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  guessRow: {
    flexDirection: 'row',
    justifyContent: 'center',
  },
  guessSquare: {
    borderColor: '#d3d6da',
    borderWidth: 2,
    width: 50,
    height: 50,
    alignItems: 'center',
    justifyContent: 'center',
    margin: 5,
  },
  guessLetter: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#878a8c',
  },
  guessedLetter: {
    color: '#fff',
  },
  guessCorrect: {
    backgroundColor: '#6aaa64',
    borderColor: '#6aaa64',
  },
  guessInWord: {
    backgroundColor: '#c9b458',
    borderColor: '#c9b458',
  },
  guessNotInWord: {
    backgroundColor: '#787c7e',
    borderColor: '#787c7e',
  },

  container: {
    justifyContent: 'space-between',
    flex: 1,
  },

  // keyboard
  keyboard: { flexDirection: 'column' },
  keyboardRow: {
    flexDirection: 'row',
    justifyContent: 'center',
    marginBottom: 10,
  },
  key: {
    backgroundColor: '#d3d6da',
    padding: 10,
    margin: 3,
    borderRadius: 5,
  },
  keyLetter: {
    fontWeight: '500',
    fontSize: 15,
  },

  // Game complete
  gameCompleteWrapper: {
    alignItems: 'center',
  },
  bold: {
    fontWeight: 'bold',
  },
});
Войти в полноэкранный режим Выйти из полноэкранного режима

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *