Skip to content

Instantly share code, notes, and snippets.

@Jach
Created December 31, 2024 09:04
Show Gist options
  • Save Jach/7e7058b9b10c1c8dddfb9798ce7405a6 to your computer and use it in GitHub Desktop.
Save Jach/7e7058b9b10c1c8dddfb9798ce7405a6 to your computer and use it in GitHub Desktop.
Jumping Coyopotato Game, made 99.999% with Claude
<!DOCTYPE html>
<html>
<head>
<title>Jumping Potato Game</title>
<style>
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #87CEEB;
font-family: Arial, sans-serif;
}
#game-container {
width: 800px;
height: 400px;
background: #f0f0f0;
position: relative;
overflow: hidden;
border: 4px solid #654321;
}
#score {
position: absolute;
top: 20px;
right: 20px;
font-size: 24px;
color: #333;
}
#game-over {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(255, 255, 255, 0.9);
padding: 20px;
border-radius: 10px;
}
.ground {
position: absolute;
bottom: 0;
width: 100%;
height: 50px;
background: #654321;
}
#restart-btn {
padding: 10px 20px;
font-size: 18px;
cursor: pointer;
background: #4CAF50;
color: white;
border: none;
border-radius: 5px;
margin-top: 10px;
}
#restart-btn:hover {
background: #45a049;
}
</style>
</head>
<body>
<div id="game-container">
<div id="score">Score: 0</div>
<div class="ground"></div>
<div id="game-over">
<h2>Game Over!</h2>
<p>Final Score: <span id="final-score">0</span></p>
<button id="restart-btn">Play Again</button>
</div>
</div>
<script>
class Game {
constructor() {
this.container = document.getElementById('game-container');
this.scoreElement = document.getElementById('score');
this.gameOverElement = document.getElementById('game-over');
this.finalScoreElement = document.getElementById('final-score');
this.restartBtn = document.getElementById('restart-btn');
this.potato = null;
this.jumpArcPreview = null;
this.obstacles = [];
this.score = 0;
this.isGameOver = false;
this.groundHeight = 50;
this.gravity = 0.8;
this.obstacleSpeed = 6; // Increased speed
this.minObstacleDistance = 200; // Reduced minimum distance
this.init();
}
init() {
this.createPotato();
this.createJumpArcPreview();
this.setupEventListeners();
this.startGame();
}
createPotato() {
const potatoSvgOrig = `
<svg width="50" height="60" viewBox="0 0 50 60">
<g transform="translate(0,0)">
<path d="M25 5 Q40 5, 45 25 Q50 45, 25 55 Q0 45, 5 25 Q10 5, 25 5"
fill="#D2B48C" stroke="#8B4513" stroke-width="2"/>
<ellipse cx="20" cy="25" rx="3" ry="4" fill="#8B4513"/> <!-- left eye -->
<ellipse cx="30" cy="25" rx="3" ry="4" fill="#8B4513"/> <!-- right eye -->
<path d="M20 35 Q25 40, 30 35" fill="none" stroke="#8B4513" stroke-width="2"/> <!-- smile -->
</g>
</svg>
`;
// Coyopotato:
const potatoSvg = `
<svg width="80" height="70" viewBox="0 0 80 70">
<g transform="translate(5,5)">
<!-- Fluffy tail (behind potato) -->
<path d="M45 35
Q60 25, 70 15
L72 17
L68 20
L73 22
L69 25
L74 28
Q65 35, 45 35"
fill="#E6C097" stroke="#8B4513" stroke-width="2"/>
<!-- Main potato body -->
<path d="M25 15 Q40 15, 45 30 Q50 45, 25 55 Q0 45, 5 30 Q10 15, 25 15"
fill="#D2B48C" stroke="#8B4513" stroke-width="2"/>
<!-- Ears -->
<path d="M15 17 L10 5 L20 15 Z"
fill="#E6C097" stroke="#8B4513" stroke-width="2"/>
<path d="M35 17 L40 5 L30 15 Z"
fill="#E6C097" stroke="#8B4513" stroke-width="2"/>
<!-- Face -->
<ellipse cx="20" cy="30" rx="2" ry="3" fill="#8B4513"/> <!-- left eye -->
<ellipse cx="30" cy="30" rx="2" ry="3" fill="#8B4513"/> <!-- right eye -->
<path d="M25 35 L23 37 L27 37" fill="none" stroke="#8B4513" stroke-width="2"/> <!-- nose is mouth -->
<!-- Blush marks -->
<ellipse cx="15" cy="37" rx="4" ry="3" fill="#FFB6C1" opacity="0.5"/>
<ellipse cx="35" cy="37" rx="4" ry="3" fill="#FFB6C1" opacity="0.5"/>
</g>
</svg>
`;
this.potato = document.createElement('div');
this.potato.style.position = 'absolute';
this.potato.style.left = '50px';
this.potato.style.bottom = `${this.groundHeight}px`;
this.potato.style.width = '50px';
this.potato.style.height = '60px';
this.potato.innerHTML = potatoSvg;
this.container.appendChild(this.potato);
this.potatoProps = {
y: this.groundHeight,
velocity: 0,
isJumping: false
};
}
createObstacle() {
const rockSvg = `
<svg width="30" height="40" viewBox="0 0 30 40">
<path d="M5 35 L2 20 L10 10 L20 15 L25 25 L20 35 Z"
fill="#808080" stroke="#666666" stroke-width="2"/>
</svg>
`;
const obstacle = document.createElement('div');
obstacle.style.position = 'absolute';
obstacle.style.right = '0';
obstacle.style.bottom = `${this.groundHeight}px`;
obstacle.style.width = '30px';
obstacle.style.height = '40px';
obstacle.innerHTML = rockSvg;
this.container.appendChild(obstacle);
this.obstacles.push({
element: obstacle,
x: this.container.offsetWidth
});
}
setupEventListeners() {
this.spacePressed = false;
this.spaceHoldStartTime = 0;
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
if (this.isGameOver) {
this.resetGame();
} else if (!this.spacePressed && !this.potatoProps.isJumping) {
this.spacePressed = true;
this.spaceHoldStartTime = Date.now();
}
}
});
document.addEventListener('keyup', (e) => {
if (e.code === 'Space' && this.spacePressed) {
const holdDuration = Date.now() - this.spaceHoldStartTime;
const baseVelocity = 12;
const maxVelocity = 20;
const maxHoldTime = 500; // max hold time in ms
// Calculate velocity based on hold duration
const velocityIncrease = Math.min(holdDuration / maxHoldTime, 1) * (maxVelocity - baseVelocity);
this.potatoProps.velocity = baseVelocity + velocityIncrease;
this.potatoProps.isJumping = true;
this.spacePressed = false;
}
});
this.restartBtn.addEventListener('click', () => {
this.resetGame();
});
}
updatePotato() {
// Visual feedback for charging jump
if (this.spacePressed) {
const holdDuration = Date.now() - this.spaceHoldStartTime;
const maxHoldTime = 500;
const chargeProgress = Math.min(holdDuration / maxHoldTime, 1);
// Make the potato squish down while charging
const scaleY = 1 - (chargeProgress * 0.2);
const scaleX = 1 + (chargeProgress * 0.2);
this.potato.style.transform = `scaleX(${scaleX}) scaleY(${scaleY})`;
} else if (this.potatoProps.isJumping) {
this.potato.style.transform = 'scale(1)';
this.potatoProps.y += this.potatoProps.velocity;
this.potatoProps.velocity -= 0.8;
if (this.potatoProps.y <= this.groundHeight) {
this.potatoProps.y = this.groundHeight;
this.potatoProps.velocity = 0;
this.potatoProps.isJumping = false;
}
this.potato.style.bottom = `${this.potatoProps.y}px`;
}
}
createJumpArcPreview() {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.style.position = 'absolute';
svg.style.left = '0';
svg.style.top = '0';
svg.style.width = '100%';
svg.style.height = '100%';
svg.style.pointerEvents = 'none';
svg.style.display = 'none';
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute('stroke', 'rgba(255, 255, 255, 0.7)');
path.setAttribute('stroke-width', '2');
path.setAttribute('fill', 'none');
path.setAttribute('stroke-dasharray', '5,5');
const landingMarker = document.createElementNS("http://www.w3.org/2000/svg", "circle");
landingMarker.setAttribute('r', '4');
landingMarker.setAttribute('fill', 'red');
svg.appendChild(path);
svg.appendChild(landingMarker);
this.container.appendChild(svg);
this.jumpArcPreview = { svg, path, landingMarker };
}
updateJumpArcPreview() {
if (this.spacePressed) {
const holdDuration = Date.now() - this.spaceHoldStartTime;
const maxHoldTime = 500;
const baseVelocity = 12;
const maxVelocity = 20;
const velocityIncrease = Math.min(holdDuration / maxHoldTime, 1) * (maxVelocity - baseVelocity);
const initialVelocity = baseVelocity + velocityIncrease;
// Calculate jump arc points
let points = [];
let x = parseInt(this.potato.style.left) + 25; // center of potato
let y = this.potatoProps.y;
let vy = initialVelocity;
let landingPoint = null;
let time = 0;
while (y >= this.groundHeight) {
points.push([x + (this.obstacleSpeed * time), this.container.offsetHeight - y]);
vy -= this.gravity;
y += vy;
time += 1;
if (y <= this.groundHeight && !landingPoint) {
landingPoint = [x + (this.obstacleSpeed * time), this.container.offsetHeight - this.groundHeight];
}
}
// Create SVG path
const pathData = points.map((p, i) =>
(i === 0 ? 'M' : 'L') + p[0] + ' ' + p[1]
).join(' ');
this.jumpArcPreview.path.setAttribute('d', pathData);
this.jumpArcPreview.path.setAttribute('stroke', 'rgba(50, 50, 50, 0.7)'); // Darker color
if (landingPoint) {
this.jumpArcPreview.landingMarker.setAttribute('cx', landingPoint[0]);
this.jumpArcPreview.landingMarker.setAttribute('cy', landingPoint[1]);
}
this.jumpArcPreview.svg.style.display = 'block';
} else {
this.jumpArcPreview.svg.style.display = 'none';
}
}
canSpawnObstacle() {
if (this.obstacles.length === 0) return true;
const lastObstacle = this.obstacles[this.obstacles.length - 1];
return lastObstacle.x < this.container.offsetWidth - this.minObstacleDistance;
}
updateObstacles() {
if (Math.random() < 0.025 && this.canSpawnObstacle()) {
this.createObstacle();
}
for (let i = this.obstacles.length - 1; i >= 0; i--) {
const obstacle = this.obstacles[i];
obstacle.x -= 5;
obstacle.element.style.right = `${this.container.offsetWidth - obstacle.x}px`;
// Check collision
if (this.checkCollision(obstacle)) {
this.gameOver();
return;
}
// Remove obstacles that are off screen
if (obstacle.x + 30 < 0) {
this.container.removeChild(obstacle.element);
this.obstacles.splice(i, 1);
this.score++;
this.scoreElement.textContent = `Score: ${this.score}`;
}
}
}
checkCollision(obstacle) {
const potatoRect = this.potato.getBoundingClientRect();
const obstacleRect = obstacle.element.getBoundingClientRect();
return !(potatoRect.right < obstacleRect.left ||
potatoRect.left > obstacleRect.right ||
potatoRect.bottom < obstacleRect.top ||
potatoRect.top > obstacleRect.bottom);
}
gameOver() {
this.isGameOver = true;
this.gameOverElement.style.display = 'block';
this.finalScoreElement.textContent = this.score;
}
resetGame() {
// Clear obstacles
this.obstacles.forEach(obstacle => {
this.container.removeChild(obstacle.element);
});
this.obstacles = [];
// Reset score and game state
this.score = 0;
this.scoreElement.textContent = 'Score: 0';
this.isGameOver = false;
this.gameOverElement.style.display = 'none';
// Reset potato position
this.potatoProps.y = this.groundHeight;
this.potatoProps.velocity = 0;
this.potatoProps.isJumping = false;
this.potato.style.bottom = `${this.groundHeight}px`;
}
startGame() {
const gameLoop = () => {
if (!this.isGameOver) {
this.updatePotato();
this.updateObstacles();
this.updateJumpArcPreview();
}
requestAnimationFrame(gameLoop);
};
gameLoop();
}
}
// Start the game
new Game();
</script>
</body>
</html>
@Jach
Copy link
Author

Jach commented Dec 31, 2024

Play here: https://gistpreview.github.io/?7e7058b9b10c1c8dddfb9798ce7405a6

Initial prompt was:

Come up with an idea for a simple jumping potato game, embed potato-looking images as svgs, and give the full implementation as a single HTML file with necessary css/javascript included that I can save and run in my browser (or upload to my private server and run from there).

From there, some adjustments for jump feel, rock spacing, and adding the jump arc preview were done, so that this is basically 'revision 10'. I then tried to get it to add a high score feature, using local storage for persistence, but it continuously failed and couldn't fix its own undefined/null reference errors. Backed out of that, uploaded my drawing of coyopotato and gave a description and had it try to adjust the svg to match it more. It turned out ok, took an iteration or two to get the tail length better. (Original potato svg still in code.) I modified it to remove the 'smile' since coyopotato's mouth is kind of nose shaped, and shrunk the eyes a bit.

@Jach
Copy link
Author

Jach commented Jan 2, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment