Vibe coding is 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.
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: paddleBounceAngle is what makes the paddle feel controllable rather than a flat wall. It measures how far from the paddle's center the ball landed, normalizes that to a range from -1 to 1, and converts it into an angle, so a center hit bounces the ball nearly straight up, while an edge hit sends it off at a sharp angle you can aim with. The very first line of brickCollision, if (brick.destroyed) return false, is the single most important line in the whole file; without it, a destroyed brick's rectangle would still register collisions forever, a classic bug that makes the ball behave as if invisible walls are still standing where bricks used to be.
Step 6: Scoring and Lives
Prompt: "Wire the collision logic into the main update loop: increase the score by 10 for each brick destroyed, and lose a life when the ball falls past the paddle. Reset the ball's position after losing a life, unless lives reach zero."
for (const brick of bricks) {
if (brickCollision(ball, brick)) {
brick.destroyed = true;
ball.dy *= -1;
score += 10;
scoreEl.textContent = `Score: ${score}`;
break; // resolve only one brick per frame
}
}
if (ball.y - ball.radius > canvas.height) {
lives -= 1;
livesEl.textContent = `Lives: ${lives}`;
if (lives <= 0) {
gameOver = true;
} else {
resetBallAndPaddle();
}
}
What this does: the break statement after a brick is destroyed is a small but deliberate choice; without it, if the ball happened to overlap two adjacent bricks in the same frame, both would be destroyed, and both would flip the ball's direction, which can cancel out into a bounce that looks visually wrong. Resolving one collision per frame keeps the physics predictable. The life-loss branch checks whether lives has actually hit zero before deciding whether to reset the ball or end the game, which is the exact logic that separates "you get another try" from "the game is genuinely over."
Step 7: Win and Loss States
Prompt: "Add a win condition when every brick is destroyed, and stop the game loop entirely once the game is won or lost, displaying a message either way."
if (bricks.every((b) => b.destroyed)) {
gameWon = true;
messageEl.textContent = "You Win! Refresh to play again.";
}
function loop() {
if (!gameOver && !gameWon) {
updatePaddle();
updateBall();
}
draw();
requestAnimationFrame(loop);
}
What this does: Array.every() checks that every single brick's destroyed flag is true, a clean, readable way to express "all of them" without a manual counter to keep in sync. The loop function still keeps rendering after the game ends, calling draw() unconditionally, but stops calling the update functions, which is why the final frame stays frozen on screen instead of the whole page becoming unresponsive.
Every piece above is combined into the complete working file in this repo.
Shipping the App #
Once it plays the way you want, getting it online costs nothing and takes a few minutes. GitHub Pages is the simplest path for a static file like this one: push the HTML file to a GitHub repository, enable Pages in the repository settings, and it's live at a public URL with no server to manage.
Netlify offers largely the same free static-hosting deal with drag-and-drop deployment if you'd rather skip Git entirely. For a project specifically framed as a game rather than a general web app, itch.io is worth considering too; it's built specifically for indie game distribution, with a built-in audience that's actually looking for exactly this kind of project, rather than a general web hosting audience that has to be told what it's looking at.
Wrapping Up #
Vibe coding didn't remove the work of understanding what got built; it moved that work to a different place. You didn't type out the collision math character by character, but you still had to know that a destroyed brick needs a guard clause, that a paddle needs boundary clamping, and that "it plays fine when I click around" isn't the same claim as "the win and loss states are both verified correct."
That's the skill this tutorial was really teaching underneath the Breakout build: not how to prompt an AI assistant — that part is genuinely easy — but how to read, question, and verify what it hands back, which is the part that decides whether a vibe-coded project is a real, working game or just one that looks like it until someone finds the bug you didn't check for.
[Shittu Olumide](https://www.linkedin.com/in/olumide-shittu/) is a software engineer and technical writer passionate about leveraging cutting-edge technologies to craft compelling narratives, with a keen eye for detail and a knack for simplifying complex concepts. You can also find Shittu on Twitter.