Update 2025-04-17_20:33:11

This commit is contained in:
root
2025-04-17 20:33:11 +02:00
commit 77205e8193
8 changed files with 682 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@ -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/

0
.gitkeep Normal file
View File

24
main.py Normal file
View File

@ -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})

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
fastapi
uvicorn

363
static/app.js Normal file
View File

@ -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! 🗺️<br>` +
`🔍 ${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();
});

23
static/config.js Normal file
View File

@ -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)
};

66
static/index.html Normal file
View File

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2 Cars Game</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="game-container">
<div class="game-area">
<!-- Left buttons -->
<div class="button-column">
<div class="pipe" data-pipe="1">
<div class="square top" data-position="top"></div>
</div>
<div class="pipe" data-pipe="2">
<div class="square top" data-position="top"></div>
</div>
<div class="pipe" data-pipe="3">
<div class="square top" data-position="top"></div>
</div>
</div>
<!-- Roads -->
<div class="road-container">
<div class="road-pair">
<div class="road" data-road="1"></div>
<div class="road" data-road="2"></div>
</div>
<div class="road-pair">
<div class="road" data-road="3"></div>
<div class="road" data-road="4"></div>
</div>
<div class="road-pair">
<div class="road" data-road="5"></div>
<div class="road" data-road="6"></div>
</div>
</div>
<!-- Right buttons -->
<div class="button-column">
<div class="pipe" data-pipe="4">
<div class="square top" data-position="top"></div>
</div>
<div class="pipe" data-pipe="5">
<div class="square top" data-position="top"></div>
</div>
<div class="pipe" data-pipe="6">
<div class="square top" data-position="top"></div>
</div>
</div>
</div>
<div class="controls">
<div class="score-container">
<div class="score" id="score">🧭 Adventures: 0</div>
<div class="timer"><span id="time">⏱️ 2:00</span></div>
</div>
<button id="start-game">Start Game</button>
<div class="collection" id="collection">No adventures yet! 🎒</div>
</div>
</div>
<script type="module" src="/static/config.js"></script>
<script type="module" src="/static/app.js"></script>
</body>
</html>

182
static/style.css Normal file
View File

@ -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;