Building Who's That Pokémon: A Multi-Mode Game with Immersive Audio & Stats Tracking
A comprehensive look at building a nostalgic Pokémon guessing game featuring multiple game modes, an immersive audio system, persistent stats tracking, and full accessibility compliance using Next.js 16 and modern web APIs.
Building Who's That Pokémon: A Multi-Mode Game with Immersive Audio & Stats Tracking
What started as a simple silhouette guessing game evolved into a feature-rich, multi-mode experience that captures the nostalgia of the classic "Who's That Pokémon?" TV segment while leveraging modern web technologies. In this post, I'll walk through the technical implementation of the game's key features.
The Evolution: From Simple to Comprehensive
The initial concept was straightforward: show a Pokémon silhouette and let users guess. But as development progressed, I realized there was an opportunity to create something more engaging—a complete Pokémon guessing experience with multiple play modes, comprehensive audio, and persistent progress tracking.
Game Modes: Three Ways to Play
Silhouette Mode (Classic)
The heart of the game—users identify Pokémon from their silhouettes. The silhouette effect is achieved using CSS filters:
.pokemon-silhouette {
filter: brightness(0) contrast(100%);
transition: filter 0.5s ease;
}
Generation filtering (Gen 1-9) allows players to customize difficulty. The filtering logic ensures fair random selection within the chosen generations.
Trivia Mode
A twist on the classic format—users guess Pokémon from censored Pokédex descriptions with optional type hints. The censoring algorithm intelligently replaces the Pokémon's name while preserving readability:
function censorPokemonName(description: string, pokemonName: string): string {
const regex = new RegExp(pokemonName, 'gi');
return description.replace(regex, '█████');
}
Type hints provide strategic assistance without giving away the answer, making this mode accessible yet challenging.
Trainer Stats
A dedicated view for tracking progress across both game modes. Displays current streaks, best streaks, win rates, and daily play streaks—all persisted via localStorage.
Audio System Architecture
The audio system was one of the most complex features to implement, requiring careful handling of browser audio policies and state management.
Background Music Playlist
A curated playlist of authentic Pokémon tracks (Pallet Town, Cinnabar Island, etc.) plays continuously with a minimizable audio player widget:
const useAudioPlayer = () => {
const [currentTrack, setCurrentTrack] = useState(0);
const audioRef = useRef(null);
const playNext = () => {
setCurrentTrack((prev) => (prev + 1) % playlist.length);
};
useEffect(() => {
if (audioRef.current) {
audioRef.current.play().catch(() => {
// Handle autoplay blocking
});
}
}, [currentTrack]);
return { audioRef, playNext, currentTrack };
};
Web Audio API for Sound Effects
Retro UI sounds (button clicks, correct/incorrect answers) are synthesized using the Web Audio API, eliminating the need for external audio files:
const playBeep = (frequency: number, duration: number) => {
const audioContext = new AudioContext();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.type = 'square';
oscillator.frequency.value = frequency;
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(
0.01,
audioContext.currentTime + duration
);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + duration);
};
Pokémon Cries
Authentic Pokémon cries are fetched from PokeAPI and played when a Pokémon is revealed, adding to the immersive experience.
Browser Audio Policy Handling
Modern browsers block autoplay until user interaction. The solution involves unlocking the audio context on first user interaction:
const unlockAudioContext = () => {
if (audioContext.state === 'suspended') {
audioContext.resume();
}
};
document.addEventListener('click', unlockAudioContext, { once: true });
Stats Tracking System
Persistent stats tracking was essential for player engagement. The stats manager uses localStorage to track:
interface GameStats {
silhouetteStreak: number;
silhouetteBestStreak: number;
triviaStreak: number;
triviaBestStreak: number;
totalGames: number;
totalWins: number;
dailyStreak: number;
lastPlayedDate: string;
}
const updateStats = (mode: 'silhouette' | 'trivia', won: boolean) => {
const stats = getStats();
if (won) {
stats[`${mode}Streak`]++;
stats[`${mode}BestStreak`] = Math.max(
stats[`${mode}BestStreak`],
stats[`${mode}Streak`]
);
stats.totalWins++;
} else {
stats[`${mode}Streak`] = 0;
}
stats.totalGames++;
updateDailyStreak(stats);
saveStats(stats);
};
Accessibility & Mobile UX
Full WCAG 2.1 compliance was a priority from the start:
ARIA Labels & Keyboard Navigation
Every interactive element has descriptive ARIA labels and supports keyboard navigation:
Screen Reader Announcements
Live regions announce game state changes for screen reader users:
{gameMessage}
Mobile Optimization
The responsive navbar collapses into a hamburger menu on mobile devices, with touch-friendly 44px minimum touch targets. The Pokédex interface scales appropriately across all screen sizes.
Release Notes System
A Kalos Pokédex-styled release notes modal appears on version updates, with dismissal state stored in localStorage:
const checkVersionUpdate = () => {
const lastSeenVersion = localStorage.getItem('lastSeenVersion');
if (lastSeenVersion !== CURRENT_VERSION) {
setShowReleaseNotes(true);
}
};
const dismissReleaseNotes = () => {
localStorage.setItem('lastSeenVersion', CURRENT_VERSION);
setShowReleaseNotes(false);
};
Technical Challenges Overcome
API Rate Limiting
PokeAPI can be unreliable during high traffic. Implemented exponential backoff retry logic:
const fetchWithRetry = async (url: string, retries = 3): Promise => {
try {
return await fetch(url);
} catch (error) {
if (retries > 0) {
await new Promise(resolve => setTimeout(resolve, 1000 * (4 - retries)));
return fetchWithRetry(url, retries - 1);
}
throw error;
}
};
Anti-Cheat Measures
Trivia mode required anti-cheat measures to prevent users from inspecting network requests to find answers. The Pokémon name is censored server-side (via PokeAPI data) before being sent to the client.
State Management Across Modes
Managing state across three distinct game modes while maintaining clean code required careful component architecture and custom hooks for shared logic.
What I Learned
1. Browser Audio Policies: Require careful handling and user-friendly fallbacks
2. localStorage Persistence: Essential for user engagement and progress tracking
3. Accessibility First: Building accessibility in from the start is easier than retrofitting
4. Component Modularity: Modular architecture made adding new game modes straightforward
5. API Reliability: Always implement retry logic and error handling for external APIs
Future Enhancements
Building "Who's That Pokémon?" was an incredible learning experience that combined nostalgia with modern web development best practices. The result is a game that's not just fun to play, but also accessible, performant, and maintainable.
**Play the game at [pokeguess.site](https://pokeguess.site)** and see these features in action!