
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!
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.
- 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.
performLevelOperation
function is implemented:// 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...
}
```
- 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.
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:- 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. - 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. - 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. - 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.
- 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.
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);
}
if (bonusWords.includes(joinedWord.toUpperCase())) {
return console.log("You have found this VALID word already!!");
}
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 },
]);
}
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.
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:- 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.
- 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
andarr2
) into sets and filters out words that exist in both arrays. This ensures that only unique words from each array are returned.
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
});
};
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 [
...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
];
};
```
- shuffleWord:
- Randomly shuffles the letters in the
prevWord
array and updates theword
state with the shuffled result. - The Fisher-Yates shuffle is an efficient algorithm to ensure randomness with minimal computational overhead.
- Dynamic Gameplay: Shuffling the word offers a fresh experience for the player each time they try to form a valid word.
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:- 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.
- Picking a Random Letter:
- If there are available letters to unlock (
availableOptions
is not empty), a random choice is selected usingMath.random()
. - This random choice represents a letter that the player can unlock to aid them in solving the word.
- Updating the State:
- Once a random letter is chosen, it is added to the
unlockedLetters
array (which keeps track of all unlocked letters) usingsetUnlockedLetters
.
- 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.
const availableOptions: { word: string; index: number }[] = [];
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 (availableOptions.length === 0) {
console.log("All letters are unlocked!");
return;
}
const randomChoice = availableOptions[Math.floor(Math.random() * availableOptions.length)];
setUnlockedLetters([...unlockedLetters, randomChoice]);
};
```
- Filtering Available Words:
- The function ensures that the letters from already found words are not available to unlock.
- Random Selection:
- By using
Math.random()
, the function selects a random letter to unlock, making each gameplay session unique.
- State Management:
- The
setUnlockedLetters
function updates the state, storing the letters that have been unlocked by the player.
- 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.
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:- 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.
- 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.
- 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.
- Return the Unlocked Word:
- The function returns the unlocked word, providing feedback on the word that was unlocked.
unlockWord
function:// 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)];
}
```
- 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.
- Random Selection:
- Like the letter unlock mechanism, the word unlock mechanism is randomized, providing a variety of gameplay experiences each time.
- 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.
- 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.
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.- Getting the Current User: The
getCurrentUser()
function fetches theuserId
of the currently logged-in user. - Fetching Game Data: Using the
userId
, the function queries theGameData
model, filtering byuserId
, 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.
const { userId } = await getCurrentUser();
const client = generateClient<Schema>();
// 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'
});
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.");
}
await createGameData();
return;
}
const gameData = gameDataList[0]; // Get the first record
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!));
} catch (error) {
console.error("Error in fetchGameDataByUserId:", error);
setIsLoading(false);
toast.show("Error in fetchGameDataByUserId: " + error, {
type: "warning",
placement: "bottom",
duration: 4000,
});
throw error;
}
};
```
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
.const fetchData = async () => {
setIsLoading(true);
await fetchGameDataByUserId();
setIsLoading(false);
};
}, []);
```
- 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. - Error Handling: Any errors in fetching data are handled gracefully by displaying an
ActivityIndicator
and logging them to the console. - Loading State: The loading state (
setIsLoading
) helps show a loading spinner or indicator to the user while data is being fetched.
- YouTube: https://youtu.be/VJ51AvuKnEU