document.addEventListener('DOMContentLoaded', () => { // ====== CONSTANTS & STATE ====== const suits = ['♥', '♦', '♣', '♠']; const values = ['A','2','3','4','5','6','7','8','9','10','J','Q','K']; const PILE_BORDER_WIDTH = 2; const SPACING_FACE_DOWN = 10; const SPACING_FACE_UP = 32; let DRAW_COUNT = 3; let deck, stock, waste, foundations, tableau; let cardElements = {}; let dragged = { cards: [], sourcePile: null, startIndex: -1, ghostEl: null, offsetX: 0, offsetY: 0 }; const history = []; let isAutoCompleting = false; let autoCompleteInterval = null; let isDealing = false; let score = 0; let highScore = 0; let isActive = false; const CARD_BACKS = [ { id: 'waves', name: 'Blue Waves', className: 'card-back-waves' }, { id: 'herringbone', name: 'Green Herringbone', className: 'card-back-herringbone' }, { id: 'sunset', name: 'Gradient Sunset', className: 'card-back-sunset' }, { id: 'linen', name: 'Cozy Linen', className: 'card-back-linen' }, { id: 'nordic', name: 'Nordic Cross', className: 'card-back-nordic' }, { id: 'midnight', name: 'Midnight Grid', className: 'card-back-midnight' }, { id: 'diamond', name: 'Diamond Weave', className: 'card-back-diamond' }, { id: 'royal', name: 'Royal Blue Stripe', className: 'card-back-royal' }, { id: 'fade', name: 'Matte Gradient Fade', className: 'card-back-fade' } ]; // ====== DOM ====== const klondikeBoard = document.getElementById('klondike-board'); const restartBtn = document.getElementById('restart-btn'); const winMessage = document.getElementById('win-message'); const undoBtn = document.getElementById('undo-btn'); const drawSelect = document.getElementById('draw-select'); const cardBackModal = document.getElementById('card-back-modal'); const openModalBtn = document.getElementById('change-back-btn'); const closeModalBtn = document.getElementById('modal-close-btn'); const modalOverlay = document.querySelector('.modal-overlay'); const cardBackOptionsContainer = document.getElementById('card-back-options'); const checkMovesBtn = document.getElementById('check-moves-btn'); const autoCompleteBtn = document.getElementById('autocomplete-btn'); const scoreEl = document.getElementById('current-score'); const highScoreEl = document.getElementById('high-score'); // ====== SAVE / LOAD ====== function saveGame() { const gameState = { stock, waste, foundations, tableau, history, drawCount: DRAW_COUNT, score }; localStorage.setItem('solitaire-save-game', JSON.stringify(gameState)); } function loadGame() { const savedGame = localStorage.getItem('solitaire-save-game'); if (!savedGame) return false; try { const gameState = JSON.parse(savedGame); stock = gameState.stock; waste = gameState.waste; foundations = gameState.foundations; tableau = gameState.tableau; history.length = 0; history.push(...gameState.history); DRAW_COUNT = gameState.drawCount || 3; score = gameState.score || 0; highScore = parseInt(localStorage.getItem('solitaire-high-score'), 10) || 0; drawSelect.value = DRAW_COUNT; Object.values(cardElements).forEach(el => el.remove()); cardElements = {}; const allCards = [...stock, ...waste, ...foundations.flat(), ...tableau.flat()]; allCards.forEach(cardData => { if (!cardElements[cardData.id]) { const el = createCardElement(cardData); cardElements[cardData.id] = el; klondikeBoard.appendChild(el); } }); winMessage.classList.add('hidden'); updateScoreDisplay(); updateBoard(true); return true; } catch (e) { console.error("Failed to load saved game:", e); localStorage.removeItem('solitaire-save-game'); return false; } } // ====== UNDO SNAPSHOTS ====== const snapshotState = () => JSON.stringify({ stock, waste, foundations, tableau, score }); const restoreState = (json) => { const s = JSON.parse(json); stock = s.stock; waste = s.waste; foundations = s.foundations; tableau = s.tableau; score = s.score !== undefined ? s.score : score; }; const pushHistory = () => { if (history.length > 50) history.shift(); history.push(snapshotState()); }; // ====== SETUP ====== function initState() { score = 0; highScore = parseInt(localStorage.getItem('solitaire-high-score'), 10) || 0; updateScoreDisplay(); isAutoCompleting = false; if (autoCompleteInterval) clearInterval(autoCompleteInterval); Object.values(cardElements).forEach(el => el.remove()); cardElements = {}; deck = []; stock = []; waste = []; foundations = [[],[],[],[]]; tableau = Array(7).fill(null).map(()=>[]); suits.forEach(suit => values.forEach(value => { deck.push({ suit, value, faceUp:false, id:`${value}${suit}-${Math.random().toString(36).slice(2,8)}` }); })); 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]]; } deck.forEach(cardData => { const el = createCardElement(cardData); cardElements[cardData.id] = el; klondikeBoard.appendChild(el); // Initially position cards at the stock location to animate out from there const stockEl = document.getElementById('stock'); if (stockEl) { const rect = stockEl.getBoundingClientRect(); const boardRect = klondikeBoard.getBoundingClientRect(); el.style.top = `${rect.top - boardRect.top + PILE_BORDER_WIDTH}px`; el.style.left = `${rect.left - boardRect.left + PILE_BORDER_WIDTH}px`; } }); // Separate cards for tableau and stock const tableauCards = []; for (let i=0; i<7; i++){ for (let j=i; j<7; j++) { const card = deck.pop(); tableau[j].push(card); tableauCards.push({ card, pileIndex: j }); } } stock = deck; winMessage.classList.add('hidden'); history.length = 0; pushHistory(); // Start dealing animation dealTableauSequentially(tableauCards); } async function dealTableauSequentially(cardsToDeal) { isDealing = true; // Turn off interactivity during deal klondikeBoard.style.pointerEvents = 'none'; for (let i = 0; i < cardsToDeal.length; i++) { const { card, pileIndex } = cardsToDeal[i]; // Only flip the last card of each pile const pile = tableau[pileIndex]; if (card === pile[pile.length - 1]) { card.faceUp = true; } updateBoard(true); await new Promise(r => setTimeout(r, 60)); } isDealing = false; klondikeBoard.style.pointerEvents = 'auto'; updateBoard(); saveGame(); } // ====== RENDER ====== function updateBoard(initial=false) { if (!isActive) return; updateScoreDisplay(); const gameBoardRect = klondikeBoard.getBoundingClientRect(); const updatePile = (pileData, pileEl, type) => { if (!pileEl) return; const { top, left } = pileEl.getBoundingClientRect(); const baseTop = top - gameBoardRect.top + PILE_BORDER_WIDTH; const baseLeft = left - gameBoardRect.left + PILE_BORDER_WIDTH; let currentOffset = 0; pileData.forEach((cardData, i) => { const cardEl = cardElements[cardData.id]; if(!cardEl) return; // Don't update position for cards being dragged (ghost) if (dragged.ghostEl && dragged.cards.some(c => c.id === cardData.id)) { cardEl.style.visibility = 'hidden'; // Hide the original card return; } else { cardEl.style.visibility = 'visible'; } if (type === 'tableau') { cardEl.style.top = `${baseTop + currentOffset}px`; currentOffset += cardData.faceUp ? SPACING_FACE_UP : SPACING_FACE_DOWN; } else { cardEl.style.top = `${baseTop}px`; } cardEl.style.left = `${baseLeft}px`; cardEl.style.pointerEvents = 'auto'; cardEl.classList.toggle('is-stacked', i < pileData.length - 1 && type !== 'tableau'); if (cardData.faceUp) { cardEl.classList.add('is-flipped'); cardEl.draggable = true; } else { cardEl.classList.remove('is-flipped'); cardEl.draggable = false; } const zBase = type === 'tableau' ? 100 : type === 'foundation' ? 400 : 600; cardEl.style.zIndex = zBase + i; }); }; tableau.forEach((p,i)=>updatePile(p, document.getElementById(`tableau-${i}`), 'tableau')); foundations.forEach((p,i)=>updatePile(p, document.getElementById(`foundation-${i}`), 'foundation')); updatePile(stock, document.getElementById('stock'), 'stock'); stock.forEach(cardData => { if(cardElements[cardData.id]) cardElements[cardData.id].style.pointerEvents = 'none'; }); const wasteEl = document.getElementById('waste'); if (wasteEl) { const { top:wt, left:wl } = wasteEl.getBoundingClientRect(); const baseTop = wt - gameBoardRect.top + PILE_BORDER_WIDTH; const baseLeft = wl - gameBoardRect.left + PILE_BORDER_WIDTH; waste.forEach((cardData, i) => { const cardEl = cardElements[cardData.id]; if(!cardEl) return; if (dragged.ghostEl && dragged.cards.some(c => c.id === cardData.id)) { cardEl.style.visibility = 'hidden'; return; } else { cardEl.style.visibility = 'visible'; } const isTopCard = i === waste.length - 1; const visibleFanIndexStart = Math.max(0, waste.length - DRAW_COUNT); let leftOffset = 0; if (i >= visibleFanIndexStart) { leftOffset = (i - visibleFanIndexStart) * 18; } cardEl.style.top = `${baseTop}px`; cardEl.style.left = `${baseLeft + leftOffset}px`; cardEl.style.zIndex = 800 + i; cardEl.classList.add('is-flipped'); cardEl.classList.toggle('is-stacked', !isTopCard); cardEl.draggable = isTopCard; cardEl.style.pointerEvents = isTopCard ? 'auto' : 'none'; }); } if (!initial) checkWin(); } function createCardElement(cardData) { const el = document.createElement('div'); el.classList.add('card'); el.dataset.id = cardData.id; const cardInner = document.createElement('div'); cardInner.classList.add('card-inner'); const color = (cardData.suit === '♥' || cardData.suit === '♦') ? 'red' : 'black'; const front = document.createElement('div'); front.className = `card-face card-face--front ${color}`; front.innerHTML = `
${cardData.value}${cardData.suit}
${cardData.value}${cardData.suit}
${cardData.suit}
`; const back = document.createElement('div'); back.className = 'card-face card-face--back'; cardInner.append(front, back); el.appendChild(cardInner); return el; } // ====== THEMES & MODAL ====== function applyCardBack(backId) { const back = CARD_BACKS.find(b => b.id === backId) || CARD_BACKS[0]; CARD_BACKS.forEach(b => document.body.classList.remove(b.className)); document.body.classList.add(back.className); document.querySelectorAll('.back-option').forEach(el => el.classList.toggle('active', el.dataset.id === backId)); } function setupThemeSelector() { if (!cardBackOptionsContainer) return; cardBackOptionsContainer.innerHTML = ''; CARD_BACKS.forEach(back => { const optionEl = document.createElement('div'); optionEl.className = `back-option ${back.className}`; optionEl.dataset.id = back.id; optionEl.title = back.name; const previewInner = document.createElement('div'); previewInner.className = 'card-face card-face--back'; optionEl.appendChild(previewInner); cardBackOptionsContainer.appendChild(optionEl); }); cardBackOptionsContainer.addEventListener('click', e => { const target = e.target.closest('.back-option'); if (target) { const backId = target.dataset.id; applyCardBack(backId); localStorage.setItem('solitaire-card-back', backId); cardBackModal.classList.add('hidden'); } }); const savedBack = localStorage.getItem('solitaire-card-back'); applyCardBack(savedBack || 'waves'); } // ====== HELPERS ====== function updateScoreDisplay() { if (!isActive) return; scoreEl.textContent = score; highScoreEl.textContent = highScore; } function getValueRank(v) { if (v==='A') return 1; if (v==='K') return 13; if (v==='Q') return 12; if (v==='J') return 11; return parseInt(v,10); } function getPileArrayFromElement(el) { if (!el) return null; const [type, index] = el.id.split('-'); if (type === 'tableau') return tableau[Number(index)]; if (type === 'foundation') return foundations[Number(index)]; if (type === 'waste') return waste; if (type === 'stock') return stock; return null; } // Helper to get the actual pile that a card belongs to function getPileContainingCard(cardId) { const checkPile = (pile) => pile.find(c => c.id === cardId); if (checkPile(waste)) return waste; for (const pile of tableau) { if (checkPile(pile)) return pile; } for (const pile of foundations) { if (checkPile(pile)) return pile; } return null; } function findCardData(cardEl) { const id = cardEl.dataset.id; let cardIndex; // Check waste (only top card can be moved) if (waste.length > 0 && waste[waste.length - 1].id === id) { return { sourcePile: waste, cardIndex: waste.length - 1, card: waste[waste.length - 1], cardsToMove: [waste[waste.length - 1]] }; } // Check tableau piles (any face-up card and cards above it can be moved) for (const pile of tableau) { cardIndex = pile.findIndex(c => c.id === id); if (cardIndex > -1 && pile[cardIndex].faceUp) { const cardsToMove = pile.slice(cardIndex); return { sourcePile: pile, cardIndex, card: cardsToMove[0], cardsToMove }; } } // Check foundations (only top card can be moved) for (const pile of foundations) { if (pile.length > 0 && pile[pile.length - 1].id === id) { return { sourcePile: pile, cardIndex: pile.length - 1, card: pile[pile.length - 1], cardsToMove: [pile[pile.length - 1]] }; } } return {}; // Return empty object if card not found or not movable } function canMove(card, destPile) { if (!card || !destPile) return false; // Prevent moving to the stock pile if (destPile === stock) return false; const destType = (foundations.includes(destPile)) ? 'foundation' : (tableau.includes(destPile)) ? 'tableau' : 'other'; if (destType === 'other' && destPile !== waste) return false; // Only allow tableau, foundation, or waste as actual destinations // Waste pile can only receive cards if it's not the source for the current drag (handled by dragged.sourcePile check) // Also, waste doesn't typically accept cards from other piles in Klondike. // We ensure we don't try to drop on the waste pile itself unless it's for internal logic like recycling stock. if (destPile === waste) return false; // Generally, cards don't move TO waste. if (destPile.length === 0) { return (destType==='tableau' && card.value==='K') || (destType==='foundation' && card.value==='A'); } const top = destPile[destPile.length - 1]; if (destType === 'tableau') { const opposite = ((['♥','♦'].includes(card.suit) && ['♣','♠'].includes(top.suit)) || (['♣','♠'].includes(card.suit) && ['♥','♦'].includes(top.suit))); return opposite && getValueRank(card.value) === getValueRank(top.value) - 1; } else if (destType === 'foundation') { return card.suit === top.suit && getValueRank(card.value) === getValueRank(top.value) + 1; } return false; } function moveSequence(sourcePile, startIndex, destPile) { const seq = sourcePile.splice(startIndex); destPile.push(...seq); } function performMoveCleanup(sourcePile) { if (tableau.includes(sourcePile) && sourcePile.length > 0) { const topCard = sourcePile[sourcePile.length - 1]; if (!topCard.faceUp) { topCard.faceUp = true; score += 5; } } updateBoard(); saveGame(); if (!isAutoCompleting) { checkAndTriggerAutoComplete(); } } function checkWin() { if (foundations.every(p=>p.length===13)) { if (score > highScore) { highScore = score; localStorage.setItem('solitaire-high-score', highScore); updateScoreDisplay(); } winMessage.classList.remove('hidden'); triggerBouncingCards(); } } function triggerBouncingCards() { const boardRect = klondikeBoard.getBoundingClientRect(); const cardsToBounce = []; // Gather all top cards from foundations to start the bounce sequence // We'll process them one suit at a time for the classic effect const animationFoundations = foundations.map(f => [...f]); // Clone function bounceNextCard() { let cardFound = false; for (let i = 0; i < 4; i++) { if (animationFoundations[i].length > 0) { const cardData = animationFoundations[i].pop(); const cardEl = cardElements[cardData.id]; if (cardEl) { startCardPhysics(cardEl); cardFound = true; break; // One card per trigger } } } if (cardFound) { setTimeout(bounceNextCard, 180); } } function startCardPhysics(el) { const rect = el.getBoundingClientRect(); let x = rect.left; let y = rect.top; let vx = (Math.random() * 4 + 2) * (Math.random() > 0.5 ? 1 : -1); let vy = Math.random() * -5 - 2; const gravity = 0.5; const friction = 0.75; // Remove transition for physics el.style.transition = 'none'; el.style.position = 'fixed'; el.style.zIndex = 3000; el.style.pointerEvents = 'none'; function step() { x += vx; y += vy; vy += gravity; if (y + rect.height > window.innerHeight) { y = window.innerHeight - rect.height; vy *= -friction; if (Math.abs(vy) < 2) vy = 0; // Stop vertical bounce if too small } if (x + rect.width < 0 || x > window.innerWidth) { el.remove(); return; // Card off screen } el.style.left = `${x}px`; el.style.top = `${y}px`; if (Math.abs(vx) > 0.1 || Math.abs(vy) > 0.1 || y < window.innerHeight - rect.height) { requestAnimationFrame(step); } else { // Small slide before stopping vx *= 0.95; if (Math.abs(vx) > 0.1) requestAnimationFrame(step); else el.remove(); } } requestAnimationFrame(step); } bounceNextCard(); } window.triggerBouncingCards = triggerBouncingCards; // ====== ACTIONS ====== function handleStockClick() { if (isAutoCompleting) return; pushHistory(); if (stock.length > 0) { const drawCount = Math.min(stock.length, DRAW_COUNT); const cardsToMove = stock.splice(stock.length - drawCount, drawCount).reverse(); cardsToMove.forEach(c => c.faceUp = true); waste.push(...cardsToMove); } else if (waste.length > 0) { score = Math.max(0, score - 20); stock = waste.reverse(); stock.forEach(c => c.faceUp = false); waste = []; } updateBoard(); saveGame(); } function performAutoMove(cardEl) { if (isAutoCompleting) return; const { sourcePile, cardIndex, cardsToMove } = findCardData(cardEl); if (!sourcePile || !cardsToMove || cardsToMove.length === 0) return; const cardToMove = cardsToMove[0]; // Priority 1: Try to move a single card to a foundation. if (cardsToMove.length === 1) { for (const f of foundations) { if (canMove(cardToMove, f)) { score += 10; pushHistory(); moveSequence(sourcePile, cardIndex, f); performMoveCleanup(sourcePile); return; } } } // Priority 2: Try to move a stack to another tableau pile. for (const t of tableau) { if (t !== sourcePile && canMove(cardToMove, t)) { if (sourcePile === waste) score += 5; if (foundations.includes(sourcePile)) score -= 15; pushHistory(); moveSequence(sourcePile, cardIndex, t); performMoveCleanup(sourcePile); return; } } } function undo() { if (isAutoCompleting) return; if (history.length <= 1) return; history.pop(); restoreState(history[history.length-1]); winMessage.classList.add('hidden'); updateBoard(true); saveGame(); } function checkAndTriggerAutoComplete() { if (isAutoCompleting) return; const isWinnable = stock.length === 0 && tableau.flat().every(card => card.faceUp); if (isWinnable) { autoComplete(); } } function autoComplete() { const isWinnable = stock.length === 0 && tableau.flat().every(card => card.faceUp); if (!isWinnable) { Swal.fire({ title: 'Cannot Auto-Complete', text: 'You can only auto-complete when all cards are face-up and the stock is empty.', icon: 'info' }); return; } isAutoCompleting = true; autoCompleteInterval = setInterval(() => { let movedCard = false; let source = null; if (waste.length > 0) { const card = waste[waste.length - 1]; for (const f of foundations) { if (canMove(card, f)) { score += 10; source = waste; moveSequence(source, source.length - 1, f); movedCard = true; break; } } } if (!movedCard) { for (const t of tableau) { if (t.length > 0) { const card = t[t.length - 1]; for (const f of foundations) { if (canMove(card, f)) { score += 10; source = t; moveSequence(source, source.length - 1, f); movedCard = true; break; } } } if (movedCard) break; } } if (source) performMoveCleanup(source); if (!movedCard) { clearInterval(autoCompleteInterval); autoCompleteInterval = null; isAutoCompleting = false; checkWin(); } }, 120); } function checkForAnyAvailableMove() { const topWasteCard = waste.length > 0 ? waste[waste.length - 1] : null; if (topWasteCard) { for (const f of foundations) if (canMove(topWasteCard, f)) return true; for (const t of tableau) if (canMove(topWasteCard, t)) return true; } for (const sourcePile of tableau) { if (sourcePile.length > 0) { const topCard = sourcePile[sourcePile.length-1]; for(const f of foundations) if(canMove(topCard, f)) return true; const movableStackIndex = sourcePile.findIndex(card => card.faceUp); if (movableStackIndex > -1) { const cardToMove = sourcePile[movableStackIndex]; for (const destPile of tableau) { if (sourcePile !== destPile && canMove(cardToMove, destPile)) return true; } } } } return false; } function handleStuckCheck() { if (checkForAnyAvailableMove()) { Swal.fire({ title: 'Hint!', text: 'A move is available on the board!', icon: 'info' }); return; } let tempStock = JSON.parse(JSON.stringify(stock)); let tempWaste = JSON.parse(JSON.stringify(waste)); const originalStockSize = tempStock.length + tempWaste.length; if (originalStockSize === 0) { Swal.fire({ title: 'No Moves Found', text: 'The game appears to be unwinnable from this state.', icon: 'warning' }); return; } let cardsCycled = 0; while (cardsCycled < originalStockSize) { if (tempStock.length === 0) { tempStock = tempWaste.reverse(); tempWaste = []; } const draw = Math.min(tempStock.length, DRAW_COUNT); const drawnCards = tempStock.splice(tempStock.length - draw, draw); tempWaste.push(...drawnCards); cardsCycled += drawnCards.length; const newTopWasteCard = tempWaste.length > 0 ? tempWaste[tempWaste.length - 1] : null; if (newTopWasteCard) { const hintText = "A move will become available by drawing from the stock!"; for (const f of foundations) if (canMove(newTopWasteCard, f)) { Swal.fire({ title: 'Hint!', text: hintText, icon: 'info' }); return; } for (const t of tableau) if (canMove(newTopWasteCard, t)) { Swal.fire({ title: 'Hint!', text: hintText, icon: 'info' }); return; } } } Swal.fire({ title: 'No Moves Found', text: 'The game appears to be unwinnable from this state.', icon: 'warning' }); } // ====== INPUT HANDLERS ====== klondikeBoard.addEventListener('click', e => { if (!isActive || isAutoCompleting) return; const clickedCardEl = e.target.closest('.card'); const clickedPileEl = e.target.closest('.pile'); // If the stock pile background was clicked, draw cards. if (clickedPileEl && clickedPileEl.id === 'stock' && !clickedCardEl) { handleStockClick(); return; } // If a card was clicked, attempt to auto-move it. if (clickedCardEl) { performAutoMove(clickedCardEl); return; } // Clicks on anything else (background, empty piles, etc.) will do nothing. }); // Custom drag and drop functionality klondikeBoard.addEventListener('dragstart', e => { if (!isActive || isAutoCompleting) { e.preventDefault(); return; } const cardEl = e.target.closest('.card'); if (!cardEl) { e.preventDefault(); return; } const { sourcePile, cardIndex, cardsToMove } = findCardData(cardEl); if (!sourcePile || !cardsToMove.length) { e.preventDefault(); return; } const rect = cardEl.getBoundingClientRect(); dragged.offsetX = e.clientX - rect.left; dragged.offsetY = e.clientY - rect.top; dragged.cards = cardsToMove; dragged.sourcePile = sourcePile; dragged.startIndex = cardIndex; // Create a "ghost" element for visual feedback dragged.ghostEl = document.createElement('div'); dragged.ghostEl.classList.add('card-ghost-stack'); // Add classes for card back if applicable const currentBackClass = document.body.className.match(/card-back-\w+/); if (currentBackClass) { dragged.ghostEl.classList.add(currentBackClass[0]); } cardsToMove.forEach((cardData, i) => { const el = cardElements[cardData.id].cloneNode(true); el.style.position = 'absolute'; // Position relative to ghostEl el.style.top = `${i * SPACING_FACE_UP}px`; el.style.left = '0'; el.style.zIndex = i + 1; // Stack visually el.classList.add('is-flipped'); // Ensure ghost cards are flipped el.style.visibility = 'visible'; dragged.ghostEl.appendChild(el); }); dragged.ghostEl.style.position = 'fixed'; dragged.ghostEl.style.pointerEvents = 'none'; // Ensure it doesn't interfere with drop targets dragged.ghostEl.style.zIndex = 2000; // Above everything else dragged.ghostEl.style.width = cardEl.offsetWidth + 'px'; // Match card width dragged.ghostEl.style.height = (cardEl.offsetHeight + (cardsToMove.length - 1) * SPACING_FACE_UP) + 'px'; document.body.appendChild(dragged.ghostEl); // Hide the original cards visually while dragging cardsToMove.forEach(c => cardElements[c.id].style.visibility = 'hidden'); // Set custom drag image (a tiny transparent image) to prevent default ghost const img = new Image(); img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; e.dataTransfer.setDragImage(img, 0, 0); // Initial position for the ghost dragged.ghostEl.style.left = `${e.clientX - dragged.offsetX}px`; dragged.ghostEl.style.top = `${e.clientY - dragged.offsetY}px`; }); klondikeBoard.addEventListener('drag', e => { if (dragged.ghostEl && e.clientX !== 0) { // Update ghost element position to follow the cursor dragged.ghostEl.style.left = `${e.clientX - dragged.offsetX}px`; dragged.ghostEl.style.top = `${e.clientY - dragged.offsetY}px`; } }); klondikeBoard.addEventListener('dragover', e => { e.preventDefault(); const targetEl = e.target.closest('.card, .pile'); let potentialDestPileEl = null; if (targetEl) { if (targetEl.classList.contains('card')) { potentialDestPileEl = targetEl.closest('.pile'); } else if (targetEl.classList.contains('pile')) { potentialDestPileEl = targetEl; } } const currentlyHighlighted = document.querySelector('.pile.drag-over'); if (currentlyHighlighted && currentlyHighlighted !== potentialDestPileEl) { currentlyHighlighted.classList.remove('drag-over'); } if (potentialDestPileEl && dragged.cards.length > 0) { const destPileData = getPileArrayFromElement(potentialDestPileEl); if (canMove(dragged.cards[0], destPileData)) { potentialDestPileEl.classList.add('drag-over'); } } }); klondikeBoard.addEventListener('dragend', () => { // Clean up ghost element and reset state if (dragged.ghostEl) { dragged.ghostEl.remove(); dragged.ghostEl = null; } // Make original cards visible again (updateBoard will re-position them) dragged.cards.forEach(c => cardElements[c.id].style.visibility = 'visible'); dragged = { cards: [], sourcePile: null, startIndex: -1, ghostEl: null }; document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over')); updateBoard(); // Re-render to ensure everything is correct }); klondikeBoard.addEventListener('drop', e => { e.preventDefault(); document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over')); if (!dragged.cards.length) return; const dropTargetEl = e.target.closest('.card, .pile'); if (!dropTargetEl) return; let destPile = null; if (dropTargetEl.classList.contains('card')) { // If dropped on a card, find the pile that card belongs to const cardId = dropTargetEl.dataset.id; const targetCardPile = getPileContainingCard(cardId); if (targetCardPile) { // We need to ensure it's the TOP card of the pile to drop ONTO it effectively if (targetCardPile[targetCardPile.length - 1].id === cardId) { destPile = targetCardPile; } } } else if (dropTargetEl.classList.contains('pile')) { // If dropped directly on a pile (e.g., an empty tableau slot) destPile = getPileArrayFromElement(dropTargetEl); } if (destPile && canMove(dragged.cards[0], destPile)) { // --- SCORING LOGIC --- if (foundations.includes(destPile)) score += 10; if (tableau.includes(destPile) && dragged.sourcePile === waste) score += 5; if (tableau.includes(destPile) && foundations.includes(dragged.sourcePile)) score -= 15; // --- END SCORING LOGIC --- pushHistory(); moveSequence(dragged.sourcePile, dragged.startIndex, destPile); performMoveCleanup(dragged.sourcePile); } // dragend will handle cleanup regardless of success }); document.addEventListener('keydown', (e)=>{ if (!isActive || isAutoCompleting) return; if (e.code === 'Space') { e.preventDefault(); handleStockClick(); } if (e.key.toLowerCase() === 'u' || (e.ctrlKey && e.key.toLowerCase() === 'z')) { e.preventDefault(); undo(); } }); // ====== BUTTONS & MODAL LISTENERS ====== restartBtn?.addEventListener('click', () => { if (isActive) initState(); }); undoBtn?.addEventListener('click', undo); drawSelect?.addEventListener('change', () => { DRAW_COUNT = parseInt(drawSelect.value, 10) || 3; initState(); }); openModalBtn?.addEventListener('click', () => cardBackModal.classList.remove('hidden')); closeModalBtn?.addEventListener('click', () => cardBackModal.classList.add('hidden')); modalOverlay?.addEventListener('click', () => cardBackModal.classList.add('hidden')); checkMovesBtn?.addEventListener('click', handleStuckCheck); autoCompleteBtn?.addEventListener('click', autoComplete); document.addEventListener('gameModeChanged', (e) => { isActive = (e.detail === 'klondike'); if (isActive) { updateScoreDisplay(); updateBoard(); } }); // ====== INIT ====== setupThemeSelector(); isActive = (localStorage.getItem('solitaire-mode') !== 'pyramid'); if (isActive) { if (!loadGame()) { initState(); } } else { loadGame(); } window.addEventListener('resize', () => { if (isActive) updateBoard(true); }); });