diff --git a/public/wordbanks/word-scramble.txt b/public/wordbanks/word-scramble.txt
new file mode 100644
index 0000000..38ee883
--- /dev/null
+++ b/public/wordbanks/word-scramble.txt
@@ -0,0 +1,163 @@
+react
+javascript
+component
+function
+frontend
+context
+state
+effect
+variable
+programming
+apple
+banana
+orange
+mango
+grape
+peach
+cherry
+berry
+lemon
+lime
+melon
+apricot
+avocado
+broccoli
+carrot
+celery
+tomato
+potato
+onion
+garlic
+pepper
+spinach
+lettuce
+cabbage
+cucumber
+pumpkin
+zucchini
+squash
+mushroom
+almond
+cashew
+walnut
+peanut
+pistachio
+cereal
+biscuit
+cookie
+donut
+muffin
+pancake
+waffle
+crepe
+burger
+pizza
+pasta
+sushi
+taco
+curry
+salad
+sandwich
+omelet
+steak
+chicken
+turkey
+salmon
+tuna
+cod
+shrimp
+lobster
+crab
+island
+planet
+galaxy
+universe
+rocket
+saturn
+jupiter
+mercury
+venus
+ear
+airplane
+airport
+station
+bridge
+tunnel
+harbor
+library
+museum
+gallery
+theatre
+concert
+festival
+holiday
+vacation
+adventure
+journey
+trek
+hiking
+camping
+backpack
+camera
+picture
+portrait
+landscape
+sunset
+sunrise
+mountain
+valley
+river
+ocean
+beach
+desert
+forest
+jungle
+savanna
+volcano
+earthquake
+weather
+climate
+season
+spring
+summer
+autumn
+winter
+school
+college
+university
+teacher
+student
+lecture
+exam
+homework
+assignment
+project
+science
+physics
+chemistry
+biology
+math
+algebra
+geometry
+calculus
+history
+geography
+politics
+economy
+finance
+market
+business
+startup
+product
+service
+coding
+developer
+debugger
+tester
+design
+ux
+ui
+prototype
+mockup
+logo
+brand
diff --git a/src/assets/games/WordScramble/word_scramble.png b/src/assets/games/WordScramble/word_scramble.png
new file mode 100644
index 0000000..2235b81
Binary files /dev/null and b/src/assets/games/WordScramble/word_scramble.png differ
diff --git a/src/data/content.js b/src/data/content.js
index 742bb0f..1652e19 100644
--- a/src/data/content.js
+++ b/src/data/content.js
@@ -8,6 +8,7 @@ import { FortuneCard } from "../pages/activities/FotuneCard";
import { SearchWord } from "../pages/activities/getDefinition";
import { Jitter } from "../pages/games/Jitter";
import { RandomMeme } from "../pages/activities/RandomMemes";
+import { WordScramble } from "../pages/games/WordScramble";
// Use wrapper components that include favorite button functionality
import { RandomJokeWithFav } from "../pages/activities_wrappers/RandomJokeWithFav";
import { RandomAnimeQuoteWithFav } from "../pages/activities_wrappers/RandomAnimeQuoteWithFav";
@@ -26,6 +27,7 @@ import { DogHttpCode } from "../pages/activities/DogHttpCode";
import { CatHttpCode } from "../pages/activities/CatHttpCode";
import FlappyBird from "../pages/games/FlappyBird";
import RandomIdentity from "../pages/activities/RandomIdentity";
+import word_scramble_icon from "../assets/games/WordScramble/word_scramble.png";
export const activities = [
{
@@ -170,5 +172,12 @@ export const games = [
icon: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSap9rEhSD7ghcjTSYN6HuXx0wejnzigvKncg&s",
urlTerm: "FlappyBird",
element: ,
+ },
+ {
+ title: "Word Scramble",
+ description: "Word chaos! Can you fix it?",
+ icon: word_scramble_icon,
+ urlTerm: "WordScramble",
+ element: ,
}
];
\ No newline at end of file
diff --git a/src/pages/games/WordScramble.js b/src/pages/games/WordScramble.js
new file mode 100644
index 0000000..162973e
--- /dev/null
+++ b/src/pages/games/WordScramble.js
@@ -0,0 +1,304 @@
+import React, { useState, useEffect } from "react";
+import "../../styles/pages/games/WordScramble.css";
+
+export const WordScramble = () => {
+ // Minimal fallback words (used if local bank fails)
+ const FALLBACK_WORDS = [
+ "react",
+ "javascript",
+ "component",
+ "function",
+ "frontend",
+ "context",
+ "state",
+ "effect",
+ "variable",
+ "programming",
+ ];
+
+ const [words, setWords] = useState(FALLBACK_WORDS);
+ const [word, setWord] = useState("");
+ const [scrambledWord, setScrambledWord] = useState("");
+ const [userInput, setUserInput] = useState("");
+ const [score, setScore] = useState(0);
+ const [message, setMessage] = useState("Press Start to Play!");
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [gameOver, setGameOver] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [attempts, setAttempts] = useState(0);
+ const [revealed, setRevealed] = useState(new Set());
+ const [showAnswer, setShowAnswer] = useState(false);
+ const [hintActive, setHintActive] = useState(false);
+
+ // Fisher-Yates shuffle for letters
+ const shuffleWord = (w) => {
+ const a = w.split("");
+ for (let i = a.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [a[i], a[j]] = [a[j], a[i]];
+ }
+ return a.join("");
+ };
+
+ const pickRandom = (arr) => arr[Math.floor(Math.random() * arr.length)];
+ // how long a revealed hint letter stays visible (ms)
+ const HINT_DURATION = 4000;
+
+ // Load a local word bank (public/wordbanks/word-scramble.txt)
+ const fetchLocalWordBank = async (path = "/wordbanks/word-scramble.txt") => {
+ const res = await fetch(path);
+ if (!res.ok) throw new Error("Local word bank not found");
+ const txt = await res.text();
+ return txt
+ .split(/\r?\n/)
+ .map((w) => w.trim().toLowerCase())
+ .filter(Boolean);
+ };
+
+ // Optionally a simple free API fallback (may be rate-limited)
+ const fetchRandomFromApi = async (number = 10) => {
+ try {
+ const url = `https://random-word-api.herokuapp.com/word?number=${number}`;
+ const res = await fetch(url);
+ if (!res.ok) throw new Error("API fetch failed");
+ const data = await res.json();
+ return data.map((w) => String(w).toLowerCase());
+ } catch (e) {
+ console.warn("Random API fallback failed:", e);
+ return [];
+ }
+ };
+
+ useEffect(() => {
+ let mounted = true;
+
+ async function load() {
+ setLoading(true);
+ try {
+ let bank = await fetchLocalWordBank();
+
+ // Filter clean words: letters only and reasonable length
+ bank = bank.filter((w) => /^[a-z]+$/.test(w) && w.length >= 4 && w.length <= 12);
+
+ if (!bank.length) {
+ // try API fallback
+ const apiWords = await fetchRandomFromApi(50);
+ bank = apiWords.filter((w) => /^[a-z]+$/.test(w) && w.length >= 4 && w.length <= 12);
+ }
+
+ if (mounted && bank.length) {
+ setWords(bank);
+ // preselect first word
+ const pick = pickRandom(bank);
+ setWord(pick);
+ let s = shuffleWord(pick);
+ if (s === pick && pick.length > 1) s = shuffleWord(pick.split("").reverse().join(""));
+ setScrambledWord(s);
+ }
+ } catch (err) {
+ console.error("Word bank load failed:", err);
+ // keep fallback words
+ } finally {
+ if (mounted) setLoading(false);
+ }
+ }
+
+ load();
+ return () => (mounted = false);
+ }, []);
+
+ const startGame = () => {
+ if (!words.length) return;
+ const randomWord = pickRandom(words);
+ let scrambled = shuffleWord(randomWord);
+ if (scrambled === randomWord && randomWord.length > 1)
+ scrambled = shuffleWord(randomWord.split("").reverse().join(""));
+
+ setWord(randomWord);
+ setScrambledWord(scrambled);
+ setUserInput("");
+ setAttempts(0);
+ setRevealed(new Set());
+ setShowAnswer(false);
+ setScore(0);
+ setIsPlaying(true);
+ setGameOver(false);
+ setMessage("Unscramble the word!");
+ };
+
+ const checkAnswer = () => {
+ const guess = userInput.trim().toLowerCase();
+ if (!guess) {
+ setMessage("Please enter a guess before submitting");
+ return;
+ }
+
+ if (guess === word.toLowerCase()) {
+ setScore((s) => s + 1);
+ setMessage("✅ Correct! Loading next word...");
+ setTimeout(() => {
+ nextWord();
+ }, 700);
+ } else {
+ // wrong answer
+ setAttempts((a) => a + 1);
+ const newAttempts = attempts + 1;
+ if (newAttempts >= 3) {
+ // after 3 wrong attempts, reveal the answer briefly
+ revealAnswer();
+ } else {
+ setMessage(`❌ Wrong! Attempts: ${newAttempts}/3`);
+ }
+ }
+ };
+
+ const nextWord = () => {
+ if (!words.length) return;
+ const randomWord = pickRandom(words);
+ let scrambled = shuffleWord(randomWord);
+ if (scrambled === randomWord && randomWord.length > 1)
+ scrambled = shuffleWord(randomWord.split("").reverse().join(""));
+
+ setWord(randomWord);
+ setScrambledWord(scrambled);
+ setUserInput("");
+ setMessage("Unscramble the word!");
+ setAttempts(0);
+ setRevealed(new Set());
+ setShowAnswer(false);
+ setHintActive(false);
+ };
+
+ const revealOneLetter = () => {
+ if (!word) return;
+ if (hintActive) {
+ setMessage("Please wait for the current hint to expire");
+ return;
+ }
+ const indices = [...Array(word.length).keys()].filter((i) => !revealed.has(i));
+ // don't reveal the final remaining letter because that would show the whole word
+ if (indices.length <= 1) {
+ setMessage("No more hints available without revealing the whole word");
+ return;
+ }
+
+ const pick = indices[Math.floor(Math.random() * indices.length)];
+ const nextSet = new Set(revealed);
+ nextSet.add(pick);
+ setRevealed(nextSet);
+ setMessage(`Hint: one letter revealed (visible for ${Math.round(HINT_DURATION/1000)}s)`);
+
+ // mark hint as active and remove the revealed letter after HINT_DURATION so it's temporary
+ setHintActive(true);
+ setTimeout(() => {
+ setRevealed((prev) => {
+ const copy = new Set(prev);
+ copy.delete(pick);
+ return copy;
+ });
+ setHintActive(false);
+ }, HINT_DURATION);
+ };
+
+ const revealAnswer = () => {
+ setShowAnswer(true);
+ setMessage(`Answer: ${word}`);
+ // then advance to next word after a short pause
+ setTimeout(() => {
+ nextWord();
+ }, 1200);
+ };
+
+ const endGame = () => {
+ setIsPlaying(false);
+ setGameOver(true);
+ setMessage(`Game Over! Your final score: ${score}`);
+ };
+
+ // Auto end after 10 points (optional)
+ useEffect(() => {
+ if (score >= 10) {
+ // inline endGame to avoid missing dependency warnings from eslint
+ setIsPlaying(false);
+ setGameOver(true);
+ setMessage(`Game Over! Your final score: ${score}`);
+ }
+ }, [score]);
+
+ return (
+
+
Word Scramble
+
+ {loading ? (
+
Loading words…
+ ) : isPlaying && !gameOver ? (
+
+
+
Score: {score}
+
{message}
+
+
{scrambledWord}
+
+ {showAnswer
+ ? word
+ : word
+ .split("")
+ .map((ch, i) => (revealed.has(i) ? ch : "_")).join(" ")}
+
+
setUserInput(e.target.value)}
+ onKeyDown={(e) => e.key === "Enter" && checkAnswer()}
+ />
+
+
+
+
+
+
+
+ ) : (
+
+ {!isPlaying ? (
+
+
+
How to play
+
+ - Unscramble the letters to form a valid English word.
+ - Type your guess and press Submit or hit Enter.
+ - Use Hint to reveal one letter temporarily (cooldown applies).
+ - After 3 wrong attempts the answer will be revealed automatically.
+ - Each correct answer gives +1 point. The game ends at 10 points.
+
+
+
+
+ ) : (
+
+ )}
+
+ )}
+
+ );
+};
+
+export default WordScramble;
diff --git a/src/styles/pages/games/WordScramble.css b/src/styles/pages/games/WordScramble.css
new file mode 100644
index 0000000..1020238
--- /dev/null
+++ b/src/styles/pages/games/WordScramble.css
@@ -0,0 +1,220 @@
+.word-scramble-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ min-height: 100vh;
+ padding: 20px 14px 40px 14px;
+ background: linear-gradient(180deg, #f7fbff 0%, #eef6fb 100%);
+ color: #0b1220;
+ font-family: "Poppins", system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial;
+ text-align: center;
+}
+
+.word-scramble-container h1 {
+ font-size: 1.9rem;
+ margin: 18px 0 20px 0;
+ font-weight: 700;
+ color: #0b1220;
+ line-height: 1.1;
+ z-index: 1;
+}
+
+/* align score and message to match the game card width */
+.game-info {
+ max-width: 560px;
+ width: 100%;
+ margin: 8px auto 14px auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 12px;
+ padding: 0 12px;
+ box-sizing: border-box;
+}
+
+/* when game-info is inside the card keep it full width and small bottom margin */
+.scramble-box .game-info { margin: 0 0 12px 0; padding: 0; }
+
+.score {
+ background: #ffffff;
+ padding: 6px 12px;
+ border-radius: 999px;
+ font-weight: 600;
+ color: #0b1220;
+ border: 1px solid rgba(15,17,36,0.04);
+ display: inline-block;
+ min-width: 96px;
+ text-align: center;
+}
+
+.message {
+ font-size: 0.95rem;
+ color: #223045;
+ text-align: right;
+ flex: 1;
+}
+
+.scramble-box {
+ background: #ffffff;
+ border-radius: 10px;
+ padding: 18px 18px;
+ box-shadow: 0 6px 14px rgba(14, 30, 60, 0.06);
+ max-width: 560px;
+ width: 100%;
+ margin-top: 72px; /* increase space between header and card */
+ position: relative;
+ padding-top: 56px; /* extra space for score/message inside header area */
+}
+
+.scrambled-word {
+ font-size: 1.6rem;
+ letter-spacing: 5px;
+ margin: 4px 0 8px 0;
+ font-weight: 700;
+ text-transform: uppercase;
+ color: #10243a;
+ display: inline-block;
+ padding: 6px 10px;
+ border-radius: 6px;
+ background: #f7fafc;
+}
+
+.hint-pattern {
+ font-family: "Courier New", monospace;
+ letter-spacing: 6px;
+ color: #10243a;
+ margin-bottom: 8px;
+ font-size: 1rem;
+ text-align: center;
+}
+
+input {
+ padding: 10px 12px;
+ font-size: 1rem;
+ border-radius: 8px;
+ border: 1px solid rgba(15,17,36,0.06);
+ outline: none;
+ width: 240px;
+ height: 44px;
+ line-height: 22px;
+ box-sizing: border-box;
+ text-align: center;
+ background: #fff;
+ color: #10243a;
+ transition: box-shadow 120ms ease, transform 100ms ease;
+}
+
+input:focus { box-shadow: 0 6px 16px rgba(16,36,58,0.06); transform: translateY(-1px); }
+
+.controls {
+ margin-top: 12px;
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.controls button {
+ cursor: pointer;
+ padding: 9px 14px;
+ border: none;
+ border-radius: 8px;
+ font-size: 0.95rem;
+ font-weight: 700;
+ transition: transform 100ms ease, opacity 120ms ease;
+}
+
+.controls button:active { transform: translateY(1px); }
+.controls button:disabled { opacity: 0.5; cursor: not-allowed; }
+
+.start-btn {
+ background: #0ea34a;
+ color: #fff;
+ padding: 9px 14px;
+ border-radius: 8px;
+ font-weight: 700;
+ font-size: 0.95rem;
+ border: none;
+ box-shadow: none;
+ transition: transform 100ms ease, opacity 120ms ease;
+ cursor: pointer;
+}
+.start-btn:hover { opacity: 0.95; }
+.start-btn:active { transform: translateY(1px); }
+.start-btn:focus { outline: 3px solid rgba(16,36,58,0.06); outline-offset: 2px; }
+.check-btn { background: #f5b400; color: #10243a; }
+.skip-btn { background: #2b9fd8; color: #fff; }
+.end-btn { background: #e14b4b; color: #fff; }
+
+/* Rules / pre-play area */
+.preplay { display: flex; flex-direction: column; align-items: center; gap: 12px; }
+.rules-box {
+ background: linear-gradient(180deg, #ffffff, #fbfdff);
+ border: 1px solid rgba(15,17,36,0.04);
+ box-shadow: 0 6px 14px rgba(14,30,60,0.04);
+ padding: 12px 16px;
+ border-radius: 8px;
+ max-width: 560px;
+ width: calc(100% - 40px);
+ text-align: left;
+ color: #0b1220;
+}
+.rules-box h3 { margin: 0 0 8px 0; font-size: 1.05rem; }
+.rules-box ol { margin: 0; padding-left: 18px; font-size: 0.95rem; color: #34495e; }
+.rules-box li { margin: 6px 0; }
+
+@media (max-width: 600px) {
+ .scrambled-word { font-size: 1.4rem; letter-spacing: 4px; }
+ input { width: 180px; }
+ .scramble-box { padding: 16px; padding-top: 52px; margin-top: 56px; }
+ .game-info { padding: 0 8px; }
+}
+
+/* mobile tweak for floating score pill */
+@media (max-width: 420px) {
+ .scramble-box .score {
+ top: -22px;
+ padding: 6px 12px;
+ min-width: 80px;
+ }
+ .scramble-box { margin-top: 56px; }
+}
+
+@media (max-width: 600px) {
+ .rules-box { width: calc(100% - 28px); padding: 10px 12px; }
+ .rules-box ol { font-size: 0.92rem; }
+}
+
+/* Positioning for score/message when inside the card header */
+.scramble-box .score {
+ position: absolute;
+ top: -26px; /* float slightly higher above the card */
+ left: 50%;
+ transform: translateX(-50%);
+ margin: 0;
+ min-width: 96px;
+ padding: 8px 18px;
+ background: #ffffff;
+ border-radius: 999px;
+ box-shadow: 0 8px 20px rgba(16,36,58,0.12);
+ border: 1px solid rgba(15,17,36,0.06);
+ font-weight: 700;
+ color: #0b1220;
+ z-index: 5;
+ text-align: center;
+}
+
+.scramble-box .message {
+ position: absolute;
+ top: 28px; /* place message below the highlighted score */
+ left: 50%;
+ transform: translateX(-50%);
+ margin: 0;
+ text-align: center;
+ width: calc(100% - 40px);
+}
+
+/* subtle focus outline for accessibility */
+button:focus, input:focus { outline: 3px solid rgba(16,36,58,0.06); outline-offset: 2px; }
+