600 lines
22 KiB
HTML
600 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
<title>Uno - KinderWelt</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; }
|
|
body {
|
|
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
|
|
min-height: 100vh; display: flex; flex-direction: column; align-items: center;
|
|
font-family: 'Comic Sans MS', cursive, sans-serif; padding: 10px;
|
|
}
|
|
h1 { color: white; margin-bottom: 5px; font-size: 24px; }
|
|
|
|
.game-info {
|
|
display: flex; gap: 20px; margin: 10px 0; color: white; font-size: 14px;
|
|
}
|
|
.info-item { text-align: center; }
|
|
.info-value { font-size: 20px; font-weight: bold; color: #feca57; }
|
|
|
|
/* Karten-Stapel */
|
|
.deck-area {
|
|
display: flex; gap: 30px; margin: 15px 0; align-items: center;
|
|
}
|
|
|
|
.card-pile {
|
|
width: 80px; height: 120px; background: linear-gradient(135deg, #333, #555);
|
|
border-radius: 10px; border: 3px solid #fff;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 30px; cursor: pointer; position: relative;
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
|
|
}
|
|
.card-pile::after {
|
|
content: ''; position: absolute; top: -3px; left: -3px;
|
|
width: 80px; height: 120px; background: linear-gradient(135deg, #333, #555);
|
|
border-radius: 10px; border: 3px solid #fff; z-index: -1;
|
|
}
|
|
|
|
.discard-pile {
|
|
width: 80px; height: 120px; border-radius: 10px; border: 3px solid #fff;
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
font-size: 32px; font-weight: bold; box-shadow: 0 5px 15px rgba(0,0,0,0.5);
|
|
color: white;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
|
}
|
|
.discard-pile.red { background: #E53E3E; }
|
|
.discard-pile.yellow { background: #D69E2E; }
|
|
.discard-pile.green { background: #38A169; }
|
|
.discard-pile.blue { background: #3182CE; }
|
|
.discard-pile.wild {
|
|
background: linear-gradient(45deg, #E53E3E, #D69E2E, #38A169, #3182CE);
|
|
}
|
|
|
|
/* Bot-Hand */
|
|
.bot-hand {
|
|
display: flex; gap: -20px; margin: 10px 0; flex-wrap: wrap; justify-content: center;
|
|
}
|
|
.bot-card {
|
|
width: 50px; height: 75px; background: linear-gradient(135deg, #333, #000);
|
|
border: 2px solid #fff; border-radius: 8px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 20px; margin-left: -20px;
|
|
box-shadow: 2px 2px 5px rgba(0,0,0,0.3);
|
|
}
|
|
.bot-card:first-child { margin-left: 0; }
|
|
|
|
/* Spieler-Hand */
|
|
.player-hand {
|
|
display: flex; gap: 10px; margin: 15px 0; flex-wrap: wrap; justify-content: center;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.card {
|
|
width: 70px; height: 100px; border-radius: 10px; border: 3px solid #fff;
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
font-size: 28px; font-weight: bold; cursor: pointer;
|
|
transition: all 0.2s; box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
|
position: relative;
|
|
}
|
|
.card:hover { transform: translateY(-10px); }
|
|
.card.disabled { opacity: 0.5; cursor: not-allowed; }
|
|
.card:not(.disabled):hover { transform: translateY(-10px) scale(1.1); }
|
|
|
|
/* Farben */
|
|
.card.red { background: #E53E3E; color: white; }
|
|
.card.yellow { background: #D69E2E; color: white; }
|
|
.card.green { background: #38A169; color: white; }
|
|
.card.blue { background: #3182CE; color: white; }
|
|
.card.wild { background: linear-gradient(45deg, #E53E3E, #D69E2E, #38A169, #3182CE); color: white; }
|
|
|
|
.card-value { font-size: 32px; }
|
|
.card-corner {
|
|
position: absolute; font-size: 12px;
|
|
top: 5px; left: 5px;
|
|
}
|
|
.card-corner-bottom {
|
|
position: absolute; font-size: 12px;
|
|
bottom: 5px; right: 5px; transform: rotate(180deg);
|
|
}
|
|
|
|
.status {
|
|
color: white; font-size: 16px; margin: 10px 0; text-align: center;
|
|
min-height: 40px;
|
|
}
|
|
|
|
.color-picker {
|
|
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
background: white; padding: 20px; border-radius: 15px;
|
|
display: none; z-index: 100; text-align: center;
|
|
box-shadow: 0 10px 50px rgba(0,0,0,0.5);
|
|
}
|
|
.color-picker h3 { margin-bottom: 15px; }
|
|
.color-options { display: flex; gap: 10px; }
|
|
.color-option {
|
|
width: 60px; height: 60px; border-radius: 10px; cursor: pointer;
|
|
border: 3px solid transparent;
|
|
}
|
|
.color-option:hover { transform: scale(1.1); border-color: #333; }
|
|
|
|
.direction-indicator {
|
|
font-size: 30px; margin: 5px 0;
|
|
}
|
|
|
|
.controls { display: flex; gap: 15px; margin-top: 15px; }
|
|
.btn {
|
|
background: white; border: none; border-radius: 10px; padding: 10px 20px;
|
|
font-size: 14px; cursor: pointer; font-family: inherit; font-weight: bold;
|
|
}
|
|
.btn-draw { background: #4ade80; color: white; }
|
|
.btn-back { background: #ff6b6b; color: white; text-decoration: none; }
|
|
|
|
.game-over {
|
|
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
background: white; padding: 30px; border-radius: 20px; text-align: center;
|
|
display: none; z-index: 100; box-shadow: 0 10px 50px rgba(0,0,0,0.5);
|
|
}
|
|
.game-over h2 { margin-bottom: 15px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>🎴 Uno</h1>
|
|
|
|
<div class="game-info">
|
|
<div class="info-item">
|
|
<div>Computer</div>
|
|
<div class="info-value" id="botCardCount">7</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div>Stapel</div>
|
|
<div class="info-value" id="deckCount">79</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div>Richtung</div>
|
|
<div class="direction-indicator" id="direction">➡️</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bot-hand" id="botHand"></div>
|
|
|
|
<div class="deck-area">
|
|
<div class="card-pile" id="deck" onclick="drawCard()">
|
|
🎴
|
|
</div>
|
|
|
|
<div class="discard-pile" id="discard">
|
|
<span id="topCard">🎴</span>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="status" id="status">Wähle eine Karte oder ziehe vom Stapel!</div>
|
|
|
|
|
|
<div class="player-hand" id="playerHand"></div>
|
|
|
|
|
|
<div class="color-picker" id="colorPicker">
|
|
<h3>🎨 Wähle eine Farbe:</h3>
|
|
<div class="color-options">
|
|
<div class="color-option" style="background:#E53E3E" onclick="selectColor('red')"></div>
|
|
<div class="color-option" style="background:#D69E2E" onclick="selectColor('yellow')"></div>
|
|
<div class="color-option" style="background:#38A169" onclick="selectColor('green')"></div>
|
|
<div class="color-option" style="background:#3182CE" onclick="selectColor('blue')"></div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="controls">
|
|
<button class="btn btn-draw" onclick="drawCard()">📥 Karte ziehen</button>
|
|
<button class="btn" onclick="newGame()">🔄 Neustart</button>
|
|
<a href="../index.html" class="btn btn-back">⬅️ Zurück</a>
|
|
</div>
|
|
|
|
|
|
<div class="game-over" id="gameOver">
|
|
<h2 id="winnerText">🎉 Gewonnen!</h2>
|
|
<button class="btn btn-draw" onclick="newGame()">🔄 Nochmal</button>
|
|
</div>
|
|
|
|
<script>
|
|
const COLORS = ['red', 'yellow', 'green', 'blue'];
|
|
const VALUES = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'skip', 'reverse', '+2'];
|
|
const EMOJIS = {
|
|
'0': '0', '1': '1', '2': '2', '3': '3', '4': '4',
|
|
'5': '5', '6': '6', '7': '7', '8': '8', '9': '9',
|
|
'skip': '🚫', 'reverse': '🔄', '+2': '+2', '+4': '+4', 'wild': '🌈'
|
|
};
|
|
|
|
let deck = [];
|
|
let playerHand = [];
|
|
let botHand = [];
|
|
let discardPile = [];
|
|
let currentColor = null;
|
|
let currentValue = null;
|
|
let isPlayerTurn = true;
|
|
let direction = 1; // 1 = vorwärts, -1 = rückwärts
|
|
let cardsToDraw = 0;
|
|
let waitingForColor = false;
|
|
let pendingWildCard = null;
|
|
|
|
function createDeck() {
|
|
deck = [];
|
|
|
|
// Farbkarten (jede Farbe 0-9, +2, skip, reverse)
|
|
COLORS.forEach(color => {
|
|
// Eine 0 pro Farbe
|
|
deck.push({ color, value: '0', emoji: '0' });
|
|
|
|
// Zwei von jeder anderen Karte
|
|
for (let i = 1; i <= 9; i++) {
|
|
deck.push({ color, value: i.toString(), emoji: i.toString() });
|
|
deck.push({ color, value: i.toString(), emoji: i.toString() });
|
|
}
|
|
|
|
// Spezialkarten
|
|
['skip', 'reverse', '+2'].forEach(val => {
|
|
deck.push({ color, value: val, emoji: EMOJIS[val] });
|
|
deck.push({ color, value: val, emoji: EMOJIS[val] });
|
|
});
|
|
});
|
|
|
|
// Joker (wild und +4)
|
|
for (let i = 0; i < 4; i++) {
|
|
deck.push({ color: 'wild', value: 'wild', emoji: '🌈' });
|
|
deck.push({ color: 'wild', value: '+4', emoji: '+4' });
|
|
}
|
|
|
|
return shuffle(deck);
|
|
}
|
|
|
|
function shuffle(array) {
|
|
for (let i = array.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[array[i], array[j]] = [array[j], array[i]];
|
|
}
|
|
return array;
|
|
}
|
|
|
|
function initGame() {
|
|
deck = createDeck();
|
|
playerHand = [];
|
|
botHand = [];
|
|
discardPile = [];
|
|
direction = 1;
|
|
cardsToDraw = 0;
|
|
waitingForColor = false;
|
|
|
|
// Karten austeilen
|
|
for (let i = 0; i < 7; i++) {
|
|
playerHand.push(deck.pop());
|
|
botHand.push(deck.pop());
|
|
}
|
|
|
|
// Startkarte (kein Joker)
|
|
let startCard;
|
|
do {
|
|
startCard = deck.pop();
|
|
} while (startCard.color === 'wild');
|
|
|
|
discardPile.push(startCard);
|
|
currentColor = startCard.color;
|
|
currentValue = startCard.value;
|
|
|
|
isPlayerTurn = true;
|
|
|
|
updateDisplay();
|
|
document.getElementById('gameOver').style.display = 'none';
|
|
}
|
|
|
|
function updateDisplay() {
|
|
// Bot-Hand
|
|
const botHandEl = document.getElementById('botHand');
|
|
botHandEl.innerHTML = '';
|
|
botHand.forEach(() => {
|
|
const card = document.createElement('div');
|
|
card.className = 'bot-card';
|
|
card.textContent = '🎴';
|
|
botHandEl.appendChild(card);
|
|
});
|
|
|
|
// Spieler-Hand
|
|
const playerHandEl = document.getElementById('playerHand');
|
|
playerHandEl.innerHTML = '';
|
|
playerHand.forEach((card, idx) => {
|
|
const cardEl = document.createElement('div');
|
|
cardEl.className = `card ${card.color}`;
|
|
|
|
// Prüfen ob spielbar
|
|
const canPlay = isPlayerTurn && !waitingForColor &&
|
|
(card.color === currentColor || card.value === currentValue || card.color === 'wild');
|
|
|
|
if (!canPlay) cardEl.classList.add('disabled');
|
|
|
|
cardEl.innerHTML = `
|
|
<span class="card-corner">${card.emoji}</span>
|
|
<span class="card-value">${card.emoji}</span>
|
|
<span class="card-corner-bottom">${card.emoji}</span>
|
|
`;
|
|
|
|
if (canPlay) {
|
|
cardEl.onclick = () => playCard(idx);
|
|
}
|
|
|
|
playerHandEl.appendChild(cardEl);
|
|
});
|
|
|
|
// Oberste Karte
|
|
const topCard = discardPile[discardPile.length - 1];
|
|
const discardEl = document.getElementById('discard');
|
|
const cardColor = topCard.color === 'wild' ? currentColor : topCard.color;
|
|
discardEl.className = `discard-pile ${cardColor}`;
|
|
|
|
// Zeige Farbnamen und Wert
|
|
const colorNames = { red: '🔴 ROT', yellow: '🟡 GELB', green: '🟢 GRÜN', blue: '🔵 BLAU' };
|
|
const displayText = topCard.color === 'wild' ? topCard.emoji : `${topCard.emoji}`;
|
|
document.getElementById('topCard').innerHTML = `
|
|
<div style="font-size:28px;margin-bottom:5px">${displayText}</div>
|
|
<div style="font-size:10px;text-transform:uppercase">${colorNames[cardColor] || cardColor}</div>
|
|
`;
|
|
|
|
// Info
|
|
document.getElementById('botCardCount').textContent = botHand.length;
|
|
document.getElementById('deckCount').textContent = deck.length;
|
|
document.getElementById('direction').textContent = direction === 1 ? '➡️' : '⬅️';
|
|
|
|
// Status
|
|
const statusEl = document.getElementById('status');
|
|
if (waitingForColor) {
|
|
statusEl.textContent = 'Wähle eine Farbe für den Joker!';
|
|
} else if (cardsToDraw > 0) {
|
|
statusEl.textContent = `Du musst ${cardsToDraw} Karten ziehen!`;
|
|
} else if (isPlayerTurn) {
|
|
statusEl.textContent = 'Du bist dran! Wähle eine Karte.';
|
|
} else {
|
|
statusEl.textContent = 'Computer denkt...';
|
|
}
|
|
}
|
|
|
|
function playCard(handIdx) {
|
|
if (!isPlayerTurn || waitingForColor) return;
|
|
|
|
const card = playerHand[handIdx];
|
|
|
|
// Prüfen ob Karte spielbar
|
|
if (!canPlayCard(card)) {
|
|
speak('Diese Karte kannst du nicht spielen!');
|
|
return;
|
|
}
|
|
|
|
// Karte spielen
|
|
playerHand.splice(handIdx, 1);
|
|
discardPile.push(card);
|
|
|
|
// Joker?
|
|
if (card.color === 'wild') {
|
|
waitingForColor = true;
|
|
pendingWildCard = card;
|
|
document.getElementById('colorPicker').style.display = 'block';
|
|
updateDisplay();
|
|
return;
|
|
}
|
|
|
|
// Normale Karte
|
|
currentColor = card.color;
|
|
currentValue = card.value;
|
|
|
|
applyCardEffect(card);
|
|
|
|
// Gewonnen?
|
|
if (playerHand.length === 0) {
|
|
endGame(true);
|
|
return;
|
|
}
|
|
|
|
updateDisplay();
|
|
|
|
if (!isPlayerTurn) {
|
|
setTimeout(botTurn, 1500);
|
|
}
|
|
}
|
|
|
|
function canPlayCard(card) {
|
|
if (cardsToDraw > 0) {
|
|
// Muss +2 oder +4 spielen
|
|
return card.value === '+2' || card.value === '+4';
|
|
}
|
|
return card.color === currentColor ||
|
|
card.value === currentValue ||
|
|
card.color === 'wild';
|
|
}
|
|
|
|
function selectColor(color) {
|
|
if (!waitingForColor) return;
|
|
|
|
currentColor = color;
|
|
currentValue = pendingWildCard.value;
|
|
waitingForColor = false;
|
|
document.getElementById('colorPicker').style.display = 'none';
|
|
|
|
applyCardEffect(pendingWildCard);
|
|
pendingWildCard = null;
|
|
|
|
updateDisplay();
|
|
|
|
if (!isPlayerTurn) {
|
|
setTimeout(botTurn, 1000);
|
|
}
|
|
}
|
|
|
|
function applyCardEffect(card) {
|
|
switch(card.value) {
|
|
case 'skip':
|
|
speak('Aussetzen!');
|
|
// Gegner überspringen
|
|
break;
|
|
case 'reverse':
|
|
speak('Richtungswechsel!');
|
|
direction *= -1;
|
|
break;
|
|
case '+2':
|
|
speak('+2! Computer zieht 2 Karten!');
|
|
cardsToDraw = 2;
|
|
break;
|
|
case '+4':
|
|
speak('+4! Computer zieht 4 Karten!');
|
|
cardsToDraw = 4;
|
|
break;
|
|
default:
|
|
speak(card.emoji);
|
|
}
|
|
|
|
isPlayerTurn = !isPlayerTurn;
|
|
}
|
|
|
|
function drawCard() {
|
|
if (!isPlayerTurn || waitingForColor) return;
|
|
|
|
if (deck.length === 0) {
|
|
// Stapel neu mischen
|
|
if (discardPile.length <= 1) return;
|
|
const topCard = discardPile.pop();
|
|
deck = shuffle(discardPile);
|
|
discardPile = [topCard];
|
|
}
|
|
|
|
// Karte ziehen
|
|
const drawn = deck.pop();
|
|
|
|
if (cardsToDraw > 0) {
|
|
// Muss Karten ziehen
|
|
playerHand.push(drawn);
|
|
cardsToDraw--;
|
|
|
|
if (cardsToDraw === 0) {
|
|
// Nächster Spieler
|
|
isPlayerTurn = false;
|
|
setTimeout(botTurn, 1000);
|
|
}
|
|
} else {
|
|
playerHand.push(drawn);
|
|
// Nach Ziehen: Nächster Spieler
|
|
isPlayerTurn = false;
|
|
setTimeout(botTurn, 1000);
|
|
}
|
|
|
|
updateDisplay();
|
|
}
|
|
|
|
function botTurn() {
|
|
if (isPlayerTurn || waitingForColor) return;
|
|
|
|
// Spielbare Karten finden
|
|
const playableCards = botHand.map((card, idx) => ({ card, idx }))
|
|
.filter(({ card }) => canPlayCard(card));
|
|
|
|
if (playableCards.length > 0) {
|
|
// Beste Karte wählen (Priorität: +4, +2, Spezial, Zahlen)
|
|
let bestIdx = playableCards[0].idx;
|
|
let bestPriority = -1;
|
|
|
|
playableCards.forEach(({ card, idx }) => {
|
|
let priority = 0;
|
|
if (card.value === '+4') priority = 4;
|
|
else if (card.value === '+2') priority = 3;
|
|
else if (['skip', 'reverse'].includes(card.value)) priority = 2;
|
|
else if (card.color === 'wild') priority = 1;
|
|
|
|
if (priority > bestPriority) {
|
|
bestPriority = priority;
|
|
bestIdx = idx;
|
|
}
|
|
});
|
|
|
|
const card = botHand[bestIdx];
|
|
botHand.splice(bestIdx, 1);
|
|
discardPile.push(card);
|
|
|
|
// Farbe wählen (meiste in Hand)
|
|
if (card.color === 'wild') {
|
|
const colorCounts = {};
|
|
botHand.forEach(c => {
|
|
if (c.color !== 'wild') colorCounts[c.color] = (colorCounts[c.color] || 0) + 1;
|
|
});
|
|
const bestColor = Object.entries(colorCounts)
|
|
.sort((a, b) => b[1] - a[1])[0]?.[0] || 'red';
|
|
currentColor = bestColor;
|
|
currentValue = card.value;
|
|
} else {
|
|
currentColor = card.color;
|
|
currentValue = card.value;
|
|
}
|
|
|
|
speak(`Computer spielt ${card.emoji}`);
|
|
applyCardEffect(card);
|
|
|
|
if (botHand.length === 0) {
|
|
endGame(false);
|
|
return;
|
|
}
|
|
} else {
|
|
// Karte ziehen
|
|
if (deck.length === 0) {
|
|
const topCard = discardPile.pop();
|
|
deck = shuffle(discardPile);
|
|
discardPile = [topCard];
|
|
}
|
|
|
|
const drawn = deck.pop();
|
|
|
|
if (cardsToDraw > 0) {
|
|
for (let i = 0; i < cardsToDraw && deck.length > 0; i++) {
|
|
botHand.push(deck.pop());
|
|
}
|
|
speak(`Computer zieht ${cardsToDraw} Karten!`);
|
|
cardsToDraw = 0;
|
|
} else {
|
|
botHand.push(drawn);
|
|
speak('Computer zieht eine Karte');
|
|
}
|
|
|
|
isPlayerTurn = true;
|
|
}
|
|
|
|
updateDisplay();
|
|
}
|
|
|
|
function endGame(playerWon) {
|
|
const modal = document.getElementById('gameOver');
|
|
const winnerText = document.getElementById('winnerText');
|
|
|
|
if (playerWon) {
|
|
winnerText.textContent = '🎉 Du hast gewonnen!';
|
|
speak('Glückwunsch! Du hast gewonnen!');
|
|
} else {
|
|
winnerText.textContent = '🤖 Computer hat gewonnen!';
|
|
speak('Computer hat gewonnen!');
|
|
}
|
|
|
|
modal.style.display = 'block';
|
|
}
|
|
|
|
function newGame() {
|
|
initGame();
|
|
}
|
|
|
|
function speak(text) {
|
|
if ('speechSynthesis' in window) {
|
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
utterance.lang = 'de-DE';
|
|
speechSynthesis.speak(utterance);
|
|
}
|
|
}
|
|
|
|
// Init
|
|
initGame();
|
|
</script>
|
|
</body>
|
|
</html> |