From 77205e8193e86b9323fee7123fcc1ef8e717810f Mon Sep 17 00:00:00 2001 From: root Date: Thu, 17 Apr 2025 20:33:11 +0200 Subject: [PATCH] Update 2025-04-17_20:33:11 --- .gitignore | 21 +++ .gitkeep | 0 main.py | 24 +++ requirements.txt | 3 + static/app.js | 363 ++++++++++++++++++++++++++++++++++++++++++++++ static/config.js | 23 +++ static/index.html | 66 +++++++++ static/style.css | 182 +++++++++++++++++++++++ 8 files changed, 682 insertions(+) create mode 100644 .gitignore create mode 100644 .gitkeep create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 static/app.js create mode 100644 static/config.js create mode 100644 static/index.html create mode 100644 static/style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b89c079 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Bytecode-Dateien +__pycache__/ +*.py[cod] + +# Virtuelle Umgebungen +.venv/ +venv/ + +# Betriebssystem-Dateien +.DS_Store +Thumbs.db + +# Logfiles und Dumps +*.log +*.bak +*.swp +*.tmp + +# IDEs und Editoren +.vscode/ +.idea/ diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py new file mode 100644 index 0000000..c326a20 --- /dev/null +++ b/main.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI, Request +from fastapi.responses import FileResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from pathlib import Path + +app = FastAPI() + +# Mount static files directory +app.mount("/static", StaticFiles(directory="static"), name="static") + +@app.get("/") +async def root(): + return FileResponse("static/index.html") + +@app.post("/api/start") +async def start_game(request: Request): + return JSONResponse({"status": "started"}) + +@app.post("/api/score") +async def submit_score(request: Request): + data = await request.json() + score = data.get("score", 0) + # TODO: Implement score saving logic + return JSONResponse({"status": "saved", "score": score}) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0af110b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +fastapi +uvicorn + diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..9cbf139 --- /dev/null +++ b/static/app.js @@ -0,0 +1,363 @@ +import { DEBUG, GAME_CONFIG } from './config.js'; + +class Game { + constructor() { + this.score = 0; + this.isRunning = false; + this.obstacles = []; + this.gameStartTime = 0; + this.gameTimer = null; + this.GAME_DURATION = 120000; // 2 minutes in milliseconds + this.lockedRoads = new Set(); // Track which roads are currently in use + this.collectedItems = new Set(); // Track unique collected items + this.init(); + } + + init() { + this.startButton = document.getElementById('start-game'); + this.scoreElement = document.getElementById('score'); + this.timeElement = document.getElementById('time'); + this.collectionElement = document.getElementById('collection'); + this.pipes = document.querySelectorAll('.pipe'); + this.roads = document.querySelectorAll('.road'); + + // Initialize UI with emojis + this.startButton.textContent = '🎮 Start Game'; + this.updateScore(); + this.timeElement.textContent = 'âąī¸ 2:00'; + this.updateCollection(); + + this.startButton.addEventListener('click', () => this.startGame()); + this.pipes.forEach(pipe => { + pipe.addEventListener('click', (e) => this.handlePipeClick(e)); + }); + + // Start collision detection loop + this.collisionDetectionLoop(); + } + + updateTimer() { + const timeLeft = Math.max(0, this.GAME_DURATION - (Date.now() - this.gameStartTime)); + const minutes = Math.floor(timeLeft / 60000); + const seconds = Math.floor((timeLeft % 60000) / 1000); + this.timeElement.textContent = `âąī¸ ${minutes}:${seconds.toString().padStart(2, '0')}`; + + if (this.isRunning) { + requestAnimationFrame(() => this.updateTimer()); + } + } + + updateCollection() { + if (!this.collectionElement) return; + + const adventureNames = { + // Places & Landmarks + 'đŸ—ēī¸': 'Treasure Map', '🧭': 'Compass', 'â›ē': 'Camp Site', '🏰': 'Castle', + 'đŸ—ŋ': 'Ancient Monument', 'đŸŽĒ': 'Circus', '🎡': 'Fair', 'đŸŽĸ': 'Adventure Park', + 'đŸ›ī¸': 'Ancient Ruins', 'â›Šī¸': 'Sacred Shrine', '🕌': 'Grand Mosque', 'â›Ē': 'Old Church', + 'đŸ¯': 'Palace', '🏭': 'Steam Factory', + + // Nature & Weather + '🌋': 'Volcano', 'đŸ”ī¸': 'Mountain Peak', 'đŸī¸': 'Desert Island', '🌅': 'Sunset Beach', + '🌠': 'Stargazing', '🌄': 'Mountain Sunrise', 'đŸžī¸': 'National Park', '🌈': 'Rainbow Valley', + '🌊': 'Ocean Waves', 'â„ī¸': 'Snow Peak', 'đŸŒē': 'Flower Garden', '🌴': 'Palm Beach', + '🍄': 'Mystic Forest', 'đŸŒĩ': 'Desert Trail', + + // Transport & Movement + '🚂': 'Steam Train', '🚤': 'Speed Boat', 'âœˆī¸': 'Sky Journey', '🎈': 'Balloon Ride', + '🛸': 'Space Travel', '🚁': 'Helicopter Tour', 'â›ĩ': 'Sailing Trip', + + // Wildlife + 'đŸĻ': 'Lion Safari', '🐘': 'Elephant Trek', 'đŸĻ’': 'Giraffe Watch', 'đŸĻœ': 'Exotic Birds', + '🐠': 'Ocean Diving', 'đŸĻ‹': 'Butterfly Garden', 'đŸĒ': 'Desert Caravan' + }; + + const totalAdventures = Object.keys(adventureNames).length; + const collectedCount = this.collectedItems.size; + + if (collectedCount === totalAdventures) { + // All adventures collected! + this.collectionElement.textContent = '🎉 Congratulations! You\'ve collected all adventures! You\'re a true explorer! 🌟'; + this.collectionElement.style.fontSize = '20px'; + this.collectionElement.style.color = '#F39C12'; // Using the highlight-orange color + } else if (collectedCount === 0) { + this.collectionElement.textContent = 'No adventures yet! 🎒'; + this.collectionElement.style.fontSize = '16px'; + this.collectionElement.style.color = ''; // Reset color + } else { + // Show progress in two lines + this.collectionElement.innerHTML = + `🎒 ${collectedCount} different adventures collected! đŸ—ēī¸
` + + `🔍 ${totalAdventures - collectedCount} more to discover! ✨`; + this.collectionElement.style.fontSize = '16px'; + this.collectionElement.style.color = ''; // Reset color + } + } + + startGame() { + if (this.isRunning) { + this.endGame(); + return; + } + + this.isRunning = true; + this.score = 0; + this.collectedItems.clear(); // Reset collection + this.gameStartTime = Date.now(); + this.updateScore(); + this.updateCollection(); + this.startButton.textContent = '🔄 Reset Game'; + this.timeElement.textContent = 'âąī¸ 2:00'; + + // Start spawning obstacles + this.spawnObstacles(); + + // Start game timer + this.gameTimer = setTimeout(() => { + this.endGame(true); + }, this.GAME_DURATION); + + // Start timer updates + this.updateTimer(); + } + + endGame(timeUp = false) { + this.isRunning = false; + // Don't clear collection on game end to show final collection + this.score = 0; + this.updateScore(); + this.lockedRoads.clear(); // Clear all road locks + + if (timeUp) { + this.startButton.textContent = '🏁 Game Over! Play Again?'; + } else { + this.startButton.textContent = '🎮 Start Game'; + } + + // Clear timer if it exists + if (this.gameTimer) { + clearTimeout(this.gameTimer); + this.gameTimer = null; + } + + // Reset squares to top position + document.querySelectorAll('.square').forEach(square => { + square.className = 'square top'; + square.dataset.position = 'top'; + }); + + // Clear obstacles + document.querySelectorAll('.obstacle').forEach(o => o.remove()); + } + + handlePipeClick(event) { + if (!this.isRunning) return; + + const pipe = event.currentTarget; + const square = pipe.querySelector('.square'); + + if (!square) return; + + // Toggle square position + const isTop = square.dataset.position === 'top'; + square.className = `square ${isTop ? 'bottom' : 'top'}`; + square.dataset.position = isTop ? 'bottom' : 'top'; + + if (DEBUG) console.log(`Pipe ${pipe.dataset.pipe} square moved to ${square.dataset.position}`); + } + + checkCollision(square, obstacle) { + const squareRect = square.getBoundingClientRect(); + const obstacleRect = obstacle.getBoundingClientRect(); + const roadRect = obstacle.parentElement.getBoundingClientRect(); + + // Only check collisions near the ends + const isNearEnds = + obstacleRect.right <= roadRect.left + 70 || // Left end + obstacleRect.left >= roadRect.right - 70; // Right end + + if (!isNearEnds) return false; + + const hasCollision = !(squareRect.right < obstacleRect.left || + squareRect.left > obstacleRect.right || + squareRect.bottom < obstacleRect.top || + squareRect.top > obstacleRect.bottom); + + if (hasCollision) { + // Add to collection when collected + this.collectedItems.add(obstacle.textContent); + this.updateCollection(); + } + + return hasCollision; + } + + collisionDetectionLoop() { + if (this.isRunning) { + const squares = document.querySelectorAll('.square'); + const obstacles = document.querySelectorAll('.obstacle'); + + squares.forEach(square => { + obstacles.forEach(obstacle => { + if (this.checkCollision(square, obstacle)) { + // Handle collision + square.classList.add('collision'); + obstacle.classList.add('collision'); + this.updateScore(1); + + // Remove collision class after animation + setTimeout(() => { + square.classList.remove('collision'); + obstacle.classList.remove('collision'); + }, 300); + } + }); + }); + } + + // Continue the loop + requestAnimationFrame(() => this.collisionDetectionLoop()); + } + + getDifficultySettings() { + const elapsedTime = Date.now() - this.gameStartTime; + const progress = Math.min(1, elapsedTime / GAME_CONFIG.difficultyRampUpTime); + + // Interpolate between initial and final values + return { + minSpeed: this.lerp(GAME_CONFIG.initial.minSpeed, GAME_CONFIG.final.minSpeed, progress), + maxSpeed: this.lerp(GAME_CONFIG.initial.maxSpeed, GAME_CONFIG.final.maxSpeed, progress), + minSpawnRate: this.lerp(GAME_CONFIG.initial.minSpawnRate, GAME_CONFIG.final.minSpawnRate, progress), + maxSpawnRate: this.lerp(GAME_CONFIG.initial.maxSpawnRate, GAME_CONFIG.final.maxSpawnRate, progress), + maxObstacles: Math.round(this.lerp(GAME_CONFIG.initial.maxObstacles, GAME_CONFIG.final.maxObstacles, progress)), + multiSpawnChance: this.lerp(GAME_CONFIG.initial.multiSpawnChance, GAME_CONFIG.final.multiSpawnChance, progress) + }; + } + + lerp(start, end, progress) { + return start + (end - start) * progress; + } + + spawnObstacles() { + if (!this.isRunning) return; + + const settings = this.getDifficultySettings(); + + // Count current obstacles + const currentObstacles = document.querySelectorAll('.obstacle').length; + if (currentObstacles >= settings.maxObstacles) { + // Schedule next check if at max + setTimeout(() => this.spawnObstacles(), 100); + return; + } + + // Get available (unlocked) roads + const availableRoads = Array.from(this.roads).filter(road => !this.lockedRoads.has(road)); + + if (availableRoads.length === 0) { + // No available roads, try again soon + setTimeout(() => this.spawnObstacles(), 100); + return; + } + + // Pick a random available road + const roadIndex = Math.floor(Math.random() * availableRoads.length); + const road = availableRoads[roadIndex]; + + // Lock the road + this.lockedRoads.add(road); + + // Create obstacle (now collectible) + const obstacle = document.createElement('div'); + obstacle.className = 'obstacle'; + + // Add random adventure emoji as collectible + const adventureEmojis = [ + // Places & Landmarks (14) + 'đŸ—ēī¸', '🧭', 'â›ē', '🏰', 'đŸ—ŋ', 'đŸŽĒ', '🎡', 'đŸŽĸ', 'đŸ›ī¸', 'â›Šī¸', '🕌', 'â›Ē', 'đŸ¯', '🏭', + + // Nature & Weather (14) + '🌋', 'đŸ”ī¸', 'đŸī¸', '🌅', '🌠', '🌄', 'đŸžī¸', '🌈', '🌊', 'â„ī¸', 'đŸŒē', '🌴', '🍄', 'đŸŒĩ', + + // Transport & Movement (7) + '🚂', '🚤', 'âœˆī¸', '🎈', '🛸', '🚁', 'â›ĩ', + + // Wildlife (7) + 'đŸĻ', '🐘', 'đŸĻ’', 'đŸĻœ', '🐠', 'đŸĻ‹', 'đŸĒ' + ]; + const randomEmoji = adventureEmojis[Math.floor(Math.random() * adventureEmojis.length)]; + obstacle.textContent = randomEmoji; + + // Set initial position (left or right side) + const goingLeft = Math.random() > 0.5; + + // Base distance is 330px + // If spawning on left and moving right: + // Start at 324px (6px less on left), end at 336px (6px more on right) + // If spawning on right and moving left: + // Start at -324px (6px less on right), end at -336px (6px more on left) + const startPos = goingLeft ? 324 : -324; + const endPos = goingLeft ? -336 : 336; + + obstacle.style.transform = `translateX(${startPos}px)`; + + road.appendChild(obstacle); + + // Random speed between current min and max speeds + const randomSpeed = settings.minSpeed + + Math.random() * (settings.maxSpeed - settings.minSpeed); + + // Animate obstacle + const animation = obstacle.animate([ + { transform: `translateX(${startPos}px)` }, + { transform: `translateX(${endPos}px)` } + ], { + duration: randomSpeed * 1000, + easing: 'linear' + }); + + animation.onfinish = () => { + if (this.isRunning) { + // Unlock the road when obstacle is removed + this.lockedRoads.delete(road); + obstacle.remove(); + } + }; + + // Random spawn rate based on current difficulty + const nextSpawnRate = settings.minSpawnRate + + Math.random() * (settings.maxSpawnRate - settings.minSpawnRate); + + // Schedule next spawn + setTimeout(() => { + if (this.isRunning) { + this.spawnObstacles(); + } + }, nextSpawnRate); + } + + updateScore(increment = 0) { + this.score += increment; + this.scoreElement.textContent = `🧭 Adventures: ${this.score}`; + + // Add sparkle effect on score increase + if (increment > 0) { + const sparkle = document.createElement('span'); + sparkle.textContent = ' ✨'; + sparkle.style.opacity = '1'; + this.scoreElement.appendChild(sparkle); + + // Fade out and remove sparkle + setTimeout(() => { + sparkle.style.transition = 'opacity 0.5s'; + sparkle.style.opacity = '0'; + setTimeout(() => sparkle.remove(), 500); + }, 100); + } + } +} + +// Start the game when the page loads +window.addEventListener('DOMContentLoaded', () => { + new Game(); +}); diff --git a/static/config.js b/static/config.js new file mode 100644 index 0000000..2d23f68 --- /dev/null +++ b/static/config.js @@ -0,0 +1,23 @@ +export const DEBUG = true; +export const GAME_CONFIG = { + // Initial (easier) settings + initial: { + minSpeed: 2, // Slower at start + maxSpeed: 2.5, // More consistent speed + minSpawnRate: 1000, // Slower spawns + maxSpawnRate: 1500, // More time between spawns + maxObstacles: 6, // Fewer obstacles + multiSpawnChance: 0.1 // Rare multi-spawns + }, + // Final (challenging) settings after 60 seconds + final: { + minSpeed: 1.2, // Faster minimum speed + maxSpeed: 3.5, // More speed variation + minSpawnRate: 300, // Faster spawns + maxSpawnRate: 900, // Quicker timing + maxObstacles: 12, // More simultaneous obstacles + multiSpawnChance: 0.4 // More frequent multi-spawns + }, + difficultyRampUpTime: 60000 // Time in ms to reach final difficulty (60 seconds) +}; + diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..647b8a2 --- /dev/null +++ b/static/index.html @@ -0,0 +1,66 @@ + + + + + + 2 Cars Game + + + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🧭 Adventures: 0
+
âąī¸ 2:00
+
+ +
No adventures yet! 🎒
+
+
+ + + + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..57cc78c --- /dev/null +++ b/static/style.css @@ -0,0 +1,182 @@ +:root { + --primary-blue: #4A90E2; + --highlight-orange: #F39C12; + --soft-cream: #FFF9E5; + --dark-gray: #34495E; + --success-green: #2ECC71; + --error-red: #E74C3C; + --light-gray: #ECF0F1; + --button-height: 50px; + --road-height: 30px; + --gap: 10px; +} + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background-color: var(--soft-cream); + color: var(--dark-gray); +} + +.game-container { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +.game-area { + display: grid; + grid-template-columns: 50px 1fr 50px; + gap: var(--gap); + min-width: 600px; + border: 2px solid var(--dark-gray); + border-radius: 8px; + padding: var(--gap); + background-color: var(--light-gray); +} + +.button-column { + display: flex; + flex-direction: column; + justify-content: space-between; + width: 50px; + height: 170px; +} + +.pipe { + height: 50px; + width: 50px; + background-color: var(--primary-blue); + border-radius: 4px; + cursor: pointer; + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; + box-sizing: border-box; +} + +.square { + width: 40px; + height: 20px; + background-color: var(--highlight-orange); + position: absolute; + left: 5px; + transition: top 0.3s ease-out; +} + +.square.top { + top: 5px; +} + +.square.bottom { + top: 25px; +} + +.road-container { + display: flex; + flex-direction: column; + gap: var(--gap); +} + +.road-pair { + height: var(--button-height); + display: flex; + flex-direction: column; + justify-content: space-evenly; +} + +.road { + height: 15px; + background-color: var(--primary-blue); + border-radius: 4px; + position: relative; + overflow: visible; +} + +.obstacle { + position: absolute; + width: 20px; + height: 20px; + border-radius: 50%; + top: -2.5px; + left: calc(50% - 10px); + will-change: transform; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + line-height: 1; +} + +.collision { + animation: flash 0.3s; + animation-fill-mode: both; +} + +@keyframes flash { + 0%, 100% { + opacity: 1; + transform: scale(1.2); + } + 50% { + opacity: 0.3; + transform: scale(1); + } +} + +.controls { + margin-top: var(--gap); + display: flex; + justify-content: flex-end; + align-items: center; + gap: 20px; + flex-wrap: wrap; +} + +.score-container { + flex-grow: 1; + text-align: left; +} + +button { + padding: 10px 20px; + font-size: 1.2em; + background-color: var(--primary-blue); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; + order: 2; +} + +button:hover { + background-color: var(--highlight-orange); +} + +.score, .timer { + font-size: 1.5em; + margin-top: var(--gap); +} + +.timer { + color: var(--dark-gray); +} + +.collection { + width: 100%; + margin-top: 10px; + padding: 10px; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 8px; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; + order: 3; +} + +.collection:empty { + display: none; +