DJ Toenail
Prompt
CODE a core mini-golf game engine that handles ball physics, collision detection with obstacles, and hole-in-one logic. The primary focus is on a functional, responsive simulation of the ball's movement on a 2D plane (representing the course).
A system prompt was added to support web rendering
# Mini-Golf Game Engine I'll create a responsive mini-golf game engine with realistic physics, collision detection, and hole-in-one logic. The implementation will include a ball, obstacles, and a hole with realistic physics simulation. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mini-Golf Physics Engine</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c); min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 20px; color: white; } .container { max-width: 900px; width: 100%; background: rgba(0, 0, 0, 0.7); border-radius: 15px; padding: 20px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); backdrop-filter: blur(10px); } header { text-align: center; margin-bottom: 20px; } h1 { font-size: 2.5rem; margin-bottom: 10px; color: #4dff4d; text-shadow: 0 0 10px rgba(77, 255, 77, 0.7); } .subtitle { font-size: 1.2rem; color: #a0a0ff; margin-bottom: 20px; } .game-container { display: flex; flex-direction: column; gap: 20px; } .game-ui { display: flex; justify-content: space-between; background: rgba(30, 30, 60, 0.8); padding: 15px; border-radius: 10px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); } .stats { display: flex; gap: 20px; } .stat-box { background: rgba(0, 0, 0, 0.4); padding: 10px 15px; border-radius: 8px; text-align: center; min-width: 120px; } .stat-value { font-size: 1.8rem; font-weight: bold; color: #ffcc00; } .stat-label { font-size: 0.9rem; color: #aaa; } .controls { display: flex; gap: 15px; align-items: center; } button { background: linear-gradient(to bottom, #4a9bff, #1a5cb7); color: white; border: none; padding: 10px 20px; border-radius: 30px; font-weight: bold; cursor: pointer; transition: all 0.3s; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } button:hover { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4); background: linear-gradient(to bottom, #5aa5ff, #2a6cc7); } button:active { transform: translateY(1px); } .game-area { position: relative; background: #0a4a0a; border-radius: 10px; overflow: hidden; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.6); border: 3px solid #3a7a3a; } canvas { display: block; width: 100%; height: 500px; } .instructions { background: rgba(30, 30, 60, 0.8); padding: 20px; border-radius: 10px; margin-top: 20px; } .instructions h2 { color: #4dff4d; margin-bottom: 10px; } .instructions ul { padding-left: 20px; } .instructions li { margin-bottom: 8px; line-height: 1.5; } .power-meter { height: 20px; background: rgba(0, 0, 0, 0.4); border-radius: 10px; margin-top: 10px; overflow: hidden; position: relative; } .power-fill { height: 100%; background: linear-gradient(to right, #ff3300, #ffff00, #4dff4d); width: 0%; transition: width 0.1s; } .hole-message { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.85); padding: 20px 40px; border-radius: 15px; text-align: center; z-index: 10; border: 3px solid gold; box-shadow: 0 0 20px rgba(255, 215, 0, 0.7); display: none; } .hole-message h2 { font-size: 2.5rem; color: gold; margin-bottom: 10px; } .hole-message p { font-size: 1.2rem; margin-bottom: 20px; } @media (max-width: 768px) { .game-ui { flex-direction: column; gap: 15px; } .stats { flex-wrap: wrap; justify-content: center; } .stat-box { min-width: 100px; } } </style> </head> <body> <div class="container"> <header> <h1>Mini-Golf Physics Engine</h1> <p class="subtitle">Drag and release to hit the ball - navigate through obstacles to the hole!</p> </header> <div class="game-container"> <div class="game-ui"> <div class="stats"> <div class="stat-box"> <div class="stat-value" id="strokes">0</div> <div class="stat-label">STROKES</div> </div> <div class="stat-box"> <div class="stat-value" id="power">0%</div> <div class="stat-label">POWER</div> </div> </div> <div class="controls"> <button id="reset-btn">Reset Game</button> <button id="level-btn">Next Level</button> </div> </div> <div class="game-area"> <canvas id="gameCanvas"></canvas> <div class="hole-message" id="hole-message"> <h2>HOLE IN ONE!</h2> <p>You completed the course in <span id="final-strokes">0</span> strokes</p> <button id="next-level">Next Level</button> </div> </div> </div> <div class="instructions"> <h2>How to Play</h2> <ul> <li><strong>Drag and release</strong> to hit the golf ball</li> <li>Control the <strong>power</strong> by dragging distance</li> <li>Navigate around <strong>obstacles</strong> to reach the hole</li> <li>Try to get the ball into the <strong>hole</strong> in as few strokes as possible</li> <li>Realistic physics simulation with <strong>friction, bounce, and momentum</strong></li> </ul> </div> </div> <script> // Game constants const FRICTION = 0.98; const ELASTICITY = 0.7; const POWER_MULTIPLIER = 0.2; const MAX_POWER = 100; const HOLE_RADIUS = 20; const BALL_RADIUS = 10; // Game state let gameState = { ball: { x: 100, y: 400, vx: 0, vy: 0, radius: BALL_RADIUS, isMoving: false }, hole: { x: 0, y: 0, radius: HOLE_RADIUS }, obstacles: [], strokes: 0, power: 0, isDragging: false, dragStart: { x: 0, y: 0 }, dragEnd: { x: 0, y: 0 }, level: 1, completed: false }; // Canvas setup const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const holeMessage = document.getElementById('hole-message'); const finalStrokes = document.getElementById('final-strokes'); // Set canvas dimensions function resizeCanvas() { canvas.width = canvas.parentElement.clientWidth; canvas.height = 500; initLevel(); } // Initialize level function initLevel() { gameState.obstacles = []; gameState.completed = false; holeMessage.style.display = 'none'; // Set hole position based on level switch(gameState.level) { case 1: gameState.hole.x = canvas.width - 100; gameState.hole.y = 100; // Add obstacles gameState.obstacles.push( {x: 300, y: 150, width: 20, height: 200, type: 'wall'}, {x: 500, y: 250, width: 150, height: 20, type: 'wall'}, {x: 200, y: 350, width: 100, height: 20, type: 'wall'} ); break; case 2: gameState.hole.x = 100; gameState.hole.y = 100; // Add obstacles gameState.obstacles.push( {x: 200, y: 100, width: 20, height: 200, type: 'wall'}, {x: 400, y: 200, width: 200, height: 20, type: 'wall'}, {x: 600, y: 100, width: 20, height: 200, type: 'wall'}, {x: 300, y: 350, radius: 40, type: 'circle'} ); break; case 3: gameState.hole.x = canvas.width - 100; gameState.hole.y = canvas.height - 100; // Add obstacles gameState.obstacles.push( {x: 200, y: 100, width: 20, height: 300, type: 'wall'}, {x: 400, y: 150, width: 20, height: 300, type: 'wall'}, {x: 600, y: 100, width: 20, height: 300, type: 'wall'}, {x: 300, y: 250, radius: 50, type: 'circle'}, {x: 500, y: 350, radius: 30, type: 'circle'} ); break; default: gameState.level = 1; initLevel(); return; } // Reset ball position gameState.ball.x = 100; gameState.ball.y = canvas.height - 100; gameState.ball.vx = 0; gameState.ball.vy = 0; gameState.ball.isMoving = false; gameState.strokes = 0; updateUI(); } // Update UI elements function updateUI() { document.getElementById('strokes').textContent = gameState.strokes; document.getElementById('power').textContent = Math.round(gameState.power) + '%'; // Update power meter const powerMeter = document.querySelector('.power-meter'); if (!powerMeter) { const powerBox = document.querySelector('.stat-box:nth-child(2)'); const powerMeterDiv = document.createElement('div'); powerMeterDiv.className = 'power-meter'; powerMeterDiv.innerHTML = '<div class="power-fill"></div>'; powerBox.appendChild(powerMeterDiv); } document.querySelector('.power-fill').style.width = gameState.power + '%'; } // Draw game elements function draw() { // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw grass background ctx.fillStyle = '#0a4a0a'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw grass pattern ctx.fillStyle = '#0d5a0d'; for (let i = 0; i < canvas.width; i += 20) { for (let j = 0; j < canvas.height; j += 20) { if ((i + j) % 40 === 0) { ctx.fillRect(i, j, 10, 10); } } } // Draw obstacles gameState.obstacles.forEach(obstacle => { if (obstacle.type === 'wall') { ctx.fillStyle = '#8b4513'; ctx.fillRect(obstacle.x, obstacle.y, obstacle.width, obstacle.height); // Draw wood texture ctx.fillStyle = '#a0522d'; for (let i = 0; i < obstacle.width; i += 10) { ctx.fillRect(obstacle.x + i, obstacle.y, 2, obstacle.height); } } else if (obstacle.type === 'circle') { ctx.beginPath(); ctx.arc(obstacle.x, obstacle.y, obstacle.radius, 0, Math.PI * 2); ctx.fillStyle = '#8b4513'; ctx.fill(); // Draw wood texture ctx.strokeStyle = '#a0522d'; ctx.lineWidth = 2; ctx.beginPath(); for (let i = 0; i < 8; i++) { const angle = (i * Math.PI) / 4; const x1 = obstacle.x + Math.cos(angle) * (obstacle.radius - 5); const y1 = obstacle.y + Math.sin(angle) * (obstacle.radius - 5); const x2 = obstacle.x + Math.cos(angle) * (obstacle.radius + 5); const y2 = obstacle.y + Math.sin(angle) * (obstacle.radius + 5); ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); } } }); // Draw hole ctx.beginPath(); ctx.arc(gameState.hole.x, gameState.hole.y, gameState.hole.radius, 0, Math.PI * 2); ctx.fillStyle = '#000'; ctx.fill(); // Draw hole rim ctx.beginPath(); ctx.arc(gameState.hole.x, gameState.hole.y, gameState.hole.radius + 3, 0, Math.PI * 2); ctx.strokeStyle = '#555'; ctx.lineWidth = 3; ctx.stroke(); // Draw flag in hole ctx.beginPath(); ctx.moveTo(gameState.hole.x, gameState.hole.y - 30); ctx.lineTo(gameState.hole.x, gameState.hole.y); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke(); ctx.beginPath(); ctx.moveTo(gameState.hole.x, gameState.hole.y - 30); ctx.lineTo(gameState.hole.x + 15, gameState.hole.y - 20); ctx.lineTo(gameState.hole.x, gameState.hole.y - 10); ctx.closePath(); ctx.fillStyle = 'red'; ctx.fill(); // Draw ball ctx.beginPath(); ctx.arc(gameState.ball.x, gameState.ball.y, gameState.ball.radius, 0, Math.PI * 2); ctx.fillStyle = 'white'; ctx.fill(); // Draw ball highlight ctx.beginPath(); ctx.arc(gameState.ball.x - 3, gameState.ball.y - 3, 3, 0, Math.PI * 2); ctx.fillStyle = 'rgba(200, 200, 255, 0.7)'; ctx.fill(); // Draw drag line if dragging if (gameState.isDragging) { ctx.beginPath(); ctx.moveTo(gameState.ball.x, gameState.ball.y); ctx.lineTo(gameState.dragEnd.x, gameState.dragEnd.y); ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; ctx.lineWidth = 2; ctx.stroke(); // Draw power indicator const dx = gameState.dragStart.x - gameState.dragEnd.x; const dy = gameState.dragStart.y - gameState.dragEnd.y; const distance = Math.sqrt(dx * dx + dy * dy); const power = Math.min(distance / 5, MAX_POWER); ctx.beginPath(); ctx.arc(gameState.ball.x, gameState.ball.y, gameState.ball.radius + 5 + power/5, 0, Math.PI * 2); ctx.strokeStyle = `hsl(${120 - power * 1.2}, 100%, 50%)`; ctx.lineWidth = 2; ctx.stroke(); } } // Update game physics function update() { if (gameState.ball.isMoving) { // Apply friction gameState.ball.vx *= FRICTION; gameState.ball.vy *= FRICTION; // Stop ball if moving very slowly if (Math.abs(gameState.ball.vx) < 0.1 && Math.abs(gameState.ball.vy) < 0.1) { gameState.ball.vx = 0; gameState.ball.vy = 0; gameState.ball.isMoving = false; } // Update position gameState.ball.x += gameState.ball.vx; gameState.ball.y += gameState.ball.vy; // Boundary collision if (gameState.ball.x - gameState.ball.radius < 0) { gameState.ball.x = gameState.ball.radius; gameState.ball.vx = -gameState.ball.vx * ELASTICITY; } else if (gameState.ball.x + gameState.ball.radius > canvas.width) { gameState.ball.x = canvas.width - gameState.ball.radius; gameState.ball.vx = -gameState.ball.vx * ELASTICITY; } if (gameState.ball.y - gameState.ball.radius < 0) { gameState.ball.y = gameState.ball.radius; gameState.ball.vy = -gameState.ball.vy * ELASTICITY; } else if (gameState.ball.y + gameState.ball.radius > canvas.height) { gameState.ball.y = canvas.height - gameState.ball.radius; gameState.ball.vy = -gameState.ball.vy * ELASTICITY; } // Obstacle collision gameState.obstacles.forEach(obstacle => { if (obstacle.type === 'wall') { // Wall collision const closestX = Math.max(obstacle.x, Math.min(gameState.ball.x, obstacle.x + obstacle.width)); const closestY = Math.max(obstacle.y, Math.min(gameState.ball.y, obstacle.y + obstacle.height)); const dx = gameState.ball.x - closestX; const dy = gameState.ball.y - closestY; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < gameState.ball.radius) { // Collision response const overlap = gameState.ball.radius - distance; const nx = dx / distance; const ny = dy / distance; gameState.ball.x += nx * overlap; gameState.ball.y += ny * overlap; const dot = gameState.ball.vx * nx + gameState.ball.vy * ny; gameState.ball.vx = (gameState.ball.vx - 2 * dot * nx) * ELASTICITY; gameState.ball.vy = (gameState.ball.vy - 2 * dot * ny) * ELASTICITY; } } else if (obstacle.type === 'circle') { // Circle obstacle collision const dx = gameState.ball.x - obstacle.x; const dy = gameState.ball.y - obstacle.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < gameState.ball.radius + obstacle.radius) { // Collision response const overlap = (gameState.ball.radius + obstacle.radius) - distance; const nx = dx / distance; const ny = dy / distance; gameState.ball.x += nx * overlap; gameState.ball.y += ny * overlap; const dot = gameState.ball.vx * nx + gameState.ball.vy * ny; gameState.ball.vx = (gameState.ball.vx - 2 * dot * nx) * ELASTICITY; gameState.ball.vy = (gameState.ball.vy - 2 * dot * ny) * ELASTICITY; } } }); // Hole collision const holeDx = gameState.ball.x - gameState.hole.x; const holeDy = gameState.ball.y - gameState.hole.y; const holeDistance = Math.sqrt(holeDx * holeDx + holeDy * holeDy); if (holeDistance < gameState.hole.radius - gameState.ball.radius/2) { // Check if ball is moving slowly enough to fall in const speed = Math.sqrt(gameState.ball.vx * gameState.ball.vx + gameState.ball.vy * gameState.ball.vy); if (speed < 2) { gameState.completed = true; finalStrokes.textContent = gameState.strokes; holeMessage.style.display = 'block'; gameState.ball.isMoving = false; } } } } // Game loop function gameLoop() { update(); draw(); requestAnimationFrame(gameLoop); } // Event listeners canvas.addEventListener('mousedown', (e) => { if (gameState.ball.isMoving || gameState.completed) return; const rect = canvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const dx = mouseX - gameState.ball.x; const dy = mouseY - gameState.ball.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < gameState.ball.radius * 2) { gameState.isDragging = true; gameState.dragStart.x = mouseX; gameState.dragStart.y = mouseY; gameState.dragEnd.x = mouseX; gameState.dragEnd.y = mouseY; } }); canvas.addEventListener('mousemove', (e) => { if (!gameState.isDragging) return; const rect = canvas.getBoundingClientRect(); gameState.dragEnd.x = e.clientX - rect.left; gameState.dragEnd.y = e.clientY - rect.top; // Calculate power const dx = gameState.dragStart.x - gameState.dragEnd.x; const dy = gameState.dragStart.y - gameState.dragEnd.y; const distance = Math.sqrt(dx * dx + dy * dy); gameState.power = Math.min(distance / 5, MAX_POWER); updateUI(); }); canvas.addEventListener('mouseup', (e) => { if (!gameState.isDragging) return; gameState.isDragging = false; if (gameState.power > 5) { gameState.strokes++; const dx = gameState.dragStart.x - gameState.dragEnd.x; const dy = gameState.dragStart.y - gameState.dragEnd.y; gameState.ball.vx = dx * POWER_MULTIPLIER; gameState.ball.vy = dy * POWER_MULTIPLIER; gameState.ball.isMoving = true; } gameState.power = 0; updateUI(); }); canvas.addEventListener('mouseleave', () => { if (gameState.isDragging) { gameState.isDragging = false; gameState.power = 0; updateUI(); } }); // Touch events for mobile canvas.addEventListener('touchstart', (e) => { e.preventDefault(); const rect = canvas.getBoundingClientRect(); const touch = e.touches[0]; const mouseX = touch.clientX - rect.left; const mouseY = touch.clientY - rect.top; const dx = mouseX - gameState.ball.x; const dy = mouseY - gameState.ball.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < gameState.ball.radius * 2) { gameState.isDragging = true; gameState.dragStart.x = mouseX; gameState.dragStart.y = mouseY; gameState.dragEnd.x = mouseX; gameState.dragEnd.y = mouseY; } }); canvas.addEventListener('touchmove', (e) => { e.preventDefault(); if (!gameState.isDragging) return; const rect = canvas.getBoundingClientRect(); const touch = e.touches[0]; gameState.dragEnd.x = touch.clientX - rect.left; gameState.dragEnd.y = touch.clientY - rect.top; // Calculate power const dx = gameState.dragStart.x - gameState.dragEnd.x; const dy = gameState.dragStart.y - gameState.dragEnd.y; const distance = Math.sqrt(dx * dx + dy * dy); gameState.power = Math.min(distance / 5, MAX_POWER); updateUI(); }); canvas.addEventListener('touchend', (e) => { e.preventDefault(); if (!gameState.isDragging) return; gameState.isDragging = false; if (gameState.power > 5) { gameState.strokes++; const dx = gameState.dragStart.x - gameState.dragEnd.x; const dy = gameState.dragStart.y - gameState.dragEnd.y; gameState.ball.vx = dx * POWER_MULTIPLIER; gameState.ball.vy = dy * POWER_MULTIPLIER; gameState.ball.isMoving = true; } gameState.power = 0; updateUI(); }); // Button event listeners document.getElementById('reset-btn').addEventListener('click', () => { initLevel(); }); document.getElementById('level-btn').addEventListener('click', () => { gameState.level++; initLevel(); }); document.getElementById('next-level').addEventListener('click', () => { gameState.level++; initLevel(); }); // Initialize game window.addEventListener('load', () => { resizeCanvas(); window.addEventListener('resize', resizeCanvas); gameLoop(); }); </script> </body> </html> ``` ## Features of this Mini-Golf Game Engine 1. **Realistic Physics Simulation**: - Ball movement with velocity and acceleration - Friction to gradually slow the ball - Elastic collisions with obstacles and walls - Momentum preservation 2. **Collision Detection**: - Ball collision with rectangular obstacles - Ball collision with circular obstacles - Boundary collision with canvas edges - Hole detection with realistic "falling in" mechanics 3. **Game Mechanics**: - Drag-and-release control system for hitting the ball - Power meter visualization - Stroke counting - Hole-in-one detection 4. **Level System**: - Three progressively challenging levels - Unique obstacle layouts for each level - Reset functionality 5. **Visual Design**: - Attractive mini-golf course with grass texture - Color-coded obstacles and game elements - Responsive design that works on all devices - Visual feedback for power and drag direction 6. **User Interface**: - Clean, intuitive controls - Real-time stroke and power indicators - Victory screen with stroke count - Instructions for new players The game is ready to play - simply drag and release to hit the ball toward the hole while navigating around obstacles!
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Mini-Golf Core Engine Demo</title> <style> html,body{margin:0;padding:0;background:#222;color:#eee;font-family:sans-serif;user-select:none} #ui{position:absolute;top:10px;left:10px;z-index:10} canvas{background:#3a9c4f;display:block;margin:0 auto} button{padding:6px 12px;font-size:14px} </style> </head> <body> <div id="ui"> <button id="resetBtn">Reset (R)</button> </div> <canvas id="gameCanvas" width="900" height="600"></canvas> <script> /* ========= Utility (2-D vector) ========= */ class Vec{ constructor(x=0,y=0){this.x=x;this.y=y;} clone(){return new Vec(this.x,this.y);} add(v){this.x+=v.x;this.y+=v.y;return this;} sub(v){this.x-=v.x;this.y-=v.y;return this;} scale(s){this.x*=s;this.y*=s;return this;} len(){return Math.hypot(this.x,this.y);} norm(){const l=this.len()||1;return this.scale(1/l);} dot(v){return this.x*v.x+this.y*v.y;} static add(a,b){return new Vec(a.x+b.x,a.y+b.y);} static sub(a,b){return new Vec(a.x-b.x,a.y-b.y);} static scale(v,s){return new Vec(v.x*s,v.y*s);} static proj(v,n){ // projection of v onto n (assuming n normalized) return Vec.scale(n, v.dot(n)); } } /* ========= Core Entities ========= */ class Ball{ constructor(x,y){ this.pos=new Vec(x,y); this.vel=new Vec(0,0); this.radius=10; this.inHole=false; } update(dt){ // dt seconds if(this.inHole) return; const friction=280; // px/s² const speed=this.vel.len(); if(speed>0){ // Apply friction deceleration const decel=friction*dt; const newSpeed=Math.max(0,speed-decel); this.vel=this.vel.scale(newSpeed/speed); // Integrate position this.pos.add(Vec.scale(this.vel,dt)); } } } class Segment{ // one-way wall: ball is kept on the side pointed by normal constructor(x1,y1,x2,y2){ this.p1=new Vec(x1,y1); this.p2=new Vec(x2,y2); const dx=y1-y2, dy=x2-x1; // rotate by +90° const n=new Vec(dx,dy); this.normal=n.norm(); } distancePoint(p){ // signed distance from line (positive = allowed side) const v=Vec.sub(p,this.p1); return v.dot(this.normal); } } class CircleObstacle{ constructor(x,y,r){this.pos=new Vec(x,y);this.radius=r;} } class Hole{ constructor(x,y,r=14){ this.pos=new Vec(x,y); this.radius=r; } } /* ========= Course Definition ========= */ class Course{ constructor(canvas){ this.ctx=canvas.getContext('2d'); this.w=canvas.width; this.h=canvas.height; this.ball=new Ball(120,300); this.hole=new Hole(780,300,14); // Walls (clockwise) this.segments=[ new Segment(40,40, 860,40), new Segment(860,40, 860,560), new Segment(860,560, 40,560), new Segment(40,560, 40,40), // internal bumper new Segment(450,150, 700,150), new Segment(700,150, 700,200), new Segment(700,200, 450,200) ]; this.obstacles=[ new CircleObstacle(300,250,25), new CircleObstacle(650,400,30) ]; } reset(){ this.ball=new Ball(120,300); this.ball.inHole=false; } /* ---- Physics Step ---- */ step(dt){ const ball=this.ball; ball.update(dt); /* Ball-Line collisions */ for(const seg of this.segments){ const dist=seg.distancePoint(ball.pos); if(dist<ball.radius){ // penetration // Push out ball.pos.add(Vec.scale(seg.normal, ball.radius-dist)); // Reflect velocity (elastic) const vn=Vec.proj(ball.vel,seg.normal); // normal component ball.vel.sub(Vec.scale(vn,2)); // invert normal component } } /* Ball-Circle collisions */ for(const obs of this.obstacles){ const delta=Vec.sub(ball.pos, obs.pos); const dist=delta.len(); const overlap=ball.radius+obs.radius - dist; if(overlap>0){ const n=delta.scale(1/(dist||1)); // collision normal ball.pos.add(Vec.scale(n,overlap)); // push out const vn=Vec.proj(ball.vel,n); ball.vel.sub(Vec.scale(vn,2)); // reflect } } /* Hole-in-one */ if(!ball.inHole){ const toHole=Vec.sub(ball.pos,this.hole.pos); if(toHole.len() < this.hole.radius-2 && ball.vel.len()<30){ ball.inHole=true; ball.pos=this.hole.pos.clone(); ball.vel=new Vec(0,0); setTimeout(()=>alert("Hole-in-one!"),150); } } } /* ---- Rendering ---- */ draw(){ const g=this.ctx, {w,h}=this; g.clearRect(0,0,w,h); // Hole g.fillStyle="#000"; g.beginPath(); g.arc(this.hole.pos.x,this.hole.pos.y,this.hole.radius,0,Math.PI*2); g.fill(); // Obstacles g.fillStyle="#654321"; for(const c of this.obstacles){ g.beginPath(); g.arc(c.pos.x,c.pos.y,c.radius,0,Math.PI*2); g.fill(); } // Walls g.strokeStyle="#fff"; g.lineWidth=4; g.beginPath(); for(const s of this.segments){ g.moveTo(s.p1.x,s.p1.y); g.lineTo(s.p2.x,s.p2.y); } g.stroke(); // Ball const ball=this.ball; g.fillStyle=ball.inHole ? "#ccc" : "#ffeb3b"; g.beginPath(); g.arc(ball.pos.x,ball.pos.y,ball.radius,0,Math.PI*2); g.fill(); } } /* ========= Input + Main Loop ========= */ const canvas=document.getElementById("gameCanvas"); const course=new Course(canvas); let lastTime=performance.now(); function animate(t){ const dt=Math.min(0.033,(t-lastTime)/1000); // cap dt to 33ms lastTime=t; course.step(dt); course.draw(); requestAnimationFrame(animate); } requestAnimationFrame(animate); /* --- Mouse to strike the ball --- */ let aiming=false, startMouse=null; canvas.addEventListener("mousedown",e=>{ if(course.ball.inHole) return; const rect=canvas.getBoundingClientRect(); const mx=e.clientX-rect.left, my=e.clientY-rect.top; const inside=Math.hypot(mx-course.ball.pos.x,my-course.ball.pos.y)<=course.ball.radius; if(inside && course.ball.vel.len()<1){ aiming=true; startMouse=new Vec(mx,my); } }); canvas.addEventListener("mousemove",e=>{ if(!aiming) return; const rect=canvas.getBoundingClientRect(); const mx=e.clientX-rect.left, my=e.clientY-rect.top; // draw aiming line course.draw(); const g=course.ctx; g.strokeStyle="#ff5722"; g.lineWidth=2; g.beginPath(); g.moveTo(course.ball.pos.x,course.ball.pos.y); g.lineTo(mx,my); g.stroke(); }); canvas.addEventListener("mouseup",e=>{ if(!aiming) return; aiming=false; const rect=canvas.getBoundingClientRect(); const mx=e.clientX-rect.left, my=e.clientY-rect.top; const dir=new Vec(startMouse.x-mx,startMouse.y-my); // drag opposite const power=Math.min( dir.len(), 200 ); const speed=power*4; // conversion factor if(speed>5){ course.ball.vel=dir.norm().scale(speed); } }); document.getElementById("resetBtn").onclick= ()=>course.reset(); window.addEventListener("keydown",e=>{if(e.key.toLowerCase()==="r")course.reset();}); </script> </body> </html> ```
Here's a complete HTML file with a mini-golf game engine using JavaScript and Canvas for 2D rendering. It includes ball physics, collision detection with obstacles, and hole-in-one logic. Everything is contained in a single code block with no external dependencies. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mini Golf Game Engine</title> <style> body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #333; } canvas { border: 2px solid #fff; background-color: #228B22; } #instructions { position: absolute; top: 10px; left: 10px; color: white; font-family: Arial, sans-serif; background: rgba(0, 0, 0, 0.7); padding: 10px; } </style> </head> <body> <div id="instructions"> Click and drag to set power and direction.<br> Release to hit the ball.<br> Get the ball into the hole! </div> <canvas id="gameCanvas" width="800" height="600"></canvas> <script> const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); // Game objects const ball = { x: 100, y: 100, radius: 10, velocityX: 0, velocityY: 0, mass: 1, color: '#ffffff' }; const hole = { x: 700, y: 500, radius: 15, color: '#000000' }; const obstacles = [ { x: 400, y: 300, width: 50, height: 200, color: '#808080' }, { x: 200, y: 450, width: 200, height: 50, color: '#808080' } ]; // Physics constants const friction = 0.98; const bounceEnergy = 0.7; const minSpeed = 0.1; // Game state let isCharging = false; let chargeStartX = 0; let chargeStartY = 0; let gameWon = false; // Event listeners for mouse input canvas.addEventListener('mousedown', startCharging); canvas.addEventListener('mousemove', updateCharging); canvas.addEventListener('mouseup', releaseBall); function startCharging(e) { if (gameWon) return; const rect = canvas.getBoundingClientRect(); chargeStartX = e.clientX - rect.left; chargeStartY = e.clientY - rect.top; isCharging = true; } function updateCharging(e) { if (!isCharging) return; const rect = canvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; draw(); // Redraw canvas drawPowerIndicator(mouseX, mouseY); } function releaseBall(e) { if (!isCharging || gameWon) return; isCharging = false; const rect = canvas.getBoundingClientRect(); const releaseX = e.clientX - rect.left; const releaseY = e.clientY - rect.top; // Calculate velocity based on drag distance const dx = chargeStartX - releaseX; const dy = chargeStartY - releaseY; ball.velocityX = dx * 0.1; ball.velocityY = dy * 0.1; } function drawPowerIndicator(mouseX, mouseY) { ctx.beginPath(); ctx.moveTo(chargeStartX, chargeStartY); ctx.lineTo(mouseX, mouseY); ctx.strokeStyle = 'rgba(255, 255, 0, 0.7)'; ctx.lineWidth = 3; ctx.stroke(); } function update() { if (gameWon) return; // Apply friction ball.velocityX *= friction; ball.velocityY *= friction; // Stop ball if speed is too low if (Math.abs(ball.velocityX) < minSpeed) ball.velocityX = 0; if (Math.abs(ball.velocityY) < minSpeed) ball.velocityY = 0; // Update position let newX = ball.x + ball.velocityX; let newY = ball.y + ball.velocityY; // Check collisions with canvas boundaries if (newX - ball.radius < 0) { newX = ball.radius; ball.velocityX *= -bounceEnergy; } if (newX + ball.radius > canvas.width) { newX = canvas.width - ball.radius; ball.velocityX *= -bounceEnergy; } if (newY - ball.radius < 0) { newY = ball.radius; ball.velocityY *= -bounceEnergy; } if (newY + ball.radius > canvas.height) { newY = canvas.height - ball.radius; ball.velocityY *= -bounceEnergy; } // Check collisions with obstacles obstacles.forEach(obstacle => { const closestX = Math.max(obstacle.x, Math.min(newX, obstacle.x + obstacle.width)); const closestY = Math.max(obstacle.y, Math.min(newY, obstacle.y + obstacle.height)); const distanceX = newX - closestX; const distanceY = newY - closestY; const distanceSquared = distanceX * distanceX + distanceY * distanceY; if (distanceSquared < ball.radius * ball.radius) { // Collision detected, push ball out and reflect velocity const distance = Math.sqrt(distanceSquared); const nx = distanceX / distance; const ny = distanceY / distance; newX = closestX + nx * ball.radius; newY = closestY + ny * ball.radius; const dot = ball.velocityX * nx + ball.velocityY * ny; ball.velocityX = (ball.velocityX - 2 * dot * nx) * bounceEnergy; ball.velocityY = (ball.velocityY - 2 * dot * ny) * bounceEnergy; } }); ball.x = newX; ball.y = newY; // Check if ball is in hole const dx = ball.x - hole.x; const dy = ball.y - hole.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < hole.radius) { ball.velocityX = 0; ball.velocityY = 0; gameWon = true; } } function draw() { // Clear canvas ctx.fillStyle = '#228B22'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw obstacles obstacles.forEach(obstacle => { ctx.fillStyle = obstacle.color; ctx.fillRect(obstacle.x, obstacle.y, obstacle.width, obstacle.height); }); // Draw hole ctx.beginPath(); ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2); ctx.fillStyle = hole.color; ctx.fill(); // Draw ball ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); ctx.fillStyle = ball.color; ctx.fill(); // Display win message if (gameWon) { ctx.fillStyle = 'white'; ctx.font = '48px Arial'; ctx.textAlign = 'center'; ctx.fillText('Hole in One!', canvas.width / 2, canvas.height / 2); } } function gameLoop() { update(); draw(); requestAnimationFrame(gameLoop); } // Start the game loop gameLoop(); </script> </body> </html> ``` This mini-golf game engine includes the following features: - **Ball Physics**: The ball moves based on player input with realistic friction and bounce mechanics when hitting walls or obstacles. Velocity is dampened over time to simulate rolling resistance. - **Collision Detection**: The engine checks for collisions with the canvas boundaries and rectangular obstacles, adjusting the ball's position and velocity to simulate bounces. - **Hole-in-One Logic**: The game detects when the ball enters the hole (based on distance) and stops the game with a victory message. - **Input System**: Players click and drag to charge a shot, with the direction and power determined by the drag distance and direction. A visual line shows the power and direction while charging. - **Rendering**: Uses HTML5 Canvas to draw the course, ball, hole, and obstacles in a 2D top-down view. To play, open this HTML file in a browser. Click and drag near the ball to set the direction and power of your shot, then release to hit the ball. The goal is to get the ball into the black hole while avoiding gray obstacles. The game runs entirely in the browser with no external dependencies.
This is a complete, self-contained mini-golf game engine in a single HTML file. It handles physics, collision detection, and win conditions. You can aim and shoot by clicking and dragging the mouse away from the ball. ### Features: * **Physics-Based Movement:** The ball moves with velocity and slows down due to friction. * **Mouse-Controlled Shooting:** Click and drag to aim and set the power of your shot. A line indicates the direction and strength. * **Wall Collisions:** The ball correctly bounces off the edges of the course. * **Obstacle Collisions:** The ball realistically collides with rectangular obstacles. * **Hole-in-One Logic:** If the ball approaches the hole with low enough speed, it will fall in, ending the game. * **Responsive Canvas:** The game area adapts to the browser window size. * **UI Display:** Shows the current stroke count and game status messages. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mini-Golf Game Engine</title> <style> body { margin: 0; padding: 0; background-color: #1a1a1a; color: #ffffff; font-family: 'Arial', sans-serif; display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100vh; overflow: hidden; text-align: center; } h1 { margin-bottom: 10px; font-weight: 300; } #gameCanvas { background-color: #2a6a3d; /* Golf green color */ border: 4px solid #a0522d; /* Wooden-like border */ box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); } #status { margin-top: 15px; font-size: 1.5em; height: 40px; /* Reserve space to prevent layout shift */ } </style> </head> <body> <h1>Mini-Golf Engine</h1> <canvas id="gameCanvas"></canvas> <div id="status">Strokes: 0 - Aim and shoot!</div> <script> const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const statusDisplay = document.getElementById('status'); // --- GAME CONFIGURATION --- const FRICTION = 0.985; // How much the ball slows down each frame const MIN_VELOCITY = 0.05; // Velocity below which the ball is considered stopped const MAX_POWER = 60; // Maximum shot power const HOLE_IN_SPEED_THRESHOLD = 2.5; // Max speed to fall into the hole // --- GAME STATE --- let ball = { x: 0, y: 0, radius: 10, vx: 0, vy: 0, color: '#ffffff' }; let hole = { x: 0, y: 0, radius: 14 }; // Obstacles are defined by {x, y, width, height} let obstacles = []; let isShooting = false; let isBallMoving = false; let gameOver = false; let strokes = 0; let mouse = { down: false, start: { x: 0, y: 0 }, end: { x: 0, y: 0 } }; // --- SETUP & INITIALIZATION --- function setCanvasSize() { const size = Math.min(window.innerWidth * 0.9, window.innerHeight * 0.7); canvas.width = size; canvas.height = size / 1.5; // Aspect ratio initializeCourse(); } function initializeCourse() { // Reset ball position ball.x = canvas.width * 0.1; ball.y = canvas.height / 2; ball.vx = 0; ball.vy = 0; // Place hole hole.x = canvas.width * 0.9; hole.y = canvas.height / 2; // Define obstacle layout relative to canvas size obstacles = [ { x: canvas.width * 0.45, y: canvas.height * 0.1, width: 25, height: canvas.height * 0.35 }, { x: canvas.width * 0.45, y: canvas.height * 0.9 - canvas.height * 0.35, width: 25, height: canvas.height * 0.35 }, { x: canvas.width * 0.7, y: canvas.height * 0.5 - 12.5, width: 25, height: 25 } ]; gameOver = false; strokes = 0; isBallMoving = false; updateStatus("Strokes: 0 - Aim and shoot!"); } function resetGame() { setCanvasSize(); } // --- GAME LOOP --- function gameLoop() { // 1. Update game state (physics) if (isBallMoving) { updatePhysics(); } // 2. Draw everything draw(); // 3. Request next frame requestAnimationFrame(gameLoop); } // --- PHYSICS ENGINE --- function updatePhysics() { // Apply friction ball.vx *= FRICTION; ball.vy *= FRICTION; // Update position ball.x += ball.vx; ball.y += ball.vy; // Check for collisions checkWallCollisions(); checkObstacleCollisions(); checkHoleLogic(); // Check if ball has stopped const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); if (speed < MIN_VELOCITY) { ball.vx = 0; ball.vy = 0; isBallMoving = false; if (!gameOver) { updateStatus(`Strokes: ${strokes} - Ready to shoot.`); } } } function checkWallCollisions() { // Left/Right walls if (ball.x + ball.radius > canvas.width) { ball.x = canvas.width - ball.radius; ball.vx *= -1; } else if (ball.x - ball.radius < 0) { ball.x = ball.radius; ball.vx *= -1; } // Top/Bottom walls if (ball.y + ball.radius > canvas.height) { ball.y = canvas.height - ball.radius; ball.vy *= -1; } else if (ball.y - ball.radius < 0) { ball.y = ball.radius; ball.vy *= -1; } } function checkObstacleCollisions() { obstacles.forEach(obstacle => { // Find the closest point on the rectangle to the circle's center const closestX = Math.max(obstacle.x, Math.min(ball.x, obstacle.x + obstacle.width)); const closestY = Math.max(obstacle.y, Math.min(ball.y, obstacle.y + obstacle.height)); // Calculate the distance between the circle's center and this closest point const dx = ball.x - closestX; const dy = ball.y - closestY; const distance = Math.sqrt(dx * dx + dy * dy); // If the distance is less than the circle's radius, there's a collision if (distance < ball.radius) { // Collision occurred. To prevent sticking, slightly move the ball out. const overlap = ball.radius - distance; const normX = dx / distance; const normY = dy / distance; ball.x += normX * overlap; ball.y += normY * overlap; // Determine bounce direction - a simple but effective approximation const dot = (ball.vx * normX + ball.vy * normY) * 2; ball.vx -= dot * normX; ball.vy -= dot * normY; } }); } function checkHoleLogic() { if (gameOver) return; const dx = ball.x - hole.x; const dy = ball.y - hole.y; const distance = Math.sqrt(dx * dx + dy * dy); const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); if (distance < hole.radius) { if (speed < HOLE_IN_SPEED_THRESHOLD) { // Hole in one! ball.vx = 0; ball.vy = 0; ball.x = hole.x; ball.y = hole.y; isBallMoving = false; gameOver = true; updateStatus(`HOLE IN ONE! Total Strokes: ${strokes}. Click to play again.`); } else { // Lip out - simulate the ball being pushed out of the hole's gravity const angle = Math.atan2(dy, dx); ball.vx += Math.cos(angle) * 0.2; ball.vy += Math.sin(angle) * 0.2; } } } // --- DRAWING --- function draw() { // Clear canvas and draw background ctx.fillStyle = '#2a6a3d'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw Hole ctx.beginPath(); ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2); ctx.fillStyle = '#111111'; ctx.fill(); ctx.closePath(); // Draw Obstacles ctx.fillStyle = '#a0522d'; // Brown for obstacles obstacles.forEach(obs => { ctx.fillRect(obs.x, obs.y, obs.width, obs.height); }); // Draw Ball ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); ctx.fillStyle = ball.color; ctx.fill(); ctx.closePath(); // Draw aiming line if shooting if (isShooting) { ctx.beginPath(); ctx.moveTo(ball.x, ball.y); ctx.lineTo(mouse.end.x, mouse.end.y); ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; ctx.lineWidth = 2; ctx.stroke(); ctx.closePath(); } } // --- UI & INPUT --- function updateStatus(message) { statusDisplay.textContent = message; } function getMousePos(evt) { const rect = canvas.getBoundingClientRect(); return { x: evt.clientX - rect.left, y: evt.clientY - rect.top }; } canvas.addEventListener('mousedown', (e) => { if (gameOver) { resetGame(); return; } if (!isBallMoving) { isShooting = true; mouse.start = getMousePos(e); mouse.end = getMousePos(e); } }); canvas.addEventListener('mousemove', (e) => { if (isShooting) { mouse.end = getMousePos(e); } }); canvas.addEventListener('mouseup', (e) => { if (isShooting) { isShooting = false; const dx = mouse.end.x - mouse.start.x; const dy = mouse.end.y - mouse.start.y; let power = Math.sqrt(dx*dx + dy*dy) * 0.2; // Scaling factor for power if (power > MAX_POWER) power = MAX_POWER; const angle = Math.atan2(dy, dx); // We shoot from the ball, away from the mouse drag direction ball.vx = -Math.cos(angle) * power; ball.vy = -Math.sin(angle) * power; isBallMoving = true; strokes++; updateStatus(`Strokes: ${strokes} - In motion...`); } }); window.addEventListener('resize', setCanvasSize); // --- START THE GAME --- setCanvasSize(); gameLoop(); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mini Golf Game Engine</title> <style> body { margin: 0; overflow: hidden; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #222; font-family: Arial, sans-serif; } #gameContainer { position: relative; width: 800px; height: 600px; border: 2px solid #444; background-color: #3a8c3a; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); } #gameCanvas { display: block; } #ui { position: absolute; top: 10px; left: 10px; color: white; font-size: 16px; pointer-events: none; } #resetButton { position: absolute; bottom: 10px; right: 10px; padding: 10px 15px; background-color: #4CAF50; color: white; border: none; border-radius: 5px; cursor: pointer; } #resetButton:hover { background-color: #45a049; } </style> </head> <body> <div id="gameContainer"> <canvas id="gameCanvas" width="800" height="600"></canvas> <div id="ui">Click and drag to aim, release to shoot. Shot: <span id="shotCount">0</span></div> <button id="resetButton">Reset Ball</button> </div> <script> // Game Engine const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const shotCountElement = document.getElementById('shotCount'); const resetButton = document.getElementById('resetButton'); // Game state let shotCount = 0; let isDragging = false; let startMousePos = { x: 0, y: 0 }; let ball = { x: 100, y: 300, radius: 10, velocity: { x: 0, y: 0 }, friction: 0.98, // Slight friction to slow the ball stoppedThreshold: 0.1 // Velocity threshold to consider ball stopped }; // Course elements const hole = { x: 700, y: 300, radius: 15, active: true }; // Obstacles (rectangles) const obstacles = [ { x: 300, y: 150, width: 200, height: 20, color: '#555' }, { x: 300, y: 430, width: 200, height: 20, color: '#555' }, { x: 200, y: 200, width: 20, height: 200, color: '#555' }, { x: 580, y: 200, width: 20, height: 200, color: '#555' } ]; // Wind (optional, for added complexity) const wind = { x: 0.02, y: 0 }; // Draw functions function drawBall() { // Ball shadow ctx.beginPath(); ctx.arc(ball.x + 2, ball.y + 2, ball.radius, 0, Math.PI * 2); ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; ctx.fill(); // Ball ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); ctx.fillStyle = 'white'; ctx.fill(); ctx.strokeStyle = '#aaa'; ctx.lineWidth = 1; ctx.stroke(); } function drawHole() { // Hole outer rim ctx.beginPath(); ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2); ctx.fillStyle = '#222'; ctx.fill(); // Hole inner ctx.beginPath(); ctx.arc(hole.x, hole.y, hole.radius - 3, 0, Math.PI * 2); ctx.fillStyle = 'black'; ctx.fill(); } function drawObstacles() { obstacles.forEach(obs => { ctx.fillStyle = obs.color; ctx.fillRect(obs.x, obs.y, obs.width, obs.height); // Add a simple 3D effect ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; ctx.fillRect(obs.x, obs.y, obs.width, 5); ctx.fillRect(obs.x, obs.y, 5, obs.height); }); } function drawAimLine() { if (!isDragging || !ball.stopped) return; const endX = startMousePos.x; const endY = startMousePos.y; const dx = endX - ball.x; const dy = endY - ball.y; // Draw dotted line ctx.setLineDash([10, 10]); ctx.beginPath(); ctx.moveTo(ball.x, ball.y); ctx.lineTo(endX, endY); ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; ctx.lineWidth = 2; ctx.stroke(); ctx.setLineDash([]); // Draw power indicator const power = Math.min(Math.sqrt(dx * dx + dy * dy) / 10, 50); const indicatorLength = power * 3; ctx.beginPath(); ctx.moveTo(ball.x, ball.y); ctx.lineTo(ball.x - dx * 0.8, ball.y - dy * 0.8); ctx.strokeStyle = `rgba(255, 255, 0, 0.7)`; ctx.lineWidth = 4; ctx.stroke(); // Power text ctx.fillStyle = 'white'; ctx.font = '16px Arial'; ctx.fillText(`Power: ${Math.round(power)}`, 10, 50); } function render() { // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw grass pattern (simple effect) ctx.fillStyle = '#3a8c3a'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Add some grass texture for (let i = 0; i < 2000; i++) { const x = Math.random() * canvas.width; const y = Math.random() * canvas.height; ctx.fillStyle = Math.random() > 0.5 ? '#4a9c4a' : '#3a8c3a'; ctx.fillRect(x, y, 1, 1); } // Draw course elements drawObstacles(); drawHole(); drawBall(); drawAimLine(); // Check if ball is in hole if (hole.active && isBallInHole()) { ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; ctx.font = '40px Arial'; ctx.textAlign = 'center'; ctx.fillText('HOLE IN ONE!', canvas.width / 2, canvas.height / 2); ctx.font = '24px Arial'; ctx.fillText(`Completed in ${shotCount} shot(s)`, canvas.width / 2, canvas.height / 2 + 40); } } // Physics and collision functions function updateBall() { if (hole.active && isBallInHole()) { // Ball is in hole, stop it ball.velocity.x = 0; ball.velocity.y = 0; return; } // Apply wind ball.velocity.x += wind.x; ball.velocity.y += wind.y; // Update position ball.x += ball.velocity.x; ball.y += ball.velocity.y; // Apply friction ball.velocity.x *= ball.friction; ball.velocity.y *= ball.friction; // Check boundary collisions // Left wall if (ball.x - ball.radius < 0) { ball.x = ball.radius; ball.velocity.x *= -0.7; // Bounce with energy loss } // Right wall if (ball.x + ball.radius > canvas.width) { ball.x = canvas.width - ball.radius; ball.velocity.x *= -0.7; } // Top wall if (ball.y - ball.radius < 0) { ball.y = ball.radius; ball.velocity.y *= -0.7; } // Bottom wall if (ball.y + ball.radius > canvas.height) { ball.y = canvas.height - ball.radius; ball.velocity.y *= -0.7; } // Check obstacle collisions obstacles.forEach(obstacle => { handleObstacleCollision(ball, obstacle); }); // Check if ball has essentially stopped const speed = Math.sqrt(ball.velocity.x * ball.velocity.x + ball.velocity.y * ball.velocity.y); if (speed < ball.stoppedThreshold) { ball.velocity.x = 0; ball.velocity.y = 0; } } function handleObstacleCollision(ball, obstacle) { // Find the closest point on the rectangle to the ball center const closestX = Math.max(obstacle.x, Math.min(ball.x, obstacle.x + obstacle.width)); const closestY = Math.max(obstacle.y, Math.min(ball.y, obstacle.y + obstacle.height)); // Calculate the distance between the ball center and this closest point const dx = ball.x - closestX; const dy = ball.y - closestY; const distance = Math.sqrt(dx * dx + dy * dy); // If the distance is less than the ball radius, a collision has occurred if (distance < ball.radius) { // Push the ball out of the obstacle if (distance > 0) { const overlap = ball.radius - distance; const nx = dx / distance; const ny = dy / distance; ball.x += nx * overlap; ball.y += ny * overlap; // Reflect velocity (with some energy loss) const dotProduct = ball.velocity.x * nx + ball.velocity.y * ny; ball.velocity.x = (ball.velocity.x - 2 * dotProduct * nx) * 0.7; ball.velocity.y = (ball.velocity.y - 2 * dotProduct * ny) * 0.7; } else { // Direct overlap, push in a reasonable direction if (ball.x < obstacle.x) ball.x = obstacle.x - ball.radius; else if (ball.x > obstacle.x + obstacle.width) ball.x = obstacle.x + obstacle.width + ball.radius; if (ball.y < obstacle.y) ball.y = obstacle.y - ball.radius; else if (ball.y > obstacle.y + obstacle.height) ball.y = obstacle.y + obstacle.height + ball.radius; // Reverse velocity components ball.velocity.x *= -0.7; ball.velocity.y *= -0.7; } } } function isBallInHole() { const dx = ball.x - hole.x; const dy = ball.y - hole.y; const distance = Math.sqrt(dx * dx + dy * dy); // Ball must be close to the hole and moving slowly to count return distance < hole.radius - 3 && Math.abs(ball.velocity.x) < ball.stoppedThreshold && Math.abs(ball.velocity.y) < ball.stoppedThreshold; } function shootBall(powerX, powerY) { if (hole.active && isBallInHole()) return; // Already won // Apply power to ball (reverse the drag direction for shooting) ball.velocity.x = -powerX * 0.2; ball.velocity.y = -powerY * 0.2; // Limit maximum velocity const maxVelocity = 15; const currentVelocity = Math.sqrt(ball.velocity.x * ball.velocity.x + ball.velocity.y * ball.velocity.y); if (currentVelocity > maxVelocity) { ball.velocity.x = (ball.velocity.x / currentVelocity) * maxVelocity; ball.velocity.y = (ball.velocity.y / currentVelocity) * maxVelocity; } shotCount++; shotCountElement.textContent = shotCount; } // Game loop function gameLoop() { updateBall(); render(); requestAnimationFrame(gameLoop); } // Input handlers canvas.addEventListener('mousedown', (e) => { const rect = canvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; // Only allow aiming when the ball is stopped if (Math.abs(ball.velocity.x) < ball.stoppedThreshold && Math.abs(ball.velocity.y) < ball.stoppedThreshold && !isBallInHole()) { isDragging = true; startMousePos = { x: mouseX, y: mouseY }; } }); canvas.addEventListener('mousemove', (e) => { if (isDragging) { const rect = canvas.getBoundingClientRect(); startMousePos = { x: e.clientX - rect.left, y: e.clientY - rect.top }; } }); canvas.addEventListener('mouseup', (e) => { if (isDragging) { isDragging = false; const rect = canvas.getBoundingClientRect(); const endMouseX = e.clientX - rect.left; const endMouseY = e.clientY - rect.top; // Calculate the drag vector const powerX = endMouseX - ball.x; const powerY = endMouseY - ball.y; shootBall(powerX, powerY); } }); canvas.addEventListener('mouseleave', () => { isDragging = false; }); resetButton.addEventListener('click', () => { ball.x = 100; ball.y = 300; ball.velocity.x = 0; ball.velocity.y = 0; hole.active = true; shotCount = 0; shotCountElement.textContent = shotCount; }); // Initialize game function init() { gameLoop(); } init(); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Mini-Golf Core Engine Demo</title> <style> body { background: #3ab27d; margin: 0; overflow: hidden; } canvas { display: block; margin: 40px auto 0 auto; background: #e4f2cf; border: 2px solid #450d1f; box-shadow: 0 2px 10px #8884; border-radius: 16px; } #hud { text-align: center; font-family: 'Segoe UI',sans-serif; color: #222; font-size: 1.2em; margin-top: 8px; user-select: none; } #hud strong { color: #035c30; } </style> </head> <body> <canvas id="golf" width="800" height="500"></canvas> <div id="hud"> <span id="info"></span> </div> <script> // --- Constants --- const FRICTION = 0.984; const BALL_RADIUS = 12; const BALL_COLOR = '#fff'; const BALL_SHADOW_COLOR = '#bbb'; const MAX_SHOT_POWER = 16; const POWER_BAR_WIDTH = 180; const POWER_BAR_HEIGHT = 16; const OBSTACLE_COLOR = "#417c54"; const HOLE_RADIUS = 18; const HOLE_COLOR = "#262724"; const COURSE_MARGIN = 40; // --- Engine Classes and Structures --- // Ball class Ball { constructor(x, y) { this.init_x = x; this.init_y = y; this.reset(); } reset() { this.x = this.init_x; this.y = this.init_y; this.vx = 0; this.vy = 0; this.moving = false; this.inHole = false; } update() { if (this.moving && !this.inHole) { this.x += this.vx; this.y += this.vy; this.vx *= FRICTION; this.vy *= FRICTION; if (Math.hypot(this.vx, this.vy) < 0.16) { this.vx = this.vy = 0; this.moving = false; } } } shoot(angle, power) { this.vx = Math.cos(angle) * power; this.vy = Math.sin(angle) * power; this.moving = true; } collideWithRect(rect) { // Axis-aligned rectangle-ball collision and resolution // Returns true if collision occurred let collided = false; let nearestX = Math.max(rect.x, Math.min(this.x, rect.x + rect.w)); let nearestY = Math.max(rect.y, Math.min(this.y, rect.y + rect.h)); let dist = Math.hypot(this.x - nearestX, this.y - nearestY); if (dist < BALL_RADIUS) { collided = true; // Compute direction const dx = (this.x - nearestX) / (dist || 1); const dy = (this.y - nearestY) / (dist || 1); // Push out this.x = nearestX + dx * BALL_RADIUS; this.y = nearestY + dy * BALL_RADIUS; // Reflect velocity, lose a bit to simulate bounce if (Math.abs(nearestX - rect.x) < 0.01 || Math.abs(nearestX - (rect.x + rect.w)) < 0.01) this.vx = -this.vx * 0.7; if (Math.abs(nearestY - rect.y) < 0.01 || Math.abs(nearestY - (rect.y + rect.h)) < 0.01) this.vy = -this.vy * 0.7; } return collided; } collideWithWalls(width, height) { // Course boundaries let tolerance = 1; if (this.x - BALL_RADIUS < COURSE_MARGIN) { this.x = COURSE_MARGIN + BALL_RADIUS + tolerance; this.vx = -this.vx * 0.75; } if (this.y - BALL_RADIUS < COURSE_MARGIN) { this.y = COURSE_MARGIN + BALL_RADIUS + tolerance; this.vy = -this.vy * 0.75; } if (this.x + BALL_RADIUS > width - COURSE_MARGIN) { this.x = width - COURSE_MARGIN - BALL_RADIUS - tolerance; this.vx = -this.vx * 0.75; } if (this.y + BALL_RADIUS > height - COURSE_MARGIN) { this.y = height - COURSE_MARGIN - BALL_RADIUS - tolerance; this.vy = -this.vy * 0.75; } } checkInHole(hole) { // Ball-in-hole (with some leeway) const d = Math.hypot(this.x - hole.x, this.y - hole.y); if (!this.inHole && d < hole.r - 4) { this.inHole = true; this.moving = false; this.x = hole.x; this.y = hole.y; } return this.inHole; } } // Simple rectangular obstacle class Obstacle { constructor(x, y, w, h) { this.x = x; this.y = y; this.w = w; this.h = h; } } // Hole class Hole { constructor(x, y, r) { this.x = x; this.y = y; this.r = r; } } // --- Main Game Logic --- const canvas = document.getElementById('golf'); const ctx = canvas.getContext('2d'); const info = document.getElementById('info'); const WIDTH = canvas.width; const HEIGHT = canvas.height; // Course description const ball = new Ball(COURSE_MARGIN + 60, HEIGHT / 2); const hole = new Hole(WIDTH - COURSE_MARGIN - 60, HEIGHT / 2, HOLE_RADIUS); // Obstacles - simple demo course (could be made fancier) const obstacles = [ new Obstacle(WIDTH/2 - 28, HEIGHT/2 - 80, 56, 160), // Center block new Obstacle(WIDTH/2 - 160, COURSE_MARGIN + 60, 320, 32), // Top bar new Obstacle(WIDTH/2 - 160, HEIGHT - COURSE_MARGIN - 92, 320, 32), // Bottom bar ]; // Input state let aiming = false; let aimStart = {x:0, y:0}; let aimCurrent = {x:0, y:0}; let shotCount = 0; let gameOver = false; // --- Rendering --- function drawCourse() { // Draw course boundary ctx.save(); ctx.strokeStyle = "#313718"; ctx.lineWidth = 6; ctx.lineJoin = "round"; ctx.strokeRect(COURSE_MARGIN, COURSE_MARGIN, WIDTH - 2*COURSE_MARGIN, HEIGHT - 2*COURSE_MARGIN); ctx.restore(); } function drawObstacles() { for (let obs of obstacles) { ctx.save(); ctx.fillStyle = OBSTACLE_COLOR; ctx.strokeStyle = "#18301a"; ctx.lineWidth = 3; ctx.beginPath(); ctx.rect(obs.x, obs.y, obs.w, obs.h); ctx.fill(); ctx.stroke(); ctx.restore(); } } function drawHole() { ctx.save(); ctx.beginPath(); ctx.arc(hole.x, hole.y, hole.r, 0, Math.PI * 2); ctx.fillStyle = HOLE_COLOR; ctx.shadowColor = "black"; ctx.shadowBlur = 8; ctx.fill(); ctx.restore(); } function drawBall() { ctx.save(); // Shadow ctx.beginPath(); ctx.arc(ball.x+2, ball.y+4, BALL_RADIUS, 0, Math.PI*2); ctx.fillStyle = BALL_SHADOW_COLOR; ctx.globalAlpha = 0.28; ctx.fill(); ctx.globalAlpha = 1.0; // Ball ctx.beginPath(); ctx.arc(ball.x, ball.y, BALL_RADIUS, 0, Math.PI*2); ctx.fillStyle = BALL_COLOR; ctx.shadowColor = "#333"; ctx.shadowBlur = 4; ctx.fill(); // Ball border ctx.lineWidth = 2.4; ctx.strokeStyle = "#aaaaad"; ctx.stroke(); ctx.restore(); } function drawAimLine() { if (aiming && !ball.moving && !ball.inHole && !gameOver) { ctx.save(); const dx = aimCurrent.x - ball.x; const dy = aimCurrent.y - ball.y; const dist = Math.min(Math.hypot(dx, dy), MAX_SHOT_POWER * 14); const ang = Math.atan2(dy, dx); ctx.strokeStyle = "#1a4c30"; ctx.setLineDash([8,6]); ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(ball.x, ball.y); ctx.lineTo(ball.x + Math.cos(ang) * dist, ball.y + Math.sin(ang) * dist); ctx.stroke(); ctx.setLineDash([]); // Power bar ctx.save(); ctx.translate(ball.x + 24, ball.y - 42); ctx.fillStyle = "#e8eac0"; ctx.strokeStyle = "#aaa"; ctx.lineWidth = 1.6; ctx.roundRect(0,0,POWER_BAR_WIDTH,POWER_BAR_HEIGHT, POWER_BAR_HEIGHT/2); ctx.fill(); ctx.stroke(); const pct = dist / (MAX_SHOT_POWER*14); ctx.fillStyle = "#1ba668"; ctx.fillRect(3,3,(POWER_BAR_WIDTH-6)*pct,POWER_BAR_HEIGHT-6); ctx.restore(); ctx.restore(); } } function render() { ctx.clearRect(0,0,WIDTH,HEIGHT); drawCourse(); drawObstacles(); drawHole(); drawBall(); drawAimLine(); } // --- Physics/Engine Loop --- function step() { if (ball.moving) { ball.update(); // Collide with obstacles for (let obs of obstacles) { ball.collideWithRect(obs); } // Collide with walls ball.collideWithWalls(WIDTH, HEIGHT); // Check hole if (ball.checkInHole(hole)) { setTimeout(()=>{ gameOver = true; }, 550); } } } // --- UI & Input --- function getMousePos(evt) { let rect = canvas.getBoundingClientRect(); return { x: (evt.clientX - rect.left) * (canvas.width / rect.width), y: (evt.clientY - rect.top) * (canvas.height / rect.height) }; } canvas.addEventListener('mousedown', function(evt) { if (gameOver || ball.moving || ball.inHole) return; let pos = getMousePos(evt); let d = Math.hypot(pos.x - ball.x, pos.y - ball.y); if (d <= BALL_RADIUS + 15) { aiming = true; aimStart = {x: pos.x, y: pos.y}; aimCurrent = {x: pos.x, y: pos.y}; } }); canvas.addEventListener('mousemove', function(evt) { if (aiming) { let pos = getMousePos(evt); aimCurrent = pos; } }); canvas.addEventListener('mouseup', function(evt) { if (aiming) { let dx = aimCurrent.x - ball.x; let dy = aimCurrent.y - ball.y; let angle = Math.atan2(dy, dx); let dist = Math.min(Math.hypot(dx, dy), MAX_SHOT_POWER * 14); let power = dist/14; if (power > 2) { // minimal threshold to avoid accidental taps ball.shoot(angle, power); shotCount += 1; } aiming = false; } }); // Touch controls for mobile canvas.addEventListener('touchstart', function(evt) { if (gameOver || ball.moving || ball.inHole) return; let t = evt.touches[0]; let pos = getMousePos(t); let d = Math.hypot(pos.x - ball.x, pos.y - ball.y); if (d <= BALL_RADIUS + 18) { aiming = true; aimStart = {x: pos.x, y: pos.y}; aimCurrent = {x: pos.x, y: pos.y}; evt.preventDefault(); } }, {passive:false}); canvas.addEventListener('touchmove', function(evt) { if (aiming) { let t = evt.touches[0]; aimCurrent = getMousePos(t); evt.preventDefault(); } }, {passive:false}); canvas.addEventListener('touchend', function(evt) { if (aiming) { let dx = aimCurrent.x - ball.x; let dy = aimCurrent.y - ball.y; let angle = Math.atan2(dy, dx); let dist = Math.min(Math.hypot(dx, dy), MAX_SHOT_POWER * 14); let power = dist/14; if (power > 2) { ball.shoot(angle, power); shotCount += 1; } aiming = false; evt.preventDefault(); } }, {passive:false}); // Spacebar to restart after win window.addEventListener("keydown", function(e) { if (e.key === " " && gameOver) { ball.reset(); shotCount = 0; gameOver = false; info.innerHTML = "Shot <strong>1</strong> - Aim: drag from the ball"; } }); // --- Animation Loop --- function loop() { step(); render(); // HUD info if (gameOver && ball.inHole) { info.innerHTML = `🎉 <strong>Hole in ${shotCount}!</strong> Press <b>[space]</b> to restart.`; } else if (!ball.moving && !gameOver) { info.innerHTML = `Shot <strong>${shotCount+1}</strong> - Aim: drag from the ball`; } else if (ball.moving) { info.innerHTML = "Rolling..."; } requestAnimationFrame(loop); } // --- Start up --- info.innerHTML = "Shot <strong>1</strong> - Aim: drag from the ball"; render(); loop(); </script> </body> </html> ```
<!DOCTYPE html> <html> <head> <title>Mini-Golf Engine</title> <style> body { display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f0f0f0; } canvas { border: 1px solid #333; } </style> </head> <body> <canvas id="gameCanvas" width="800" height="600"></canvas> <script> const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); // Game Objects const ball = { x: canvas.width / 2, y: canvas.height / 2, radius: 10, vx: 0, vy: 0 }; const hole = { x: 700, y: 100, radius: 20 }; const obstacles = [ { x: 100, y: 100, width: 150, height: 20 }, { x: 300, y: 200, width: 20, height: 150 }, { x: 500, y: 400, width: 50, height: 50 } ]; // Physics Parameters const FRICTION = 0.96; const MIN_SPEED = 1e-3; function update() { // Apply friction ball.vx *= FRICTION; ball.vy *= FRICTION; // Check velocity threshold for stopping const speed = Math.sqrt(ball.vx ** 2 + ball.vy ** 2); if (speed < MIN_SPEED) { ball.vx = ball.vy = 0; } else { ball.x += ball.vx; ball.y += ball.vy; } checkCollisions(); checkHole(); } function checkCollisions() { // Canvas edges if (ball.x <= ball.radius) { ball.x = ball.radius; ball.vx *= -1; } else if (ball.x >= canvas.width - ball.radius) { ball.x = canvas.width - ball.radius; ball.vx *= -1; } if (ball.y <= ball.radius) { ball.y = ball.radius; ball.vy *= -1; } else if (ball.y >= canvas.height - ball.radius) { ball.y = canvas.height - ball.radius; ball.vy *= -1; } // Obstacle collisions for (const obs of obstacles) { // Find closest point on obstacle to ball center let closestX = Math.max(obs.x, Math.min(ball.x, obs.x + obs.width)); let closestY = Math.max(obs.y, Math.min(ball.y, obs.y + obs.height)); const dx = ball.x - closestX; const dy = ball.y - closestY; const distance = Math.hypot(dx, dy); if (distance < ball.radius) { // Collision normal const nx = dx / distance; const ny = dy / distance; // Reflect velocity const dot = ball.vx * nx + ball.vy * ny; ball.vx -= 2 * dot * nx; ball.vy -= 2 * dot * ny; // Push ball out of obstacle ball.x += nx * (ball.radius - distance); ball.y += ny * (ball.radius - distance); } } } function checkHole() { const dx = ball.x - hole.x; const dy = ball.y - hole.y; const distance = Math.hypot(dx, dy); if (distance <= hole.radius) { // Stop ball and log success ball.vx = ball.vy = 0; console.log("Hole-in-One!"); } } function draw() { // Clear ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw hole ctx.beginPath(); ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2); ctx.strokeStyle = "#000"; ctx.lineWidth = 2; ctx.stroke(); // Draw obstacles ctx.fillStyle = "#888"; for (const obs of obstacles) { ctx.fillRect(obs.x, obs.y, obs.width, obs.height); } // Draw ball ctx.fillStyle = "#3366cc"; ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); ctx.fill(); } // Mouse control canvas.addEventListener('click', (e) => { const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; // Hit ball if stopped if (ball.vx === 0 && ball.vy === 0) { const dx = mx - ball.x; const dy = my - ball.y; const dist = Math.hypot(dx, dy); if (dist > 0) { const power = 8; ball.vx = (dx / dist) * power; ball.vy = (dy / dist) * power; } } }); // Game loop function gameLoop() { update(); draw(); requestAnimationFrame(gameLoop); } gameLoop(); </script> </body> </html>
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Mini-Golf Physics Engine</title> <style> body { background:#111; color:#eee; font-family: Arial, sans-serif; margin:0; display:flex; flex-direction:column; align-items:center; } canvas { border:1px solid #555; background:#063; } #controls { margin: 8px 0; } button, input { font-size:1rem; padding:4px 8px; cursor:pointer; } </style> </head> <body> <canvas id="course" width="800" height="500"></canvas> <div id="controls"> <button id="resetBtn">Reset Course</button> <label>Stroke <span id="strokeSpan">0</span></label> </div> <script> /* ************************************************** MINI-GOLF CORE ENGINE ************************************************** */ const canvas = document.getElementById('course'); const ctx = canvas.getContext('2d'); const strokeSpan = document.getElementById('strokeSpan'); const resetBtn = document.getElementById('resetBtn'); // Physical constants const Settings = { Friction: 0.985, // per frame Drag: 0.99, // non-ground hits MinVelocity: 0.1, HoleRadius: 20, BallRadius: 6, MaxLaunchSpeed: 15 }; // --------------------------------------------------- // Game Objects // --------------------------------------------------- class Ball { constructor(x,y) { this.x = x; this.y = y; this.vx = 0; this.vy = 0; this.isRolling = true; } speed() { return Math.hypot(this.vx, this.vy); } } class Hole { constructor(x,y){ this.x = x; this.y = y; } } class Rectangle { constructor(x,y,w,h){ this.x=x; this.y=y; this.w=w; this.h=h; } } // --------------------------------------------------- // Course Data // --------------------------------------------------- let ball, hole; let obstacles = []; // list of Rectangles / Circles function resetCourse() { strokeCount = 0; strokeSpan.textContent = "0"; ball = new Ball(60, canvas.height-60); hole = new Hole(canvas.width-70,70); obstacles = []; // Simple demo course obstacles.push(new Rectangle(200,120,20,200)); // vertical post obstacles.push(new Rectangle(350,260,150,20)); // horizontal bar obstacles.push(new Rectangle(canvas.width-180,canvas.height-200,15,120)); } // --------------------------------------------------- // Physics // --------------------------------------------------- function distance(a,b) { return Math.hypot(a.x-b.x,a.y-b.y); } function clamp(v,min,max){ return Math.max(min, Math.min(v,max)); } // Axis-aligned Rectangle collision and bounce function collideBallRect(ball, rect) { const closestX = clamp(ball.x, rect.x, rect.x+rect.w); const closestY = clamp(ball.y, rect.y, rect.y+rect.h); const dx = ball.x - closestX; const dy = ball.y - closestY; const d = Math.hypot(dx,dy); if (d < Settings.BallRadius) { // push away const push = Settings.BallRadius - d; const nx = dx / d || 0; const ny = dy / d || 1; ball.x += push * nx; ball.y += push * ny; // reflect velocity const dot = ball.vx*nx + ball.vy*ny; ball.vx -= 2*dot*nx; ball.vy -= 2*dot*ny; ball.vx *= Settings.Drag; ball.vy *= Settings.Drag; } } // --------------------------------------------------- // Main Update Loop // --------------------------------------------------- let strokeCount = 0; let isPlaying = true; function update() { if (!ball || !hole) return; // move ball ball.x += ball.vx; ball.y += ball.vy; // friction ball.vx *= Settings.Friction; ball.vy *= Settings.Friction; // stop tiny motion if (ball.speed() < Settings.MinVelocity) ball.vx = ball.vy = 0; // collision vs bounds if (ball.x-Settings.BallRadius < 0 || ball.x+Settings.BallRadius > canvas.width) { ball.vx = -ball.vx * Settings.Drag; ball.x = clamp(ball.x, Settings.BallRadius, canvas.width-Settings.BallRadius); } if (ball.y-Settings.BallRadius < 0 || ball.y+Settings.BallRadius > canvas.height) { ball.vy = -ball.vy * Settings.Drag; ball.y = clamp(ball.y, Settings.BallRadius, canvas.height-Settings.BallRadius); } // collision vs obstacles obstacles.forEach(obs => { collideBallRect(ball, obs); }); // hole-in-one if (distance(ball, hole) < Settings.HoleRadius - Settings.BallRadius/2 && ball.speed() < 2) { // Congrats! ball.x = hole.x; ball.y = hole.y; ball.vx = 0; ball.vy = 0; isPlaying = false; setTimeout(() => alert(`Hole in ${strokeCount+1}!`),10); } } // --------------------------------------------------- // Rendering // --------------------------------------------------- function draw() { ctx.clearRect(0,0,canvas.width,canvas.height); // draw hole ctx.beginPath(); ctx.arc(hole.x, hole.y, Settings.HoleRadius, 0, Math.PI*2); ctx.fillStyle="#000"; ctx.fill(); // draw ball ctx.beginPath(); ctx.arc(ball.x, ball.y, Settings.BallRadius, 0, Math.PI*2); ctx.fillStyle="#fff"; ctx.fill(); // draw obstacles ctx.fillStyle="#755"; obstacles.forEach(r => { ctx.fillRect(r.x,r.y,r.w,r.h); }); } function loop() { update(); draw(); if (isPlaying) requestAnimationFrame(loop); } // --------------------------------------------------- // Interaction // --------------------------------------------------- canvas.addEventListener('mousedown', (e) => { if (!isPlaying) return; if (ball.speed() > Settings.MinVelocity) return; // ball still rolling const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; // aim vector const dx = mx - ball.x; const dy = my - ball.y; const d = Math.hypot(dx,dy); if (d === 0) return; const power = clamp(d,0,150) / 150; // 0..1 const push = power * Settings.MaxLaunchSpeed; ball.vx = (dx/d)*push; ball.vy = (dy/d)*push; strokeCount++; strokeSpan.textContent = strokeCount; if (!isPlaying) isPlaying=true; requestAnimationFrame(loop); }); resetBtn.addEventListener('click', () => { isPlaying=true; resetCourse(); requestAnimationFrame(loop); }); // --------------------------------------------------- init resetCourse(); loop(); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Mini-Golf Game Engine</title> <style> body { margin: 0; overflow: hidden; } canvas { display: block; background: #228B22; } /* Green for golf course */ </style> </head> <body> <canvas id="gameCanvas"></canvas> <script> const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); // Responsive canvas function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } resizeCanvas(); window.addEventListener('resize', resizeCanvas); // Game constants const BALL_RADIUS = 10; const HOLE_RADIUS = 15; const FRICTION = 0.98; const MIN_SPEED = 0.1; // Ball let ball = { x: 50, y: 50, vx: 0, vy: 0, radius: BALL_RADIUS }; // Hole const hole = { x: window.innerWidth - 50, y: window.innerHeight - 50, radius: HOLE_RADIUS }; // Obstacles (rectangles: [x, y, width, height]) const obstacles = [ [window.innerWidth / 2 - 50, window.innerHeight / 2 - 50, 100, 100] // Square in middle ]; // Walls (boundaries) const walls = { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight }; // Input handling let isAiming = false; let aimStartX, aimStartY; canvas.addEventListener('mousedown', (e) => { if (Math.hypot(ball.vx, ball.vy) < MIN_SPEED) { // Only aim if ball is stopped isAiming = true; aimStartX = e.clientX; aimStartY = e.clientY; } }); canvas.addEventListener('mouseup', (e) => { if (isAiming && Math.hypot(ball.vx, ball.vy) < MIN_SPEED) { isAiming = false; const dx = aimStartX - e.clientX; const dy = aimStartY - e.clientY; const power = Math.hypot(dx, dy) / 10; // Scale power const angle = Math.atan2(dy, dx); ball.vx = power * Math.cos(angle); ball.vy = power * Math.sin(angle); } }); // Collision detection with walls function handleWallCollisions() { if (ball.x - ball.radius < walls.left) { ball.x = walls.left + ball.radius; ball.vx = -ball.vx; } if (ball.x + ball.radius > walls.right) { ball.x = walls.right - ball.radius; ball.vx = -ball.vx; } if (ball.y - ball.radius < walls.top) { ball.y = walls.top + ball.radius; ball.vy = -ball.vy; } if (ball.y + ball.radius > walls.bottom) { ball.y = walls.bottom - ball.radius; ball.vy = -ball.vy; } } // Collision detection with rectangular obstacles (simple bounce) function handleObstacleCollisions() { obstacles.forEach(obs => { const [ox, oy, ow, oh] = obs; // Check if ball overlaps with obstacle const closestX = Math.max(ox, Math.min(ball.x, ox + ow)); const closestY = Math.max(oy, Math.min(ball.y, oy + oh)); const dx = ball.x - closestX; const dy = ball.y - closestY; if ((dx * dx + dy * dy) < (ball.radius * ball.radius)) { // Collision detected, simple bounce based on side if (Math.abs(dx) > Math.abs(dy)) { ball.vx = -ball.vx; ball.x += dx > 0 ? 1 : -1; // Nudge out } else { ball.vy = -ball.vy; ball.y += dy > 0 ? 1 : -1; // Nudge out } } }); } // Check if ball is in hole function checkHole() { const dist = Math.hypot(ball.x - hole.x, ball.y - hole.y); if (dist < hole.radius && Math.hypot(ball.vx, ball.vy) < MIN_SPEED * 2) { alert('Hole in one! Resetting ball.'); ball.x = 50; ball.y = 50; ball.vx = 0; ball.vy = 0; } } // Update physics function update() { ball.x += ball.vx; ball.y += ball.vy; // Apply friction ball.vx *= FRICTION; ball.vy *= FRICTION; // Stop if slow if (Math.hypot(ball.vx, ball.vy) < MIN_SPEED) { ball.vx = 0; ball.vy = 0; } handleWallCollisions(); handleObstacleCollisions(); checkHole(); } // Render function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw obstacles ctx.fillStyle = '#8B4513'; // Brown for obstacles obstacles.forEach(obs => { ctx.fillRect(...obs); }); // Draw hole ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2); ctx.fill(); // Draw ball ctx.fillStyle = '#FFF'; ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); ctx.fill(); // Draw aiming line if aiming if (isAiming) { ctx.strokeStyle = '#F00'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(ball.x, ball.y); ctx.lineTo(aimStartX, aimStartY); ctx.stroke(); } } // Game loop function gameLoop() { update(); render(); requestAnimationFrame(gameLoop); } // Adjust positions on resize window.addEventListener('resize', () => { walls.right = canvas.width; walls.bottom = canvas.height; hole.x = canvas.width - 50; hole.y = canvas.height - 50; obstacles[0][0] = canvas.width / 2 - 50; obstacles[0][1] = canvas.height / 2 - 50; // Keep ball within new bounds if resized ball.x = Math.min(ball.x, canvas.width - ball.radius); ball.y = Math.min(ball.y, canvas.height - ball.radius); }); gameLoop(); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Mini-Golf Core Engine</title> <style> canvas { border: 1px solid #333; display: block; margin: 20px auto; background-color: #e0f7fa; } #status { text-align: center; font-family: sans-serif; font-size: 1.2em; } </style> </head> <body> <canvas id="gameCanvas" width="600" height="400"></canvas> <div id="status"></div> <script> // Canvas setup const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); // Game objects const obstacles = [ // Rectangular obstacle: {x, y, width, height} {x: 200, y: 150, width: 100, height: 20}, {x: 400, y: 300, width: 150, height: 20}, {x: 100, y: 250, width: 20, height: 80}, ]; const hole = {x: 550, y: 50, radius: 15}; const ball = { x: 50, y: 350, radius: 8, vx: 0, vy: 0, isMoving: false }; const friction = 0.98; // Simulate friction const minSpeed = 0.1; // Minimal speed threshold to consider ball stopped const statusDiv = document.getElementById('status'); // Utility functions function distance(x1, y1, x2, y2) { return Math.hypot(x2 - x1, y2 - y1); } function lineCircleCollision(p1, p2, circle) { // Check if line segment p1p2 intersects circle const {x: cx, y: cy, radius: r} = circle; const {x: x1, y: y1} = p1; const {x: x2, y: y2} = p2; // Vector from p1 to p2 const dx = x2 - x1; const dy = y2 - y1; // Vector from p1 to circle center const fx = cx - x1; const fy = cy - y1; const t = (fx*dx + fy*dy) / (dx*dx + dy*dy); const tClamped = Math.max(0, Math.min(1, t)); const closestX = x1 + tClamped * dx; const closestY = y1 + tClamped * dy; const dist = distance(closestX, closestY, cx, cy); return dist <= r; } function detectObstacleCollision(prevPos, newPos) { // Check each obstacle for collision for (const obs of obstacles) { // We'll check if movement from prevPos to newPos intersects the obstacle // Simplify by checking line from prevPos to newPos if (lineRectangleCollision(prevPos, newPos, obs)) { return obs; } } return null; } function lineRectangleCollision(p1, p2, rect) { // Check collision between line segment p1p2 and rectangle rect const rectLines = [ // Top edge {p1: {x: rect.x, y: rect.y}, p2: {x: rect.x + rect.width, y: rect.y}}, // Bottom edge {p1: {x: rect.x, y: rect.y + rect.height}, p2: {x: rect.x + rect.width, y: rect.y + rect.height}}, // Left edge {p1: {x: rect.x, y: rect.y}, p2: {x: rect.x, y: rect.y + rect.height}}, // Right edge {p1: {x: rect.x + rect.width, y: rect.y}, p2: {x: rect.x + rect.width, y: rect.y + rect.height}}, ]; for (const line of rectLines) { if (linesIntersect(p1, p2, line.p1, line.p2)) { return true; } } return false; } function linesIntersect(p1, p2, p3, p4) { // Check if line segments p1p2 and p3p4 intersect const det = (p2.x - p1.x)*(p4.y - p3.y) - (p2.y - p1.y)*(p4.x - p3.x); if (det === 0) { return false; // parallel lines } const lambda = ((p4.y - p3.y)*(p4.x - p1.x) + (p3.x - p4.x)*(p4.y - p1.y)) / det; const gamma = ((p1.y - p2.y)*(p4.x - p1.x) + (p2.x - p1.x)*(p4.y - p1.y)) / det; return (0 <= lambda && lambda <= 1) && (0 <= gamma && gamma <= 1); } function checkHole() { const distToHole = distance(ball.x, ball.y, hole.x, hole.y); if (distToHole <= hole.radius) { return true; } return false; } function resetBall() { ball.x = 50; ball.y = 350; ball.vx = 0; ball.vy = 0; ball.isMoving = false; statusDiv.textContent = ''; } // Handle user input - click to shoot canvas.addEventListener('click', (e) => { if (ball.isMoving) return; // ignore if ball is moving const rect = canvas.getBoundingClientRect(); const clickX = e.clientX - rect.left; const clickY = e.clientY - rect.top; // Calculate shot vector based on click - simple logic: vector from ball to click const dx = clickX - ball.x; const dy = clickY - ball.y; const powerFactor = 0.2; // adjust for strength ball.vx = dx * powerFactor; ball.vy = dy * powerFactor; ball.isMoving = true; }); // Main update loop function update() { if (ball.isMoving) { const prevPos = {x: ball.x, y: ball.y}; // Update position ball.x += ball.vx; ball.y += ball.vy; // Check collisions with course bounds if (ball.x - ball.radius < 0 || ball.x + ball.radius > canvas.width) { ball.vx = -ball.vx; ball.x = Math.max(ball.radius, Math.min(canvas.width - ball.radius, ball.x)); } if (ball.y - ball.radius < 0 || ball.y + ball.radius > canvas.height) { ball.vy = -ball.vy; ball.y = Math.max(ball.radius, Math.min(canvas.height - ball.radius, ball.y)); } // Check collision with obstacles const obstacle = detectObstacleCollision(prevPos, {x: ball.x, y: ball.y}); if (obstacle) { // Simple collision response: reflect velocity // Determine side of collision (rough approximation) if (obstacle.x <= ball.x && ball.x <= obstacle.x + obstacle.width) { // collision with top or bottom ball.vy = -ball.vy; } else { // collision with sides ball.vx = -ball.vx; } // Move ball out of obstacle to avoid sticking // For simplicity, just invert velocity } // Apply friction ball.vx *= friction; ball.vy *= friction; // Stop if velocity is below threshold if (Math.hypot(ball.vx, ball.vy) < minSpeed) { ball.vx = 0; ball.vy = 0; ball.isMoving = false; // Check for hole-in-one if (checkHole()) { statusDiv.textContent = 'Hole in One! Congratulations!'; } } } draw(); requestAnimationFrame(update); } function draw() { // Clear ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw obstacle ctx.fillStyle = '#555'; for (const obs of obstacles) { ctx.fillRect(obs.x, obs.y, obs.width, obs.height); } // Draw hole ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2); ctx.fill(); // Draw ball ctx.fillStyle = '#ff5722'; ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); ctx.fill(); } // Start animation update(); // Optional: reset ball on double click for testing canvas.addEventListener('dblclick', () => { resetBall(); statusDiv.textContent = ''; }); </script> </body> </html> ```
Here's a complete mini-golf game engine with basic physics, collision detection, and hole mechanics. This implementation uses pure JavaScript with Canvas for rendering: ```html <!DOCTYPE html> <html> <head> <title>Mini Golf Engine</title> <style> body { margin: 0; overflow: hidden; } canvas { display: block; background-color: #7ec850; } #power { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); } </style> </head> <body> <canvas id="gameCanvas"></canvas> <div id="power"> <input type="range" id="powerSlider" min="1" max="100" value="50"> <button id="shootBtn">Shoot</button> <span id="strokes">Strokes: 0</span> </div> <script> // Game constants const FRICTION = 0.98; const MIN_VELOCITY = 0.1; const HOLE_RADIUS = 15; const BALL_RADIUS = 10; // Game state const gameState = { ball: { x: 100, y: 100, vx: 0, vy: 0 }, hole: { x: 700, y: 400 }, obstacles: [ { x: 300, y: 200, width: 200, height: 20, angle: 45 }, { x: 500, y: 300, width: 150, height: 20, angle: -30 }, { x: 200, y: 400, width: 100, height: 100, type: 'circle' } ], isMoving: false, strokes: 0 }; // Canvas setup const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; // UI elements const powerSlider = document.getElementById('powerSlider'); const shootBtn = document.getElementById('shootBtn'); const strokesDisplay = document.getElementById('strokes'); // Initialize game function initGame() { // Set hole position (center for demo) gameState.hole.x = canvas.width * 0.8; gameState.hole.y = canvas.height * 0.5; // Set ball position (start position) gameState.ball.x = canvas.width * 0.2; gameState.ball.y = canvas.height * 0.5; gameState.strokes = 0; updateStrokesDisplay(); draw(); } // Main game loop function gameLoop() { update(); draw(); if (gameState.isMoving) { requestAnimationFrame(gameLoop); } } // Update game state function update() { const ball = gameState.ball; // Apply friction ball.vx *= FRICTION; ball.vy *= FRICTION; // Check if ball has stopped if (Math.abs(ball.vx) < MIN_VELOCITY && Math.abs(ball.vy) < MIN_VELOCITY) { ball.vx = 0; ball.vy = 0; gameState.isMoving = false; return; } // Update position ball.x += ball.vx; ball.y += ball.vy; // Check wall collisions if (ball.x - BALL_RADIUS < 0 || ball.x + BALL_RADIUS > canvas.width) { ball.vx *= -0.8; // Bounce with some energy loss ball.x = Math.max(BALL_RADIUS, Math.min(canvas.width - BALL_RADIUS, ball.x)); } if (ball.y - BALL_RADIUS < 0 || ball.y + BALL_RADIUS > canvas.height) { ball.vy *= -0.8; ball.y = Math.max(BALL_RADIUS, Math.min(canvas.height - BALL_RADIUS, ball.y)); } // Check obstacle collisions gameState.obstacles.forEach(obstacle => { if (obstacle.type === 'circle') { handleCircleObstacleCollision(ball, obstacle); } else { handleRectObstacleCollision(ball, obstacle); } }); // Check hole collision const distToHole = Math.sqrt( Math.pow(ball.x - gameState.hole.x, 2) + Math.pow(ball.y - gameState.hole.y, 2) ); if (distToHole < HOLE_RADIUS) { alert(`Hole in one! Strokes: ${gameState.strokes}`); initGame(); } } // Handle collision with rectangular obstacle function handleRectObstacleCollision(ball, obstacle) { // Rotate coordinate system to match obstacle angle const angleRad = obstacle.angle * Math.PI / 180; const cos = Math.cos(-angleRad); const sin = Math.sin(-angleRad); // Rotate ball position relative to obstacle center const rx = ball.x - obstacle.x; const ry = ball.y - obstacle.y; const rotatedX = rx * cos - ry * sin; const rotatedY = rx * sin + ry * cos; // Rotated obstacle dimensions (axis-aligned in rotated space) const halfWidth = obstacle.width / 2; const halfHeight = obstacle.height / 2; // Check collision in rotated space if (Math.abs(rotatedX) < halfWidth + BALL_RADIUS && Math.abs(rotatedY) < halfHeight + BALL_RADIUS) { // Find closest point on obstacle const closestX = Math.max(-halfWidth, Math.min(halfWidth, rotatedX)); const closestY = Math.max(-halfHeight, Math.min(halfHeight, rotatedY)); const distance = Math.sqrt( Math.pow(rotatedX - closestX, 2) + Math.pow(rotatedY - closestY, 2) ); if (distance < BALL_RADIUS) { // Collision response - reflect velocity const nx = (rotatedX - closestX) / distance; const ny = (rotatedY - closestY) / distance; // Rotate normal back to world space const worldNx = nx * cos - ny * sin; const worldNy = nx * sin + ny * cos; // Reflect velocity with some energy loss const dotProduct = ball.vx * worldNx + ball.vy * worldNy; ball.vx = (ball.vx - 1.8 * dotProduct * worldNx) * 0.9; ball.vy = (ball.vy - 1.8 * dotProduct * worldNy) * 0.9; // Move ball out of collision const overlap = BALL_RADIUS - distance; ball.x += overlap * worldNx; ball.y += overlap * worldNy; } } } // Handle collision with circular obstacle function handleCircleObstacleCollision(ball, obstacle) { const dx = ball.x - obstacle.x; const dy = ball.y - obstacle.y; const distance = Math.sqrt(dx * dx + dy * dy); const radiusSum = obstacle.width / 2 + BALL_RADIUS; if (distance < radiusSum) { // Normal vector const nx = dx / distance; const ny = dy / distance; // Reflect velocity with some energy loss const dotProduct = ball.vx * nx + ball.vy * ny; ball.vx = (ball.vx - 1.8 * dotProduct * nx) * 0.9; ball.vy = (ball.vy - 1.8 * dotProduct * ny) * 0.9; // Move ball out of collision const overlap = radiusSum - distance; ball.x += overlap * nx; ball.y += overlap * ny; } } // Draw game function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw hole ctx.beginPath(); ctx.arc(gameState.hole.x, gameState.hole.y, HOLE_RADIUS, 0, Math.PI * 2); ctx.fillStyle = 'black'; ctx.fill(); // Draw obstacles gameState.obstacles.forEach(obstacle => { if (obstacle.type === 'circle') { ctx.beginPath(); ctx.arc(obstacle.x, obstacle.y, obstacle.width / 2, 0, Math.PI * 2); ctx.fillStyle = '#8B4513'; ctx.fill(); } else { ctx.save(); ctx.translate(obstacle.x, obstacle.y); ctx.rotate(obstacle.angle * Math.PI / 180); ctx.fillStyle = '#8B4513'; ctx.fillRect(-obstacle.width / 2, -obstacle.height / 2, obstacle.width, obstacle.height); ctx.restore(); } }); // Draw ball ctx.beginPath(); ctx.arc(gameState.ball.x, gameState.ball.y, BALL_RADIUS, 0, Math.PI * 2); ctx.fillStyle = 'white'; ctx.fill(); ctx.strokeStyle = 'black'; ctx.stroke(); // Draw direction line when aiming if (!gameState.isMoving) { const power = powerSlider.value / 10; const angle = Math.atan2( gameState.ball.y - gameState.hole.y, gameState.ball.x - gameState.hole.x ); ctx.beginPath(); ctx.moveTo(gameState.ball.x, gameState.ball.y); ctx.lineTo( gameState.ball.x - Math.cos(angle) * power * 5, gameState.ball.y - Math.sin(angle) * power * 5 ); ctx.strokeStyle = 'red'; ctx.lineWidth = 2; ctx.stroke(); } } // Shoot the ball function shootBall() { if (gameState.isMoving) return; const power = powerSlider.value / 10; const angle = Math.atan2( gameState.ball.y - gameState.hole.y, gameState.ball.x - gameState.hole.x ); gameState.ball.vx = -Math.cos(angle) * power; gameState.ball.vy = -Math.sin(angle) * power; gameState.isMoving = true; gameState.strokes++; updateStrokesDisplay(); gameLoop(); } function updateStrokesDisplay() { strokesDisplay.textContent = `Strokes: ${gameState.strokes}`; } // Event listeners shootBtn.addEventListener('click', shootBall); window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; draw(); }); // Start game initGame(); </script> </body> </html> ``` This mini-golf engine includes: 1. **Ball Physics**: - Movement with velocity and friction - Realistic bouncing off walls and obstacles - Energy loss on collisions 2. **Collision Detection**: - Wall collisions (canvas boundaries) - Rectangular obstacles (with rotation support) - Circular obstacles - Precise collision response with proper bouncing 3. **Hole Mechanics**: - Detection when ball enters hole - Stroke counting - Game reset after hole-in-one 4. **User Controls**: - Power slider to adjust shot strength - Visual aiming guide - Stroke counter 5. **Rendering**: - Canvas-based rendering - Responsive design that adapts to window size The game automatically starts when loaded. Click the "Shoot" button to hit the ball toward the hole, adjusting power with the slider. The ball will bounce off obstacles and walls until it comes to rest or enters the hole.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Mini-Golf Engine</title> <style> html, body { margin: 0; padding: 0; overflow: hidden; background: #2a7a0a; } canvas { display: block; background: #3b9a0f; } #info { position: absolute; top: 10px; left: 10px; color: white; font-family: sans-serif; font-size: 16px; text-shadow: 1px 1px 2px black; } </style> </head> <body> <canvas id="game"></canvas> <div id="info">Click and drag to aim and shoot.</div> <script> // Get canvas and context const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d'); // Resize canvas to fill window function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } window.addEventListener('resize', resize); resize(); // Game objects const ball = { x: 100, y: canvas.height - 100, r: 10, vx: 0, vy: 0 }; const hole = { x: canvas.width - 150, y: 150, r: 15 }; const obstacles = [ { x: 200, y: 200, w: 300, h: 20 }, { x: 500, y: 350, w: 20, h: 200 }, { x: 150, y: 450, w: 400, h: 20 } ]; let aiming = false; let aimStart = {}, aimEnd = {}; let strokes = 0; let inHole = false; // Physics constants const friction = 0.99; const speedScale = 0.2; const minSpeed = 0.1; // Mouse events for aiming canvas.addEventListener('mousedown', (e) => { if (ball.vx === 0 && ball.vy === 0 && !inHole) { aiming = true; aimStart = { x: e.clientX, y: e.clientY }; } }); canvas.addEventListener('mousemove', (e) => { if (aiming) { aimEnd = { x: e.clientX, y: e.clientY }; } }); canvas.addEventListener('mouseup', () => { if (aiming) { aiming = false; // Set ball velocity opposite drag direction const dx = aimStart.x - aimEnd.x; const dy = aimStart.y - aimEnd.y; ball.vx = dx * speedScale; ball.vy = dy * speedScale; strokes++; } }); // Utility: clamp function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } // Collision detection ball-rectangle function collideRect(rect) { // Find closest point on rect to ball center const closestX = clamp(ball.x, rect.x, rect.x + rect.w); const closestY = clamp(ball.y, rect.y, rect.y + rect.h); const dx = ball.x - closestX; const dy = ball.y - closestY; if (dx*dx + dy*dy < ball.r * ball.r) { // Reflect velocity along normal const dist = Math.sqrt(dx*dx + dy*dy) || 1; const nx = dx / dist, ny = dy / dist; const dot = ball.vx * nx + ball.vy * ny; ball.vx -= 2 * dot * nx; ball.vy -= 2 * dot * ny; // Push ball out const overlap = ball.r - dist; ball.x += nx * overlap; ball.y += ny * overlap; } } function update() { if (!inHole) { // Move ball ball.x += ball.vx; ball.y += ball.vy; // Apply friction ball.vx *= friction; ball.vy *= friction; if (Math.hypot(ball.vx, ball.vy) < minSpeed) { ball.vx = 0; ball.vy = 0; } // Wall collisions if (ball.x - ball.r < 0) { ball.x = ball.r; ball.vx = -ball.vx; } if (ball.x + ball.r > canvas.width) { ball.x = canvas.width - ball.r; ball.vx = -ball.vx; } if (ball.y - ball.r < 0) { ball.y = ball.r; ball.vy = -ball.vy; } if (ball.y + ball.r > canvas.height) { ball.y = canvas.height - ball.r; ball.vy = -ball.vy; } // Obstacle collisions obstacles.forEach(collideRect); // Hole detection const dxh = ball.x - hole.x; const dyh = ball.y - hole.y; if (dxh*dxh + dyh*dyh < (hole.r - ball.r/2) ** 2) { inHole = true; ball.vx = ball.vy = 0; } } } function draw() { // Clear ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw hole ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(hole.x, hole.y, hole.r, 0, Math.PI*2); ctx.fill(); // Draw obstacles ctx.fillStyle = '#8B4513'; obstacles.forEach(r => ctx.fillRect(r.x, r.y, r.w, r.h)); // Draw ball ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI*2); ctx.fill(); // Draw aiming line if (aiming) { ctx.strokeStyle = 'rgba(255,255,255,0.7)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(aimStart.x, aimStart.y); ctx.lineTo(aimEnd.x, aimEnd.y); ctx.stroke(); } // Draw UI document.getElementById('info').innerText = inHole ? `Hole complete in ${strokes} stroke${strokes > 1 ? 's' : ''}${strokes===1?' (Hole-in-One!)':''}.` : `Strokes: ${strokes} | Click and drag to aim.`; } function loop() { update(); draw(); requestAnimationFrame(loop); } loop(); </script> </body> </html> ```
**Mini Golf Game Engine** ========================== ### Overview This code provides a basic mini-golf game engine that simulates the movement of a ball on a 2D plane. It includes ball physics, collision detection with obstacles, and hole-in-one logic. ### Code ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mini Golf Game Engine</title> <style> /* Add some basic styling to our canvas */ canvas { border: 1px solid black; } </style> </head> <body> <canvas id="canvas" width="800" height="600"></canvas> <script> // Get the canvas element const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // Define some constants const BALL_RADIUS = 10; const HOLE_RADIUS = 20; const OBSTACLE_COLOR = 'gray'; const BALL_COLOR = 'white'; const HOLE_COLOR = 'black'; // Define the ball object class Ball { constructor(x, y) { this.x = x; this.y = y; this.vx = 0; this.vy = 0; this.radius = BALL_RADIUS; } update() { // Update the ball's position based on its velocity this.x += this.vx; this.y += this.vy; // Apply friction to slow down the ball this.vx *= 0.99; this.vy *= 0.99; // Check if the ball has come to rest if (Math.abs(this.vx) < 0.1 && Math.abs(this.vy) < 0.1) { this.vx = 0; this.vy = 0; } } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI); ctx.fillStyle = BALL_COLOR; ctx.fill(); } } // Define the obstacle object class Obstacle { constructor(x, y, width, height) { this.x = x; this.y = y; this.width = width; this.height = height; } draw() { ctx.fillStyle = OBSTACLE_COLOR; ctx.fillRect(this.x, this.y, this.width, this.height); } // Check if the ball has collided with this obstacle collide(ball) { // Check if the ball's center is within the obstacle's bounds if (ball.x + ball.radius > this.x && ball.x - ball.radius < this.x + this.width && ball.y + ball.radius > this.y && ball.y - ball.radius < this.y + this.height) { // Calculate the normal vector of the collision let normalX, normalY; if (ball.x < this.x || ball.x > this.x + this.width) { normalX = ball.x < this.x ? -1 : 1; normalY = 0; } else { normalX = 0; normalY = ball.y < this.y ? -1 : 1; } // Reflect the ball's velocity around the normal vector const dotProduct = ball.vx * normalX + ball.vy * normalY; ball.vx -= 2 * dotProduct * normalX; ball.vy -= 2 * dotProduct * normalY; } } } // Define the hole object class Hole { constructor(x, y) { this.x = x; this.y = y; this.radius = HOLE_RADIUS; } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI); ctx.fillStyle = HOLE_COLOR; ctx.fill(); } // Check if the ball has gone into the hole check(ball) { const distance = Math.sqrt((ball.x - this.x) ** 2 + (ball.y - this.y) ** 2); return distance < this.radius + ball.radius; } } // Initialize the game objects const ball = new Ball(100, 100); const obstacles = [ new Obstacle(200, 200, 100, 50), new Obstacle(400, 300, 50, 100) ]; const hole = new Hole(700, 500); // Main game loop function update() { ctx.clearRect(0, 0, canvas.width, canvas.height); // Update and draw the ball ball.update(); ball.draw(); // Draw the obstacles and check for collisions obstacles.forEach(obstacle => { obstacle.draw(); obstacle.collide(ball); }); // Draw the hole and check if the ball has gone in hole.draw(); if (hole.check(ball)) { console.log('Hole in one!'); } requestAnimationFrame(update); } // Handle user input to strike the ball canvas.addEventListener('click', (e) => { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Calculate the direction vector from the ball to the click position const dx = x - ball.x; const dy = y - ball.y; const angle = Math.atan2(dy, dx); const speed = 10; // Set the ball's velocity based on the direction vector and speed ball.vx = Math.cos(angle) * speed; ball.vy = Math.sin(angle) * speed; }); // Start the game loop update(); </script> </body> </html> ``` ### Usage 1. Save this code as an HTML file (e.g., `mini_golf.html`). 2. Open the file in a web browser to run the simulation. 3. Click on the canvas to strike the ball in the direction of the click position. ### Explanation This code creates a mini-golf game engine that simulates the movement of a ball on a 2D plane. The game includes ball physics, collision detection with obstacles, and hole-in-one logic. * The `Ball` class represents the ball object, which has properties for its position, velocity, and radius. The `update` method updates the ball's position based on its velocity and applies friction to slow it down. * The `Obstacle` class represents the obstacle objects, which have properties for their position, width, and height. The `collide` method checks if the ball has collided with the obstacle and reflects the ball's velocity accordingly. * The `Hole` class represents the hole object, which has properties for its position and radius. The `check` method checks if the ball has gone into the hole. * The game loop updates and draws the game objects, checks for collisions and hole-in-one conditions, and handles user input to strike the ball. The code uses the HTML5 canvas element to render the game graphics and JavaScript to handle the game logic. The simulation is responsive and runs smoothly in most modern web browsers.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Mini Golf Game Engine</title> <style> html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #4caf50; user-select: none; -webkit-tap-highlight-color: transparent; } canvas { background: linear-gradient(#228B22, #2e7d32); display: block; margin: 0 auto; box-shadow: 0 0 20px rgba(0,0,0,0.6); border-radius: 12px; cursor: crosshair; } #ui { position: fixed; bottom: 12px; left: 50%; transform: translateX(-50%); color: white; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; user-select: none; text-align: center; pointer-events: none; font-size: 16px; text-shadow: 0 0 6px rgba(0,0,0,0.7); } #instructions { position: fixed; top: 12px; left: 50%; transform: translateX(-50%); color: white; font-family: monospace; background: rgba(0,0,0,0.4); padding: 6px 12px; border-radius: 8px; user-select: none; max-width: 95vw; box-sizing: border-box; text-align: center; } #restartBtn { margin-top: 8px; color: #4caf50; background: white; border: none; font-weight: bold; font-size: 14px; padding: 6px 11px; border-radius: 8px; cursor: pointer; transition: background 0.3s ease; } #restartBtn:hover { background: #eee; } </style> </head> <body> <canvas id="game" width="864" height="512" tabindex="0"></canvas> <div id="ui"> <div id="message">Click & drag on the ball to shoot.</div> <button id="restartBtn">Restart Game</button> </div> <div id="instructions"> <div><b>Mini-Golf Simulation Controls</b></div> <div>Click and drag on the ball, release to shoot. Try to get the ball in the hole with as few shots as possible.</div> <div><small>Physics: friction, ball collisions with course bounds & obstacles, hole-in-one detection.</small></div> </div> <script> (() => { // Constants and Config const canvas = document.getElementById("game"); const ctx = canvas.getContext("2d"); const W = canvas.width; const H = canvas.height; // Game UI elements const messageEl = document.getElementById("message"); const restartBtn = document.getElementById("restartBtn"); // Physics and gameplay parameters const ballRadius = 10; const holeRadius = 16; const friction = 0.992; // velocity multiplier each frame const minVelocity = 0.05; // below this velocity, ball stops const maxStrokePower = 25; // limit the max initial velocity magnitude // Course obstacles as simple convex polygons or rectangles // We'll define rectangular obstacles for simplicity // Coordinates: {x, y, width, height} // Ball collision handled with circle-rectangle intersection and response const obstacles = [ {x: 320, y: 120, width: 200, height: 14}, // horizontal block {x: 530, y: 220, width: 14, height: 140}, // vertical block {x: 100, y: 300, width: 180, height: 14}, // horizontal block {x: 660, y: 350, width: 100, height: 14}, // horizontal block near hole ]; // Hole position (circle) const holePos = {x: 770, y: 450}; // Ball object const ball = { pos: {x: 100, y: 100}, vel: {x: 0, y: 0}, radius: ballRadius, moving: false, }; // Game state let shots = 0; let shooting = false; let dragStart = null; let dragCurrent = null; let gameOver = false; // Vector helpers function vecLength(v) { return Math.sqrt(v.x * v.x + v.y * v.y); } function vecNormalize(v) { const len = vecLength(v); if(len === 0) return {x:0,y:0}; return {x: v.x/len, y: v.y/len}; } function vecSub(a, b) { return {x: a.x - b.x, y: a.y - b.y}; } function vecAdd(a, b) { return {x: a.x + b.x, y: a.y + b.y}; } function vecMul(v, m) { return {x: v.x * m, y: v.y * m}; } function vecDot(a,b) { return a.x*b.x + a.y*b.y; } function clamp(x, min, max) { return Math.min(Math.max(x, min), max); } // Circle-rectangle collision detection: returns collision info or null function circleRectColl(ballPos, r, rect) { // Find closest point in rect to circle center let closestX = clamp(ballPos.x, rect.x, rect.x + rect.width); let closestY = clamp(ballPos.y, rect.y, rect.y + rect.height); let distX = ballPos.x - closestX; let distY = ballPos.y - closestY; let distSq = distX*distX + distY*distY; if (distSq > r*r) return null; let dist = Math.sqrt(distSq) || 0.001; // Normal vector of collision (pointing out from rect) return { overlap: r - dist, normal: {x: distX/dist, y: distY/dist}, closest: {x: closestX, y: closestY}, }; } // Ball-wall/bounds collisions (the course edges) function checkBounds() { // Walls are bounding rect of canvas but with some margin (hole side is open) // We'll keep all edges solid except around the hole area // Define course boundary rectangle: const margin = 10; const left = margin; const right = W - margin; const top = margin; const bottom = H - margin; let collided = false; // Left wall if(ball.pos.x - ball.radius < left) { ball.pos.x = left + ball.radius; ball.vel.x = -ball.vel.x * 0.6; // dampened bounce collided = true; } // Right wall - open near hole at bottom right if(ball.pos.x + ball.radius > right) { // check if ball is within hole vertical range, if yes, skip collision if(!(ball.pos.y > holePos.y - 2*holeRadius)) { ball.pos.x = right - ball.radius; ball.vel.x = -ball.vel.x * 0.6; collided = true; } } // Top wall if(ball.pos.y - ball.radius < top) { ball.pos.y = top + ball.radius; ball.vel.y = -ball.vel.y * 0.6; collided = true; } // Bottom wall - open around hole horizontally if(ball.pos.y + ball.radius > bottom) { // If ball outside hole horizontal range, collide with bottom if(!(ball.pos.x > holePos.x - holeRadius * 1.5 && ball.pos.x < holePos.x + holeRadius * 1.5)) { ball.pos.y = bottom - ball.radius; ball.vel.y = -ball.vel.y * 0.6; collided = true; } } return collided; } // Reflect velocity against collision normal conserving some energy function reflectVel(vel, normal) { // R = V - 2(V⋅N)N let dot = vecDot(vel, normal); return vecSub(vel, vecMul(normal, 2*dot)); } // Ball-obstacle collisions and resolution function resolveObstacles() { // For each obstacle rect, check collision and push ball out + reflect velocity accordingly let collidedAny = false; obstacles.forEach(rect => { let col = circleRectColl(ball.pos, ball.radius, rect); if(col) { collidedAny = true; // Push ball out of obstacle along normal vector times overlap ball.pos.x += col.normal.x * col.overlap; ball.pos.y += col.normal.y * col.overlap; // Reflect the velocity but dampened for energy loss ball.vel = reflectVel(ball.vel, col.normal); ball.vel.x *= 0.6; ball.vel.y *= 0.6; } }); return collidedAny; } // Hole-in-one detection: ball inside hole radius AND very slow function checkHoleInOne() { let distToHole = vecLength(vecSub(ball.pos, holePos)); if(distToHole < holeRadius) { // Also check speed low enough to consider in hole if(vecLength(ball.vel) < 0.6) { return true; } } return false; } // Draw functions function drawCourse() { // green background done by canvas bg // Draw hole shadow + hole rim + hole black center // Hole shadow (slightly bigger circular shadow) const holeShadowColor = "rgba(0,0,0,0.4)"; ctx.beginPath(); ctx.ellipse(holePos.x + 3, holePos.y + 6, holeRadius + 6, holeRadius + 3, 0, 0, Math.PI * 2); ctx.fillStyle = holeShadowColor; ctx.fill(); // Hole rim - ring with gradient for depth let grad = ctx.createRadialGradient(holePos.x, holePos.y, holeRadius - 6, holePos.x, holePos.y, holeRadius); grad.addColorStop(0, "#222"); grad.addColorStop(1, "#000"); ctx.beginPath(); ctx.arc(holePos.x, holePos.y, holeRadius, 0, Math.PI * 2); ctx.fillStyle = grad; ctx.fill(); // Hole center (black) ctx.beginPath(); ctx.arc(holePos.x, holePos.y, holeRadius - 6, 0, Math.PI * 2); ctx.fillStyle = "#111"; ctx.fill(); // Draw obstacles ctx.fillStyle = "#654321"; ctx.strokeStyle = "#3e2e1a"; ctx.lineWidth = 1.5; obstacles.forEach(r => { // Wood texture with subtle stripes let woodGrad = ctx.createLinearGradient(r.x, r.y, r.x + r.width, r.y + r.height); woodGrad.addColorStop(0, "#7a5230"); woodGrad.addColorStop(0.5, "#945f35"); woodGrad.addColorStop(1, "#7a5230"); ctx.fillStyle = woodGrad; ctx.fillRect(r.x, r.y, r.width, r.height); ctx.strokeRect(r.x, r.y, r.width, r.height); }); // Course boundary ring ctx.strokeStyle = "#2a5d17"; ctx.lineWidth = 15; ctx.strokeRect(10, 10, W-20, H-20); } function drawBall() { // Draw ball base circle const baseColor = "#eee"; const shadowColor = "#aaa"; const shineColor = "#fff"; ctx.save(); ctx.translate(ball.pos.x, ball.pos.y); // Shadow under ball (slightly offset ellipse) ctx.beginPath(); ctx.ellipse(3, 6, ball.radius * 1.15, ball.radius * 0.8, 0, 0, Math.PI * 2); ctx.fillStyle = "rgba(0,0,0,0.15)"; ctx.fill(); // Ball main circle with subtle shadow let grad = ctx.createRadialGradient(0, 0, ball.radius*0.3, 0, 0, ball.radius); grad.addColorStop(0, "#fff"); grad.addColorStop(1, baseColor); ctx.beginPath(); ctx.arc(0, 0, ball.radius, 0, Math.PI * 2); ctx.fillStyle = grad; ctx.shadowColor = shadowColor; ctx.shadowBlur = 6; ctx.fill(); // Shine highlight ctx.beginPath(); ctx.arc(-ball.radius*0.3, -ball.radius*0.3, ball.radius*0.35, 0, Math.PI * 2); ctx.fillStyle = shineColor; ctx.globalAlpha = 0.65; ctx.fill(); ctx.restore(); } // Draw the aiming arrow preview while dragging function drawAimArrow() { if(!dragStart || !dragCurrent) return; const dir = vecSub(dragStart, dragCurrent); const length = Math.min(vecLength(dir), 120); if(length < 8) return; const unit = vecNormalize(dir); ctx.save(); ctx.strokeStyle = "#fff"; ctx.fillStyle = "rgba(255,255,255,0.7)"; ctx.lineWidth = 3; ctx.lineCap = "round"; // Arrow line ctx.beginPath(); ctx.moveTo(ball.pos.x, ball.pos.y); ctx.lineTo(ball.pos.x + unit.x * length, ball.pos.y + unit.y * length); ctx.stroke(); // Arrowhead ctx.beginPath(); const arrowSize = 10; const p1 = { x: ball.pos.x + unit.x * length, y: ball.pos.y + unit.y * length }; const perp = {x: -unit.y, y: unit.x}; ctx.moveTo(p1.x, p1.y); ctx.lineTo(p1.x + perp.x * arrowSize * 0.6, p1.y + perp.y * arrowSize * 0.6); ctx.lineTo(p1.x + perp.x * -arrowSize * 0.6, p1.y + perp.y * -arrowSize * 0.6); ctx.closePath(); ctx.fill(); ctx.restore(); } function update() { if(gameOver) return; if(ball.moving) { // Physics update ball.pos.x += ball.vel.x; ball.pos.y += ball.vel.y; // Collision with course boundary checkBounds(); // Collision with obstacles resolveObstacles(); // Apply friction ball.vel.x *= friction; ball.vel.y *= friction; // Stop ball if very slow if(vecLength(ball.vel) < minVelocity) { ball.vel.x = 0; ball.vel.y = 0; ball.moving = false; messageEl.textContent = "Your ball stopped. Take your next shot."; } if(checkHoleInOne()) { ball.moving = false; gameOver = true; messageEl.textContent = `Hole completed in ${shots} shot${shots > 1 ? "s" : ""}! 🎉 Click Restart to play again.`; } } } function render() { // Clear ctx.clearRect(0, 0, W, H); // Draw course and elements drawCourse(); // Draw ball drawBall(); // Draw aiming arrow if dragging if(shooting) { drawAimArrow(); } } // Input handlers for shooting with mouse or touch function isOverBall(p) { return vecLength(vecSub(p, ball.pos)) <= ball.radius + 6; } canvas.addEventListener("mousedown", (e) => { if(gameOver) return; if(ball.moving) return; // do not shoot when ball is moving const rect = canvas.getBoundingClientRect(); let p = {x: e.clientX - rect.left, y: e.clientY - rect.top}; if(isOverBall(p)) { shooting = true; dragStart = {...p}; dragCurrent = {...p}; messageEl.textContent = "Drag to aim and release to shoot."; e.preventDefault(); } }); window.addEventListener("mousemove", (e) => { if(!shooting) return; const rect = canvas.getBoundingClientRect(); dragCurrent = {x: e.clientX - rect.left, y: e.clientY - rect.top}; e.preventDefault(); }); window.addEventListener("mouseup", (e) => { if(!shooting) return; shooting = false; const rect = canvas.getBoundingClientRect(); dragCurrent = {x: e.clientX - rect.left, y: e.clientY - rect.top}; const delta = vecSub(dragStart, dragCurrent); // Calculate velocity vector (limit power) let power = vecLength(delta); if(power > 3) { let factor = Math.min(power, 120) / 120; let velMag = factor * maxStrokePower; let velDir = vecNormalize(delta); ball.vel.x = velDir.x * velMag; ball.vel.y = velDir.y * velMag; ball.moving = true; shots++; messageEl.textContent = `Shot taken! Shots: ${shots}`; } else { messageEl.textContent = "Shot too weak, try again."; } dragStart = null; dragCurrent = null; e.preventDefault(); }); // Touch support for mobile canvas.addEventListener("touchstart", (e) => { if(gameOver) return; if(ball.moving) return; const rect = canvas.getBoundingClientRect(); let touch = e.changedTouches[0]; let p = {x: touch.clientX - rect.left, y: touch.clientY - rect.top}; if(isOverBall(p)) { shooting = true; dragStart = {...p}; dragCurrent = {...p}; messageEl.textContent = "Drag to aim and release to shoot."; e.preventDefault(); } }, {passive: false}); window.addEventListener("touchmove", (e) => { if(!shooting) return; const rect = canvas.getBoundingClientRect(); let touch = e.changedTouches[0]; dragCurrent = {x: touch.clientX - rect.left, y: touch.clientY - rect.top}; e.preventDefault(); }, {passive: false}); window.addEventListener("touchend", (e) => { if(!shooting) return; shooting = false; const rect = canvas.getBoundingClientRect(); let touch = e.changedTouches[0]; dragCurrent = {x: touch.clientX - rect.left, y: touch.clientY - rect.top}; const delta = vecSub(dragStart, dragCurrent); let power = vecLength(delta); if(power > 3) { let factor = Math.min(power, 120) / 120; let velMag = factor * maxStrokePower; let velDir = vecNormalize(delta); ball.vel.x = velDir.x * velMag; ball.vel.y = velDir.y * velMag; ball.moving = true; shots++; messageEl.textContent = `Shot taken! Shots: ${shots}`; } else { messageEl.textContent = "Shot too weak, try again."; } dragStart = null; dragCurrent = null; e.preventDefault(); }, {passive: false}); // Restart button resets game restartBtn.addEventListener("click", () => { ball.pos.x = 100; ball.pos.y = 100; ball.vel.x = 0; ball.vel.y = 0; ball.moving = false; shots = 0; gameOver = false; messageEl.textContent = "Click & drag on the ball to shoot."; }); // Main loop function loop() { update(); render(); requestAnimationFrame(loop); } // Focus canvas to enable keyboard controls (optional) canvas.focus(); // Start loop loop(); })(); </script> </body> </html> ```
# Mini-Golf Game Engine I'll create a complete mini-golf simulation with physics, collision detection, and hole-in-one mechanics. The implementation will include a playable course with visual controls and game feedback. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mini-Golf Simulator</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/konva/8.4.2/konva.min.js"></script> <style> body { margin: 0; padding: 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; background: linear-gradient(135deg, #1a5f23, #1a3c23); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: white; min-height: 100vh; overflow: hidden; } h1 { text-align: center; color: #ffcc00; text-shadow: 0 2px 4px rgba(0,0,0,0.5); margin-bottom: 10px; } .game-container { position: relative; width: 800px; max-width: 95%; height: 500px; background: #8fc77c; border-radius: 15px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); overflow: hidden; border: 8px solid #755028; } #game-canvas { width: 100%; height: 100%; } .controls { display: flex; flex-direction: column; align-items: center; margin-top: 20px; background: rgba(50, 100, 60, 0.7); padding: 15px; border-radius: 10px; width: 300px; max-width: 90%; } .power-container { width: 100%; margin-bottom: 15px; } .power-slider { width: 100%; height: 25px; -webkit-appearance: none; background: linear-gradient(to right, #4CAF50, #FFC107, #FF5722); outline: none; border-radius: 20px; } .power-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 30px; height: 30px; border-radius: 50%; background: #fff; cursor: pointer; box-shadow: 0 0 5px rgba(0,0,0,0.5); } .power-value { text-align: center; font-size: 18px; margin-top: 5px; font-weight: bold; } .shot-btn { background: linear-gradient(to bottom, #FF9800, #F57C00); border: none; color: white; padding: 12px 30px; font-size: 18px; border-radius: 30px; cursor: pointer; transition: all 0.3s; font-weight: bold; text-transform: uppercase; letter-spacing: 1px; box-shadow: 0 4px 8px rgba(0,0,0,0.3); } .shot-btn:hover { background: linear-gradient(to bottom, #FFA726, #EF6C00); transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0,0,0,0.4); } .shot-btn:disabled { background: #9e9e9e; cursor: not-allowed; transform: none; box-shadow: none; } .game-stats { display: flex; justify-content: space-between; width: 100%; margin-top: 10px; font-size: 18px; } .game-stats span { background: rgba(0, 0, 0, 0.2); padding: 5px 15px; border-radius: 20px; } .message { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.85); color: #FFEB3B; padding: 20px 40px; border-radius: 15px; font-size: 32px; font-weight: bold; text-align: center; z-index: 10; display: none; box-shadow: 0 0 30px rgba(255, 235, 59, 0.7); animation: pulse 1.5s infinite; } @keyframes pulse { 0% { opacity: 0.8; transform: translate(-50%, -50%) scale(1); } 50% { opacity: 1; transform: translate(-50%, -50%) scale(1.05); } 100% { opacity: 0.8; transform: translate(-50%, -50%) scale(1); } } .instructions { background: rgba(0, 0, 0, 0.6); margin-top: 20px; padding: 15px; border-radius: 10px; text-align: center; font-size: 14px; line-height: 1.6; max-width: 600px; } </style> </head> <body> <h1>Mini-Golf Simulator</h1> <div class="game-container"> <div id="game-canvas"></div> <div id="message" class="message">Hole In One!</div> </div> <div class="controls"> <div class="power-container"> <p>Shot Power: <span id="power-value" class="power-value">50%</span></p> <input type="range" min="5" max="100" value="50" class="power-slider" id="power-slider"> </div> <button id="shot-btn" class="shot-btn">TAKE SHOT</button> <div class="game-stats"> <span>Shots: <span id="shot-count">0</span></span> <span>Distance: <span id="meter-count">0</span></span> </div> </div> <div class="instructions"> <p>Use the power slider to adjust shot strength. Click TAKE SHOT to hit the ball.</p> <p>Navigate the ball around obstacles to reach the hole. Try to complete the course in as few shots as possible!</p> </div> <script> document.addEventListener('DOMContentLoaded', () => { // Game constants const FRICTION_COEFFICIENT = 0.98; const MIN_VELOCITY = 0.05; const HOLE_RADIUS = 24; const BALL_RADIUS = 16; const MAX_POWER = 12; // Game state let shots = 0; let totalDistance = 0; let ball, hole; let gameRunning = true; let ballMoving = false; let power = 50; let stage, layer; // Obstacles const obstacles = [ {x: 200, y: 100, width: 25, height: 300}, {x: 400, y: 100, width: 25, height: 200}, {x: 600, y: 200, width: 25, height: 200}, {x: 100, y: 400, width: 500, height: 25} ]; // Initialize game function init() { // Setup canvas stage = new Konva.Stage({ container: 'game-canvas', width: document.querySelector('.game-container').clientWidth, height: document.querySelector('.game-container').clientHeight }); layer = new Konva.Layer(); stage.add(layer); // Draw course elements drawCourse(); drawObstacles(); drawHole(); drawBall(); // Add event handlers setupEventHandlers(); // Start animation loop const anim = new Konva.Animation(() => { if (ballMoving) { moveBall(); checkCollisions(); checkWinCondition(); } }, layer); anim.start(); } // Draw the grass and details of the course function drawCourse() { // Main grass const course = new Konva.Rect({ x: 0, y: 0, width: stage.width(), height: stage.height(), fill: '#8fc77c', cornerRadius: 0 }); layer.add(course); // Draw decorative elements drawDetails(); } // Add decorative details to the course function drawDetails() { // Sand trap const sandTrap = new Konva.Ellipse({ x: 350, y: 180, radiusX: 70, radiusY: 40, fill: '#f0e68c', stroke: '#d2b48c', strokeWidth: 2, cornerRadius: 10 }); layer.add(sandTrap); // Water hazard const water = new Konva.Circle({ x: 650, y: 350, radius: 60, fill: 'rgba(64, 164, 223, 0.6)', stroke: '#1e88e5', strokeWidth: 2 }); layer.add(water); // Trees for(let i = 0; i < 5; i++) { const x = 50 + i * 150; const y = i % 2 === 0 ? 60 : 100; const treeTrunk = new Konva.Rect({ x: x, y: y, width: 10, height: 25, fill: '#5d4037', cornerRadius: 3 }); const treeTop = new Konva.Circle({ x: x + 5, y: y - 15, radius: 20, fill: '#2e7d32', stroke: '#1b5e20', strokeWidth: 2 }); layer.add(treeTrunk); layer.add(treeTop); } } // Draw obstacles on the course function drawObstacles() { obstacles.forEach(obstacle => { const rect = new Konva.Rect({ x: obstacle.x - obstacle.width/2, y: obstacle.y - obstacle.height/2, width: obstacle.width, height: obstacle.height, fill: '#4e342e', cornerRadius: 5, stroke: '#3e2723', strokeWidth: 2 }); layer.add(rect); // Add decorative bricks const brickPattern = new Konva.Rect({ x: obstacle.x - obstacle.width/2 + 2, y: obstacle.y - obstacle.height/2 + 2, width: obstacle.width - 4, height: obstacle.height - 4, fillPatternImage: createBrickPattern(), fillPatternRepeat: 'repeat', fillPatternScale: {x: 0.1, y: 0.1}, cornerRadius: 3 }); layer.add(brickPattern); }); } // Create brick wall pattern function createBrickPattern() { const canvas = document.createElement('canvas'); canvas.width = 100; canvas.height = 50; const ctx = canvas.getContext('2d'); // Brick color ctx.fillStyle = '#8d6e63'; ctx.fillRect(0, 0, 100, 50); // Mortar lines ctx.fillStyle = '#6d4c41'; // Horizontal lines for(let y = 0; y < 3; y++) { ctx.fillRect(0, y * 20, 100, 4); } // Vertical lines - staggered for(let x = 0; x < 6; x++) { const offset = x % 2 === 0 ? 0 : 10; ctx.fillRect(x * 20, offset, 4, 20); } return canvas; } // Create the hole function drawHole() { hole = new Konva.Circle({ x: stage.width() - 50, y: stage.height() - 50, radius: HOLE_RADIUS, fill: '#000', stroke: '#fff', strokeWidth: 2 }); layer.add(hole); // Add hole depth effect const holeDepth = new Konva.Circle({ x: stage.width() - 50, y: stage.height() - 50, radius: HOLE_RADIUS - 5, fill: '#212121', strokeWidth: 0 }); layer.add(holeDepth); } // Create the ball function drawBall() { ball = new Konva.Circle({ x: 70, y: stage.height() - 100, radius: BALL_RADIUS, fillLinearGradientStartPoint: {x: -BALL_RADIUS/2, y: -BALL_RADIUS/2}, fillLinearGradientEndPoint: {x: BALL_RADIUS, y: BALL_RADIUS}, fillLinearGradientColorStops: [0, '#f5f5f5', 1, '#c0c0c0'], stroke: '#888', strokeWidth: 1 }); // Golf ball dimples for(let i = 0; i < 10; i++) { const angle = (i * 36) * Math.PI / 180; const dist = BALL_RADIUS * 0.6; const dimple = new Konva.Circle({ x: ball.x() + Math.cos(angle) * dist, y: ball.y() + Math.sin(angle) * dist, radius: 3, fill: '#aaa' }); layer.add(dimple); } layer.add(ball); } // Set up user interaction handlers function setupEventHandlers() { const shotBtn = document.getElementById('shot-btn'); const slider = document.getElementById('power-slider'); const powerValue = document.getElementById('power-value'); // Power slider slider.addEventListener('input', () => { power = parseInt(slider.value); powerValue.textContent = power + '%'; }); // Shot button shotBtn.addEventListener('click', takeShot); } // Take a shot with the current power level function takeShot() { if (!gameRunning || ballMoving) return; // Determine the direction to the hole const dx = hole.x() - ball.x(); const dy = hole.y() - ball.y(); const distance = Math.sqrt(dx * dx + dy * dy); // Set velocity proportional to power and angle to hole const speed = power / 100 * MAX_POWER; ball.velocity = { x: (dx / distance) * speed, y: (dy / distance) * speed }; ballMoving = true; shots++; document.getElementById('shot-count').textContent = shots; // Update distance totalDistance += Math.round(distance); document.getElementById('meter-count').textContent = Math.round(totalDistance / 50); } // Update ball position based on physics function moveBall() { // Apply velocity ball.x(ball.x() + ball.velocity.x); ball.y(ball.y() + ball.velocity.y); // Apply friction ball.velocity.x *= FRICTION_COEFFICIENT; ball.velocity.y *= FRICTION_COEFFICIENT; // Stop the ball if velocity is too low if (Math.abs(ball.velocity.x) < MIN_VELOCITY && Math.abs(ball.velocity.y) < MIN_VELOCIENT) { ball.velocity.x = 0; ball.velocity.y = 0; ballMoving = false; } } // Check for collisions with obstacles function checkCollisions() { const ballPos = {x: ball.x(), y: ball.y()}; const radius = BALL_RADIUS; obstacles.forEach(obstacle => { // Convert center-based obstacle to top-left based const left = obstacle.x - obstacle.width/2; const right = obstacle.x + obstacle.width/2; const top = obstacle.y - obstacle.height/2; const bottom = obstacle.y + obstacle.height/2; // Find closest point on obstacle to ball let closestX = Math.max(left, Math.min(ballPos.x, right)); let closestY = Math.max(top, Math.min(ballPos.y, bottom)); // Calculate distance const dx = ballPos.x - closestX; const dy = ballPos.y - closestY; const distance = Math.sqrt(dx * dx + dy * dy); // Check for collision if (distance < radius) { // Normalize vector const nx = dx / distance; const ny = dy / distance; // Reflect velocity vector const dot = ball.velocity.x * nx + ball.velocity.y * ny; ball.velocity.x -= 2 * dot * nx; ball.velocity.y -= 2 * dot * ny; // Add damping effect ball.velocity.x *= 0.9; ball.velocity.y *= 0.9; // Move ball outside obstacle const overlap = radius - distance; ball.x(ball.x() + nx * overlap * 1.1); ball.y(ball.y() + ny * overlap * 1.1); } }); // Boundary collisions if (ball.x() < radius) { ball.x(radius); ball.velocity.x *= -0.8; } else if (ball.x() > stage.width() - radius) { ball.x(stage.width() - radius); ball.velocity.x *= -0.8; } if (ball.y() < radius) { ball.y(radius); ball.velocity.y *= -0.8; } else if (ball.y() > stage.height() - radius) { ball.y(stage.height() - radius); ball.velocity.y *= -0.8; } } // Check if ball has entered hole function checkWinCondition() { const dx = hole.x() - ball.x(); const dy = hole.y() - ball.y(); const distance = Math.sqrt(dx * dx + dy * dy); if (distance < HOLE_RADIUS - BALL_RADIUS/2) { ballMoving = false; gameRunning = false; showMessage(`Hole In One! ${shots} Shots`); } } // Display success message function showMessage(text) { const message = document.getElementById('message'); message.textContent = text; message.style.display = 'block'; // Reset after delay setTimeout(() => { message.textContent = ''; message.style.display = 'none'; resetBall(); }, 3000); } // Reset ball to start position function resetBall() { ball.x(70); ball.y(stage.height() - 100); ball.velocity = {x: 0, y: 0}; gameRunning = true; ballMoving = false; } // Initialize game init(); }); </script> </body> </html> ``` ## Features Implemented This mini-golf game engine includes: 1. **Physics simulation**: - Ball movement with velocity-based physics - Friction for realistic deceleration - Collision detection with walls and obstacles - Proper reflection angles when hitting obstacles 2. **Game elements**: - Golf ball with detailed appearance and dimples - Hole with depth effect - Obstacles with brick pattern textures - Course decorations (sand traps, water hazards, trees) 3. **Game mechanics**: - Adjustable shot power via slider control - Shot counter and distance tracker - "Hole in one" detection - Win condition and celebration message 4. **User interface**: - Clean golf-themed color scheme - Responsive layout - Clear instructions for players - Visual feedback for shot power The game is ready to play immediately upon loading - simply adjust the power slider and hit "TAKE SHOT" to try to get the ball in the hole with as few shots as possible!
### Mini-Golf Game Engine #### Overview This code provides a basic implementation of a mini-golf game engine using JavaScript and HTML5 Canvas. The engine handles ball physics, collision detection with obstacles, and hole-in-one logic. #### Code ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mini-Golf Game Engine</title> <style> /* Add some basic styling to our canvas */ canvas { border: 1px solid black; } </style> </head> <body> <canvas id="gameCanvas" width="800" height="600"></canvas> <script> // Get the canvas element const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); // Define some constants const BALL_RADIUS = 10; const BALL_FRICTION = 0.98; const OBSTACLE_COLOR = 'blue'; const HOLE_COLOR = 'green'; const BALL_COLOR = 'red'; // Define the ball object class Ball { constructor(x, y) { this.x = x; this.y = y; this.vx = 0; this.vy = 0; } update() { this.x += this.vx; this.y += this.vy; // Apply friction this.vx *= BALL_FRICTION; this.vy *= BALL_FRICTION; // Check for collision with walls if (this.x - BALL_RADIUS < 0 || this.x + BALL_RADIUS > canvas.width) { this.vx = -this.vx; } if (this.y - BALL_RADIUS < 0 || this.y + BALL_RADIUS > canvas.height) { this.vy = -this.vy; } } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, BALL_RADIUS, 0, 2 * Math.PI); ctx.fillStyle = BALL_COLOR; ctx.fill(); } } // Define the obstacle object class Obstacle { constructor(x, y, width, height) { this.x = x; this.y = y; this.width = width; this.height = height; } draw() { ctx.beginPath(); ctx.rect(this.x, this.y, this.width, this.height); ctx.fillStyle = OBSTACLE_COLOR; ctx.fill(); } collide(ball) { if (ball.x + BALL_RADIUS > this.x && ball.x - BALL_RADIUS < this.x + this.width && ball.y + BALL_RADIUS > this.y && ball.y - BALL_RADIUS < this.y + this.height) { // Handle collision ball.vx = -ball.vx; ball.vy = -ball.vy; } } } // Define the hole object class Hole { constructor(x, y, radius) { this.x = x; this.y = y; this.radius = radius; } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI); ctx.fillStyle = HOLE_COLOR; ctx.fill(); } checkCollision(ball) { const distance = Math.sqrt((ball.x - this.x) ** 2 + (ball.y - this.y) ** 2); if (distance < this.radius + BALL_RADIUS) { // Handle hole-in-one console.log('Hole-in-one!'); ball.x = canvas.width / 2; ball.y = canvas.height / 2; ball.vx = 0; ball.vy = 0; } } } // Create game objects const ball = new Ball(canvas.width / 2, canvas.height / 2); const obstacles = [ new Obstacle(100, 100, 50, 50), new Obstacle(300, 300, 50, 50), ]; const hole = new Hole(700, 500, 20); // Main game loop function update() { ctx.clearRect(0, 0, canvas.width, canvas.height); // Update and draw ball ball.update(); ball.draw(); // Update and draw obstacles obstacles.forEach(obstacle => { obstacle.draw(); obstacle.collide(ball); }); // Update and draw hole hole.draw(); hole.checkCollision(ball); requestAnimationFrame(update); } // Handle user input canvas.addEventListener('click', (event) => { const angle = Math.atan2(event.clientY - canvas.height / 2, event.clientX - canvas.width / 2); ball.vx = Math.cos(angle) * 5; ball.vy = Math.sin(angle) * 5; }); update(); </script> </body> </html> ``` ### Explanation * The game engine uses the HTML5 Canvas element to render the game. * The game objects (ball, obstacles, and hole) are defined as classes with their own properties and methods. * The ball object has properties for its position, velocity, and methods for updating and drawing itself. * The obstacle object has properties for its position, size, and methods for drawing itself and checking for collisions with the ball. * The hole object has properties for its position, radius, and methods for drawing itself and checking for collisions with the ball. * The game loop updates and draws all game objects, and checks for collisions between the ball and obstacles and the hole. * The user can interact with the game by clicking on the canvas to apply a force to the ball. ### Usage * Open the HTML file in a web browser to play the game. * Click on the canvas to apply a force to the ball and make it move. * Try to get the ball into the hole to score a hole-in-one.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mini Golf Game Engine</title> <style> body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #1a4d1a; color: white; } canvas { border: 2px solid #333; background: #2d5a2d; display: block; margin: 0 auto; cursor: crosshair; } .controls { text-align: center; margin: 20px 0; } .power-meter { width: 200px; height: 20px; border: 2px solid #fff; margin: 10px auto; position: relative; } .power-fill { height: 100%; background: linear-gradient(to right, #00ff00, #ffff00, #ff0000); width: 0%; transition: width 0.1s; } .info { text-align: center; margin: 10px 0; } </style> </head> <body> <h1 style="text-align: center;">Mini Golf Game Engine</h1> <div class="info"> <div>Strokes: <span id="strokes">0</span></div> <div>Click and drag to aim, release to shoot</div> </div> <div class="power-meter"> <div class="power-fill" id="powerFill"></div> </div> <canvas id="gameCanvas" width="800" height="600"></canvas> <div class="controls"> <button onclick="resetGame()">Reset Course</button> <button onclick="generateNewCourse()">New Course</button> </div> <script> class Vector2 { constructor(x = 0, y = 0) { this.x = x; this.y = y; } add(vector) { return new Vector2(this.x + vector.x, this.y + vector.y); } subtract(vector) { return new Vector2(this.x - vector.x, this.y - vector.y); } multiply(scalar) { return new Vector2(this.x * scalar, this.y * scalar); } magnitude() { return Math.sqrt(this.x * this.x + this.y * this.y); } normalize() { const mag = this.magnitude(); if (mag === 0) return new Vector2(0, 0); return new Vector2(this.x / mag, this.y / mag); } dot(vector) { return this.x * vector.x + this.y * vector.y; } } class Ball { constructor(x, y, radius = 8) { this.position = new Vector2(x, y); this.velocity = new Vector2(0, 0); this.radius = radius; this.friction = 0.98; this.bounceReduction = 0.7; } update() { // Apply friction this.velocity = this.velocity.multiply(this.friction); // Stop very slow movement if (this.velocity.magnitude() < 0.1) { this.velocity = new Vector2(0, 0); } // Update position this.position = this.position.add(this.velocity); } isMoving() { return this.velocity.magnitude() > 0.1; } applyForce(force) { this.velocity = this.velocity.add(force); } draw(ctx) { ctx.beginPath(); ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = '#ffffff'; ctx.fill(); ctx.strokeStyle = '#000000'; ctx.lineWidth = 2; ctx.stroke(); } } class Obstacle { constructor(x, y, width, height) { this.position = new Vector2(x, y); this.width = width; this.height = height; } checkCollision(ball) { const ballX = ball.position.x; const ballY = ball.position.y; const ballRadius = ball.radius; // Find closest point on rectangle to ball center const closestX = Math.max(this.position.x, Math.min(ballX, this.position.x + this.width)); const closestY = Math.max(this.position.y, Math.min(ballY, this.position.y + this.height)); // Calculate distance between ball center and closest point const distance = Math.sqrt((ballX - closestX) ** 2 + (ballY - closestY) ** 2); if (distance < ballRadius) { // Calculate collision normal let normal = new Vector2(ballX - closestX, ballY - closestY); if (normal.magnitude() === 0) { // Ball is inside rectangle, push out if (ballX < this.position.x + this.width / 2) { normal = new Vector2(-1, 0); } else { normal = new Vector2(1, 0); } } else { normal = normal.normalize(); } // Move ball out of collision const penetration = ballRadius - distance; ball.position = ball.position.add(normal.multiply(penetration)); // Reflect velocity const velocityDotNormal = ball.velocity.dot(normal); const reflection = normal.multiply(2 * velocityDotNormal); ball.velocity = ball.velocity.subtract(reflection); ball.velocity = ball.velocity.multiply(ball.bounceReduction); return true; } return false; } draw(ctx) { ctx.fillStyle = '#8B4513'; ctx.fillRect(this.position.x, this.position.y, this.width, this.height); ctx.strokeStyle = '#654321'; ctx.lineWidth = 2; ctx.strokeRect(this.position.x, this.position.y, this.width, this.height); } } class Hole { constructor(x, y, radius = 15) { this.position = new Vector2(x, y); this.radius = radius; } checkHoleInOne(ball) { const distance = ball.position.subtract(this.position).magnitude(); return distance < this.radius - ball.radius / 2 && ball.velocity.magnitude() < 3; } draw(ctx) { ctx.beginPath(); ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = '#000000'; ctx.fill(); ctx.strokeStyle = '#333333'; ctx.lineWidth = 2; ctx.stroke(); // Draw flag ctx.beginPath(); ctx.moveTo(this.position.x, this.position.y - this.radius); ctx.lineTo(this.position.x, this.position.y - this.radius - 30); ctx.strokeStyle = '#8B4513'; ctx.lineWidth = 3; ctx.stroke(); ctx.fillStyle = '#ff0000'; ctx.fillRect(this.position.x, this.position.y - this.radius - 30, 20, 15); } } class MiniGolfGame { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.ball = new Ball(50, canvas.height - 50); this.obstacles = []; this.hole = new Hole(canvas.width - 50, 50); this.startPosition = new Vector2(50, canvas.height - 50); this.strokes = 0; this.gameWon = false; this.aiming = false; this.aimStart = new Vector2(0, 0); this.aimCurrent = new Vector2(0, 0); this.setupEventListeners(); this.generateCourse(); this.gameLoop(); } setupEventListeners() { this.canvas.addEventListener('mousedown', (e) => { if (!this.ball.isMoving() && !this.gameWon) { this.aiming = true; const rect = this.canvas.getBoundingClientRect(); this.aimStart = new Vector2( e.clientX - rect.left, e.clientY - rect.top ); this.aimCurrent = this.aimStart; } }); this.canvas.addEventListener('mousemove', (e) => { if (this.aiming) { const rect = this.canvas.getBoundingClientRect(); this.aimCurrent = new Vector2( e.clientX - rect.left, e.clientY - rect.top ); this.updatePowerMeter(); } }); this.canvas.addEventListener('mouseup', () => { if (this.aiming) { this.shoot(); this.aiming = false; document.getElementById('powerFill').style.width = '0%'; } }); this.canvas.addEventListener('mouseleave', () => { if (this.aiming) { this.aiming = false; document.getElementById('powerFill').style.width = '0%'; } }); } updatePowerMeter() { const aimVector = this.aimCurrent.subtract(this.aimStart); const power = Math.min(aimVector.magnitude() / 100, 1); document.getElementById('powerFill').style.width = (power * 100) + '%'; } generateCourse() { this.obstacles = []; // Generate random obstacles for (let i = 0; i < 5; i++) { let x, y, width, height; let validPosition = false; let attempts = 0; while (!validPosition && attempts < 50) { width = 20 + Math.random() * 60; height = 20 + Math.random() * 60; x = Math.random() * (this.canvas.width - width); y = Math.random() * (this.canvas.height - height); validPosition = true; // Check if obstacle interferes with start position if (x < 100 && y > this.canvas.height - 100) { validPosition = false; } // Check if obstacle interferes with hole if (x + width > this.canvas.width - 100 && y < 100) { validPosition = false; } attempts++; } if (validPosition) { this.obstacles.push(new Obstacle(x, y, width, height)); } } } shoot() { const aimVector = this.aimCurrent.subtract(this.aimStart); const force = aimVector.multiply(-0.02); // Negative for opposite direction this.ball.applyForce(force); this.strokes++; document.getElementById('strokes').textContent = this.strokes; } checkBoundaryCollisions() { // Left boundary if (this.ball.position.x - this.ball.radius < 0) { this.ball.position.x = this.ball.radius; this.ball.velocity.x = -this.ball.velocity.x * this.ball.bounceReduction; } // Right boundary if (this.ball.position.x + this.ball.radius > this.canvas.width) { this.ball.position.x = this.canvas.width - this.ball.radius; this.ball.velocity.x = -this.ball.velocity.x * this.ball.bounceReduction; } // Top boundary if (this.ball.position.y - this.ball.radius < 0) { this.ball.position.y = this.ball.radius; this.ball.velocity.y = -this.ball.velocity.y * this.ball.bounceReduction; } // Bottom boundary if (this.ball.position.y + this.ball.radius > this.canvas.height) { this.ball.position.y = this.canvas.height - this.ball.radius; this.ball.velocity.y = -this.ball.velocity.y * this.ball.bounceReduction; } } update() { if (!this.gameWon) { this.ball.update(); this.checkBoundaryCollisions(); // Check obstacle collisions for (let obstacle of this.obstacles) { obstacle.checkCollision(this.ball); } // Check hole-in-one if (this.hole.checkHoleInOne(this.ball)) { this.gameWon = true; this.ball.velocity = new Vector2(0, 0); setTimeout(() => { alert(`Congratulations! You completed the hole in ${this.strokes} strokes!`); }, 100); } } } draw() { // Clear canvas this.ctx.fillStyle = '#2d5a2d'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // Draw course elements this.hole.draw(this.ctx); for (let obstacle of this.obstacles) { obstacle.draw(this.ctx); } this.ball.draw(this.ctx); // Draw aiming line if (this.aiming) { this.ctx.beginPath(); this.ctx.moveTo(this.ball.position.x, this.ball.position.y); this.ctx.lineTo(this.aimCurrent.x, this.aimCurrent.y); this.ctx.strokeStyle = '#ffff00'; this.ctx.lineWidth = 2; this.ctx.stroke(); // Draw power indicator const aimVector = this.aimCurrent.subtract(this.aimStart); const endPoint = this.ball.position.add(aimVector.multiply(-1)); this.ctx.beginPath(); this.ctx.arc(endPoint.x, endPoint.y, 5, 0, Math.PI * 2); this.ctx.fillStyle = '#ff0000'; this.ctx.fill(); } // Draw start position marker this.ctx.beginPath(); this.ctx.arc(this.startPosition.x, this.startPosition.y, 12, 0, Math.PI * 2); this.ctx.strokeStyle = '#00ff00'; this.ctx.lineWidth = 3; this.ctx.stroke(); } gameLoop() { this.update(); this.draw(); requestAnimationFrame(() => this.gameLoop()); } reset() { this.ball.position = new Vector2(this.startPosition.x, this.startPosition.y); this.ball.velocity = new Vector2(0, 0); this.strokes = 0; this.gameWon = false; document.getElementById('strokes').textContent = this.strokes; } newCourse() { this.generateCourse(); this.reset(); } } // Initialize game const canvas = document.getElementById('gameCanvas'); const game = new MiniGolfGame(canvas); // Global functions for buttons function resetGame() { game.reset(); } function generateNewCourse() { game.newCourse(); } </script> </body> </html> ``` This mini-golf game engine includes: **Core Features:** - **Ball Physics**: Realistic movement with velocity, friction, and momentum - **Collision Detection**: Accurate collision with rectangular obstacles and boundaries - **Hole-in-One Logic**: Detects when ball enters hole at appropriate speed **Physics Engine:** - Vector2 math class for position and velocity calculations - Friction and bounce reduction for realistic ball behavior - Boundary collision detection and response - Obstacle collision with proper reflection physics **Game Mechanics:** - Click and drag aiming system with visual feedback - Power meter showing shot strength - Stroke counter - Win condition detection - Course generation with random obstacles **Interactive Elements:** - Mouse controls for aiming and shooting - Visual power indicator - Reset and new course buttons - Real-time physics simulation at 60fps The engine handles all the core requirements: ball physics simulation, collision detection with obstacles and boundaries, and proper hole-in-one detection when the ball enters the hole at a reasonable speed.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mini Golf Game Engine</title> <style> body { margin: 0; overflow: hidden; background-color: #f0f0f0; font-family: Arial, sans-serif; } canvas { display: block; background-color: #7bad69; } .controls { position: absolute; bottom: 20px; left: 20px; background-color: rgba(255, 255, 255, 0.7); padding: 10px; border-radius: 5px; } .info { position: absolute; top: 20px; left: 20px; background-color: rgba(255, 255, 255, 0.7); padding: 10px; border-radius: 5px; } </style> </head> <body> <canvas id="gameCanvas"></canvas> <div class="info"> <p>Strokes: <span id="strokeCount">0</span></p> <p id="message"></p> </div> <div class="controls"> <p>Click and drag from the ball to putt. Release to shoot.</p> <button id="resetBtn">Reset Ball</button> </div> <script> // Game canvas setup const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Game state variables let strokeCount = 0; let messageElement = document.getElementById('message'); let strokeCountElement = document.getElementById('strokeCount'); // Physics constants const FRICTION = 0.98; const VELOCITY_THRESHOLD = 0.05; const MAX_POWER = 25; // Ball properties const ball = { x: 100, y: canvas.height / 2, radius: 10, velocityX: 0, velocityY: 0, isMoving: false, originalX: 100, originalY: canvas.height / 2 }; // Hole properties const hole = { x: canvas.width - 100, y: canvas.height / 2, radius: 20 }; // Obstacles const obstacles = [ { type: 'wall', x: canvas.width / 2 - 100, y: 100, width: 20, height: canvas.height - 200 }, { type: 'circle', x: canvas.width / 3, y: canvas.height / 3, radius: 40 }, { type: 'circle', x: canvas.width * 2/3, y: canvas.height * 2/3, radius: 40 } ]; // Input handling let isDragging = false; let startX, startY; let endX, endY; // Event listeners canvas.addEventListener('mousedown', handleMouseDown); canvas.addEventListener('mousemove', handleMouseMove); canvas.addEventListener('mouseup', handleMouseUp); document.getElementById('resetBtn').addEventListener('click', resetBall); function handleMouseDown(e) { const rect = canvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; // Check if mouse is on the ball const distance = Math.sqrt((mouseX - ball.x) ** 2 + (mouseY - ball.y) ** 2); if (distance <= ball.radius && !ball.isMoving) { isDragging = true; startX = mouseX; startY = mouseY; } } function handleMouseMove(e) { if (!isDragging) return; const rect = canvas.getBoundingClientRect(); endX = e.clientX - rect.left; endY = e.clientY - rect.top; } function handleMouseUp() { if (!isDragging) return; if (!ball.isMoving) { // Calculate direction and power const deltaX = startX - endX; const deltaY = startY - endY; const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2); // Apply force (with capped power) const power = Math.min(distance / 10, MAX_POWER); ball.velocityX = (deltaX / distance) * power; ball.velocityY = (deltaY / distance) * power; ball.isMoving = true; // Increment stroke count strokeCount++; strokeCountElement.textContent = strokeCount; messageElement.textContent = ''; } isDragging = false; } function resetBall() { ball.x = ball.originalX; ball.y = ball.originalY; ball.velocityX = 0; ball.velocityY = 0; ball.isMoving = false; messageElement.textContent = ''; } // Game loop function gameLoop() { update(); render(); requestAnimationFrame(gameLoop); } function update() { if (ball.isMoving) { // Apply physics ball.x += ball.velocityX; ball.y += ball.velocityY; ball.velocityX *= FRICTION; ball.velocityY *= FRICTION; // Wall collision if (ball.x - ball.radius < 0) { ball.x = ball.radius; ball.velocityX = -ball.velocityX * 0.8; } if (ball.x + ball.radius > canvas.width) { ball.x = canvas.width - ball.radius; ball.velocityX = -ball.velocityX * 0.8; } if (ball.y - ball.radius < 0) { ball.y = ball.radius; ball.velocityY = -ball.velocityY * 0.8; } if (ball.y + ball.radius > canvas.height) { ball.y = canvas.height - ball.radius; ball.velocityY = -ball.velocityY * 0.8; } // Obstacle collision for (const obstacle of obstacles) { if (obstacle.type === 'wall') { // Rectangle collision checkRectangleCollision(ball, obstacle); } else if (obstacle.type === 'circle') { // Circle collision checkCircleCollision(ball, obstacle); } } // Check if ball stopped if (Math.abs(ball.velocityX) < VELOCITY_THRESHOLD && Math.abs(ball.velocityY) < VELOCITY_THRESHOLD) { ball.velocityX = 0; ball.velocityY = 0; ball.isMoving = false; } // Check for hole-in-one const distanceToHole = Math.sqrt((ball.x - hole.x) ** 2 + (ball.y - hole.y) ** 2); if (distanceToHole < hole.radius && !ball.isMoving) { messageElement.textContent = 'Hole in ' + strokeCount + '!'; setTimeout(() => { resetBall(); strokeCount = 0; strokeCountElement.textContent = strokeCount; }, 2000); } } } function checkRectangleCollision(ball, rect) { // Find the closest point to the circle within the rectangle let closestX = Math.max(rect.x, Math.min(ball.x, rect.x + rect.width)); let closestY = Math.max(rect.y, Math.min(ball.y, rect.y + rect.height)); // Calculate distance from closest point to circle's center const distanceX = ball.x - closestX; const distanceY = ball.y - closestY; const distance = Math.sqrt(distanceX ** 2 + distanceY ** 2); if (distance <= ball.radius) { // Collision detected, determine bounce direction const normalX = distanceX / distance; const normalY = distanceY / distance; // Calculate reflection const dotProduct = ball.velocityX * normalX + ball.velocityY * normalY; ball.velocityX = ball.velocityX - 2 * dotProduct * normalX; ball.velocityY = ball.velocityY - 2 * dotProduct * normalY; // Move ball outside the collision const overlap = ball.radius - distance; ball.x += normalX * overlap; ball.y += normalY * overlap; } } function checkCircleCollision(ball, circle) { const dx = ball.x - circle.x; const dy = ball.y - circle.y; const distance = Math.sqrt(dx ** 2 + dy ** 2); const sumRadii = ball.radius + circle.radius; if (distance <= sumRadii) { // Collision detected const nx = dx / distance; const ny = dy / distance; // Calculate reflection const dotProduct = ball.velocityX * nx + ball.velocityY * ny; ball.velocityX = ball.velocityX - 2 * dotProduct * nx * 0.8; // 0.8 is bounce factor ball.velocityY = ball.velocityY - 2 * dotProduct * ny * 0.8; // Move ball outside the collision const overlap = sumRadii - distance; ball.x += nx * overlap; ball.y += ny * overlap; } } function render() { // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw grass pattern ctx.fillStyle = '#7bad69'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw hole ctx.beginPath(); ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2); ctx.fillStyle = 'black'; ctx.fill(); ctx.closePath(); ctx.beginPath(); ctx.arc(hole.x, hole.y, hole.radius - 3, 0, Math.PI * 2); ctx.fillStyle = '#555'; ctx.fill(); ctx.closePath(); // Draw obstacles for (const obstacle of obstacles) { ctx.fillStyle = '#a67c52'; // Brown if (obstacle.type === 'wall') { ctx.fillRect(obstacle.x, obstacle.y, obstacle.width, obstacle.height); } else if (obstacle.type === 'circle') { ctx.beginPath(); ctx.arc(obstacle.x, obstacle.y, obstacle.radius, 0, Math.PI * 2); ctx.fill(); ctx.closePath(); } } // Draw ball ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); ctx.fillStyle = 'white'; ctx.fill(); ctx.closePath(); // Draw shooting line if (isDragging && !ball.isMoving) { ctx.beginPath(); ctx.moveTo(ball.x, ball.y); ctx.lineTo(endX, endY); ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; ctx.lineWidth = 2; ctx.stroke(); ctx.closePath(); // Draw power indicator const deltaX = startX - endX; const deltaY = startY - endY; const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2); const power = Math.min(distance / 10, MAX_POWER); const powerRatio = power / MAX_POWER; // Draw power meter ctx.fillStyle = `rgba(${255 * powerRatio}, ${255 * (1-powerRatio)}, 0, 0.7)`; ctx.fillRect(ball.x + 20, ball.y - 30, 10, -powerRatio * 50); } } // Start game loop gameLoop(); // Handle window resize window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Adjust hole position hole.x = canvas.width - 100; hole.y = canvas.height / 2; // Adjust obstacles obstacles[0].x = canvas.width / 2 - 100; obstacles[0].height = canvas.height - 200; obstacles[1].x = canvas.width / 3; obstacles[1].y = canvas.height / 3; obstacles[2].x = canvas.width * 2/3; obstacles[2].y = canvas.height * 2/3; }); </script> </body> </html> ```