Neu: Soltär
@ -214,6 +214,10 @@
|
||||
<h2 class="tool-title">2048</h2>
|
||||
<p class="tool-description">2048 in HTML</p>
|
||||
</a>
|
||||
<a href="https://tools.ponywave.de/solitaire" class="tool-bubble">
|
||||
<h2 class="tool-title">Solitaire</h2>
|
||||
<p class="tool-description">Klassisches Solitaire mit Dark Mode und Highscore-System</p>
|
||||
</a>
|
||||
<a href="https://tools.ponywave.de/ba_memory" class="tool-bubble">
|
||||
<h2 class="tool-title">Blue Archive Memory</h2>
|
||||
<p class="tool-description">Memory-Spiel mit Blue Archive Charactere</p>
|
||||
|
29
sitemap.txt
@ -1,18 +1,19 @@
|
||||
https://tools.ponywave.de/
|
||||
https://tools.ponywave.de/2048
|
||||
https://tools.ponywave.de/ba_memory
|
||||
https://tools.ponywave.de/bohne
|
||||
https://tools.ponywave.de/cell
|
||||
https://tools.ponywave.de/checkwave
|
||||
https://tools.ponywave.de/depp_gpt
|
||||
https://tools.ponywave.de/dogify
|
||||
https://tools.ponywave.de/emoji
|
||||
https://tools.ponywave.de/emoji_jump
|
||||
https://tools.ponywave.de/flash_dl
|
||||
https://tools.ponywave.de/kemonogen
|
||||
https://tools.ponywave.de/sys_info
|
||||
https://tools.ponywave.de/text_cleaner
|
||||
https://tools.ponywave.de/text_sorter
|
||||
https://tools.ponywave.de/pinkie_timer
|
||||
https://tools.ponywave.de/yt_thumb
|
||||
https://tools.ponywave.de/flash_dl
|
||||
https://tools.ponywave.de/kemonogen
|
||||
https://tools.ponywave.de/emoji_jump
|
||||
https://tools.ponywave.de/2048
|
||||
https://tools.ponywave.de/solitaire
|
||||
https://tools.ponywave.de/ba_memory
|
||||
https://tools.ponywave.de/cell
|
||||
https://tools.ponywave.de/shape_shifter
|
||||
https://tools.ponywave.de/sys_info
|
||||
https://tools.ponywave.de/yt_thumb
|
||||
https://tools.ponywave.de/checkwave
|
||||
https://tools.ponywave.de/dogify
|
||||
https://tools.ponywave.de/bohne
|
||||
https://tools.ponywave.de/pinkie_timer
|
||||
https://tools.ponywave.de/depp_gpt
|
||||
https://tools.ponywave.de/emoji
|
BIN
solitaire/cards/10_club.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/10_diamond.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/10_heart.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/10_spade.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/11_club.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
solitaire/cards/11_diamond.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
solitaire/cards/11_heart.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
solitaire/cards/11_spade.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
solitaire/cards/12_club.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/12_diamond.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
solitaire/cards/12_heart.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
solitaire/cards/12_spade.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/13_club.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/13_diamond.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
solitaire/cards/13_heart.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
solitaire/cards/13_spade.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/1_club.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/1_diamond.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
solitaire/cards/1_heart.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/1_spade.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/2_club.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/2_diamond.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/2_heart.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/2_spade.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/3_club.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/3_diamond.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/3_heart.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/3_spade.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/4_club.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
solitaire/cards/4_diamond.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
solitaire/cards/4_heart.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
solitaire/cards/4_spade.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/5_club.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/5_diamond.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
solitaire/cards/5_heart.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/5_spade.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/6_club.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/6_diamond.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/6_heart.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/6_spade.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/7_club.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
solitaire/cards/7_diamond.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
solitaire/cards/7_heart.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
solitaire/cards/7_spade.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/8_club.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/8_diamond.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/8_heart.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/8_spade.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
solitaire/cards/9_club.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/9_diamond.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/9_heart.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
solitaire/cards/9_spade.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
solitaire/cards/card_back.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
840
solitaire/index.html
Normal file
@ -0,0 +1,840 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Solitaire</title>
|
||||
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
|
||||
<!-- Umami Tracking -->
|
||||
<script defer src="https://stats.ponywave.de/script" data-website-id="9ef713d2-adb9-4906-9df5-708d8a8b9131" data-tag="solitaire"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: #ffffff;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
background: #1e3d2f;
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.top-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.score-container {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.score, .time, .moves {
|
||||
font-size: 1.2em;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: scale(1.05);
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.game-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 100px);
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
grid-template-areas:
|
||||
"stock waste foundation1 foundation2 foundation3 foundation4 ."
|
||||
"tableau1 tableau2 tableau3 tableau4 tableau5 tableau6 tableau7";
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
.card-stack {
|
||||
min-height: 144px;
|
||||
width: 100px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stock {
|
||||
grid-area: stock;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.waste {
|
||||
grid-area: waste;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.waste .card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.waste .card:not(:last-child) {
|
||||
filter: brightness(0.7);
|
||||
}
|
||||
|
||||
.foundation1 { grid-area: foundation1; }
|
||||
.foundation2 { grid-area: foundation2; }
|
||||
.foundation3 { grid-area: foundation3; }
|
||||
.foundation4 { grid-area: foundation4; }
|
||||
|
||||
.tableau1 { grid-area: tableau1; }
|
||||
.tableau2 { grid-area: tableau2; }
|
||||
.tableau3 { grid-area: tableau3; }
|
||||
.tableau4 { grid-area: tableau4; }
|
||||
.tableau5 { grid-area: tableau5; }
|
||||
.tableau6 { grid-area: tableau6; }
|
||||
.tableau7 { grid-area: tableau7; }
|
||||
|
||||
.tableau:empty {
|
||||
border: 2px dashed rgba(255, 255, 255, 0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tableau:empty:hover {
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.stock:empty {
|
||||
border: 2px dashed rgba(255, 255, 255, 0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stock:empty:hover {
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100px;
|
||||
height: 144px;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
|
||||
user-select: none;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.card.face-down {
|
||||
background-image: url('cards/card_back.png');
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.card[data-suit="hearts"] { color: red; }
|
||||
.card[data-suit="diamonds"] { color: red; }
|
||||
.card[data-suit="clubs"] { color: black; }
|
||||
.card[data-suit="spades"] { color: black; }
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.foundation {
|
||||
border: 2px dashed rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #1e3d2f;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.rules-content {
|
||||
text-align: left;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.rules-content ul {
|
||||
margin: 15px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.rules-content li {
|
||||
margin: 10px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.rules-content button {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.highscores {
|
||||
margin-top: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.highscore-entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@keyframes cardFlip {
|
||||
0% { transform: rotateY(0deg); }
|
||||
100% { transform: rotateY(180deg); }
|
||||
}
|
||||
|
||||
@keyframes cardMove {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(var(--moveX), var(--moveY)); }
|
||||
}
|
||||
|
||||
.card.flipping {
|
||||
animation: cardFlip 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.card.moving {
|
||||
animation: cardMove 0.3s ease-out;
|
||||
}
|
||||
|
||||
.credits {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
background: #1e3d2f;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.credits:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.credits a {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.credits a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.card.selected {
|
||||
box-shadow: 0 0 10px #3498db;
|
||||
transform: translateY(-10px);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#invalidMoveModal .modal-content {
|
||||
background: #c0392b;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
#invalidMoveModal button {
|
||||
margin-top: 15px;
|
||||
background: #e74c3c;
|
||||
}
|
||||
|
||||
#invalidMoveModal button:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.modal.show .modal-content {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="credits">
|
||||
Karten-Assets von <a href="https://natomarcacini.itch.io/card-asset-pack" target="_blank">natomarcacini</a>
|
||||
</div>
|
||||
<div class="game-container">
|
||||
<div class="top-section">
|
||||
<div class="score-container">
|
||||
<div class="score">Punkte: <span id="score">0</span></div>
|
||||
<div class="time">Zeit: <span id="time">0:00</span></div>
|
||||
<div class="moves">Züge: <span id="moves">0</span></div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button onclick="newGame()">Neues Spiel</button>
|
||||
<button onclick="showHighscores()">Highscores</button>
|
||||
<button onclick="takeScreenshot()">Screenshot</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="game-board" id="gameBoard">
|
||||
<!-- Karten werden hier dynamisch eingefügt -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="highscoreModal">
|
||||
<div class="modal-content">
|
||||
<h2>Highscores</h2>
|
||||
<div class="highscores" id="highscoreList">
|
||||
<!-- Highscores werden hier dynamisch eingefügt -->
|
||||
</div>
|
||||
<button onclick="closeHighscores()">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="rulesModal">
|
||||
<div class="modal-content">
|
||||
<h2>Spielregeln</h2>
|
||||
<div class="rules-content">
|
||||
<p>Willkommen bei Solitaire! Hier sind die wichtigsten Regeln:</p>
|
||||
<ul>
|
||||
<li>Ziel ist es, alle Karten nach Farben sortiert auf die Foundation-Stapel zu bringen</li>
|
||||
<li>Auf dem Tableau können Karten in absteigender Reihenfolge mit alternierenden Farben gestapelt werden</li>
|
||||
<li>Nur Könige können auf leere Tableau-Felder gelegt werden</li>
|
||||
<li>Foundation-Stapel werden mit Assen begonnen und in aufsteigender Reihenfolge mit gleicher Farbe aufgebaut</li>
|
||||
<li>Durch Klick auf den Kartenstapel werden neue Karten aufgedeckt</li>
|
||||
<li>Wenn der Kartenstapel leer ist, können die Karten vom Ablagestapel wieder verwendet werden</li>
|
||||
</ul>
|
||||
<button onclick="closeRules()">Verstanden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="invalidMoveModal">
|
||||
<div class="modal-content">
|
||||
<h2>Ungültiger Zug!</h2>
|
||||
<p>Dieser Zug ist nicht erlaubt.</p>
|
||||
<button onclick="closeInvalidMove()">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Spielvariablen
|
||||
let score = 0;
|
||||
let moves = 0;
|
||||
let gameTime = 0;
|
||||
let timerInterval;
|
||||
let selectedCard = null;
|
||||
let gameStarted = false;
|
||||
let stockIndex = 0;
|
||||
|
||||
// Kartendeck erstellen
|
||||
const suits = ['heart', 'diamond', 'club', 'spade'];
|
||||
const values = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13'];
|
||||
let deck = [];
|
||||
|
||||
// Spielfunktionen
|
||||
function createDeck() {
|
||||
deck = [];
|
||||
for (let suit of suits) {
|
||||
for (let value of values) {
|
||||
deck.push({ suit, value });
|
||||
}
|
||||
}
|
||||
shuffleDeck();
|
||||
}
|
||||
|
||||
function shuffleDeck() {
|
||||
for (let i = deck.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[deck[i], deck[j]] = [deck[j], deck[i]];
|
||||
}
|
||||
}
|
||||
|
||||
function dealCards() {
|
||||
const gameBoard = document.getElementById('gameBoard');
|
||||
gameBoard.innerHTML = '';
|
||||
|
||||
// Stock erstellen
|
||||
const stock = document.createElement('div');
|
||||
stock.className = 'card-stack stock';
|
||||
stock.addEventListener('click', handleStockClick);
|
||||
gameBoard.appendChild(stock);
|
||||
|
||||
// Waste (Ablagestapel) erstellen
|
||||
const waste = document.createElement('div');
|
||||
waste.className = 'card-stack waste';
|
||||
gameBoard.appendChild(waste);
|
||||
|
||||
// Foundation-Stapel erstellen
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const foundation = document.createElement('div');
|
||||
foundation.className = `card-stack foundation foundation${i + 1}`;
|
||||
foundation.dataset.foundation = i;
|
||||
gameBoard.appendChild(foundation);
|
||||
}
|
||||
|
||||
// Tableau erstellen (7 Spalten)
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const stack = document.createElement('div');
|
||||
stack.className = `card-stack tableau tableau${i + 1}`;
|
||||
stack.dataset.column = i;
|
||||
|
||||
// Event-Listener für leere Tableau-Felder
|
||||
stack.addEventListener('click', (e) => {
|
||||
if (e.target === stack && selectedCard) {
|
||||
tryMoveCard(null);
|
||||
}
|
||||
});
|
||||
|
||||
// Karten für diese Spalte
|
||||
for (let j = 0; j <= i; j++) {
|
||||
const card = deck.pop();
|
||||
const cardElement = createCardElement(card, j === i);
|
||||
cardElement.style.top = `${j * 30}px`;
|
||||
stack.appendChild(cardElement);
|
||||
}
|
||||
|
||||
gameBoard.appendChild(stack);
|
||||
}
|
||||
|
||||
// Restliche Karten auf den Stock legen
|
||||
while (deck.length > 0) {
|
||||
const card = deck.pop();
|
||||
const cardElement = createCardElement(card, false);
|
||||
cardElement.style.top = '0';
|
||||
stock.appendChild(cardElement);
|
||||
}
|
||||
}
|
||||
|
||||
function createCardElement(card, faceUp) {
|
||||
const cardElement = document.createElement('div');
|
||||
cardElement.className = `card ${faceUp ? '' : 'face-down'}`;
|
||||
cardElement.dataset.suit = card.suit;
|
||||
cardElement.dataset.value = card.value;
|
||||
|
||||
if (faceUp) {
|
||||
cardElement.style.backgroundImage = `url('cards/${card.value}_${card.suit}.png')`;
|
||||
}
|
||||
|
||||
cardElement.addEventListener('click', handleCardClick);
|
||||
return cardElement;
|
||||
}
|
||||
|
||||
function getSuitSymbol(suit) {
|
||||
const symbols = {
|
||||
hearts: '♥',
|
||||
diamonds: '♦',
|
||||
clubs: '♣',
|
||||
spades: '♠'
|
||||
};
|
||||
return symbols[suit];
|
||||
}
|
||||
|
||||
function handleCardClick(e) {
|
||||
const card = e.target.closest('.card');
|
||||
if (!card || card.classList.contains('face-down')) return;
|
||||
|
||||
if (!gameStarted) {
|
||||
startGame();
|
||||
}
|
||||
|
||||
// Erlauben Sie das Auswählen von Karten vom Ablagestapel
|
||||
if (!selectedCard) {
|
||||
if (card.parentElement.classList.contains('waste') && card === card.parentElement.lastElementChild) {
|
||||
selectCard(card);
|
||||
} else if (card.parentElement.classList.contains('tableau') ||
|
||||
card.parentElement.classList.contains('foundation')) {
|
||||
selectCard(card);
|
||||
}
|
||||
} else {
|
||||
if (card === selectedCard) {
|
||||
// Abwählen der Karte
|
||||
selectedCard.classList.remove('selected');
|
||||
selectedCard = null;
|
||||
} else if (card.parentElement.classList.contains('tableau') ||
|
||||
card.parentElement.classList.contains('foundation')) {
|
||||
tryMoveCard(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectCard(card) {
|
||||
selectedCard = card;
|
||||
card.classList.add('selected');
|
||||
}
|
||||
|
||||
function tryMoveCard(targetCard) {
|
||||
const sourceStack = selectedCard.parentElement;
|
||||
const targetStack = targetCard ? targetCard.parentElement : sourceStack.parentElement.querySelector('.tableau:empty');
|
||||
|
||||
if (!targetCard && targetStack) {
|
||||
// Versuch, auf ein leeres Feld zu legen
|
||||
if (isValidMove(selectedCard, null)) {
|
||||
moveCard(selectedCard, targetStack);
|
||||
moves++;
|
||||
updateScore(10);
|
||||
checkWinCondition();
|
||||
} else {
|
||||
showInvalidMove();
|
||||
}
|
||||
} else if (targetCard && isValidMove(selectedCard, targetCard)) {
|
||||
moveCard(selectedCard, targetStack);
|
||||
moves++;
|
||||
updateScore(10);
|
||||
checkWinCondition();
|
||||
} else {
|
||||
showInvalidMove();
|
||||
}
|
||||
|
||||
selectedCard.classList.remove('selected');
|
||||
selectedCard = null;
|
||||
}
|
||||
|
||||
function isValidMove(sourceCard, targetCard) {
|
||||
const sourceValue = parseInt(sourceCard.dataset.value);
|
||||
const sourceSuit = sourceCard.dataset.suit;
|
||||
|
||||
// Prüfe auf leeres Tableau-Feld für König
|
||||
if (!targetCard) {
|
||||
// Nur Könige auf leere Tableau-Felder erlauben
|
||||
return sourceValue === 13;
|
||||
}
|
||||
|
||||
const targetValue = parseInt(targetCard.dataset.value);
|
||||
const targetSuit = targetCard.dataset.suit;
|
||||
|
||||
// Grundlegende Regeln für Tableau-Bewegungen
|
||||
if (targetCard.parentElement.classList.contains('tableau')) {
|
||||
return (sourceValue === targetValue - 1) &&
|
||||
((sourceSuit === 'heart' || sourceSuit === 'diamond') !==
|
||||
(targetSuit === 'heart' || targetSuit === 'diamond'));
|
||||
}
|
||||
// Regeln für Foundation-Stapel
|
||||
else if (targetCard.parentElement.classList.contains('foundation')) {
|
||||
return (sourceValue === targetValue + 1) && (sourceSuit === targetSuit);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function moveCard(card, targetStack) {
|
||||
const sourceStack = card.parentElement;
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
const targetRect = targetStack.getBoundingClientRect();
|
||||
|
||||
card.style.setProperty('--moveX', `${targetRect.left - cardRect.left}px`);
|
||||
card.style.setProperty('--moveY', `${targetRect.top - cardRect.top}px`);
|
||||
card.classList.add('moving');
|
||||
|
||||
// Sammle nur die ausgewählte Karte und ihre gültigen Nachfolgekarten
|
||||
const cards = [];
|
||||
let currentCard = card;
|
||||
|
||||
// Für Tableau-zu-Tableau Bewegungen
|
||||
if (sourceStack.classList.contains('tableau') && targetStack.classList.contains('tableau')) {
|
||||
while (currentCard) {
|
||||
// Prüfe, ob die Karte sichtbar ist und Teil der gültigen Sequenz
|
||||
if (!currentCard.classList.contains('face-down')) {
|
||||
cards.push(currentCard);
|
||||
currentCard = currentCard.nextElementSibling;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Für alle anderen Bewegungen nur die einzelne Karte verschieben
|
||||
cards.push(card);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// Bestimme die Basis-Position für den Stapel
|
||||
let basePosition = 0;
|
||||
if (targetStack.classList.contains('tableau')) {
|
||||
const existingCards = targetStack.children.length;
|
||||
basePosition = existingCards * 30;
|
||||
}
|
||||
|
||||
// Füge die Karten in der richtigen Reihenfolge hinzu
|
||||
cards.forEach((c, index) => {
|
||||
c.style.zIndex = basePosition + index;
|
||||
targetStack.appendChild(c);
|
||||
c.classList.remove('moving');
|
||||
if (targetStack.classList.contains('tableau')) {
|
||||
c.style.top = `${basePosition + (index * 30)}px`;
|
||||
} else {
|
||||
c.style.top = '0';
|
||||
}
|
||||
});
|
||||
|
||||
// Aufdecken der nächsten Karte im Quellstapel
|
||||
const lastCard = sourceStack.lastElementChild;
|
||||
if (lastCard && lastCard.classList.contains('face-down')) {
|
||||
flipCard(lastCard);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function flipCard(card) {
|
||||
card.classList.add('flipping');
|
||||
setTimeout(() => {
|
||||
card.classList.remove('face-down', 'flipping');
|
||||
const value = card.dataset.value;
|
||||
const suit = card.dataset.suit;
|
||||
card.style.backgroundImage = `url('cards/${value}_${suit}.png')`;
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function updateScore(points) {
|
||||
score += points;
|
||||
document.getElementById('score').textContent = score;
|
||||
document.getElementById('moves').textContent = moves;
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
gameStarted = true;
|
||||
timerInterval = setInterval(updateTimer, 1000);
|
||||
}
|
||||
|
||||
function updateTimer() {
|
||||
gameTime++;
|
||||
const minutes = Math.floor(gameTime / 60);
|
||||
const seconds = gameTime % 60;
|
||||
document.getElementById('time').textContent =
|
||||
`${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function checkWinCondition() {
|
||||
const foundations = document.querySelectorAll('.foundation');
|
||||
let complete = true;
|
||||
|
||||
foundations.forEach(foundation => {
|
||||
if (foundation.children.length !== 13) {
|
||||
complete = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (complete) {
|
||||
endGame();
|
||||
}
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
clearInterval(timerInterval);
|
||||
const finalScore = calculateFinalScore();
|
||||
updateHighscores(finalScore);
|
||||
showWinModal(finalScore);
|
||||
}
|
||||
|
||||
function calculateFinalScore() {
|
||||
return score + Math.max(0, 1000 - (gameTime * 2) - (moves * 10));
|
||||
}
|
||||
|
||||
function updateHighscores(newScore) {
|
||||
let highscores = JSON.parse(localStorage.getItem('solitaire_highscores') || '[]');
|
||||
highscores.push({
|
||||
score: newScore,
|
||||
time: gameTime,
|
||||
moves: moves,
|
||||
date: new Date().toISOString()
|
||||
});
|
||||
highscores.sort((a, b) => b.score - a.score);
|
||||
highscores = highscores.slice(0, 10); // Nur die Top 10 behalten
|
||||
localStorage.setItem('solitaire_highscores', JSON.stringify(highscores));
|
||||
}
|
||||
|
||||
function showHighscores() {
|
||||
const modal = document.getElementById('highscoreModal');
|
||||
const list = document.getElementById('highscoreList');
|
||||
const highscores = JSON.parse(localStorage.getItem('solitaire_highscores') || '[]');
|
||||
|
||||
list.innerHTML = highscores.map((score, index) => `
|
||||
<div class="highscore-entry">
|
||||
<span>${index + 1}. ${score.score} Punkte</span>
|
||||
<span>${Math.floor(score.time / 60)}:${(score.time % 60).toString().padStart(2, '0')} - ${score.moves} Züge</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
modal.classList.add('show');
|
||||
}
|
||||
|
||||
function closeHighscores() {
|
||||
document.getElementById('highscoreModal').classList.remove('show');
|
||||
}
|
||||
|
||||
function takeScreenshot() {
|
||||
html2canvas(document.querySelector('.game-container')).then(canvas => {
|
||||
const link = document.createElement('a');
|
||||
link.download = 'solitaire-screenshot.png';
|
||||
link.href = canvas.toDataURL();
|
||||
link.click();
|
||||
});
|
||||
}
|
||||
|
||||
function newGame() {
|
||||
clearInterval(timerInterval);
|
||||
score = 0;
|
||||
moves = 0;
|
||||
gameTime = 0;
|
||||
gameStarted = false;
|
||||
selectedCard = null;
|
||||
stockIndex = 0;
|
||||
document.getElementById('score').textContent = '0';
|
||||
document.getElementById('moves').textContent = '0';
|
||||
document.getElementById('time').textContent = '0:00';
|
||||
createDeck();
|
||||
dealCards();
|
||||
initFoundations();
|
||||
}
|
||||
|
||||
// Füge Foundation-Stapel-Logik hinzu
|
||||
function initFoundations() {
|
||||
const foundations = document.querySelectorAll('.foundation');
|
||||
foundations.forEach((foundation, index) => {
|
||||
foundation.addEventListener('click', () => {
|
||||
if (selectedCard) {
|
||||
const cards = foundation.children;
|
||||
if (cards.length === 0) {
|
||||
// Nur Asse können auf leere Foundation-Stapel
|
||||
if (selectedCard.dataset.value === '1') {
|
||||
moveCard(selectedCard, foundation);
|
||||
selectedCard.classList.remove('selected');
|
||||
selectedCard = null;
|
||||
moves++;
|
||||
updateScore(10);
|
||||
} else {
|
||||
showInvalidMove();
|
||||
}
|
||||
} else {
|
||||
const topCard = cards[cards.length - 1];
|
||||
if (isValidMove(selectedCard, topCard)) {
|
||||
moveCard(selectedCard, foundation);
|
||||
selectedCard.classList.remove('selected');
|
||||
selectedCard = null;
|
||||
moves++;
|
||||
updateScore(10);
|
||||
} else {
|
||||
showInvalidMove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleStockClick() {
|
||||
if (!gameStarted) {
|
||||
startGame();
|
||||
}
|
||||
|
||||
const stock = document.querySelector('.stock');
|
||||
const waste = document.querySelector('.waste');
|
||||
|
||||
// Wenn Stock leer ist und Karten im Ablagestapel sind
|
||||
if (stock.children.length === 0 && waste.children.length > 0) {
|
||||
// Alle Karten vom Ablagestapel zurück auf den Stock
|
||||
while (waste.children.length > 0) {
|
||||
const card = waste.lastElementChild;
|
||||
card.classList.add('face-down');
|
||||
card.style.backgroundImage = `url('cards/card_back.png')`;
|
||||
stock.appendChild(card);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Wenn Karten im Stock sind, oberste Karte aufdecken und auf Ablagestapel legen
|
||||
if (stock.children.length > 0) {
|
||||
const card = stock.lastElementChild;
|
||||
card.classList.remove('face-down');
|
||||
const value = card.dataset.value;
|
||||
const suit = card.dataset.suit;
|
||||
card.style.backgroundImage = `url('cards/${value}_${suit}.png')`;
|
||||
|
||||
// Position für gestapelte Darstellung
|
||||
if (waste.children.length > 0) {
|
||||
card.style.top = '0';
|
||||
card.style.left = '0';
|
||||
}
|
||||
|
||||
waste.appendChild(card);
|
||||
moves++;
|
||||
updateScore(-1);
|
||||
}
|
||||
}
|
||||
|
||||
function showRules() {
|
||||
const modal = document.getElementById('rulesModal');
|
||||
modal.classList.add('show');
|
||||
}
|
||||
|
||||
function closeRules() {
|
||||
document.getElementById('rulesModal').classList.remove('show');
|
||||
}
|
||||
|
||||
function showInvalidMove() {
|
||||
const modal = document.getElementById('invalidMoveModal');
|
||||
modal.classList.add('show');
|
||||
setTimeout(() => {
|
||||
modal.classList.remove('show');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function closeInvalidMove() {
|
||||
document.getElementById('invalidMoveModal').classList.remove('show');
|
||||
}
|
||||
|
||||
// Spiel initialisieren
|
||||
newGame();
|
||||
showRules(); // Zeige Spielregeln beim Start
|
||||
</script>
|
||||
</body>
|
||||
</html>
|