import { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router'; import cw from 'cw'; import '../styles.css'; import '../components/OptionButtons.css'; import Footer from '../components/Footer'; declare global { interface Window { cw: any; AudioContext: typeof AudioContext; webkitAudioContext: typeof AudioContext; } } const HomePage = () => { // Game variables const [gameStarted, setGameStarted] = useState(false); const [score, setScore] = useState(0); const [totalPlayed, setTotalPlayed] = useState(0); const [currentChar, setCurrentChar] = useState(''); const [options, setOptions] = useState([]); const [wpm, setWpm] = useState(20); const [showHints, setShowHints] = useState(false); const [showHowToPlay, setShowHowToPlay] = useState(false); const [selectedOption, setSelectedOption] = useState(null); const [_correctAnswers, setCorrectAnswers] = useState([]); const [_incorrectAnswers, setIncorrectAnswers] = useState([]); const actxRef = useRef(null); const optionButtonsRef = useRef>([null, null, null, null]); // Define available characters const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; const numbers = '0123456789'; const specialChars = '.,?/='; const allChars = letters + numbers + specialChars; // Initialize the game when started - this happens on user interaction (button click) const startGame = () => { try { // Initialize audio context on user interaction actxRef.current = cw.initAudioContext({ tone: 600 }); setGameStarted(true); newRound(); } catch (error) { console.error('Failed to initialize audio context:', error); alert('Failed to initialize audio. Please try again.'); } }; // Generate a new round const newRound = () => { setSelectedOption(null); // Reset button states optionButtonsRef.current.forEach(button => { if (button) { button.disabled = false; button.classList.remove('correct', 'incorrect', 'key-pressed'); } }); // Generate a random character const randomIndex = Math.floor(Math.random() * allChars.length); const newChar = allChars[randomIndex]; setCurrentChar(newChar); // Generate options (one correct, three random) const generateOptions = (correctChar: string): string[] => { const possibleChars = allChars.split(""); const filteredChars = possibleChars.filter(char => char !== correctChar); const shuffled = [...filteredChars].sort(() => 0.5 - Math.random()); const options = [correctChar, ...shuffled.slice(0, 3)]; return options.sort(() => 0.5 - Math.random()); }; const newOptions = generateOptions(newChar); setOptions(newOptions); // Play the Morse code for the character playMorseCode(newChar); }; // Play Morse code for a character const playMorseCode = (char: string) => { if (!char) return; try { if (!actxRef.current) { actxRef.current = cw.initAudioContext({ tone: 600 }); } cw.play(char, { wpm: wpm, actx: actxRef.current }); } catch (error) { console.error('Error playing Morse code:', error); } }; // Check if the answer is correct const checkAnswer = (selectedChar: string) => { setSelectedOption(selectedChar); const isCorrect = selectedChar === currentChar; if (isCorrect) { setScore(prevScore => prevScore + 1); // Add to score tracker const scoreTracker = document.getElementById('scoreTracker'); if (scoreTracker) { const marker = document.createElement('div'); marker.className = 'score-marker correct'; scoreTracker.appendChild(marker); } } else { // Add to score tracker const scoreTracker = document.getElementById('scoreTracker'); if (scoreTracker) { const marker = document.createElement('div'); marker.className = 'score-marker incorrect'; scoreTracker.appendChild(marker); } // Highlight the correct answer const correctIndex = options.findIndex(option => option === currentChar); if (correctIndex !== -1) { const correctButton = optionButtonsRef.current[correctIndex]; if (correctButton) { // Add a small delay before showing the correct answer setTimeout(() => { correctButton.classList.add('correct'); }, 300); } } } setTotalPlayed(prevTotal => prevTotal + 1); // Proceed to next round after a delay setTimeout(() => { newRound(); }, 1500); // Increased delay to give time to see the correct answer }; // Handle keyboard input useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (!gameStarted) return; const key = event.key.toUpperCase(); // Find if any button has this character const index = options.findIndex(option => option === key); if (index !== -1) { const button = optionButtonsRef.current[index]; if (button && !button.disabled) { button.classList.add('key-pressed'); setTimeout(() => { button.classList.remove('key-pressed'); handleOptionClick(key, index); }, 100); } } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [gameStarted, options]); // Get Morse code representation for a character const getMorseCode = (char: string) => { const morseMap: { [key: string]: string } = { 'A': '·−', 'B': '−···', 'C': '−·−·', 'D': '−··', 'E': '·', 'F': '··−·', 'G': '−−·', 'H': '····', 'I': '··', 'J': '·−−−', 'K': '−·−', 'L': '·−··', 'M': '−−', 'N': '−·', 'O': '−−−', 'P': '·−−·', 'Q': '−−·−', 'R': '·−·', 'S': '···', 'T': '−', 'U': '··−', 'V': '···−', 'W': '·−−', 'X': '−··−', 'Y': '−·−−', 'Z': '−−··', '0': '−−−−−', '1': '·−−−−', '2': '··−−−', '3': '···−−', '4': '····−', '5': '·····', '6': '−····', '7': '−−···', '8': '−−−··', '9': '−−−−·', '.': '·−·−·−', ',': '−−··−−', '?': '··−−··', '/': '−··−·', '=': '−···−' }; return morseMap[char] || ''; }; // Reset game to initial state const resetGame = () => { setGameStarted(false); setScore(0); setTotalPlayed(0); setCurrentChar(''); setOptions([]); setCorrectAnswers([]); setIncorrectAnswers([]); // Clear score tracker const scoreTracker = document.getElementById('scoreTracker'); if (scoreTracker) { scoreTracker.innerHTML = ''; } // Stop any playing audio if (actxRef.current) { try { actxRef.current.close(); actxRef.current = null; } catch (error) { console.error('Error closing audio context:', error); } } }; // Handle option button click const handleOptionClick = (option: string, index: number) => { const button = optionButtonsRef.current[index]; if (button) { // Disable all buttons to prevent multiple selections optionButtonsRef.current.forEach(btn => { if (btn) btn.disabled = true; }); // Add appropriate class based on correctness if (option === currentChar) { button.classList.add('correct'); } else { button.classList.add('incorrect'); } // Check the answer checkAnswer(option); } }; return (

Morse Hero

{ !gameStarted ? (

Master Morse Code Through Play

Morse Hero turns learning Morse code into an engaging game. Listen to the signals, identify the characters, and improve your skills with every round.

{showHowToPlay && (

How to Play Morse Hero

  1. Listen to the Morse code sound played
  2. Select the correct character from the four options
  3. Use keyboard keys that match the displayed characters for faster play
  4. Enable hints if you need help seeing the Morse patterns
  5. Adjust the speed to match your skill level

Did You Know?

Morse code was developed by Samuel Morse and Alfred Vail in the 1830s and revolutionized long-distance communication.

)}
View Morse Code Chart
🎮

Interactive Learning

Learn by doing with our interactive game format

🔊

Audio Recognition

Train your ear to recognize Morse code sounds

📈

Track Progress

See your improvement with each practice session

Adjustable Speed

Set your own pace as you build your skills

) : (
Score:
{score} / {totalPlayed}

Listen to the Morse code and select the correct character

{options.map((option, index) => ( ))}
setShowHints(e.target.checked)} />

Pro Tip: Use your keyboard to select options faster!

) }
); }; export default HomePage;