Try it live at 30days.abduarrahman.com — and the source code is on GitHub.
The Origin
It started with a random comment in a group chat: "You should build one web feature every day for 30 days." Someone said it as a joke. I took it seriously.
Day 1 had to set the tone. Something fun, something with sound, something that hooks you in. I thought about dice, timers, and the classic Magikarp meme — why not make people chase a bouncing fish before they even start the challenge?
The Magikarp Countdown Challenge was born: roll 4 dice to get a random target (1000-9999 milliseconds), then try to stop a precision timer at exactly that number. Hit it? The countdown drops. Miss it? Time goes up. Run out of time? Game over. But there's an easter egg...
What I Built
A multi-phase mini-game with:
- Bouncing Magikarp landing screen — a fish that bounces around the screen with physics, bubble trails, and ripple effects. Click it to start
- 4-dice rolling sequence — dice fly in from random angles, land with satisfying sounds, and combine into a target number
- Precision timer matching — stop the millisecond timer at exactly the target number
- Live countdown — the timer ticks down in real-time with color changes (blue → yellow → red)
- Easter egg: the Gyarados glitch — get within ±100ms of the target and the screen glitches out with a multi-layered digital crash sound, then reveals Gyarados
- Synthesized audio — all sounds are generated with the Web Audio API (no audio files needed)
How It Works
Dice Rolling with Sound Synthesis
Each die rolls with a rapid randomization animation, then lands on its final value. The sound is synthesized on-the-fly using the Web Audio API — a noise burst through a bandpass filter that mimics dice clatter:
const playDiceSound = useCallback(() => {
try {
const ctx = new AudioContext();
const duration = 0.15;
const bufferSize = ctx.sampleRate * duration;
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
// Generate noise burst (sounds like dice clatter)
for (let i = 0; i < bufferSize; i++) {
const t = i / ctx.sampleRate;
const envelope = Math.exp(-t * 30);
data[i] = (Math.random() * 2 - 1) * envelope * 0.3;
}
const source = ctx.createBufferSource();
source.buffer = buffer;
// Band-pass filter for dice-like tone
const filter = ctx.createBiquadFilter();
filter.type = "bandpass";
filter.frequency.value = 3000;
filter.Q.value = 1;
source.connect(filter);
filter.connect(ctx.destination);
source.start();
source.stop(ctx.currentTime + duration);
setTimeout(() => ctx.close(), 500);
} catch {}
}, []);
The dice fly to random positions using pre-generated landing spots:
function generateDiceLandingSpots() {
return [0, 1, 2, 3].map(() => ({
x: (Math.random() - 0.5) * 80, // -40% to +40% from center
y: (Math.random() - 0.5) * 60, // -30% to +30% from center
rotate: Math.floor(Math.random() * 90 - 45),
}));
}
Timer Matching Logic
The core mechanic: 4 dice combine into a target number (e.g. 4-2-7-1 = 4271ms). You click to start a count-up timer, then click again to stop it. If you hit the exact number, the countdown drops. Miss it, and time goes up:
const handleClick = () => {
if (gameState !== "counting") return;
cancelAnimationFrame(rafRef.current);
setIsCounting(false);
const clickedMs = Math.round(countUpRef.current);
const gap = Math.abs(clickedMs - targetNumber);
if (clickedMs === targetNumber) {
// Exact match — countdown drops
setMatchResult("exact");
const newCountdown = liveCountdown - targetNumber / 1000;
if (newCountdown <= 0) {
setGameState("victory");
}
} else if (gap <= 100) {
// Easter egg: ±100ms → crash → glitch → Gyarados!
setMatchResult("exact");
setTimeout(() => setGameState("glitch"), 500);
setTimeout(() => setGameState("victory"), 4500);
} else {
// Miss — countdown increases
setMatchResult("miss");
const newCountdown = liveCountdown + targetNumber / 1000;
setCountdown(newCountdown);
}
};
The Glitch Easter Egg
When you hit within ±100ms, the game triggers a 4-layer glitch sound: a sawtooth oscillator sweep, a square wave digital screech, gated white noise, and rapid digital beeps — all synthesized in real-time with the Web Audio API.
Magikarp Landing Physics
The landing screen features a bouncing Magikarp driven by requestAnimationFrame:
const animate = (time: number) => {
if (!lastTimeRef.current) lastTimeRef.current = time;
const delta = Math.min((time - lastTimeRef.current) / 16, 3);
lastTimeRef.current = time;
const { vx, vy } = velRef.current;
const { x, y } = posRef.current;
let nx = x + vx * delta;
let ny = y + vy * delta;
// Bounce off walls
if (nx < marginX) { nx = marginX; bvx = Math.abs(bvx); }
if (nx > 100 - marginX) { nx = 100 - marginX; bvx = -Math.abs(bvx); }
if (ny < marginY) { ny = marginY; bvy = Math.abs(bvy); }
if (ny > 90 - marginY) { ny = 90 - marginY; bvy = -Math.abs(bvy); }
// Randomize direction on bounce
if (bounced) {
bvx += (Math.random() - 0.5) * 0.3;
bvy += (Math.random() - 0.5) * 0.3;
const speed = Math.sqrt(bvx * bvx + bvy * bvy);
const targetSpeed = 0.2 + Math.random() * 0.15;
if (speed > 0) {
bvx = (bvx / speed) * targetSpeed;
bvy = (bvy / speed) * targetSpeed;
}
}
};
Tech Stack
| Technology | Purpose |
|---|---|
| Next.js | React framework with static export |
| TypeScript | Type-safe game logic |
| Framer Motion | Dice animations, card reveals, sparkle effects |
| Web Audio API | Synthesized dice sounds, glitch effects, tick sounds |
| CSS Animations | Background particles, ambient glow effects |
Links
- Live Demo: 30days.abduarrahman.com
- Source Code: github.com/ab2rahman/30days-web-challenge
-
Key Files:
Day1Challenge.tsx,MagikarpLanding.tsx
Follow the challenge:
- Instagram: @abduarrahman
- YouTube: @abduarrahmanscode
- TikTok: @anduarrahmans
Support the challenge:
- Ko-fi: ko-fi.com/abduarrahman
Originally published at abduarrahman.com
Top comments (0)