AWS Logo
Menu
From Idea to Reality: Creating Beyond Words, A Fun and Competitive Word Game Built with React Native & Amplify

From Idea to Reality: Creating Beyond Words, A Fun and Competitive Word Game Built with React Native & Amplify

Beyond Words is a fun and challenging word game where players form words from a set of letters, unlock hints, and discover bonus words. With online tournaments powered by AWS Amplify, you can compete with friends and players worldwide. Built with React Native, it offers a smooth and engaging experience, featuring swipe-based input, animations, and cloud-saved progress. Test your vocabulary and strategy in this exciting word puzzle adventure!

Published Jan 28, 2025
Inspired by one of my favorite word games, Zen World, I wanted to create something similar—something I could enjoy playing whenever I tried to relax or take a break from a busy day. Since I was already familiar with the logic behind the game, I knew it would be straightforward conceptually (but not so easy to implement 😉). The core mechanic is simple: swipe the given letters to form valid English words.
When it came to choosing the right platform to build Beyond Words, I had two options—Unity and React Native. Initially, I considered using Unity, but it felt like overkill for a word game. On the other hand, I hesitated with React Native because it’s primarily a UI framework for building web, Android, and iOS applications, not typically used for game development.
However, after some research, I discovered how React Native handles the PanResponder event—a native event system that consolidates multiple touch gestures into a single, seamless interaction. This allows for smooth and intuitive swipe gestures, making it perfect for a word game like Beyond Words. At that moment, I knew React Native was the right choice for handling swipe input which is an important feature of the game and bringing my vision to life.

Implementing Authentication and Online Data Storage in Beyond Words

To make signing into Beyond Words as simple and user-friendly as possible, I decided to implement passwordless authentication using email-based sign-up and sign-in. This allows players to easily access their accounts without the need to remember or enter a password. All they need to do is enter their email, and a secure one-time code is sent to them for instant access.
To implement this system, I used AWS Amplify, which seamlessly handles the authentication process. This setup ensures that every player's game data is securely stored online while maintaining easy and convenient access without the need for passwords. By integrating passwordless authentication, I created a smoother, more secure sign-in process that benefits both new and returning players.

Implementing the Game Logic in Beyond Words

The performLevelOperation function

Implementing the game logic for Beyond Words was a bit challenging, but I’m going to break down the key parts to explain how it works.
Let’s start with the performLevelOperation function. This function is responsible for setting up the game levels and handling how the game progresses. Here's how the logic works for different levels:
  • Levels 1-5: The player needs to find exactly 4 words that match the target words chosen by the game. If a word is valid but not the target word, it gets saved as a bonus word with lower points. As the levels progress, more words will be required.
  • Levels 6-10: The number of words to find increases to 5.
  • Levels 11-15: Players will have to find 8 words.
  • Levels 16-21: The challenge ramps up to 10 words.
  • Levels above 21: The number of words required increases further to 14.
The game dynamically adjusts the challenge based on the player's progress through these levels.
Here’s how the performLevelOperation function is implemented:
```
const performLevelOperation = (): void => {
// Operation for levels 1-5
if (level >= 1 && level <= 5) {
const randomLevel = Math.floor(Math.random() * allAnagrams.length);
console.log({ randomLevel });
const selectedCurrentAnagram = getRandomItems(allAnagrams[randomLevel], 4);
const allCurrentAnagram = allAnagrams[randomLevel];
const sortedCurrentAnagram = selectedCurrentAnagram.sort((a, b) => a.length - b.length);
const sortedAllCurrentAnagram = allCurrentAnagram.sort((a, b) => a.length - b.length);

setSortedAnagrams(sortedCurrentAnagram);
setAnagrams(allCurrentAnagram.sort((a, b) => a.length - b.length));

const wordWithAssignedId = assignIDsToWord(sortedAllCurrentAnagram[sortedAllCurrentAnagram.length - 1]);
setWord(wordWithAssignedId);
shuffleWord();
}
// Repeat the similar structure for levels 6-10, 11-15, and so on...
}
```

Key Features of This Logic:

  • The game chooses a random anagram set from an array of all possible anagrams for each level.
  • The number of required words increases as the player progresses through levels, ensuring the game remains challenging.
  • It sorts the anagram words by length to structure the gameplay.
  • A word is selected and assigned an ID, which is used to manage and track the word in the game.
This logic will evolve as I continue to gather inspiration and player feedback, but it sets the foundation for the game's challenge and progression mechanics.

The checkWord Logic

The checkWord function in Beyond Words handles word validation and checks whether a player has already found the word, or if the word is valid but not the target word. Here's an overview of how the logic works:
  1. Forming the Word: The word formed by the player is either passed as an argument (wordToUnlock) or derived from the player's selected letters. The letters are then joined together to form the word, and it's converted to uppercase for uniformity.
  2. Checking for Duplicates: The function first checks if the word has already been found by comparing it with the words stored in the foundWords array. If the word exists, the function updates the list of found words and exits.
  3. Checking Bonus Words: If the word is valid but not the target word, it's checked against the bonusWords array. If the word is already a bonus word, the game will inform the player, but no further action will be taken.
  4. Target Word Found: If the word is part of the anagram (i.e., it's a valid target word), the game will:
    • Animate the word’s movement on the screen.
    • Award the player coins based on the length of the word.
    • Add the word to the list of foundWords and mark it as not a duplicate.
  5. Bonus Word: If the word is valid but not part of the anagram, it's added to the bonusWords list, and the player is informed that it's a valid word, but not the one they were looking for. Bonus words are rewarded with fewer coins compared to the target words.
Here's the implementation of the logic:
```JavaScript
const checkWord = (wordToUnlock?: string) => {
const joinedWord = wordToUnlock || Array.from(selectedLetterSequence)
.map((letter) => letter.char.toUpperCase())
.join("");

const wordExists = foundWords.some((fw) => fw.word === joinedWord);

// Check if the word has already been found
if (wordExists) {
const updatedWords = checkDuplicate(joinedWord);
console.log("You have found this word already");
return setFoundWords(updatedWords);
}
// Check if the word is in the bonus words list
if (bonusWords.includes(joinedWord.toUpperCase())) {
return console.log("You have found this VALID word already!!");
}
// Check if the word is part of the anagram (target word)
if (sortedAnagrams.includes(joinedWord.toUpperCase())) {
const index = sortedAnagrams.findIndex((word) => word === joinedWord);
setAnimatedWord([wordPositions[index].word]);
moveText(wordPositions[index].word.length, wordPositions[index].x, wordPositions[index].y);
setCoins(coins + joinedWord.length * 50);

return setFoundWords([
...foundWords,
{ word: joinedWord, animate: true, isDuplicate: false },
]);
}
// Check if the word is valid but not the target (bonus word)
if (!sortedAnagrams.includes(joinedWord.toUpperCase()) && anagrams.includes(joinedWord.toUpperCase())) {
setBonusWords([...bonusWords, joinedWord]);
console.log({ bonusWordPositionX, bonusWordPositionY });
setIsMovingBonusWord(true);
setAnimatedWord([joinedWord]);
moveText(4, bonusWordPositionX - moderateScale(43), bonusWordPositionY + moderateScale(420));
setCoins(coins + joinedWord.length * 10);

return console.log("It's a valid word, but not the word I'm looking for");
}
};
```
  • Duplicate Checking: Prevents players from submitting the same word multiple times.
  • Bonus Words: Adds words to the bonus list if they are valid but not part of the target anagram, rewarding the player with fewer coins.
  • Anagram Validation: Validates the word against the target anagram and rewards the player for correctly solving it.
  • Word Animation: Words are animated when found, providing a more engaging user experience.
This logic makes the game feel dynamic and provides clear feedback to players as they progress. It also encourages them to explore more valid words beyond the target ones to earn bonus rewards.

The shuffleWord Logic

The shuffleWord function is used to randomly shuffle the letters given to the player to form words. This is an important part of the game's dynamic gameplay as it presents a fresh challenge each time the letters are shuffled. Here's how the logic works:
  1. Shuffling the Letters:
    • The setWord function is used to update the word displayed to the player.
    • The previous word is cloned into a new array (shuffledWord), and then the Fisher-Yates shuffle algorithm is applied to randomly shuffle the letters of the word.
    • The Fisher-Yates algorithm works by iterating through the array in reverse order, and for each element, it picks a random index from the elements before it and swaps them.
  2. Ensuring Unique Words:
    • The getUniqueWords function is used to compare two arrays of words and find words that are present in one array but not the other.
    • It converts both arrays (arr1 and arr2) into sets and filters out words that exist in both arrays. This ensures that only unique words from each array are returned.
Here is the code for both functions:
```
// Shuffle the letters of the word to form a new word
const shuffleWord = () => {
setWord((prevWord) => {
const shuffledWord = [...prevWord]; // Clone the current word into a new array
for (let i = shuffledWord.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); // Get a random index
[shuffledWord[i], shuffledWord[j]] = [shuffledWord[j], shuffledWord[i]]; // Swap the elements
}
return shuffledWord; // Return the shuffled word
});
};
// Function to find unique words from two arrays
const getUniqueWords = (arr1: string[], arr2: string[]): string[] => {
const set1 = new Set(arr1); // Convert the first array to a set
const set2 = new Set(arr2); // Convert the second array to a set
// Return the words that are in arr1 but not in arr2, and in arr2 but not in arr1
return [
...arr1.filter(word => !set2.has(word)), // Words in arr1 but not in arr2
...arr2.filter(word => !set1.has(word)), // Words in arr2 but not in arr1
];
};
```

Explanation:

  • shuffleWord:
    • Randomly shuffles the letters in the prevWord array and updates the word state with the shuffled result.
    • The Fisher-Yates shuffle is an efficient algorithm to ensure randomness with minimal computational overhead.

Key Points:

  • Dynamic Gameplay: Shuffling the word offers a fresh experience for the player each time they try to form a valid word.
This logic keeps the game engaging and encourages players to discover new words while maintaining fairness in the gameplay.

The unlockRandomLetter Function

The unlockRandomLetter function provides a mechanism for players to unlock a random letter from the remaining letters in a word, which helps them make progress when they're stuck. This feature adds a layer of strategic gameplay, where players can unlock clues for a price (although the price logic isn't implemented yet). Here’s how the function works:
  1. Identifying Available Options:
    • The function first defines an empty array availableOptions that will store possible options for unlocking a letter.
    • It then calls getUniqueWords to filter out words that have already been found by the player from the list of all available words (sortedAnagrams).
    • It iterates through each word in the filtered list (words), and checks if each letter in the word has been unlocked yet.
    • The condition !unlockedLetters.some(...) ensures that only letters that haven’t been unlocked yet are considered for the unlocking options.
  2. Picking a Random Letter:
    • If there are available letters to unlock (availableOptions is not empty), a random choice is selected using Math.random().
    • This random choice represents a letter that the player can unlock to aid them in solving the word.
  3. Updating the State:
    • Once a random letter is chosen, it is added to the unlockedLetters array (which keeps track of all unlocked letters) using setUnlockedLetters.
  4. Edge Case - No Available Letters:
    • If all letters in the word have already been unlocked, the function logs a message saying, "All letters are unlocked!" and exits.
Here’s the code:
```
const unlockRandomLetter = () => {
const availableOptions: { word: string; index: number }[] = [];
// Get the words that have not been found yet
const words = getUniqueWords(sortedAnagrams, foundWords.map(item => item.word));

// Iterate through words and find available letters to unlock
words.forEach((word) => {
for (let i = 0; i < word.length; i++) {
if (!unlockedLetters.some((entry) => entry.word === word && entry.index === i)) {
availableOptions.push({ word, index: i });
}
}
});
// If there are no letters to unlock, log a message and return
if (availableOptions.length === 0) {
console.log("All letters are unlocked!");
return;
}
// Pick a random available option (letter to unlock)
const randomChoice = availableOptions[Math.floor(Math.random() * availableOptions.length)];
// Update the unlocked letters state
setUnlockedLetters([...unlockedLetters, randomChoice]);
};
```

Key Components:

  1. Filtering Available Words:
    • The function ensures that the letters from already found words are not available to unlock.
  2. Random Selection:
    • By using Math.random(), the function selects a random letter to unlock, making each gameplay session unique.
  3. State Management:
    • The setUnlockedLetters function updates the state, storing the letters that have been unlocked by the player.

Next Steps:

  • Price Implementation: You could implement a price for each letter unlock, deducting coins or other in-game currency when a letter is unlocked.
  • UI Update: Visual cues for unlocked letters and the number of letters remaining could be added to the game interface, giving the player feedback.

The unlockWord Function

The unlockWord function allows the player to unlock a complete word rather than just a single letter. This is especially useful when a player is struggling with an entire word and wants to gain a hint by unlocking a valid word from the set of possible anagrams. Here’s how the logic works:
  1. Filtering Unfound Words:
    • It calls the getUniqueWords function, similar to the letter unlocking logic, but in this case, it filters out words that have already been found.
    • This ensures the list of words contains only those that still need to be unlocked by the player.
  2. Selecting a Random Word:
    • Once the list of available words is filtered, the function randomly selects a word from that list using Math.random().
    • This randomly selected word is logged to the console for debugging or visibility purposes.
  3. Validating and Unlocking the Word:
    • The selected word is passed into the checkWord function, which is responsible for validating the word.
    • The checkWord function checks if the word is valid, adds it to the found words list, or places it in the bonus words list if applicable.
  4. Return the Unlocked Word:
    • The function returns the unlocked word, providing feedback on the word that was unlocked.
Here’s the code for the unlockWord function:
```
const unlockWord = () => {
// Get unique words that haven't been found yet
const words = getUniqueWords(sortedAnagrams, foundWords.map(item => item.word));

// If there are no words left to unlock, return null
if (words.length === 0) return null;

// Log the random word to be unlocked (for visibility)
console.log(words[Math.floor(Math.random() * words.length)]);

// Check if the selected word is valid
checkWord(words[Math.floor(Math.random() * words.length)]);

// Return the word that was unlocked
return words[Math.floor(Math.random() * words.length)];
}
```

Key Features:

  1. Unlocking a Full Word:
    • This function lets players unlock a complete word from the list of available words that they haven't yet discovered. It's an efficient way to help players move forward if they're stuck.
  2. Random Selection:
    • Like the letter unlock mechanism, the word unlock mechanism is randomized, providing a variety of gameplay experiences each time.
  3. Game State Update:
    • Once a word is unlocked, it is passed through the checkWord function, which handles adding the word to the list of found words or the bonus word list, and applying any relevant animations or score updates.

Next Steps:

  • Price Implementation: Like the unlockRandomLetter function, you can add a price system to unlock full words, making it a strategic choice for players to unlock hints as needed.

Fetching and Loading User Game Data

In Beyond Words, the game data is fetched and loaded from a backend service, ensuring that user progress is saved and retrieved whenever they re-enter the game. Below is the implementation of how game data is fetched by the user ID and then loaded into the application state.

1. Fetching the Game Data by User ID:

The fetchGameDataByUserId function handles fetching the user's game data. It uses a client (generated via generateClient<Schema>) to interact with the backend service and query game data specific to the logged-in user.

Key steps:

  • Getting the Current User: The getCurrentUser() function fetches the userId of the currently logged-in user.
  • Fetching Game Data: Using the userId, the function queries the GameData model, filtering by userId, and returns a list of game records. Only one record per user is expected (limit: 1).
  • Handling Errors: If there are any errors during the data fetch, they are logged to the console, and a warning message is displayed to the user.
  • Handling Empty or Missing Data: If no data is found for the user, the createGameData() function is called to initialize a new game data record.
  • Updating Application State: Once the game data is successfully fetched, it is parsed and saved into the React state using setLevel, setCoins, setFoundWords, etc.

Code for Fetching Game Data:

```
const fetchGameDataByUserId = async () => {
const { userId } = await getCurrentUser();
const client = generateClient<Schema>();
try {
// Fetch game data using list and filter by userId
const { data: gameDataList, errors } = await client.models.GameData.list({
filter: { userId: { eq: userId } },
limit: 1, // Assuming one record per user
authMode: 'userPool'
});
if (errors) {
console.error(`Error fetching GameData: ${errors.map((e) => e.message).join(", ")}`);
toast.show(`Error fetching GameData: ${errors.map((e) => e.message).join(", ")}`, {
type: "warning",
placement: "bottom",
duration: 4000,
});
throw new Error("Failed to fetch GameData.");
}
if (!gameDataList || !gameDataList.length) {
await createGameData();
return;
}
console.log({ gameDataList: gameDataList.length });
const gameData = gameDataList[0]; // Get the first record
// Load game data into state
setLevel(gameData.level!);
setCoins(gameData.coins!);
setFoundWords(JSON.parse(gameData.foundWords!));
setBonusWords(JSON.parse(gameData.bonusWords!));
setSortedAnagrams(JSON.parse(gameData.sortedAnagrams!));
setWord(JSON.parse(gameData.word!));
setAnagrams(JSON.parse(gameData.anagrams!));
setUnlockedLetters(JSON.parse(gameData.unlockedLetters!));
return gameData; // Return the fetched game data
} catch (error) {
console.error("Error in fetchGameDataByUserId:", error);
setIsLoading(false);
toast.show("Error in fetchGameDataByUserId: " + error, {
type: "warning",
placement: "bottom",
duration: 4000,
});
throw error;
}
};
```

2. Using useEffect to Fetch Data on Component Mount:

The fetchData function, wrapped inside a useEffect, is called when the component mounts (i.e., on the first render). The data is fetched asynchronously, and once the fetch is complete, the loading state is set to false.
```
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
await fetchGameDataByUserId();
setIsLoading(false);
};
fetchData();
}, []);
```

Key Concepts:

  1. State Management: The state variables such as level, coins, foundWords, etc., are updated after fetching the game data, ensuring that the user’s progress is reflected in the UI.
  2. Error Handling: Any errors in fetching data are handled gracefully by displaying an ActivityIndicator and logging them to the console.
  3. Loading State: The loading state (setIsLoading) helps show a loading spinner or indicator to the user while data is being fetched.

Tournament

In Beyond Words, the tournament feature is what truly sets the game apart. Unlike most word games, where you’re either playing solo or against a random opponent, Beyond Words lets you directly compete against other players. What makes this even more exciting is the ability to challenge up to 5 players at once (this might evolve with future updates!). This creates a much more dynamic and competitive environment, offering a fresh and thrilling experience.
In these tournaments, players can showcase their word skills while competing in real-time. The fast-paced, multiplayer nature adds an exciting layer of strategy and engagement, something that’s relatively rare in the world of word games. Players can truly test themselves against multiple opponents simultaneously, which is why this feature makes Beyond Words stand out in the genre.
To see the tournament feature in action, I’ve added a YouTube link to the blog. Watching the gameplay in real time will give you a firsthand look at how the tournament works and how players compete against each other. It’s a great way to experience the excitement and energy of direct competition in Beyond Words.

Summary

In this blog post, I’ve covered a lot of exciting details about Beyond Words, especially the tournament feature, which really makes the game stand out. Unlike most word games, Beyond Words lets players compete directly against each other, up to five players at once (though that number might change!).
I also talked about how the game loads and saves data, so players can always pick up where they left off. This includes tracking things like progress, found words, and coins, making sure players’ data is updated and saved when they log back in.
With real-time score tracking, players can see their standing as the tournament progresses, which adds to the excitement and competitive nature of the game. Plus, the gameplay features like unlocking words, swiping letters, and bonus words keep things fun and engaging.

Conclusion

This is just the start for Beyond Words. I’ve got some awesome features already, but there’s a lot more coming—like high scores and other cool multiplayer modes—to keep players coming back for more.
Next up, I’ll be focusing on getting Beyond Words onto more platforms, including Android, iOS, and other devices that React Native supports. This will let players enjoy the game no matter what device they’re using. As I keep building and improving the game, there’s a lot to look forward to in terms of new features and making the game even bigger.

Comments