
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>
```