Building a Game with My Son: Night Home Run Derby

My son wanted to make a game. We had a Saturday afternoon. No deadlines, no requirements, just a kid excited to build something.

We made Night Home Run Derby - a timing-based baseball game where you click to swing when the ball is in the strike zone. Perfect timing sends it 400+ feet. Miss and you strike out. Three outs and the game ends.

It took about 2 hours. Here's what we built and why we built it this way.


The Game

You're the batter at home plate in a night stadium. The pitcher throws. A ball comes toward you, growing larger as it approaches. Click (or tap on mobile) when it's in the sweet spot.

The timing determines the result:

  • Perfect (0.85-0.95): 400-500 feet, "PERFECT!" message
  • Good (0.70-0.85 or 0.95-1.0): 300-400 feet, "GOOD HIT!"
  • Off (0.50-0.70): 100-300 feet, "HIT!"
  • Miss (< 0.50 or > 1.0): Strike, out

Track your longest hit and total distance across the session. Three outs and it's game over.


The Tech Stack

Here's the entire stack:

  • index.html (1 file)
  • HTML5 Canvas API
  • Vanilla JavaScript
  • CSS (inline in <style> tags)

That's it. No React. No build step. No npm install. No node_modules folder with 847 packages.

Why no framework?

Speed: We went from idea to playable game in 2 hours. No time spent on configuration, bundling, or dependency management.

Transparency: My son could see every line of code. When he asked "how does the ball get bigger?", I could point to exactly the line that does it. No abstraction layers, no magic.

Portability: The game is a single HTML file. Open it in any browser. Put it on a USB stick. Host it anywhere. No server required.

Focus: The goal was "make a fun game", not "learn a framework" or "set up a proper project". Constraints breed creativity.


Key Code Snippets

The Game Loop

Standard requestAnimationFrame pattern:

function gameLoop() {
    updateGame();
    draw();
    requestAnimationFrame(gameLoop);
}

gameLoop();

Ball Physics

The ball's Y position is a value from 0 to 1, where 0 is the pitcher and 1 is home plate:

if (game.state === 'pitching') {
    game.ballY += 0.02;  // Move toward batter

    if (game.ballY >= 1) {
        handleMiss();  // Didn't swing in time
    }
}

The ball grows as it approaches to simulate depth:

function drawBall() {
    const progress = game.ballY;
    const startY = canvas.height * 0.5;
    const endY = canvas.height - 90;

    const y = startY + (endY - startY) * progress;
    const scale = 0.3 + (progress * 0.7);  // 0.3 → 1.0
    const radius = 8 * scale;

    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2);
    ctx.fill();
}

Timing-Based Hitting

The magic is in calculateHit():

function calculateHit() {
    const timing = game.ballY;  // 0 to 1

    if (timing < 0.5) {
        return { type: 'miss', distance: 0 };  // Way too early
    } else if (timing >= 0.85 && timing <= 0.95) {
        // Perfect timing!
        const distance = 400 + Math.floor(Math.random() * 100);
        return { type: 'perfect', distance };
    } else if (timing >= 0.7 && timing < 0.85) {
        // Good - slightly early
        const distance = 300 + Math.floor(Math.random() * 100);
        return { type: 'good', distance };
    } else if (timing > 0.95 && timing <= 1.0) {
        // Good - slightly late
        const distance = 300 + Math.floor(Math.random() * 100);
        return { type: 'good', distance };
    } else if (timing >= 0.5 && timing < 0.7) {
        // Off - early
        const distance = 100 + Math.floor(Math.random() * 200);
        return { type: 'off', distance };
    } else {
        return { type: 'miss', distance: 0 };
    }
}

The random component adds variance so perfect swings don't always produce identical results.

The Swing Animation

When you click, the bat rotates:

function handleSwing() {
    if (game.state !== 'pitching') return;

    game.state = 'swinging';

    let swingProgress = 0;
    const swingInterval = setInterval(() => {
        swingProgress += 0.15;
        game.swingAngle = Math.sin(swingProgress * Math.PI) * 2;
        if (swingProgress >= 1) {
            clearInterval(swingInterval);
            game.swingAngle = 0;
        }
    }, 16);

    const result = calculateHit();
    // ... handle result
}

The sine function gives a natural arc to the swing motion.


Drawing the Scene

All graphics are drawn with Canvas primitives:

function drawBackground() {
    // Night sky
    ctx.fillStyle = '#0a0a1a';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Stars
    ctx.fillStyle = '#ffffff';
    for (let i = 0; i < 30; i++) {
        const x = Math.random() * canvas.width;
        const y = Math.random() * (canvas.height * 0.4);
        ctx.beginPath();
        ctx.arc(x, y, Math.random() * 2 + 1, 0, Math.PI * 2);
        ctx.fill();
    }

    // Grass field
    ctx.fillStyle = '#2d5a27';
    ctx.fillRect(0, canvas.height * 0.6, canvas.width, canvas.height * 0.4);

    // Stadium lights
    ctx.fillStyle = '#ffff88';
    ctx.beginPath();
    ctx.arc(34, 40, 15, 0, Math.PI * 2);
    ctx.fill();

    // ... more elements
}

The batter, pitcher, crowd silhouettes, and home plate are all drawn with rectangles, arcs, and paths. No images, no sprites, no asset loading.


What My Son Learned

Building together was the real goal. Along the way, he picked up:

1. The game loop concept

"The computer draws everything 60 times per second. That's why it looks like it's moving."

2. Coordinates and positioning

"X goes left-right, Y goes up-down. The top of the screen is Y=0."

3. State machines

"The game is in different states: title screen, pitching, swinging, result, game over. Different things happen in each state."

4. Cause and effect in code

"When you click, it runs handleClick(). That function checks what state we're in and does the right thing."

5. Iteration

"Let's make the stadium lights glow. Let's add a timing indicator. Let's make the ball spin." Each change was a small, visible improvement.


What I Re-learned

Simplicity is underrated

The game is ~550 lines of code in one file. I could have reached for Three.js, added a build system, used TypeScript. None of that would have made the game more fun.

Constraints unlock creativity

"One HTML file, no dependencies" forced us to solve problems with basic tools. The night stadium with glowing lights? Just overlapping circles and rectangles.

Building with kids is different

No deadlines. No "best practices". No code review. Just "does this make the game more fun?" If yes, ship it. If no, try something else.

The best projects might not be production-grade

This game will never be on the App Store. It doesn't need analytics or A/B testing. It exists because we had fun making it, and we have fun playing it. That's enough.


Try It

Open the HTML file in any browser. Click to start, click to swing. Works on desktop and mobile (touch support included).

The source is exactly what it looks like - readable, hackable, no build required. Want the ball faster? Change game.ballY += 0.02 to 0.03. Want more outs? Change the game over condition. Want a day mode? Change the background colors.

That's the beauty of simple code. Anyone can modify it.


The Takeaway

Not every project needs to scale. Not every project needs a framework. Sometimes the best tool is the simplest one that gets the job done.

And sometimes the best projects are the ones you build with your kids on a Saturday afternoon.


Built with HTML5 Canvas, vanilla JavaScript, and a 9-year-old's enthusiasm. Total development time: ~2 hours. Total lines of code: 547.