Vibe Coding Breakout: Building a Classic Game with an AI Assistant

A complete, step-by-step tutorial for building the Breakout arcade game using vibe coding and an AI assistant, with every prompt and code block explained.

Vibe Coding Breakout: Building a Classic Game with an AI Assistant

Vibe Coding Gaming Tutorial

Vibe coding is a genuinely new way of building software, not a rebrand of an old one, and it deserves a real definition before this tutorial uses it a hundred more times. The term was coined by Andrej Karpathy in early 2025 to describe a workflow where you describe what you want in natural language and let an AI agent handle code generation, execution, and iteration rather than typing every line yourself. It caught on fast enough that Collins English Dictionary named it their Word of the Year for 2025, and by 2026 it’s not a novelty; it’s a standard way real software gets built.

This article is a genuine, start-to-finish vibe coding project: building the classic arcade game Breakout, using an AI coding assistant to generate every piece, with every prompt shown, every resulting code block explained in real detail, and the complete, tested game assembled at the end. This isn’t a toy example. Every line of code in this article was actually written and verified before publication; the collision physics, the win and loss conditions, all of it, ran through real automated tests rather than being assumed to work.

What Is Vibe Coding?

The mechanical description is simple: you describe a goal, an AI agent generates code toward it, you review and iterate, and you repeat until it does what you need. What’s changed since the term appeared is the sophistication of the agents doing the generating; current tools don’t just autocomplete a line, they can read an entire project, edit multiple files, run the code, and fix what breaks, all from a natural-language instruction. Airbnb, for example, reports that AI now writes around 60% of its new code.

The current tool landscape is worth knowing before picking one:

  • Claude Code is a terminal-native agent well suited to exactly this kind of project: describe a feature, and watch it write and test the code directly in your project folder
  • Cursor is an AI-native code editor for people who want that same power inside a familiar IDE
  • Replit Agent is built with beginners specifically in mind, and it walks through a visible plan before writing code, which is genuinely useful for understanding the “why” behind each step, not just the “what”
  • Bolt.new and v0 are faster paths to a working prototype for people who don’t want to touch a terminal at all

The Game We’re Building: Breakout

Breakout is one of the most influential arcade games ever made, and its backstory matters because it explains why the game is structured the way it is. Atari released Breakout on May 13, 1976, conceptualized by Nolan Bushnell and Steve Bristow as a single-player evolution of Pong, and built by a young engineer named Steve Wozniak, working alongside his friend Steve Jobs. Wozniak’s design was so aggressively minimized in its chip count that Atari’s own manufacturing line reportedly couldn’t replicate it and had to ship a version with more components instead. The two Steves would go on to found Apple together that same year, and Breakout’s hardware logic directly influenced parts of the original Apple II’s design.

The rules are simple enough, which is exactly why it’s a good vibe coding project: complex enough to need real logic, simple enough to fully understand end to end. A wall of bricks lines the top of the screen. A ball bounces around the play area, and you control a paddle at the bottom, moving it left and right to keep the ball in play. Every time the ball hits a brick, the brick is destroyed and you score points. If the ball gets past your paddle and falls off the bottom of the screen, you lose a life. Destroy every brick, and you win; run out of lives, and the game ends.

Prerequisites

You need less than you’d think: a modern web browser (Chrome, Firefox, or Safari all work fine), a text editor, and one AI coding assistant. This tutorial has zero build tools, zero frameworks, and zero dependencies to install; the entire game is one HTML file that runs by opening it in a browser.

Setting up Claude Code: install it with npm install -g @anthropic-ai/claude-code, then run claude from inside an empty project folder to start a session. If you’d rather use Cursor, Replit, or another tool from the list above, every prompt below works identically; just paste it into whichever assistant you’ve chosen.

Here’s the actual build, broken into the same stages you’d naturally prompt an AI assistant through, one real capability at a time rather than asking for the whole game in one shot, which tends to produce something harder to debug when it doesn’t quite work. You can find the full code in this repo.

Step 1: The Canvas and Game Skeleton

Prompt: “Create a single HTML file for a Breakout game. Set up an HTML5 canvas 480 pixels wide and 400 pixels tall, centered on the page, with a dark background. Add a heading, and placeholder text elements for score and lives above the canvas. Don’t add any game logic yet, just the skeleton.”

<canvas id="gameCanvas" width="480" height="400"></canvas>
<div id="hud">
  <span id="score">Score: 0</span>
  <span id="lives">Lives: 3</span>
</div>

What this does: the canvas element is where every visual frame of the game gets drawn; it’s a blank rectangle you control at the pixel level through JavaScript. Starting with just the skeleton, no logic, is deliberate: it lets you confirm the page structure actually renders correctly before adding anything that could break. That matters more with AI-generated code than hand-written code, since it’s easier to spot exactly where something went wrong when you’re only checking one small piece at a time.

Step 2: The Paddle

Prompt: “Add a paddle: an 80x10 pixel rectangle near the bottom of the canvas. Make it move left and right with the arrow keys, and keep it from moving off either edge of the canvas.”

const paddle = { width: 80, height: 10, x: (canvas.width - 80) / 2, y: canvas.height - 20, speed: 7, dx: 0 };

const keys = { left: false, right: false };
document.addEventListener("keydown", (e) => {
  if (e.key === "ArrowLeft") keys.left = true;
  if (e.key === "ArrowRight") keys.right = true;
});
document.addEventListener("keyup", (e) => {
  if (e.key === "ArrowLeft") keys.left = false;
  if (e.key === "ArrowRight") keys.right = false;
});

function updatePaddle() {
  if (keys.left) paddle.dx = -paddle.speed;
  else if (keys.right) paddle.dx = paddle.speed;
  else paddle.dx = 0;
  paddle.x += paddle.dx;
  paddle.x = Math.max(0, Math.min(canvas.width - paddle.width, paddle.x));
}

What this does: keydown and keyup listeners track which arrow keys are currently held, rather than reacting once per press, which is what makes the paddle move smoothly while a key is held down instead of jumping once per keystroke. The last line of updatePaddle is the boundary check; Math.max and Math.min together clamp the paddle’s position so it can never move past either edge of the canvas, a one-line fix for a bug that’s easy to forget entirely until you watch the paddle vanish off-screen.

Step 3: The Ball and Its Physics

Prompt: “Add a ball: a small circle that starts near the bottom center of the canvas and moves diagonally. Make it bounce off the left, right, and top walls.”

const ball = { x: canvas.width / 2, y: canvas.height - 30, radius: 6, speed: 4, dx: 3, dy: -3 };

function updateBall() {
  ball.x += ball.dx;
  ball.y += ball.dy;
  if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) ball.dx *= -1;
  if (ball.y - ball.radius < 0) ball.dy *= -1;
}

What this does: dx and dy are the ball’s velocity, how far it moves on each axis every frame, and updating position by simply adding them each frame is the entire basis of the game’s motion. The wall-bounce logic reverses direction (multiplying by -1) the moment the ball’s edge, not its center, crosses a boundary; using radius in the check is what stops the ball from visually clipping halfway into the wall before bouncing.

Step 4: The Brick Grid

Prompt: “Add a grid of bricks: 4 rows of 7 bricks each, near the top of the canvas, with small gaps between them. Each brick should track whether it’s been destroyed.”

const brickInfo = { rows: 4, cols: 7, width: 55, height: 18, padding: 8, offsetTop: 40, offsetLeft: 20 };
let bricks = [];
function buildBricks() {
  bricks = [];
  for (let r = 0; r < brickInfo.rows; r++) {
    for (let c = 0; c < brickInfo.cols; c++) {
      bricks.push({
        x: brickInfo.offsetLeft + c * (brickInfo.width + brickInfo.padding),
        y: brickInfo.offsetTop + r * (brickInfo.height + brickInfo.padding),
        width: brickInfo.width, height: brickInfo.height, destroyed: false,
      });
    }
  }
}

What this does: the nested loop places each brick using its row and column position multiplied by the brick’s width or height plus padding, which is what produces an evenly spaced grid rather than requiring 28 separately hand-placed rectangles. The destroyed: false flag on every brick is doing quiet, important work; it’s the single piece of state that Step 5’s collision logic and the win condition both depend on.

Step 5: Collision Detection

This is the step worth taking seriously, since it’s where a rushed vibe coding pass most often produces a game that looks right but plays wrong.

Prompt: “Add collision detection: the ball should bounce off the paddle at an angle depending on where it hits (center bounces straight up, edges bounce at an angle), and destroy any brick it touches while bouncing away. A destroyed brick should never be hit again.”

function ballHitsPaddle(ball, paddle) {
  return (
    ball.y + ball.radius >= paddle.y &&
    ball.y - ball.radius <= paddle.y + paddle.height &&
    ball.x >= paddle.x && ball.x <= paddle.x + paddle.width
  );
}

function paddleBounceAngle(ball, paddle) {
  const hitPos = (ball.x - (paddle.x + paddle.width / 2)) / (paddle.width / 2);
  const clamped = Math.max(-1, Math.min(1, hitPos));
  return clamped * (Math.PI / 3); // up to 60 degrees at the paddle's edges
}

function brickCollision(ball, brick) {
  if (brick.destroyed) return false;
  return (
    ball.x + ball.radius > brick.x && ball.x - ball.radius < brick.x + brick.width &&
    ball.y + ball.radius > brick.y && ball.y - ball.radius < brick.y + brick.height
  );
}

What this does: the paddle collision function checks all four overlap conditions simultaneously, which is the standard axis-aligned bounding box test. The angle calculation maps where on the paddle the ball lands — from -1 at the far left edge to +1 at the far right — to a launch angle up to 60 degrees from vertical, giving the player meaningful control over where the ball goes rather than always bouncing it straight back. The brick collision function short-circuits on destroyed bricks first, so already-cleared bricks never trigger a bounce or re-register a hit.

Step 6: Win, Loss, and Game State

Prompt: “Add win and loss conditions: the player loses a life when the ball falls below the canvas, resets the ball and paddle if lives remain, and sees a game-over screen when lives run out. The player wins when all bricks are destroyed.”

function checkBallLost() {
  return ball.y - ball.radius > canvas.height;
}

function allBricksDestroyed() {
  return bricks.every(b => b.destroyed);
}

function resetBallAndPaddle() {
  ball.x = canvas.width / 2;
  ball.y = canvas.height - 30;
  ball.dx = 3;
  ball.dy = -3;
  paddle.x = (canvas.width - paddle.width) / 2;
}

function handleLifeLost() {
  lives -= 1;
  if (lives <= 0) {
    gameState = "gameover";
  } else {
    resetBallAndPaddle();
  }
}

What this does: keeping win and loss logic in small, named functions rather than inline conditionals makes each rule readable in isolation and easy to test independently. The allBricksDestroyed check using .every() is O(n) but runs once per frame against a small fixed array, so performance isn’t a concern; what matters is that it’s unambiguous. The gameState variable is what controls which screen the game draws on each frame, separating display logic from game logic cleanly.

Putting It All Together

Each step above produces a working, testable slice of the game. The complete assembled version — all six steps integrated into a single HTML file with a game loop, rendering, scoring, and state management — is available in the project repository. Opening that file in any modern browser runs the finished game with no installation required.

The structure of this build reflects a principle worth carrying into any vibe coding project: prompt for one capability at a time, verify it works before moving on, and treat the AI’s output as something to understand rather than just accept. The collision detection step in particular rewards careful reading; physics bugs that slip through at that stage tend to surface as subtle, hard-to-reproduce misbehavior later rather than obvious crashes. Building incrementally, with small prompts and real verification at each stage, is what separates a vibe coding session that produces reliable software from one that produces something that mostly works.