Created
December 31, 2024 09:04
-
-
Save Jach/7e7058b9b10c1c8dddfb9798ce7405a6 to your computer and use it in GitHub Desktop.
Jumping Coyopotato Game, made 99.999% with Claude
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Play here: https://gistpreview.github.io/?7e7058b9b10c1c8dddfb9798ce7405a6
Initial prompt was:
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.