1047 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			1047 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
| <!DOCTYPE html>
 | |
| <html lang="de">
 | |
| <head>
 | |
|     <meta charset="UTF-8">
 | |
|     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | |
|     <title>Solitaire</title>
 | |
|     <meta property="og:title" content="Solitaire">
 | |
|     <meta property="og:description" content="Klassisches Solitaire Kartenspiel">
 | |
|     <meta property="og:type" content="website">
 | |
|     <meta property="og:url" content="https://tools.ponywave.de/solitaire">
 | |
|     <meta property="og:image" content="https://tools.ponywave.de/solitaire/cards/card_back.png">
 | |
|     <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);
 | |
|         }
 | |
| 
 | |
|         /* Touch-spezifische Styles */
 | |
|         @media (hover: none) {
 | |
|             .card:hover {
 | |
|                 transform: none;
 | |
|             }
 | |
|             
 | |
|             .tableau:empty:hover,
 | |
|             .stock:empty:hover {
 | |
|                 border-color: rgba(255, 255, 255, 0.3);
 | |
|                 background: none;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         .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;
 | |
|         }
 | |
| 
 | |
|         .modal-buttons {
 | |
|             display: flex;
 | |
|             gap: 10px;
 | |
|             justify-content: center;
 | |
|             margin-top: 20px;
 | |
|         }
 | |
| 
 | |
|         .give-up-btn {
 | |
|             background: #e74c3c;
 | |
|         }
 | |
| 
 | |
|         .give-up-btn:hover {
 | |
|             background: #c0392b;
 | |
|         }
 | |
|     </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="showRules()">Regeln</button>
 | |
|                 <button onclick="takeScreenshot()">Screenshot</button>
 | |
|                 <button onclick="showGiveUpConfirmation()" class="give-up-btn">Aufgeben</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>
 | |
|                     <li>Bei vorzeitiger Aufgabe des Spiels werden die erspielten Punkte halbiert</li>
 | |
|                 </ul>
 | |
|                 <div style="margin-top: 15px; display: flex; align-items: center;">
 | |
|                     <input type="checkbox" id="dontShowRulesAgain" style="margin-right: 10px;">
 | |
|                     <label for="dontShowRulesAgain">Regeln beim Start nicht mehr anzeigen</label>
 | |
|                 </div>
 | |
|                 <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>
 | |
| 
 | |
|     <div class="modal" id="giveUpModal">
 | |
|         <div class="modal-content">
 | |
|             <h2>Spiel aufgeben?</h2>
 | |
|             <p>Möchtest du das Spiel wirklich aufgeben?</p>
 | |
|             <p style="color: #e74c3c; margin-top: 10px;">Achtung: Bei Aufgabe werden deine Punkte halbiert!</p>
 | |
|             <p style="margin-top: 10px;">Der finale Punktestand wird als Highscore gespeichert.</p>
 | |
|             <div class="modal-buttons">
 | |
|                 <button onclick="giveUp()">Ja, aufgeben</button>
 | |
|                 <button onclick="closeGiveUpModal()">Nein, weiterspielen</button>
 | |
|             </div>
 | |
|         </div>
 | |
|     </div>
 | |
| 
 | |
|     <div class="modal" id="winModal">
 | |
|         <div class="modal-content">
 | |
|             <h2>Glückwunsch!</h2>
 | |
|             <p>Du hast das Spiel gewonnen!</p>
 | |
|             <p style="margin-top: 10px;">Dein finaler Punktestand: <span id="finalScore">0</span></p>
 | |
|             <p style="margin-top: 5px;">Zeit: <span id="finalTime">0:00</span> - Züge: <span id="finalMoves">0</span></p>
 | |
|             <div class="modal-buttons">
 | |
|                 <button onclick="newGame()">Neues Spiel</button>
 | |
|                 <button onclick="showHighscores()">Highscores</button>
 | |
|             </div>
 | |
|         </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 = [];
 | |
| 
 | |
|         // Touch-Variablen
 | |
|         let touchStartX = 0;
 | |
|         let touchStartY = 0;
 | |
|         let isDragging = false;
 | |
|         let touchedCard = null;
 | |
| 
 | |
|         // 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')`;
 | |
|             }
 | |
| 
 | |
|             // Touch-Events hinzufügen
 | |
|             cardElement.addEventListener('touchstart', handleTouchStart, { passive: false });
 | |
|             cardElement.addEventListener('touchmove', handleTouchMove, { passive: false });
 | |
|             cardElement.addEventListener('touchend', handleTouchEnd);
 | |
|             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();
 | |
|                             }
 | |
|                         }
 | |
|                     }
 | |
|                 });
 | |
|             });
 | |
| 
 | |
|             // Touch-Support für Stock-Stapel
 | |
|             const stock = document.querySelector('.stock');
 | |
|             stock.addEventListener('touchstart', (e) => {
 | |
|                 e.preventDefault();
 | |
|                 handleStockClick();
 | |
|             });
 | |
|         }
 | |
| 
 | |
|         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');
 | |
|             
 | |
|             // Prüfe, ob die Checkbox angehakt ist
 | |
|             const dontShowAgain = document.getElementById('dontShowRulesAgain').checked;
 | |
|             if (dontShowAgain) {
 | |
|                 localStorage.setItem('solitaire_hide_rules', 'true');
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         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');
 | |
|         }
 | |
| 
 | |
|         function handleTouchStart(e) {
 | |
|             e.preventDefault();
 | |
|             const touch = e.touches[0];
 | |
|             touchStartX = touch.clientX;
 | |
|             touchStartY = touch.clientY;
 | |
|             touchedCard = e.target.closest('.card');
 | |
| 
 | |
|             if (touchedCard && !touchedCard.classList.contains('face-down')) {
 | |
|                 if (!gameStarted) {
 | |
|                     startGame();
 | |
|                 }
 | |
|                 selectCard(touchedCard);
 | |
|                 isDragging = true;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         function handleTouchMove(e) {
 | |
|             if (!isDragging || !touchedCard) return;
 | |
|             e.preventDefault();
 | |
| 
 | |
|             const touch = e.touches[0];
 | |
|             const deltaX = touch.clientX - touchStartX;
 | |
|             const deltaY = touch.clientY - touchStartY;
 | |
| 
 | |
|             touchedCard.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
 | |
|             touchedCard.style.zIndex = '1000';
 | |
|         }
 | |
| 
 | |
|         function handleTouchEnd(e) {
 | |
|             if (!isDragging || !touchedCard) return;
 | |
| 
 | |
|             const touch = e.changedTouches[0];
 | |
|             const endX = touch.clientX;
 | |
|             const endY = touch.clientY;
 | |
| 
 | |
|             // Finde das Element unter dem Finger
 | |
|             const elemBelow = document.elementFromPoint(endX, endY);
 | |
|             const targetStack = elemBelow ? elemBelow.closest('.card-stack') : null;
 | |
|             const targetCard = elemBelow ? elemBelow.closest('.card') : null;
 | |
| 
 | |
|             touchedCard.style.transform = '';
 | |
|             touchedCard.style.zIndex = '';
 | |
| 
 | |
|             if (targetStack) {
 | |
|                 if (targetCard && targetCard !== touchedCard) {
 | |
|                     tryMoveCard(targetCard);
 | |
|                 } else if (!targetCard && targetStack.classList.contains('tableau')) {
 | |
|                     tryMoveCard(null);
 | |
|                 } else if (targetStack.classList.contains('foundation')) {
 | |
|                     const cards = targetStack.children;
 | |
|                     if (cards.length === 0) {
 | |
|                         if (touchedCard.dataset.value === '1') {
 | |
|                             moveCard(touchedCard, targetStack);
 | |
|                             moves++;
 | |
|                             updateScore(10);
 | |
|                         } else {
 | |
|                             showInvalidMove();
 | |
|                         }
 | |
|                     } else {
 | |
|                         const topCard = cards[cards.length - 1];
 | |
|                         if (isValidMove(touchedCard, topCard)) {
 | |
|                             moveCard(touchedCard, targetStack);
 | |
|                             moves++;
 | |
|                             updateScore(10);
 | |
|                         } else {
 | |
|                             showInvalidMove();
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             isDragging = false;
 | |
|             touchedCard = null;
 | |
|             if (selectedCard) {
 | |
|                 selectedCard.classList.remove('selected');
 | |
|                 selectedCard = null;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         function showGiveUpConfirmation() {
 | |
|             const modal = document.getElementById('giveUpModal');
 | |
|             modal.classList.add('show');
 | |
|         }
 | |
| 
 | |
|         function closeGiveUpModal() {
 | |
|             document.getElementById('giveUpModal').classList.remove('show');
 | |
|         }
 | |
| 
 | |
|         function giveUp() {
 | |
|             clearInterval(timerInterval);
 | |
|             const finalScore = Math.floor(score / 2); // Halbiere den Score beim Aufgeben
 | |
|             updateHighscores(finalScore);
 | |
|             closeGiveUpModal();
 | |
|             showHighscores();
 | |
|             newGame();
 | |
|         }
 | |
| 
 | |
|         function showWinModal(finalScore) {
 | |
|             const modal = document.getElementById('winModal');
 | |
|             document.getElementById('finalScore').textContent = finalScore;
 | |
|             
 | |
|             const minutes = Math.floor(gameTime / 60);
 | |
|             const seconds = gameTime % 60;
 | |
|             document.getElementById('finalTime').textContent = 
 | |
|                 `${minutes}:${seconds.toString().padStart(2, '0')}`;
 | |
|             
 | |
|             document.getElementById('finalMoves').textContent = moves;
 | |
|             
 | |
|             modal.classList.add('show');
 | |
|         }
 | |
| 
 | |
|         // Prüfe, ob die Regeln angezeigt werden sollen
 | |
|         function shouldShowRules() {
 | |
|             return localStorage.getItem('solitaire_hide_rules') !== 'true';
 | |
|         }
 | |
| 
 | |
|         // Spiel initialisieren
 | |
|         newGame();
 | |
|         if (shouldShowRules()) {
 | |
|             showRules(); // Zeige Spielregeln beim Start nur, wenn nicht deaktiviert
 | |
|         }
 | |
|     </script>
 | |
| </body>
 | |
| </html>  |