{"slug": "vibe-coding-a-classic-game-building-breakout-step-by-step-with-an-ai-assistant", "title": "Vibe Coding a Classic Game: Building Breakout Step by Step with an AI Assistant", "summary": "A step-by-step tutorial demonstrates building the 1976 Atari arcade game Breakout entirely through \"vibe coding,\" a workflow coined by Andrej Karpathy in early 2025 in which a user describes goals in natural language and an AI agent generates, runs and iterates on the code. The article cites Airbnb's report that AI now writes around 60% of its new code and compares tools including Claude Code, Cursor, Replit Agent, Bolt.new and v0, noting Collins English Dictionary named \"vibe coding\" its Word of the Year for 2025. Every code block in the project was written and verified with automated tests covering collision physics and win/loss conditions before publication.", "body_md": "# Vibe Coding a Classic Game: Building Breakout Step by Step with an AI Assistant\n\nVibe 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.\n\nVibe 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](https://x.com/karpathy/status/1886192184808149383) 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](https://www.collinsdictionary.com/woty), and by 2026 it's not a novelty; it's a standard way real software gets built.\n\nThis article is a genuine, start-to-finish vibe coding project: building the classic arcade game [Breakout](<https://en.wikipedia.org/wiki/Breakout_(video_game)>), 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.\n\n## What Is Vibe Coding?\n\nThe 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](https://techcrunch.com/2026/05/08/airbnb-says-ai-now-writes-60-of-its-new-code/).\n\nThe current tool landscape is worth knowing before picking one:\n\n- **[Claude Code](https://claude.com/product/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\n- **[Cursor](https://cursor.com/)** is an AI-native code editor for people who want that same power inside a familiar IDE\n- **[Replit Agent](https://replit.com/products/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\"\n- **[Bolt.new](https://bolt.new/)** and**[v0](https://v0.app/)** are faster paths to a working prototype for people who don't want to touch a terminal at all\n\n## The Game We're Building: Breakout\n\nBreakout 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](<https://en.wikipedia.org/wiki/Breakout_(video_game)>), 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.\n\nThe 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.\n\n#### Prerequisites\n\nYou 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.\n\n**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.\n\nHere'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](https://github.com/zenUnicorn/VibeGame).\n\n#### Step 1: The Canvas and Game Skeleton\n\n**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.\"*\n\n```\n<canvas id=\"gameCanvas\" width=\"480\" height=\"400\"></canvas>\n<div id=\"hud\">\n  <span id=\"score\">Score: 0</span>\n  <span id=\"lives\">Lives: 3</span>\n</div>\n```\n\n**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.\n\n#### Step 2: The Paddle\n\n**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.\"*\n\n``` js\nconst paddle = { width: 80, height: 10, x: (canvas.width - 80) / 2, y: canvas.height - 20, speed: 7, dx: 0 };\n\nconst keys = { left: false, right: false };\ndocument.addEventListener(\"keydown\", (e) => {\n  if (e.key === \"ArrowLeft\") keys.left = true;\n  if (e.key === \"ArrowRight\") keys.right = true;\n});\ndocument.addEventListener(\"keyup\", (e) => {\n  if (e.key === \"ArrowLeft\") keys.left = false;\n  if (e.key === \"ArrowRight\") keys.right = false;\n});\n\nfunction updatePaddle() {\n  if (keys.left) paddle.dx = -paddle.speed;\n  else if (keys.right) paddle.dx = paddle.speed;\n  else paddle.dx = 0;\n  paddle.x += paddle.dx;\n  paddle.x = Math.max(0, Math.min(canvas.width - paddle.width, paddle.x));\n}\n```\n\n**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.\n\n#### Step 3: The Ball and Its Physics\n\n**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.\"*\n\n``` js\nconst ball = { x: canvas.width / 2, y: canvas.height - 30, radius: 6, speed: 4, dx: 3, dy: -3 };\n\nfunction updateBall() {\n  ball.x += ball.dx;\n  ball.y += ball.dy;\n  if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) ball.dx *= -1;\n  if (ball.y - ball.radius < 0) ball.dy *= -1;\n}\n```\n\n**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.\n\n#### Step 4: The Brick Grid\n\n**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.\"*\n\n``` js\nconst brickInfo = { rows: 4, cols: 7, width: 55, height: 18, padding: 8, offsetTop: 40, offsetLeft: 20 };\nlet bricks = [];\nfunction buildBricks() {\n  bricks = [];\n  for (let r = 0; r < brickInfo.rows; r++) {\n    for (let c = 0; c < brickInfo.cols; c++) {\n      bricks.push({\n        x: brickInfo.offsetLeft + c * (brickInfo.width + brickInfo.padding),\n        y: brickInfo.offsetTop + r * (brickInfo.height + brickInfo.padding),\n        width: brickInfo.width, height: brickInfo.height, destroyed: false,\n      });\n    }\n  }\n}\n```\n\n**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.\n\n#### Step 5: Collision Detection\n\nThis 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.\n\n**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.\"*\n\n```\nfunction ballHitsPaddle(ball, paddle) {\n  return (\n    ball.y + ball.radius >= paddle.y &&\n    ball.y - ball.radius <= paddle.y + paddle.height &&\n    ball.x >= paddle.x && ball.x <= paddle.x + paddle.width\n  );\n}\n\nfunction paddleBounceAngle(ball, paddle) {\n  const hitPos = (ball.x - (paddle.x + paddle.width / 2)) / (paddle.width / 2);\n  const clamped = Math.max(-1, Math.min(1, hitPos));\n  return clamped * (Math.PI / 3); // up to 60 degrees at the paddle's edges\n}\n\nfunction brickCollision(ball, brick) {\n  if (brick.destroyed) return false;\n  return (\n    ball.x + ball.radius > brick.x && ball.x - ball.radius < brick.x + brick.width &&\n    ball.y + ball.radius > brick.y && ball.y - ball.radius < brick.y + brick.height\n  );\n}\n```\n\n**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.\n\n#### Step 6: Scoring and Lives\n\n**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.\"*\n\n``` js\nfor (const brick of bricks) {\n  if (brickCollision(ball, brick)) {\n    brick.destroyed = true;\n    ball.dy *= -1;\n    score += 10;\n    scoreEl.textContent = `Score: ${score}`;\n    break; // resolve only one brick per frame\n  }\n}\n\nif (ball.y - ball.radius > canvas.height) {\n  lives -= 1;\n  livesEl.textContent = `Lives: ${lives}`;\n  if (lives <= 0) {\n    gameOver = true;\n  } else {\n    resetBallAndPaddle();\n  }\n}\n```\n\n**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.\"\n\n#### Step 7: Win and Loss States\n\n**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.\"*\n\n``` js\nif (bricks.every((b) => b.destroyed)) {\n  gameWon = true;\n  messageEl.textContent = \"You Win! Refresh to play again.\";\n}\n\nfunction loop() {\n  if (!gameOver && !gameWon) {\n    updatePaddle();\n    updateBall();\n  }\n  draw();\n  requestAnimationFrame(loop);\n}\n```\n\n**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.\n\nEvery piece above is combined into the complete working file in this [repo](https://github.com/zenUnicorn/VibeGame).\n\n## Shipping the App\n\nOnce it plays the way you want, getting it online costs nothing and takes a few minutes. **[GitHub Pages](https://pages.github.com/)** 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.\n\n**[Netlify](https://www.netlify.com/)** 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](https://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.\n\n## Wrapping Up\n\nVibe 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.\"\n\nThat'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.\n\n \n\n \n\n[**\\[Shittu Olumide\\](https://www.linkedin.com/in/olumide-shittu/)**](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](https://twitter.com/Shittu_Olumide_).", "url": "https://wpnews.pro/news/vibe-coding-a-classic-game-building-breakout-step-by-step-with-an-ai-assistant", "canonical_source": "https://www.kdnuggets.com/vibe-coding-a-classic-game-building-breakout-step-by-step-with-an-ai-assistant", "published_at": "2026-10-30 14:00:00+00:00", "updated_at": "2026-10-30 14:18:39.506600+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "artificial-intelligence"], "entities": ["Andrej Karpathy", "Claude Code", "Cursor", "Replit Agent", "Bolt.new", "v0", "Airbnb", "Atari"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/vibe-coding-a-classic-game-building-breakout-step-by-step-with-an-ai-assistant", "markdown": "https://wpnews.pro/news/vibe-coding-a-classic-game-building-breakout-step-by-step-with-an-ai-assistant.md", "text": "https://wpnews.pro/news/vibe-coding-a-classic-game-building-breakout-step-by-step-with-an-ai-assistant.txt", "jsonld": "https://wpnews.pro/news/vibe-coding-a-classic-game-building-breakout-step-by-step-with-an-ai-assistant.jsonld"}}