Test-build

import React, { useEffect, useRef, useState, useMemo } from “react”;

// Pothole Dodger — a tiny driving game
// Controls: ←/A and →/D to steer. Space/Enter to start or restart.
// Touch: use on-screen arrows.

export default function PotholeDodger() {
// UI state
const [running, setRunning] = useState(false);
const [gameOver, setGameOver] = useState(false);
const [score, setScore] = useState(0);
const [best, setBest] = useState(() => {
const b = (typeof localStorage !== “undefined” && localStorage.getItem(“pd_best”)) || “0”;
return parseInt(b, 10) || 0;
});
const [lives, setLives] = useState(3);
const [difficulty, setDifficulty] = useState(1);

// Input state
const inputRef = useRef({ left: false, right: false });

// Canvas refs
const canvasRef = useRef(null);
const rafRef = useRef(0);
const timeRef = useRef({ last: 0, spawnAcc: 0, laneMarkOffset: 0 });

// Game objects
const carRef = useRef({ x: 0, y: 0, w: 48, h: 90, speed: 360 });
const potholesRef = useRef([]);
const worldRef = useRef({ width: 420, height: 720, lanes: 3, roadPadding: 20 });

const dpiScaleCanvas = () => {
const canvas = canvasRef.current;
if (!canvas) return { ctx: null, scale: 1 };
const dpr = window.devicePixelRatio || 1;
const parent = canvas.parentElement;
const maxW = Math.min(parent.clientWidth, 500);
const aspect = worldRef.current.height / worldRef.current.width; // ~1.71
const cssW = maxW;
const cssH = Math.round(cssW * aspect);
canvas.style.width = cssW + “px”;
canvas.style.height = cssH + “px”;
canvas.width = Math.round(cssW * dpr);
canvas.height = Math.round(cssH * dpr);
const ctx = canvas.getContext(“2d”);
ctx.setTransform(dpr * (worldRef.current.width / cssW), 0, 0, dpr * (worldRef.current.height / cssH), 0, 0);
return { ctx, scale: dpr };
};

const resetGame = () => {
setScore(0);
setLives(3);
setDifficulty(1);
timeRef.current = { last: 0, spawnAcc: 0, laneMarkOffset: 0 };
potholesRef.current = [];
const world = worldRef.current;
const car = carRef.current;
car.w = 48; car.h = 90; car.speed = 360;
car.x = world.width / 2 – car.w / 2;
car.y = world.height – car.h – 30;
};

// Spawn a pothole in a random lane
const spawnPothole = () => {
const world = worldRef.current;
const lanes = world.lanes;
const roadW = world.width – world.roadPadding * 2;
const laneW = roadW / lanes;
const lane = Math.floor(Math.random() * lanes);
const size = 36 + Math.random() * 32; // diameter
const speed = 140 + Math.random() * 80 + (difficulty – 1) * 40; // pixels/sec
const x = world.roadPadding + lane * laneW + laneW / 2 – size / 2;
potholesRef.current.push({ x, y: -size, w: size, h: size, speed });
};

// Collision check (AABB)
const intersects = (a, b) => !(a.x > b.x + b.w || a.x + a.w < b.x || a.y > b.y + b.h || a.y + a.h < b.y); // Main loop useEffect(() => {
resetGame();
const handleResize = () => dpiScaleCanvas();
window.addEventListener(“resize”, handleResize);

const loop = (t) => {
const { ctx } = dpiScaleCanvas();
if (!ctx) { rafRef.current = requestAnimationFrame(loop); return; }
const world = worldRef.current;
const car = carRef.current;

if (!timeRef.current.last) timeRef.current.last = t;
const dt = Math.min(0.033, (t – timeRef.current.last) / 1000); // clamp
timeRef.current.last = t;

// Update input -> car movement
const input = inputRef.current;
let vx = 0;
if (input.left) vx -= 1;
if (input.right) vx += 1;
car.x += vx * car.speed * dt;

// Keep car on road
const minX = world.roadPadding + 8;
const maxX = world.width – world.roadPadding – car.w – 8;
car.x = Math.max(minX, Math.min(maxX, car.x));

// Difficulty ramp based on score
const newDifficulty = 1 + Math.floor(score / 300);
if (newDifficulty !== difficulty) setDifficulty(newDifficulty);

// Spawn logic (faster with difficulty)
const spawnEvery = Math.max(0.35, 1.1 – difficulty * 0.15);
timeRef.current.spawnAcc += dt;
if (running && timeRef.current.spawnAcc >= spawnEvery) {
timeRef.current.spawnAcc = 0;
spawnPothole();
}

// Move potholes
potholesRef.current.forEach(p => p.y += p.speed * dt);
// Remove off-screen potholes and award points
const before = potholesRef.current.length;
potholesRef.current = potholesRef.current.filter(p => p.y < world.height + 60); const passed = before – potholesRef.current.length; if (running && passed > 0) setScore(s => s + passed * 10);

// Collisions
if (running) {
for (let i = 0; i < potholesRef.current.length; i++) { const p = potholesRef.current[i]; if (intersects(car, p)) { // Hit! consume one life and briefly grant mercy by popping pothole setLives(L => Math.max(0, L – 1));
potholesRef.current.splice(i, 1);
break;
}
}
}

// Game over check
if (running && lives <= 0) { setRunning(false); setGameOver(true); setBest(b => {
const nb = Math.max(b, score);
try { localStorage.setItem(“pd_best”, String(nb)); } catch {}
return nb;
});
}

// Draw
draw(ctx);

rafRef.current = requestAnimationFrame(loop);
};

rafRef.current = requestAnimationFrame(loop);

return () => {
cancelAnimationFrame(rafRef.current);
window.removeEventListener(“resize”, handleResize);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [running, lives, score, difficulty]);

const draw = (ctx) => {
const world = worldRef.current;
const car = carRef.current;

// Clear
ctx.fillStyle = “#111827”; // bg
ctx.fillRect(0, 0, world.width, world.height);

// Road
const roadX = world.roadPadding;
const roadW = world.width – world.roadPadding * 2;
ctx.fillStyle = “#374151”; // road gray
ctx.fillRect(roadX, 0, roadW, world.height);

// Lane markers (dashed)
const lanes = world.lanes;
const laneW = roadW / lanes;
const dashH = 28;
const gap = 16;
timeRef.current.laneMarkOffset = (timeRef.current.laneMarkOffset + 6) % (dashH + gap);
ctx.fillStyle = “#fef3c7”; // pale yellow
for (let i = 1; i < lanes; i++) {
const x = roadX + i * laneW – 2;
for (let y = -dashH; y < world.height + dashH; y += dashH + gap) { ctx.fillRect(x, y + timeRef.current.laneMarkOffset, 4, dashH); } } // Draw potholes for (const p of potholesRef.current) { ctx.fillStyle = “#0f172a”; // dark outline roundRect(ctx, p.x – 2, p.y – 2, p.w + 4, p.h + 4, 8); ctx.fill(); const grad = ctx.createRadialGradient(p.x + p.w/2, p.y + p.h/2, 4, p.x + p.w/2, p.y + p.h/2, p.w/2); grad.addColorStop(0, “#111827”); grad.addColorStop(1, “#4b5563”); ctx.fillStyle = grad; roundRect(ctx, p.x, p.y, p.w, p.h, 10); ctx.fill(); } // Draw car // Car body ctx.fillStyle = “#ef4444”; // red roundRect(ctx, car.x, car.y, car.w, car.h, 10); ctx.fill(); // windows ctx.fillStyle = “#cbd5e1”; roundRect(ctx, car.x + 8, car.y + 12, car.w – 16, 24, 6); ctx.fill(); roundRect(ctx, car.x + 8, car.y + car.h – 36, car.w – 16, 24, 6); ctx.fill(); // wheels ctx.fillStyle = “#1f2937”; ctx.fillRect(car.x – 8, car.y + 16, 8, 20); ctx.fillRect(car.x + car.w, car.y + 16, 8, 20); ctx.fillRect(car.x – 8, car.y + car.h – 36, 8, 20); ctx.fillRect(car.x + car.w, car.y + car.h – 36, 8, 20); // HUD shadow panel ctx.globalAlpha = 0.85; ctx.fillStyle = “#111827”; roundRect(ctx, 12, 12, 180, 64, 10); ctx.fill(); ctx.globalAlpha = 1; // HUD text ctx.fillStyle = “#e5e7eb”; ctx.font = “16px Inter, ui-sans-serif”; ctx.fillText(`Score: ${score}`, 24, 36); ctx.fillText(`Lives: ${lives}`, 24, 56); ctx.fillText(`Best: ${best}`, 24, 76); if (!running && !gameOver) { drawCenterText(ctx, “Pothole Dodger”, 36, world.height / 2 – 40); drawCenterText(ctx, “Press Space or Tap ▶ to Start”, 18, world.height / 2 + 4); drawCenterText(ctx, “←/A and →/D to steer”, 16, world.height / 2 + 28); } if (gameOver) { drawCenterText(ctx, “Game Over”, 36, world.height / 2 – 40); drawCenterText(ctx, `Score: ${score}`, 20, world.height / 2 + 0); drawCenterText(ctx, “Press Space or Tap ↻ to Restart”, 16, world.height / 2 + 28); } }; // Helpers const drawCenterText = (ctx, text, size, y) => {
ctx.fillStyle = “#e5e7eb”;
ctx.font = `${size}px Inter, ui-sans-serif`;
const metrics = ctx.measureText(text);
const x = (worldRef.current.width – metrics.width) / 2;
ctx.fillText(text, x, y);
};

function roundRect(ctx, x, y, w, h, r) {
const radius = Math.min(r, w/2, h/2);
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.arcTo(x + w, y, x + w, y + h, radius);
ctx.arcTo(x + w, y + h, x, y + h, radius);
ctx.arcTo(x, y + h, x, y, radius);
ctx.arcTo(x, y, x + w, y, radius);
ctx.closePath();
}

// Keyboard controls
useEffect(() => {
const down = (e) => {
if (e.repeat) return;
if (e.key === “ArrowLeft” || e.key.toLowerCase() === “a”) inputRef.current.left = true;
if (e.key === “ArrowRight” || e.key.toLowerCase() === “d”) inputRef.current.right = true;
if (!running && (e.code === “Space” || e.key === “Enter”)) startOrRestart();
};
const up = (e) => {
if (e.key === “ArrowLeft” || e.key.toLowerCase() === “a”) inputRef.current.left = false;
if (e.key === “ArrowRight” || e.key.toLowerCase() === “d”) inputRef.current.right = false;
};
window.addEventListener(“keydown”, down);
window.addEventListener(“keyup”, up);
return () => { window.removeEventListener(“keydown”, down); window.removeEventListener(“keyup”, up); };
}, [running, gameOver]);

const startOrRestart = () => {
if (!running && !gameOver) {
setRunning(true);
return;
}
if (gameOver) {
resetGame();
setGameOver(false);
setRunning(true);
}
};

// Touch controls
const pressLeft = (down) => { inputRef.current.left = down; };
const pressRight = (down) => { inputRef.current.right = down; };

return (

Pothole Dodger

{!running && !gameOver && (

)}
{gameOver && (

)}

{/* Touch arrows */}


Dodge potholes to score points. Passing a pothole +10. You have 3 lives. Difficulty ramps up with score. Good luck!

);
}