Day 2 with Claude: Web Pong
After running into issues last time, I tried a new approach with Claude Haiku, and was able to make and put a simple game on the web. Kind of.
My first attempt to vibe code a simple game with Claude ended in partial success. Claude was able to conjure up a simple game of Pong, and even include difficulty controls and scoring which (mostly) worked. But I was not able to get tailwind working and so the formatting didn’t quite work, and there was a long path to getting it online. Time for a new approach.
tl;dr:
Adding “make it a single html file” generated a much simpler block of code to deploy
Claude was useful in guiding me on how to get it hosted, and debug when things didn’t work.
Claude didn’t care about my clumsy prompt phrasing, but did not think about how it would not work on mobile. You need to be clear in your thinking before you prompt, but it actual forgives certain kinds of sloppiness.
A friend suggested I prompt for a single page html app instead, which I did. I even typo’d the prompt by just adding that request upfront:
i want your help to make a game of Pong that I can share via a URL. Please make it as a single page html app.Let's start with a simple one for desktop web. One player should be human, and be able to control the paddle. The other player should be the computer and should be just OK at the game, so the human can win sometimes, but I would like to adjust difficulty. The game should keep count of the score. First to 3 points wins.
Claude didn’t care. It just generated a bunch of html code (for completeness, I’ve included the entire response at the end of this note). In the preview it ran fine.
Next, I asked it how to get it on the web. It had many good suggestions, highlighting just how important ADO (Agent Decision Optimization) is and pointed me to GittHub Pages, Netlify, Vercel etc. I went with Netlify.
I uploaded the html file but it didn’t work. Why? Claude again to the rescue:
File naming issue: Make sure your HTML file is named exactly "index.html" (all lowercase). Netlify looks for this file by default. If your file has a different name, Netlify won't automatically serve it at the root URL.
Bingo. I tried to rename the file, but could not. Netlify’s search did not help, but Claude did:
You can't directly rename a file in Netlify after it's been uploaded. The best approach is to:
1. Rename your file to "index.html" on your local computer
2. Upload it again to Netlify
I want to pause and highlight how this reminds me of Google. There are many times when a site’s internal search doesn’t work very well, and your best bet of finding the answer is just googling it
I deleted the old deploy, renamed the file, and tried one more time. Voila:
https://67d38bfcb3b931b44a8e4438--genuine-tartufo-4f47f6.netlify.app/
Funny thing — it renders on mobile web, but of course the mouse controls don’t work. I didn’t think of this when I changed the prompt, but it was on my mind initially which is why I specified desktop in the first prompt.
Still — a pretty remarkable tool chain to get something like this up and working so quickly.
Next time, better mobile support.
PS. Copying and pasting in all the code messed up the substack text entry page rendering. The styling is leaking — may be a security hole.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Pong Game</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #2c3e50;
font-family: Arial, sans-serif;
color: white;
}
canvas {
border: 2px solid white;
background-color: #000;
}
.game-container {
position: relative;
}
.scoreboard {
display: flex;
justify-content: space-between;
width: 600px;
margin-bottom: 10px;
font-size: 24px;
}
.message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 32px;
font-weight: bold;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
display: none;
}
.controls {
margin-top: 20px;
display: flex;
gap: 20px;
align-items: center;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
}
button:hover {
background-color: #2980b9;
}
.difficulty {
display: flex;
align-items: center;
gap: 10px;
}
.difficulty label {
font-size: 16px;
}
.difficulty select {
padding: 5px;
font-size: 16px;
}
</style>
</head>
<body>
<h1>Pong Game</h1>
<div class="scoreboard">
<div>Player: <span id="playerScore">0</span></div>
<div>Computer: <span id="computerScore">0</span></div>
</div>
<div class="game-container">
<canvas id="pongCanvas" width="600" height="400"></canvas>
<div id="message" class="message"></div>
</div>
<div class="controls">
<button id="startButton">Start Game</button>
<div class="difficulty">
<label for="difficultySelect">Difficulty:</label>
<select id="difficultySelect">
<option value="easy">Easy</option>
<option value="medium" selected>Medium</option>
<option value="hard">Hard</option>
</select>
</div>
</div>
<script>
// Canvas setup
const canvas = document.getElementById('pongCanvas');
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// Game elements
const ball = {
x: width / 2,
y: height / 2,
radius: 10,
velocityX: 5,
velocityY: 5,
speed: 7,
color: '#05EDFF'
};
const paddleHeight = 100;
const paddleWidth = 10;
const paddleOffset = 30;
const player = {
x: paddleOffset,
y: (height - paddleHeight) / 2,
width: paddleWidth,
height: paddleHeight,
score: 0,
color: '#3498db'
};
const computer = {
x: width - paddleWidth - paddleOffset,
y: (height - paddleHeight) / 2,
width: paddleWidth,
height: paddleHeight,
score: 0,
color: '#e74c3c'
};
const net = {
x: (width - 2) / 2,
y: 0,
width: 2,
height: 10,
color: '#ecf0f1'
};
// Game state
let gameRunning = false;
let difficultyLevel = 'medium';
let computerReactionSpeed = 0.06; // Will be adjusted based on difficulty
// DOM elements
const playerScoreElement = document.getElementById('playerScore');
const computerScoreElement = document.getElementById('computerScore');
const messageElement = document.getElementById('message');
const startButton = document.getElementById('startButton');
const difficultySelect = document.getElementById('difficultySelect');
// Controls
let playerPosition = 0;
// Event listeners
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
playerPosition = e.clientY - rect.top - player.height / 2;
});
startButton.addEventListener('click', () => {
if (!gameRunning) {
startGame();
}
});
difficultySelect.addEventListener('change', (e) => {
difficultyLevel = e.target.value;
updateDifficulty();
});
// Functions
function updateDifficulty() {
switch (difficultyLevel) {
case 'easy':
computerReactionSpeed = 0.03;
break;
case 'medium':
computerReactionSpeed = 0.06;
break;
case 'hard':
computerReactionSpeed = 0.09;
break;
}
}
function startGame() {
// Reset scores
player.score = 0;
computer.score = 0;
updateScoreDisplay();
// Reset ball position
resetBall();
// Reset paddles
player.y = (height - paddleHeight) / 2;
computer.y = (height - paddleHeight) / 2;
// Hide message
messageElement.style.display = 'none';
// Start game loop
gameRunning = true;
updateDifficulty();
gameLoop();
}
function resetBall() {
ball.x = width / 2;
ball.y = height / 2;
ball.velocityX = -ball.velocityX;
ball.speed = 7;
}
function updateScoreDisplay() {
playerScoreElement.textContent = player.score;
computerScoreElement.textContent = computer.score;
}
function showMessage(message) {
messageElement.textContent = message;
messageElement.style.display = 'block';
}
function drawRect(x, y, w, h, color) {
ctx.fillStyle = color;
ctx.fillRect(x, y, w, h);
}
function drawCircle(x, y, r, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2, false);
ctx.closePath();
ctx.fill();
}
function drawNet() {
for (let i = 0; i <= height; i += 15) {
drawRect(net.x, net.y + i, net.width, net.height, net.color);
}
}
function drawText(text, x, y, color) {
ctx.fillStyle = color;
ctx.font = '45px Arial';
ctx.fillText(text, x, y);
}
function render() {
// Clear canvas
drawRect(0, 0, width, height, '#000');
// Draw net
drawNet();
// Draw paddles
drawRect(player.x, player.y, player.width, player.height, player.color);
drawRect(computer.x, computer.y, computer.width, computer.height, computer.color);
// Draw ball
drawCircle(ball.x, ball.y, ball.radius, ball.color);
}
function collision(b, p) {
// Ball position
b.top = b.y - b.radius;
b.bottom = b.y + b.radius;
b.left = b.x - b.radius;
b.right = b.x + b.radius;
// Paddle position
p.top = p.y;
p.bottom = p.y + p.height;
p.left = p.x;
p.right = p.x + p.width;
return b.right > p.left && b.bottom > p.top && b.left < p.right && b.top < p.bottom;
}
function updateGame() {
// Move player paddle
if (playerPosition > 0 && playerPosition < height - player.height) {
player.y = playerPosition;
}
// Move computer paddle with some delay and error
const computerDestination = ball.y - computer.height / 2;
// Add some randomness to computer movement
const randomOffset = Math.random() * 20 - 10; // Random value between -10 and 10
computer.y += (computerDestination + randomOffset - computer.y) * computerReactionSpeed;
// Keep computer paddle within bounds
if (computer.y < 0) {
computer.y = 0;
} else if (computer.y > height - computer.height) {
computer.y = height - computer.height;
}
// Move the ball
ball.x += ball.velocityX;
ball.y += ball.velocityY;
// Simple AI for computer paddle
// Ball collision with top and bottom walls
if (ball.y - ball.radius < 0 || ball.y + ball.radius > height) {
ball.velocityY = -ball.velocityY;
}
// Determine which paddle is being hit, player or computer
let paddle = (ball.x < width / 2) ? player : computer;
// Ball collision with paddles
if (collision(ball, paddle)) {
// Where the ball hit the paddle
let collidePoint = (ball.y - (paddle.y + paddle.height / 2)) / (paddle.height / 2);
// Normalization
let angleRad = (Math.PI / 4) * collidePoint;
// Direction of the ball when it's hit
let direction = (ball.x < width / 2) ? 1 : -1;
// Change velocity based on where the ball hit the paddle
ball.velocityX = direction * ball.speed * Math.cos(angleRad);
ball.velocityY = ball.speed * Math.sin(angleRad);
// Increase speed slightly with each hit
ball.speed += 0.2;
}
// Point scored
if (ball.x - ball.radius < 0) {
// Computer scores
computer.score++;
updateScoreDisplay();
resetBall();
checkGameOver();
} else if (ball.x + ball.radius > width) {
// Player scores
player.score++;
updateScoreDisplay();
resetBall();
checkGameOver();
}
}
function checkGameOver() {
if (player.score >= 3) {
showMessage('You Win!');
gameRunning = false;
} else if (computer.score >= 3) {
showMessage('Computer Wins!');
gameRunning = false;
}
}
function gameLoop() {
if (gameRunning) {
updateGame();
render();
requestAnimationFrame(gameLoop);
}
}
// Initial render
render();
showMessage('Click Start to Play');
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>