Updated February 22, 2022
Build a Wordle Clone with React Native
Wordle has become an increasingly popular game and it’s one that we can build with React Native... in a single file!
Here’s a demo of what we’ll be building today.
Let’s jump in, shall we?
We'll use Expo and TypeScript throughout this tutorial. Feel free to use whatever you like - nothing is Expo specific.
To create a new Expo project with TypeScript running the following commands
expo init WordleClone
Then select “blank (TypeScript)” under Managed Workflow.
Build the Wordle UI with React Native
First we’ll build the UI and then add functionality. We’ll use a SafeAreaView
so all our content is visible and not hidden by any notches potentially on a device.
import React from "react"
import { StyleSheet, View, SafeAreaView } from "react-native"
export default function App() {
return (
<SafeAreaView>
<View></View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({})
Guess Blocks
In Wordle you have 6 chances to guess a 5 letter word. We’ll first build out the grid to display our guesses.
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,
},
})
We need to then display the letters within each “block”. Since there is a decent amount of duplication (and it’s only going to increase) I’m going to break the block into a component.
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",
},
})
Keyboard
Rather than using the system keyboard we’ll build our own simple one, following the QWERTY layout.
Each key should be tappable and we’ll rely on React Native’s Flexbox implementation to figure out the size of each key.
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,
},
})
We can then leverage our KeyboardRow
component to fill out the next two levels of the keyboard. On the third row I’m using ⌫
to represent the delete key.
// ...
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>
)
}
// ...
And finally we’ll add an “ENTER” key so the user can submit their guess.
// ...
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>
)
}
// ...
Capture User Input
Right now our keys don’t do anything. We’ll create a function that we pass down to each key that will be called each time a user taps one of the keys in our keyboard.
// ...
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>
)
}
// ...
Before we actually start capturing user key input lets remove the placeholder text.
// ...
const GuessRow = () => (
<View style={styles.guessRow}>
<Block letter="" />
<Block letter="" />
<Block letter="" />
<Block letter="" />
<Block letter="" />
</View>
)
// ...
Capture & Store Letter Presses
Now we can capture the actually keys a user presses. We’ll store a user’s guess in React state and then pass that state down to the GuessRow
component. We’ll handle multiple guesses later.
// ...
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>
)
}
// ...
Handle a Complete Guess
A guess is considered complete if it is 5 characters long. We don’t want to continue to add characters to a string of 5 characters.
// ...
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>
)
}
// ...
Handle Delete Tap
Another consideration we need to make is when the delete key is pressed. We should delete the last character from the string, which can be accomplished using a string 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>
)
}
// ...
Handle Enter Tap
When a user presses “enter” we have a few considerations to make. First is if the word is too short (length less than 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>
)
}
// ...
We also need to check if the word they submitted exists in our dictionary. This prevents people from mining for words by submitting something like “aeiou” in search of vowels.
// ...
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>
)
}
// ...
Finally, lets check if the word they submitted is equal to the active word.
// ...
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>
)
}
// ...
Manage State of Multiple Guesses
Now we need to do the more complicated part - handling the (up to) 6 guesses a user can make.
We’ll do this by creating a guesses object and a guess index. When a user submits a valid guess we’ll increase our guess index by one.
We can pass each guess down to the relevant GuessRow
to be displayed.
Finally, we need to consider if they’ve reached their game guess limit when pressing 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>
)
}
// ...
Indicate Accuracy in a Guess Block
So far we’ve got no visual indication if a guess has correctly placed letters or if they’re in the word.
To determine if a letter is in the word or in the correct place we need to know:
- Has the user submitted that guess yet (we don’t want to show them if a letter is correct before they press “ENTER”)
- What is the word the user is trying to guess?
- What is the user’s guess?
By passing that information down to the Block
component we can first compare if the letter the user guessed is the same as the letter in the word at that index.
If so, we should make the text white and the background green.
In the following image the word the user is trying to guess is “LIGHT” and their guess was “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,
},
// ...
})
Next case to consider is if the letter is in the word but at a different index. We should only check this if the character in the guess and character in the word do not match.
Again, the correct word is “LIGHT” but the user guessed “GOING”
// ...
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>
)
}
// ...
Finally we’ll indicate to the user if a letter they guessed is not in the word at all by setting the background to grey, ruling that letter out for future guesses.
Once again the correct word is “LIGHT” and the user guess is “GOING.
// ...
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>
)
}
// ...
Reset Game
Unlike the real Wordle game, where you can only play once per day, we’ll set ours up so that a user can reset the game and get a new word after winning/losing the game.
When the game is reset we’ll reset all the values by listening to the changing state using a 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",
},
})
That’s all we’ll cover today. You’ve got a pretty functional version of Wordle, in one file, using React Native.
Additional features to add that ours is missing include:
- Indicate if a letter is correctly placed, in the word, or not in the word on the keyboard
- Create a shareable emoji to show the user’s guess journey when the game is complete.
Final Code
The final file for our React Native Wordle clone is:
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",
},
})