Update 2025-04-17_20:33:11
This commit is contained in:
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal 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/
|
24
main.py
Normal file
24
main.py
Normal 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
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
|
363
static/app.js
Normal file
363
static/app.js
Normal 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
23
static/config.js
Normal 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
66
static/index.html
Normal 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
182
static/style.css
Normal 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;
|
||||||
|
|
Reference in New Issue
Block a user