| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Solitaire</title> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap'); |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| user-select: none; |
| } |
| |
| body { |
| font-family: 'Roboto', sans-serif; |
| background: linear-gradient(135deg, #1e5799, #207cca, #2989d8, #7db9e8); |
| height: 100vh; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| color: #333; |
| overflow: hidden; |
| } |
| |
| .game-container { |
| width: 100%; |
| max-width: 1000px; |
| margin: 20px auto; |
| display: flex; |
| flex-direction: column; |
| height: calc(100vh - 100px); |
| } |
| |
| .header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| padding: 10px 0; |
| margin-bottom: 10px; |
| color: white; |
| } |
| |
| .score { |
| font-size: 1.2rem; |
| font-weight: bold; |
| } |
| |
| .restart-btn { |
| background: rgba(255, 255, 255, 0.2); |
| border: none; |
| color: white; |
| padding: 8px 15px; |
| border-radius: 5px; |
| cursor: pointer; |
| font-size: 1rem; |
| transition: background 0.3s; |
| } |
| |
| .restart-btn:hover { |
| background: rgba(255, 255, 255, 0.3); |
| } |
| |
| .game-board { |
| display: flex; |
| flex-direction: column; |
| flex-grow: 1; |
| } |
| |
| .top-section { |
| display: flex; |
| justify-content: space-between; |
| margin-bottom: 20px; |
| } |
| |
| .deck { |
| width: 70px; |
| height: 100px; |
| position: relative; |
| } |
| |
| .stock { |
| cursor: pointer; |
| position: relative; |
| } |
| |
| .waste { |
| position: relative; |
| margin-left: 10px; |
| } |
| |
| .foundations { |
| display: flex; |
| gap: 10px; |
| } |
| |
| .foundation { |
| width: 70px; |
| height: 100px; |
| border: 2px dashed rgba(255, 255, 255, 0.2); |
| border-radius: 5px; |
| } |
| |
| .tableau-section { |
| display: flex; |
| justify-content: space-between; |
| flex-grow: 1; |
| } |
| |
| .pile { |
| display: flex; |
| flex-direction: column; |
| gap: 3px; |
| min-height: 100px; |
| position: relative; |
| } |
| |
| .pile.bottom { |
| margin-top: 30px; |
| } |
| |
| .card { |
| width: 70px; |
| height: 100px; |
| background: white; |
| border-radius: 5px; |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); |
| position: absolute; |
| transition: transform 0.2s; |
| cursor: pointer; |
| display: flex; |
| flex-direction: column; |
| justify-content: space-between; |
| padding: 5px; |
| font-weight: bold; |
| font-size: 1.2rem; |
| overflow: hidden; |
| } |
| |
| .card.red { |
| color: red; |
| } |
| |
| .card.black { |
| color: black; |
| } |
| |
| .card.hidden { |
| background: linear-gradient(135deg, #3498db, #2980b9); |
| color: transparent; |
| box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2); |
| border: 2px solid rgba(255, 255, 255, 0.2); |
| } |
| |
| .card.hidden .card-value, |
| .card.hidden .card-suit { |
| display: none; |
| } |
| |
| .card-suit { |
| text-align: center; |
| font-size: 30px; |
| } |
| |
| .card-bottom-suit { |
| transform: rotate(180deg); |
| } |
| |
| .card-placeholder { |
| border: 2px dashed rgba(255, 255, 255, 0.3); |
| border-radius: 5px; |
| } |
| |
| .card.dragging { |
| opacity: 0.8; |
| z-index: 100; |
| transform: rotate(2deg); |
| box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3); |
| } |
| |
| .card.selected { |
| transform: translateY(-10px); |
| } |
| |
| .win-message { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: rgba(0, 0, 0, 0.7); |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| z-index: 1000; |
| color: white; |
| opacity: 0; |
| pointer-events: none; |
| transition: opacity 0.5s; |
| } |
| |
| .win-message.show { |
| opacity: 1; |
| pointer-events: all; |
| } |
| |
| .win-text { |
| font-size: 3rem; |
| margin-bottom: 20px; |
| text-align: center; |
| text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); |
| } |
| |
| .win-details { |
| font-size: 1.2rem; |
| margin-bottom: 30px; |
| } |
| |
| .move-counter { |
| font-size: 1.1rem; |
| margin-bottom: 5px; |
| color: rgba(255, 255, 255, 0.8); |
| } |
| |
| @media (max-width: 768px) { |
| .game-container { |
| transform: scale(0.8); |
| transform-origin: top center; |
| } |
| } |
| |
| @media (max-width: 480px) { |
| .game-container { |
| transform: scale(0.6); |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="game-container"> |
| <div class="header"> |
| <div class="score">Score: <span id="score">0</span></div> |
| <div class="move-counter">Moves: <span id="moves">0</span></div> |
| <button class="restart-btn" id="restart">New Game</button> |
| </div> |
|
|
| <div class="game-board"> |
| <div class="top-section"> |
| <div class="deck"> |
| <div class="card-placeholder stock" id="stock"></div> |
| <div class="card-placeholder waste" id="waste"></div> |
| </div> |
| <div class="foundations"> |
| <div class="card-placeholder foundation" id="foundation-1"></div> |
| <div class="card-placeholder foundation" id="foundation-2"></div> |
| <div class="card-placeholder foundation" id="foundation-3"></div> |
| <div class="card-placeholder foundation" id="foundation-4"></div> |
| </div> |
| </div> |
|
|
| <div class="tableau-section"> |
| <div class="pile" id="pile-1"></div> |
| <div class="pile" id="pile-2"></div> |
| <div class="pile" id="pile-3"></div> |
| <div class="pile" id="pile-4"></div> |
| <div class="pile" id="pile-5"></div> |
| <div class="pile" id="pile-6"></div> |
| <div class="pile" id="pile-7"></div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="win-message" id="win-message"> |
| <div class="win-text">You Won!</div> |
| <div class="win-details">Congratulations on completing Solitaire!</div> |
| <div>Final Score: <span id="final-score">0</span></div> |
| <div>Total Moves: <span id="final-moves">0</span></div> |
| <button class="restart-btn" style="margin-top: 20px;">Play Again</button> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| const game = { |
| deck: [], |
| stock: [], |
| waste: [], |
| foundations: [[], [], [], []], |
| piles: [[], [], [], [], [], [], []], |
| score: 0, |
| moves: 0, |
| draggingCards: [], |
| dragSource: null, |
| gameStarted: false |
| }; |
| |
| |
| const elements = { |
| stock: document.getElementById('stock'), |
| waste: document.getElementById('waste'), |
| foundations: [ |
| document.getElementById('foundation-1'), |
| document.getElementById('foundation-2'), |
| document.getElementById('foundation-3'), |
| document.getElementById('foundation-4') |
| ], |
| piles: [ |
| document.getElementById('pile-1'), |
| document.getElementById('pile-2'), |
| document.getElementById('pile-3'), |
| document.getElementById('pile-4'), |
| document.getElementById('pile-5'), |
| document.getElementById('pile-6'), |
| document.getElementById('pile-7') |
| ], |
| scoreDisplay: document.getElementById('score'), |
| movesDisplay: document.getElementById('moves'), |
| restartBtn: document.getElementById('restart'), |
| winMessage: document.getElementById('win-message'), |
| finalScore: document.getElementById('final-score'), |
| finalMoves: document.getElementById('final-moves') |
| }; |
| |
| |
| const suits = ['♥', '♦', '♠', '♣']; |
| const values = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']; |
| |
| |
| function initGame() { |
| resetGame(); |
| createDeck(); |
| shuffleDeck(); |
| dealCards(); |
| updateUI(); |
| game.gameStarted = true; |
| } |
| |
| |
| function resetGame() { |
| game.deck = []; |
| game.stock = []; |
| game.waste = []; |
| game.foundations = [[], [], [], []]; |
| game.piles = [[], [], [], [], [], [], []]; |
| game.score = 0; |
| game.moves = 0; |
| game.draggingCards = []; |
| game.dragSource = null; |
| game.gameStarted = false; |
| |
| |
| elements.piles.forEach(pile => pile.innerHTML = ''); |
| elements.foundations.forEach(foundation => foundation.innerHTML = ''); |
| elements.waste.innerHTML = ''; |
| } |
| |
| |
| function createDeck() { |
| game.deck = []; |
| for (let suitIndex = 0; suitIndex < suits.length; suitIndex++) { |
| for (let valueIndex = 0; valueIndex < values.length; valueIndex++) { |
| const card = { |
| suit: suits[suitIndex], |
| value: values[valueIndex], |
| color: (suitIndex < 2) ? 'red' : 'black', |
| rank: valueIndex + 1, |
| hidden: true |
| }; |
| game.deck.push(card); |
| } |
| } |
| } |
| |
| |
| function shuffleDeck() { |
| for (let i = game.deck.length - 1; i > 0; i--) { |
| const j = Math.floor(Math.random() * (i + 1)); |
| [game.deck[i], game.deck[j]] = [game.deck[j], game.deck[i]]; |
| } |
| } |
| |
| |
| function dealCards() { |
| let cardIndex = 0; |
| |
| |
| for (let pileIndex = 0; pileIndex < game.piles.length; pileIndex++) { |
| const pile = game.piles[pileIndex]; |
| |
| |
| for (let i = 0; i <= pileIndex; i++) { |
| const card = game.deck[cardIndex++]; |
| |
| |
| card.hidden = i !== pileIndex; |
| pile.push(card); |
| } |
| } |
| |
| |
| game.stock = game.deck.slice(cardIndex); |
| } |
| |
| |
| function updateUI() { |
| updateStock(); |
| updateWaste(); |
| updateFoundations(); |
| updatePiles(); |
| updateScore(); |
| checkWin(); |
| } |
| |
| |
| function updateStock() { |
| elements.stock.innerHTML = ''; |
| |
| if (game.stock.length > 0) { |
| const cardBack = document.createElement('div'); |
| cardBack.className = 'card hidden'; |
| elements.stock.appendChild(cardBack); |
| } else { |
| elements.stock.classList.add('empty'); |
| } |
| } |
| |
| |
| function updateWaste() { |
| elements.waste.innerHTML = ''; |
| |
| if (game.waste.length > 0) { |
| const lastCard = game.waste[game.waste.length - 1]; |
| const cardEl = createCardElement(lastCard); |
| cardEl.style.left = '0'; |
| cardEl.style.top = '0'; |
| elements.waste.appendChild(cardEl); |
| } |
| } |
| |
| |
| function updateFoundations() { |
| game.foundations.forEach((foundation, index) => { |
| elements.foundations[index].innerHTML = ''; |
| |
| if (foundation.length > 0) { |
| const lastCard = foundation[foundation.length - 1]; |
| const cardEl = createCardElement(lastCard); |
| elements.foundations[index].appendChild(cardEl); |
| } |
| }); |
| } |
| |
| |
| function updatePiles() { |
| game.piles.forEach((pile, pileIndex) => { |
| elements.piles[pileIndex].innerHTML = ''; |
| |
| pile.forEach((card, cardIndex) => { |
| const cardEl = createCardElement(card); |
| cardEl.style.top = `${cardIndex * 25}px`; |
| cardEl.setAttribute('data-pile-index', pileIndex); |
| cardEl.setAttribute('data-card-index', cardIndex); |
| elements.piles[pileIndex].appendChild(cardEl); |
| }); |
| }); |
| } |
| |
| |
| function createCardElement(card) { |
| const cardEl = document.createElement('div'); |
| cardEl.className = `card ${card.color}`; |
| cardEl.innerHTML = ` |
| <div class="card-value">${card.value}</div> |
| <div class="card-suit">${card.suit}</div> |
| <div class="card-suit card-bottom-suit">${card.suit}</div> |
| `; |
| |
| if (card.hidden) { |
| cardEl.classList.add('hidden'); |
| } |
| |
| |
| cardEl.draggable = true; |
| cardEl.addEventListener('dragstart', handleDragStart); |
| cardEl.addEventListener('mouseup', handleCardClick); |
| |
| return cardEl; |
| } |
| |
| |
| function updateScore() { |
| elements.scoreDisplay.textContent = game.score; |
| elements.movesDisplay.textContent = game.moves; |
| } |
| |
| |
| function drawCard() { |
| if (!game.gameStarted) return; |
| |
| if (game.stock.length === 0) { |
| |
| game.waste.reverse().forEach(card => { |
| card.hidden = true; |
| }); |
| game.stock = [...game.waste]; |
| game.waste = []; |
| game.score = Math.max(0, game.score - 100); |
| } else { |
| |
| const card = game.stock.pop(); |
| card.hidden = false; |
| game.waste.push(card); |
| game.score += 5; |
| } |
| |
| game.moves++; |
| updateUI(); |
| } |
| |
| |
| function handleDragStart(e) { |
| if (!game.gameStarted) return; |
| |
| const cardEl = e.target.closest('.card'); |
| if (!cardEl || cardEl.classList.contains('hidden')) { |
| e.preventDefault(); |
| return; |
| } |
| |
| const pileIndex = parseInt(cardEl.getAttribute('data-pile-index')); |
| const cardIndex = parseInt(cardEl.getAttribute('data-card-index')); |
| |
| |
| game.draggingCards = game.piles[pileIndex].slice(cardIndex); |
| game.dragSource = { type: 'pile', index: pileIndex }; |
| |
| |
| if (isNaN(pileIndex)) { |
| if (cardEl.parentElement === elements.waste) { |
| game.draggingCards = [game.waste[game.waste.length - 1]]; |
| game.dragSource = { type: 'waste' }; |
| } else { |
| const foundationIndex = elements.foundations.findIndex(f => cardEl.parentElement === f); |
| if (foundationIndex !== -1) { |
| game.draggingCards = [game.foundations[foundationIndex][game.foundations[foundationIndex].length - 1]]; |
| game.dragSource = { type: 'foundation', index: foundationIndex }; |
| } |
| } |
| } |
| |
| |
| const dragImage = cardEl.cloneNode(true); |
| dragImage.style.position = 'fixed'; |
| dragImage.style.left = '-1000px'; |
| dragImage.style.top = '0'; |
| dragImage.style.zIndex = '10000'; |
| document.body.appendChild(dragImage); |
| e.dataTransfer.setDragImage(dragImage, 35, 50); |
| setTimeout(() => document.body.removeChild(dragImage), 0); |
| |
| |
| e.dataTransfer.effectAllowed = 'move'; |
| |
| |
| setTimeout(() => cardEl.classList.add('dragging'), 0); |
| } |
| |
| |
| function handleDrop(target, e) { |
| e.preventDefault(); |
| if (!game.draggingCards.length) return; |
| |
| const card = game.draggingCards[0]; |
| |
| |
| let targetType, targetIndex; |
| if (target === elements.waste) { |
| return; |
| } else if (target === elements.stock) { |
| return; |
| } else if (elements.foundations.includes(target)) { |
| targetType = 'foundation'; |
| targetIndex = elements.foundations.indexOf(target); |
| } else if (elements.piles.includes(target)) { |
| targetType = 'pile'; |
| targetIndex = elements.piles.indexOf(target); |
| } else { |
| return; |
| } |
| |
| |
| let isValid = false; |
| |
| if (targetType === 'foundation') { |
| isValid = canMoveToFoundation(card, targetIndex); |
| } else if (targetType === 'pile') { |
| isValid = canMoveToPile(card, targetIndex); |
| } |
| |
| if (isValid) { |
| |
| if (game.dragSource.type === 'pile') { |
| game.piles[game.dragSource.index].splice( |
| game.piles[game.dragSource.index].length - game.draggingCards.length, |
| game.draggingCards.length |
| ); |
| |
| |
| const sourcePile = game.piles[game.dragSource.index]; |
| if (sourcePile.length > 0 && sourcePile[sourcePile.length - 1].hidden) { |
| sourcePile[sourcePile.length - 1].hidden = false; |
| game.score += 5; |
| } |
| } else if (game.dragSource.type === 'waste') { |
| game.waste.pop(); |
| } else if (game.dragSource.type === 'foundation') { |
| game.foundations[game.dragSource.index].pop(); |
| } |
| |
| |
| if (targetType === 'foundation') { |
| game.foundations[targetIndex].push(card); |
| game.score += 10; |
| } else if (targetType === 'pile') { |
| game.piles[targetIndex].push(...game.draggingCards); |
| |
| |
| game.score += (game.draggingCards.length === 1) ? 5 : 0; |
| } |
| |
| game.moves++; |
| game.draggingCards = []; |
| game.dragSource = null; |
| updateUI(); |
| } |
| } |
| |
| |
| function canMoveToFoundation(card, foundationIndex) { |
| const foundation = game.foundations[foundationIndex]; |
| |
| if (foundation.length === 0) { |
| |
| return card.value === 'A'; |
| } else { |
| const topCard = foundation[foundation.length - 1]; |
| |
| return topCard.suit === card.suit && card.rank === topCard.rank + 1; |
| } |
| } |
| |
| |
| function canMoveToPile(card, pileIndex) { |
| const pile = game.piles[pileIndex]; |
| |
| if (pile.length === 0) { |
| |
| return card.value === 'K'; |
| } else { |
| const topCard = pile[pile.length - 1]; |
| |
| return topCard.color !== card.color && card.rank === topCard.rank - 1; |
| } |
| } |
| |
| |
| function handleCardClick(e) { |
| if (!game.gameStarted) return; |
| |
| const cardEl = e.target.closest('.card'); |
| if (!cardEl || cardEl.classList.contains('hidden')) return; |
| |
| |
| if (cardEl.parentElement === elements.waste && game.waste.length > 0) { |
| const card = game.waste[game.waste.length - 1]; |
| |
| |
| for (let i = 0; i < game.foundations.length; i++) { |
| if (canMoveToFoundation(card, i)) { |
| game.foundations[i].push(game.waste.pop()); |
| game.score += 10; |
| game.moves++; |
| updateUI(); |
| return; |
| } |
| } |
| } |
| |
| |
| const pileIndex = parseInt(cardEl.getAttribute('data-pile-index')); |
| const cardIndex = parseInt(cardEl.getAttribute('data-card-index')); |
| |
| if (!isNaN(pileIndex) && cardIndex === game.piles[pileIndex].length - 1) { |
| const card = game.piles[pileIndex][cardIndex]; |
| |
| for (let i = 0; i < game.foundations.length; i++) { |
| if (canMoveToFoundation(card, i)) { |
| game.foundations[i].push(game.piles[pileIndex].pop()); |
| game.score += 10; |
| game.moves++; |
| |
| |
| if (game.piles[pileIndex].length > 0 && game.piles[pileIndex][game.piles[pileIndex].length - 1].hidden) { |
| game.piles[pileIndex][game.piles[pileIndex].length - 1].hidden = false; |
| game.score += 5; |
| } |
| |
| updateUI(); |
| return; |
| } |
| } |
| } |
| } |
| |
| |
| function checkWin() { |
| const allCardsInFoundations = game.foundations.every(foundation => foundation.length === 13); |
| |
| if (allCardsInFoundations) { |
| showWinMessage(); |
| } |
| } |
| |
| |
| function showWinMessage() { |
| elements.finalScore.textContent = game.score; |
| elements.finalMoves.textContent = game.moves; |
| elements.winMessage.classList.add('show'); |
| } |
| |
| |
| function setupEventListeners() { |
| |
| elements.stock.addEventListener('click', drawCard); |
| |
| |
| elements.restartBtn.addEventListener('click', initGame); |
| elements.winMessage.querySelector('.restart-btn').addEventListener('click', () => { |
| elements.winMessage.classList.remove('show'); |
| initGame(); |
| }); |
| |
| |
| document.addEventListener('dragover', e => { |
| e.preventDefault(); |
| const target = document.elementFromPoint(e.clientX, e.clientY)?.closest('.pile, .foundation, .waste, .stock'); |
| if (target) { |
| const rect = target.getBoundingClientRect(); |
| const isOver = e.clientX > rect.left && e.clientX < rect.right && |
| e.clientY > rect.top && e.clientY < rect.bottom; |
| if (isOver) { |
| if (target === elements.stock || target === elements.waste) return; |
| target.classList.add('highlight'); |
| } |
| } |
| }); |
| |
| document.addEventListener('dragleave', e => { |
| const target = document.elementFromPoint(e.clientX, e.clientY)?.closest('.pile, .foundation'); |
| if (!target || e.target !== target) { |
| elements.piles.forEach(pile => pile.classList.remove('highlight')); |
| elements.foundations.forEach(foundation => foundation.classList.remove('highlight')); |
| } |
| }); |
| |
| document.addEventListener('drop', e => { |
| e.preventDefault(); |
| elements.piles.forEach(pile => pile.classList.remove('highlight')); |
| elements.foundations.forEach(foundation => foundation.classList.remove('highlight')); |
| |
| const target = document.elementFromPoint(e.clientX, e.clientY)?.closest('.pile, .foundation, .waste, .stock'); |
| if (target && game.draggingCards.length) { |
| handleDrop(target, e); |
| } |
| }); |
| |
| document.addEventListener('dragend', () => { |
| document.querySelectorAll('.card').forEach(card => card.classList.remove('dragging')); |
| elements.piles.forEach(pile => pile.classList.remove('highlight')); |
| elements.foundations.forEach(foundation => foundation.classList.remove('highlight')); |
| }); |
| |
| |
| document.addEventListener('keydown', e => { |
| if (e.code === 'Space' && !e.target.matches('button, input, textarea')) { |
| e.preventDefault(); |
| drawCard(); |
| } else if (e.code === 'KeyR' && !e.target.matches('button, input, textarea')) { |
| e.preventDefault(); |
| initGame(); |
| } |
| }); |
| } |
| |
| |
| setupEventListeners(); |
| initGame(); |
| }); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body> |
| </html> |