managarten/games/arcade/apps/web/static/games/card_stack_rush.html
Till JS 9e82e40e16 rename(mana-games): rebrand to Arcade
Rename games/mana-games/ to games/arcade/, update all package names
(@mana-games/* → @arcade/*), appIds, display names, docker-compose
service, root scripts, and documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:31:37 +02:00

710 lines
No EOL
21 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Card Stack Rush</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
min-height: 100vh;
padding: 0;
color: #333;
overflow: hidden;
}
.top-bar {
background: rgba(255, 255, 255, 0.95);
padding: 10px 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 15px;
}
h1 {
font-size: 1.5rem;
color: #e74c3c;
margin: 0;
}
.game-controls {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.stat-group {
display: flex;
gap: 20px;
align-items: center;
}
.stat {
display: flex;
align-items: center;
gap: 5px;
font-size: 0.9rem;
}
.stat-label {
color: #666;
}
.stat-value {
font-weight: bold;
color: #e74c3c;
font-size: 1.1rem;
}
.timer-bar {
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
width: 200px;
}
.timer-fill {
height: 100%;
background: linear-gradient(90deg, #27ae60, #f39c12, #e74c3c);
transition: width 0.1s linear;
width: 100%;
}
.btn-new {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
border: none;
padding: 8px 20px;
font-size: 0.9rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.btn-new:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.game-area {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
height: calc(100vh - 60px);
justify-content: space-between;
}
.incoming-card-area {
margin-bottom: 20px;
height: 180px;
display: flex;
align-items: center;
justify-content: center;
}
.card {
width: 120px;
height: 180px;
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 3rem;
font-weight: bold;
cursor: grab;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
position: relative;
user-select: none;
}
.card:active {
cursor: grabbing;
transform: scale(1.05);
}
.card.dragging {
opacity: 0.8;
transform: scale(1.1);
z-index: 1000;
}
.card-suit {
font-size: 2.5rem;
margin-top: 10px;
}
.card.red {
background: white;
color: #e74c3c;
border: 2px solid #e74c3c;
}
.card.black {
background: white;
color: #2c3e50;
border: 2px solid #2c3e50;
}
.stacks-container {
display: flex;
gap: 40px;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 20px;
}
.stack {
width: 140px;
height: 200px;
border: 3px dashed rgba(255, 255, 255, 0.5);
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.1);
}
.stack-label {
position: absolute;
top: -25px;
color: white;
font-weight: bold;
font-size: 0.9rem;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
}
.stack.valid-drop {
border-color: #27ae60;
background: rgba(39, 174, 96, 0.2);
transform: scale(1.05);
}
.stack.invalid-drop {
border-color: #e74c3c;
background: rgba(231, 76, 60, 0.2);
animation: shake 0.3s ease;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.stack-cards {
position: relative;
width: 120px;
height: 180px;
}
.stack-card {
position: absolute;
width: 120px;
height: 180px;
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 2.5rem;
font-weight: bold;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.score-popup {
position: absolute;
font-weight: bold;
font-size: 1.2rem;
animation: scoreFloat 1s ease-out forwards;
pointer-events: none;
z-index: 100;
}
@keyframes scoreFloat {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-50px);
}
}
.combo-indicator {
position: fixed;
top: 80px;
right: 20px;
background: rgba(255, 255, 255, 0.95);
padding: 10px 20px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
display: none;
animation: comboAnimation 0.3s ease;
}
@keyframes comboAnimation {
0% { transform: scale(0.8); opacity: 0; }
50% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
.combo-text {
font-size: 1.5rem;
font-weight: bold;
color: #e74c3c;
}
.game-over {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
text-align: center;
display: none;
z-index: 1000;
}
.game-over h2 {
color: #e74c3c;
font-size: 2.5rem;
margin-bottom: 20px;
}
.final-stats {
margin: 20px 0;
font-size: 1.2rem;
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: none;
z-index: 999;
}
.rule-text {
color: white;
text-align: center;
font-size: 1.2rem;
margin: 10px 0;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
@media (max-width: 768px) {
.stacks-container {
gap: 20px;
}
.stack {
width: 100px;
height: 150px;
}
.card {
width: 90px;
height: 135px;
font-size: 2.2rem;
}
.card-suit {
font-size: 1.8rem;
}
.stack-card {
width: 90px;
height: 135px;
font-size: 1.8rem;
}
.timer-bar {
width: 150px;
}
}
</style>
</head>
<body>
<div class="top-bar">
<h1>Card Stack Rush</h1>
<div class="game-controls">
<div class="stat-group">
<div class="stat">
<span class="stat-label">Punkte:</span>
<span class="stat-value" id="score">0</span>
</div>
<div class="stat">
<span class="stat-label">Karten:</span>
<span class="stat-value" id="cardsPlaced">0</span>
</div>
<div class="stat">
<span class="stat-label">Zeit:</span>
<div class="timer-bar">
<div class="timer-fill" id="timerFill"></div>
</div>
</div>
</div>
<button class="btn-new" onclick="newGame()">Neues Spiel</button>
</div>
</div>
<div class="game-area">
<div class="rule-text" id="ruleText">Sortiere nach Farbe!</div>
<div class="incoming-card-area" id="incomingArea">
</div>
<div class="stacks-container" id="stacksContainer">
</div>
</div>
<div class="combo-indicator" id="comboIndicator">
<div class="combo-text" id="comboText">Combo x2!</div>
</div>
<div class="overlay" id="overlay"></div>
<div class="game-over" id="gameOver">
<h2>Zeit abgelaufen!</h2>
<div class="final-stats">
<p>Endpunktzahl: <strong id="finalScore">0</strong></p>
<p>Platzierte Karten: <strong id="finalCards">0</strong></p>
<p>Höchste Combo: <strong id="finalCombo">0</strong></p>
</div>
<button class="btn-new" onclick="newGame()">Neues Spiel</button>
</div>
<script>
const suits = ['♠', '♣', '♥', '♦'];
const values = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
const rules = {
color: {
name: 'Sortiere nach Farbe!',
stacks: [
{ label: 'Rot', accepts: ['♥', '♦'] },
{ label: 'Schwarz', accepts: ['♠', '♣'] }
]
},
suit: {
name: 'Sortiere nach Symbol!',
stacks: [
{ label: '♠', accepts: ['♠'] },
{ label: '♣', accepts: ['♣'] },
{ label: '♥', accepts: ['♥'] },
{ label: '♦', accepts: ['♦'] }
]
},
value: {
name: 'Sortiere nach Wert!',
stacks: [
{ label: 'Niedrig (A-5)', accepts: ['A', '2', '3', '4', '5'] },
{ label: 'Mittel (6-10)', accepts: ['6', '7', '8', '9', '10'] },
{ label: 'Hoch (J-K)', accepts: ['J', 'Q', 'K'] }
]
}
};
let score = 0;
let cardsPlaced = 0;
let combo = 0;
let maxCombo = 0;
let currentRule = 'color';
let gameActive = false;
let timeLeft = 45;
let timerInterval;
let currentCard = null;
let isDragging = false;
function createCard(value, suit) {
const card = document.createElement('div');
const isRed = suit === '♥' || suit === '♦';
card.className = `card ${isRed ? 'red' : 'black'}`;
card.draggable = true;
card.dataset.value = value;
card.dataset.suit = suit;
card.innerHTML = `
<div>${value}</div>
<div class="card-suit">${suit}</div>
`;
card.addEventListener('dragstart', handleDragStart);
card.addEventListener('dragend', handleDragEnd);
card.addEventListener('click', handleCardClick);
return card;
}
function generateRandomCard() {
const value = values[Math.floor(Math.random() * values.length)];
const suit = suits[Math.floor(Math.random() * suits.length)];
return createCard(value, suit);
}
function createStacks() {
const container = document.getElementById('stacksContainer');
container.innerHTML = '';
const rule = rules[currentRule];
rule.stacks.forEach((stack, index) => {
const stackDiv = document.createElement('div');
stackDiv.className = 'stack';
stackDiv.dataset.stackIndex = index;
const label = document.createElement('div');
label.className = 'stack-label';
label.textContent = stack.label;
const cardsDiv = document.createElement('div');
cardsDiv.className = 'stack-cards';
stackDiv.appendChild(label);
stackDiv.appendChild(cardsDiv);
stackDiv.addEventListener('dragover', handleDragOver);
stackDiv.addEventListener('drop', handleDrop);
stackDiv.addEventListener('dragleave', handleDragLeave);
stackDiv.addEventListener('click', handleStackClick);
container.appendChild(stackDiv);
});
}
function handleDragStart(e) {
isDragging = true;
currentCard = e.target;
e.target.classList.add('dragging');
}
function handleDragEnd(e) {
isDragging = false;
e.target.classList.remove('dragging');
}
function handleDragOver(e) {
e.preventDefault();
const stack = e.currentTarget;
if (isValidDrop(currentCard, stack)) {
stack.classList.add('valid-drop');
}
}
function handleDragLeave(e) {
e.currentTarget.classList.remove('valid-drop');
}
function handleDrop(e) {
e.preventDefault();
const stack = e.currentTarget;
stack.classList.remove('valid-drop');
if (currentCard && isValidDrop(currentCard, stack)) {
placeCard(currentCard, stack);
}
}
function handleCardClick(e) {
if (!isDragging && !currentCard) {
currentCard = e.target.closest('.card');
currentCard.style.transform = 'scale(1.1)';
currentCard.style.boxShadow = '0 8px 20px rgba(0, 0, 0, 0.3)';
}
}
function handleStackClick(e) {
if (currentCard && !isDragging) {
const stack = e.currentTarget;
if (isValidDrop(currentCard, stack)) {
placeCard(currentCard, stack);
} else {
stack.classList.add('invalid-drop');
setTimeout(() => stack.classList.remove('invalid-drop'), 300);
}
currentCard.style.transform = '';
currentCard.style.boxShadow = '';
currentCard = null;
}
}
function isValidDrop(card, stack) {
const stackIndex = parseInt(stack.dataset.stackIndex);
const rule = rules[currentRule];
const stackRule = rule.stacks[stackIndex];
if (currentRule === 'color') {
const isRed = card.dataset.suit === '♥' || card.dataset.suit === '♦';
return (stackRule.label === 'Rot' && isRed) ||
(stackRule.label === 'Schwarz' && !isRed);
} else if (currentRule === 'suit') {
return stackRule.accepts.includes(card.dataset.suit);
} else if (currentRule === 'value') {
return stackRule.accepts.includes(card.dataset.value);
}
return false;
}
function placeCard(card, stack) {
const stackCards = stack.querySelector('.stack-cards');
const stackCard = card.cloneNode(true);
stackCard.className = 'stack-card ' + (card.classList.contains('red') ? 'red' : 'black');
stackCard.style.transform = `translateY(${stackCards.children.length * -2}px)`;
stackCards.appendChild(stackCard);
card.remove();
cardsPlaced++;
combo++;
score += 10 * Math.max(1, Math.floor(combo / 3));
if (combo > maxCombo) maxCombo = combo;
updateStats();
showScorePopup(stack, 10 * Math.max(1, Math.floor(combo / 3)));
if (combo >= 3 && combo % 3 === 0) {
showCombo();
}
nextCard();
}
function showScorePopup(element, points) {
const popup = document.createElement('div');
popup.className = 'score-popup';
popup.textContent = `+${points}`;
popup.style.color = points > 10 ? '#27ae60' : '#e74c3c';
const rect = element.getBoundingClientRect();
popup.style.left = rect.left + rect.width / 2 + 'px';
popup.style.top = rect.top + 'px';
document.body.appendChild(popup);
setTimeout(() => popup.remove(), 1000);
}
function showCombo() {
const indicator = document.getElementById('comboIndicator');
const text = document.getElementById('comboText');
text.textContent = `${combo}x Combo!`;
indicator.style.display = 'block';
setTimeout(() => {
indicator.style.display = 'none';
}, 1500);
}
function nextCard() {
const incomingArea = document.getElementById('incomingArea');
incomingArea.innerHTML = '';
if (Math.random() < 0.25) {
changeRule();
}
const newCard = generateRandomCard();
incomingArea.appendChild(newCard);
}
function changeRule() {
const ruleKeys = Object.keys(rules);
let newRule;
do {
newRule = ruleKeys[Math.floor(Math.random() * ruleKeys.length)];
} while (newRule === currentRule);
currentRule = newRule;
document.getElementById('ruleText').textContent = rules[currentRule].name;
createStacks();
combo = 0;
}
function updateStats() {
document.getElementById('score').textContent = score;
document.getElementById('cardsPlaced').textContent = cardsPlaced;
}
function updateTimer() {
timeLeft--;
const percentage = (timeLeft / 45) * 100;
document.getElementById('timerFill').style.width = percentage + '%';
if (timeLeft <= 0) {
endGame();
}
}
function startGame() {
gameActive = true;
score = 0;
cardsPlaced = 0;
combo = 0;
maxCombo = 0;
timeLeft = 45;
currentRule = 'color';
document.getElementById('ruleText').textContent = rules[currentRule].name;
updateStats();
createStacks();
nextCard();
timerInterval = setInterval(updateTimer, 1000);
}
function endGame() {
gameActive = false;
clearInterval(timerInterval);
document.getElementById('finalScore').textContent = score;
document.getElementById('finalCards').textContent = cardsPlaced;
document.getElementById('finalCombo').textContent = maxCombo;
document.getElementById('overlay').style.display = 'block';
document.getElementById('gameOver').style.display = 'block';
}
function newGame() {
document.getElementById('overlay').style.display = 'none';
document.getElementById('gameOver').style.display = 'none';
startGame();
}
document.addEventListener('click', function(e) {
if (currentCard && !e.target.closest('.card') && !e.target.closest('.stack')) {
currentCard.style.transform = '';
currentCard.style.boxShadow = '';
currentCard = null;
}
});
startGame();
</script>
</body>
</html>