|
| 1 | +import React, { useRef, useState, useEffect } from "react"; |
| 2 | + |
| 3 | + |
| 4 | +const styles = { |
| 5 | + container: { |
| 6 | + fontFamily: "'Trebuchet MS', sans-serif", |
| 7 | + background: "linear-gradient(135deg, #00b4d8 0%, #90e0ef 100%)", |
| 8 | + height: "100vh", |
| 9 | + width: "100vw", |
| 10 | + overflow: "hidden", |
| 11 | + position: "relative", |
| 12 | + display: "flex", |
| 13 | + alignItems: "center", |
| 14 | + justifyContent: "center" |
| 15 | + }, |
| 16 | + screen: { |
| 17 | + background: "rgba(255,255,255,0.95)", |
| 18 | + borderRadius: "18px", |
| 19 | + boxShadow: "0 4px 24px rgba(0,0,0,0.15)", |
| 20 | + padding: "32px 38px", |
| 21 | + minWidth: "340px", |
| 22 | + textAlign: "center", |
| 23 | + animation: "fadeInScreen 0.7s" |
| 24 | + }, |
| 25 | + btn: { |
| 26 | + margin: "12px", |
| 27 | + padding: "10px 28px", |
| 28 | + borderRadius: "16px", |
| 29 | + border: "none", |
| 30 | + background: "linear-gradient(90deg,#00b4d8,#48cae4)", |
| 31 | + color: "#fff", |
| 32 | + fontSize: "18px", |
| 33 | + cursor: "pointer", |
| 34 | + fontWeight: 500, |
| 35 | + transition: "background .2s" |
| 36 | + }, |
| 37 | + canvasBox: { |
| 38 | + borderRadius: "14px", |
| 39 | + overflow: "hidden", |
| 40 | + boxShadow: "0 4px 16px rgba(68,202,228,0.12)", |
| 41 | + }, |
| 42 | +}; |
| 43 | + |
| 44 | +const DIFF_SETTINGS = { |
| 45 | + Easy: { pipeGap: 170, pipeSpeed: 2, duration: 45 }, |
| 46 | + Medium: { pipeGap: 140, pipeSpeed: 2.6, duration: 40 }, |
| 47 | + Hard: { pipeGap: 110, pipeSpeed: 3.6, duration: 35 }, |
| 48 | +}; |
| 49 | + |
| 50 | +const CANVAS_WIDTH = 420; |
| 51 | +const CANVAS_HEIGHT = 500; |
| 52 | + |
| 53 | +function randomPipeY(gap) { |
| 54 | + |
| 55 | + return 80 + Math.random() * (CANVAS_HEIGHT - gap - 120); |
| 56 | +} |
| 57 | + |
| 58 | +const welcomeText = ( |
| 59 | + <div> |
| 60 | + <h2>Welcome to Flappy Bird!</h2> |
| 61 | + <div style={{ fontSize: 18, marginBottom: 12 }}> |
| 62 | + <b>How To Play:</b><br /> |
| 63 | + Tap/press <b>Space</b> or click to make the bird .<br /> |
| 64 | + Avoid hitting pipes — survive as long as you can.<br /> |
| 65 | + Game lasts for a set duration depending on difficulty.<br /> |
| 66 | + <span style={{ color: "#00b4d8", fontWeight: 700 }}>Score points for passing pipes!</span> |
| 67 | + </div> |
| 68 | + </div> |
| 69 | +); |
| 70 | + |
| 71 | + |
| 72 | +function FlappyBirdGame({ difficulty, onGameEnd }) { |
| 73 | + const canvasRef = useRef(null); |
| 74 | + const [score, setScore] = useState(0); |
| 75 | + const [isRunning, setIsRunning] = useState(true); |
| 76 | + const [secondsLeft, setSecondsLeft] = useState(DIFF_SETTINGS[difficulty].duration); |
| 77 | + |
| 78 | + |
| 79 | + const bird = useRef({ |
| 80 | + x: 70, |
| 81 | + y: CANVAS_HEIGHT/2, |
| 82 | + vy: 0, |
| 83 | + radius: 18 |
| 84 | + }); |
| 85 | + |
| 86 | + const pipes = useRef([ |
| 87 | + { x: CANVAS_WIDTH + 40, y: randomPipeY(DIFF_SETTINGS[difficulty].pipeGap) } |
| 88 | + ]); |
| 89 | + |
| 90 | + |
| 91 | + useEffect(() => { |
| 92 | + |
| 93 | + let timer = setInterval(() => { |
| 94 | + setSecondsLeft(sec => { |
| 95 | + if (sec > 0 && isRunning) return sec - 1; |
| 96 | + return sec; |
| 97 | + }); |
| 98 | + }, 1000); |
| 99 | + |
| 100 | + return () => clearInterval(timer); |
| 101 | + }, [difficulty, isRunning]); |
| 102 | + |
| 103 | + useEffect(() => { |
| 104 | + if (secondsLeft === 0 && isRunning) { |
| 105 | + setIsRunning(false); |
| 106 | + setTimeout(() => onGameEnd(score), 530); |
| 107 | + } |
| 108 | + }, [secondsLeft, isRunning, onGameEnd, score]); |
| 109 | + |
| 110 | + |
| 111 | + useEffect(() => { |
| 112 | + const jump = () => { |
| 113 | + if (isRunning) bird.current.vy = -4.8; |
| 114 | + }; |
| 115 | + window.addEventListener('keydown', e => { |
| 116 | + if (e.code === 'Space') jump(); |
| 117 | + }); |
| 118 | + window.addEventListener('mousedown', jump); |
| 119 | + return () => { |
| 120 | + window.removeEventListener('keydown', () => {}); |
| 121 | + window.removeEventListener('mousedown', () => {}); |
| 122 | + }; |
| 123 | + }, [isRunning]); |
| 124 | + |
| 125 | + |
| 126 | + useEffect(() => { |
| 127 | + let requestId; |
| 128 | + const ctx = canvasRef.current.getContext("2d"); |
| 129 | + let lastPassed = 0; |
| 130 | + |
| 131 | + function draw() { |
| 132 | + |
| 133 | + ctx.fillStyle = "#caf0f8"; |
| 134 | + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); |
| 135 | + |
| 136 | + |
| 137 | + for (let c=0; c<3; ++c) { |
| 138 | + ctx.globalAlpha = 0.39 + 0.25*Math.cos(Date.now()/700 + c*2); |
| 139 | + ctx.beginPath(); |
| 140 | + ctx.arc((c*140+50 + Date.now()/c)/1.8 % CANVAS_WIDTH, 60+36*c, 36+c*10, 0, Math.PI*2); |
| 141 | + ctx.fillStyle = "#fff"; |
| 142 | + ctx.fill(); |
| 143 | + ctx.globalAlpha = 1; |
| 144 | + } |
| 145 | + |
| 146 | + |
| 147 | + ctx.save(); |
| 148 | + ctx.shadowColor = "#90e0ef80"; |
| 149 | + ctx.shadowBlur = 12; |
| 150 | + pipes.current.forEach((pipe,i) => { |
| 151 | + ctx.fillStyle = "#00b4d8"; |
| 152 | + |
| 153 | + ctx.fillRect(pipe.x, 0, 52, pipe.y); |
| 154 | + ctx.strokeStyle = "#90e0ef"; |
| 155 | + ctx.strokeRect(pipe.x, 0, 52, pipe.y); |
| 156 | + |
| 157 | + ctx.fillRect(pipe.x, pipe.y + DIFF_SETTINGS[difficulty].pipeGap, 52, CANVAS_HEIGHT - pipe.y - DIFF_SETTINGS[difficulty].pipeGap); |
| 158 | + ctx.strokeRect(pipe.x, pipe.y + DIFF_SETTINGS[difficulty].pipeGap, 52, CANVAS_HEIGHT - pipe.y - DIFF_SETTINGS[difficulty].pipeGap); |
| 159 | + }); |
| 160 | + ctx.restore(); |
| 161 | + |
| 162 | + |
| 163 | + ctx.save(); |
| 164 | + ctx.globalAlpha = 0.25; |
| 165 | + ctx.beginPath(); |
| 166 | + ctx.arc(bird.current.x, bird.current.y + 18, bird.current.radius, 0, Math.PI * 2); |
| 167 | + ctx.fillStyle = "#888"; |
| 168 | + ctx.fill(); |
| 169 | + ctx.restore(); |
| 170 | + |
| 171 | + |
| 172 | + ctx.save(); |
| 173 | + ctx.beginPath(); |
| 174 | + ctx.arc(bird.current.x, bird.current.y, bird.current.radius, 0, Math.PI*2); |
| 175 | + ctx.fillStyle = "#ffbe0b"; |
| 176 | + ctx.strokeStyle = "#ffd60a"; |
| 177 | + ctx.lineWidth = 3 + 2*Math.abs(Math.sin(Date.now()/400)); |
| 178 | + ctx.fill(); |
| 179 | + ctx.stroke(); |
| 180 | + |
| 181 | + ctx.beginPath(); |
| 182 | + ctx.arc(bird.current.x+8, bird.current.y-6, 4, 0, Math.PI*2); |
| 183 | + ctx.fillStyle = "#fff"; |
| 184 | + ctx.fill(); |
| 185 | + ctx.beginPath(); |
| 186 | + ctx.arc(bird.current.x+10, bird.current.y-6, 1.5, 0, Math.PI*2); |
| 187 | + ctx.fillStyle = "#111"; |
| 188 | + ctx.fill(); |
| 189 | + |
| 190 | + ctx.beginPath(); |
| 191 | + ctx.ellipse(bird.current.x-8, bird.current.y, 13, 7 + 8*Math.abs(Math.cos(Date.now()/260)), 0, 0, Math.PI*2); |
| 192 | + ctx.fillStyle = "#fdfcdc"; |
| 193 | + ctx.fill(); |
| 194 | + ctx.restore(); |
| 195 | + |
| 196 | + |
| 197 | + ctx.save(); |
| 198 | + ctx.font = "21px Trebuchet MS"; |
| 199 | + ctx.fillStyle = "#0077b6"; |
| 200 | + ctx.fillText(`Score: ${score}`, 24, 42); |
| 201 | + ctx.fillStyle = "#03045e"; |
| 202 | + ctx.fillText(`Time: ${secondsLeft}s`, CANVAS_WIDTH-120, 42); |
| 203 | + ctx.restore(); |
| 204 | + } |
| 205 | + |
| 206 | + function gameLoop() { |
| 207 | + if (!isRunning) return; |
| 208 | + |
| 209 | + pipes.current.forEach(pipe => pipe.x -= DIFF_SETTINGS[difficulty].pipeSpeed); |
| 210 | + |
| 211 | + |
| 212 | + if (pipes.current.length && pipes.current[0].x < -52) pipes.current.shift(); |
| 213 | + let lastPipe = pipes.current[pipes.current.length - 1]; |
| 214 | + if (lastPipe.x < CANVAS_WIDTH - 180) { |
| 215 | + pipes.current.push({ |
| 216 | + x: CANVAS_WIDTH + 44, |
| 217 | + y: randomPipeY(DIFF_SETTINGS[difficulty].pipeGap) |
| 218 | + }); |
| 219 | + } |
| 220 | + |
| 221 | + |
| 222 | + bird.current.vy += 0.34; |
| 223 | + bird.current.y += bird.current.vy; |
| 224 | + |
| 225 | + |
| 226 | + if (bird.current.y > CANVAS_HEIGHT- bird.current.radius) { |
| 227 | + bird.current.y = CANVAS_HEIGHT- bird.current.radius; |
| 228 | + bird.current.vy = 0; |
| 229 | + setIsRunning(false); |
| 230 | + setTimeout(()=>onGameEnd(score),450); |
| 231 | + } |
| 232 | + if (bird.current.y < bird.current.radius) { |
| 233 | + bird.current.y = bird.current.radius + 3; |
| 234 | + bird.current.vy = 0.5; |
| 235 | + } |
| 236 | + |
| 237 | + |
| 238 | + for (let i=0; i<pipes.current.length; ++i) { |
| 239 | + let pipe = pipes.current[i]; |
| 240 | + let cx = bird.current.x, cy = bird.current.y, r = bird.current.radius; |
| 241 | + let pipeX = pipe.x, pipeW = 52; |
| 242 | + let gapY = pipe.y, gapH = DIFF_SETTINGS[difficulty].pipeGap; |
| 243 | + |
| 244 | + if (cx + r > pipeX && cx - r < pipeX + pipeW) { |
| 245 | + if (cy - r < gapY || cy + r > gapY + gapH) { |
| 246 | + setIsRunning(false); |
| 247 | + setTimeout(()=>onGameEnd(score),480); |
| 248 | + break; |
| 249 | + } |
| 250 | + } |
| 251 | + } |
| 252 | + |
| 253 | + |
| 254 | + pipes.current.forEach((pipe,idx) => { |
| 255 | + if (!pipe.passed && bird.current.x > pipe.x + 52) { |
| 256 | + pipe.passed = true; |
| 257 | + setScore(s => s + 1); |
| 258 | + } |
| 259 | + }); |
| 260 | + |
| 261 | + draw(); |
| 262 | + requestId = requestAnimationFrame(gameLoop); |
| 263 | + } |
| 264 | + |
| 265 | + draw(); |
| 266 | + requestId = requestAnimationFrame(gameLoop); |
| 267 | + |
| 268 | + return () => cancelAnimationFrame(requestId); |
| 269 | + }, [difficulty, isRunning, onGameEnd, score, secondsLeft]); |
| 270 | + |
| 271 | + return ( |
| 272 | + <div style={styles.canvasBox}> |
| 273 | + <canvas |
| 274 | + ref={canvasRef} |
| 275 | + width={CANVAS_WIDTH} |
| 276 | + height={CANVAS_HEIGHT} |
| 277 | + style={{ display:"block", background:"#caf0f8", borderRadius:14, margin:"auto" }} |
| 278 | + /> |
| 279 | + </div> |
| 280 | + ); |
| 281 | +} |
| 282 | + |
| 283 | + |
| 284 | +export default function FlappyBirdMiniGame() { |
| 285 | + const [screen, setScreen] = useState("welcome"); |
| 286 | + const [difficulty, setDifficulty] = useState(null); |
| 287 | + const [lastScore, setLastScore] = useState(0); |
| 288 | + |
| 289 | + return ( |
| 290 | + <div style={styles.container}> |
| 291 | + {screen === "welcome" && |
| 292 | + <div style={styles.screen}> |
| 293 | + {welcomeText} |
| 294 | + <div style={{ marginTop:16 }}> |
| 295 | + <b>Select Difficulty:</b> |
| 296 | + <div> |
| 297 | + {Object.keys(DIFF_SETTINGS).map(diff => ( |
| 298 | + <button |
| 299 | + key={diff} |
| 300 | + style={styles.btn} |
| 301 | + onClick={() => { |
| 302 | + setDifficulty(diff); |
| 303 | + setScreen("game"); |
| 304 | + }} |
| 305 | + >{diff}</button> |
| 306 | + ))} |
| 307 | + </div> |
| 308 | + </div> |
| 309 | + </div> |
| 310 | + } |
| 311 | + {screen === "game" && |
| 312 | + <FlappyBirdGame |
| 313 | + difficulty={difficulty} |
| 314 | + onGameEnd={score => { |
| 315 | + setLastScore(score); |
| 316 | + setScreen("result"); |
| 317 | + }} |
| 318 | + /> |
| 319 | + } |
| 320 | + {screen === "result" && |
| 321 | + <div style={styles.screen}> |
| 322 | + <h2>Game Over!</h2> |
| 323 | + <div style={{ fontSize:22, fontWeight:600, color:'#0096c7', margin:"14px 0" }}> |
| 324 | + Final Score: {lastScore} |
| 325 | + </div> |
| 326 | + <div>Ready for another round?</div> |
| 327 | + <button |
| 328 | + style={styles.btn} |
| 329 | + onClick={() => setScreen("welcome")} |
| 330 | + >Play Again</button> |
| 331 | + </div> |
| 332 | + } |
| 333 | + <style> |
| 334 | + {` |
| 335 | + @keyframes fadeInScreen { |
| 336 | + from { opacity: 0; transform: scale(.98);} |
| 337 | + to { opacity: 1; transform: scale(1);} |
| 338 | + } |
| 339 | + `} |
| 340 | + </style> |
| 341 | + </div> |
| 342 | + ); |
| 343 | +} |
0 commit comments