From 878c8c71251d596e489c2145a372b08e5546c326 Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 31 Aug 2025 18:16:50 -0400 Subject: [PATCH] init commit --- index.html | 67 ++++++ script.js | 602 ++++++++++++++++++++++++++++++++++++++++++++++++++ script.js.bak | 475 +++++++++++++++++++++++++++++++++++++++ style.css | 481 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1625 insertions(+) create mode 100644 index.html create mode 100644 script.js create mode 100644 script.js.bak create mode 100644 style.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..93e23bb --- /dev/null +++ b/index.html @@ -0,0 +1,67 @@ + + + + + + Klondike Solitaire + + + + +
+

Klondike Solitaire

+
+ + + + + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ + + + + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..b974e6b --- /dev/null +++ b/script.js @@ -0,0 +1,602 @@ +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 }; + let selected = { card: null, sourcePile: null, cardIndex: -1 }; + let lastClick = { time: 0, cardId: null }; + const history = []; + + let isAutoCompleting = 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 gameBoard = document.querySelector('.game-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'); + + + // ====== SAVE / LOAD ====== + function saveGame() { + const gameState = { stock, waste, foundations, tableau, history, drawCount: DRAW_COUNT }; + 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; + 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; + gameBoard.appendChild(el); + } + }); + winMessage.classList.add('hidden'); + 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 }); + const restoreState = (json) => { + const s = JSON.parse(json); + stock = s.stock; waste = s.waste; foundations = s.foundations; tableau = s.tableau; + }; + const pushHistory = () => { + if (history.length > 50) history.shift(); + history.push(snapshotState()); + }; + + // ====== SETUP ====== + function initState() { + isAutoCompleting = false; // Reset on new game + 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; + gameBoard.appendChild(el); + }); + for (let i=0; i<7; i++){ + for (let j=i; j<7; j++) tableau[j].push(deck.pop()); + } + tableau.forEach(pile => { if (pile.length) pile[pile.length-1].faceUp = true; }); + stock = deck; + winMessage.classList.add('hidden'); + history.length = 0; + pushHistory(); + updateBoard(true); + saveGame(); + } + + // ====== RENDER ====== + function updateBoard(initial=false) { + const gameBoardRect = gameBoard.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; + 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; + } + cardEl.classList.toggle('selected', selected.card?.id === cardData.id); + 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; + 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 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; + } + function findCardData(cardEl) { + const id = cardEl.dataset.id; + let cardIndex; + const findIn = (pile) => pile.findIndex(c => c.id === id); + if ((cardIndex = findIn(waste)) > -1 && cardIndex === waste.length - 1) { + return { sourcePile: waste, cardIndex, card: waste[cardIndex], cardsToMove: [waste[cardIndex]] }; + } + for (const pile of tableau) { + if ((cardIndex = findIn(pile)) > -1 && pile[cardIndex].faceUp) { + const cardsToMove = pile.slice(cardIndex); + return { sourcePile: pile, cardIndex, card: cardsToMove[0], cardsToMove }; + } + } + for (const pile of foundations) { + if ((cardIndex = findIn(pile)) > -1 && cardIndex === pile.length - 1) { + return { sourcePile: pile, cardIndex, card: pile[cardIndex], cardsToMove: [pile[cardIndex]] }; + } + } + return {}; + } + function canMove(card, destPile) { + if (!card || !destPile) return false; + const destType = (foundations.includes(destPile)) ? 'foundation' : (tableau.includes(destPile)) ? 'tableau' : 'other'; + if (destType === 'other') return false; + 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 { + return card.suit === top.suit && getValueRank(card.value) === getValueRank(top.value) + 1; + } + } + function clearSelection() { + if (selected.card) cardElements[selected.card.id]?.classList.remove('selected'); + selected = { card:null, sourcePile:null, cardIndex:-1 }; + } + function moveSequence(sourcePile, startIndex, destPile) { + const seq = sourcePile.splice(startIndex); + destPile.push(...seq); + } + function performMoveCleanup(sourcePile) { + if (tableau.includes(sourcePile) && sourcePile.length > 0) { + sourcePile[sourcePile.length - 1].faceUp = true; + } + clearSelection(); + updateBoard(); + saveGame(); + if (!isAutoCompleting) { + checkAndTriggerAutoComplete(); + } + } + function checkWin() { + if (foundations.every(p=>p.length===13)) { + winMessage.classList.remove('hidden'); + try { + confetti({ particleCount: 180, spread: 75, origin:{y:0.35} }); + setTimeout(()=>confetti({ particleCount: 140, spread: 65, origin:{y:.2} }), 350); + } catch(_) {} + } + } + + // ====== ACTIONS ====== + function handleStockClick() { + if (isAutoCompleting) return; + pushHistory(); + if (stock.length > 0) { + const cardsToMove = stock.splice(stock.length - DRAW_COUNT, DRAW_COUNT).reverse(); + cardsToMove.forEach(c => c.faceUp = true); + waste.push(...cardsToMove); + } else if (waste.length > 0) { + stock = waste.reverse(); + stock.forEach(c => c.faceUp = false); + waste = []; + } + clearSelection(); + updateBoard(); + saveGame(); + } + function handleCardDoubleClick(cardEl) { + if (isAutoCompleting) return; + const { sourcePile, cardIndex, cardsToMove } = findCardData(cardEl); + if (!sourcePile || !cardsToMove || cardsToMove.length === 0) return; + const cardToMove = cardsToMove[0]; + // Try moving single cards to foundation first + if (cardsToMove.length === 1) { + for (const f of foundations) { + if (canMove(cardToMove, f)) { + pushHistory(); + moveSequence(sourcePile, cardIndex, f); + performMoveCleanup(sourcePile); + return; + } + } + } + // Then try moving any valid stack to another tableau + for (const t of tableau) { + if (t !== sourcePile && canMove(cardToMove, t)) { + 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]); + clearSelection(); + 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) { + alert("You can only auto-complete when all cards are face-up and the stock is empty."); + return; + } + isAutoCompleting = true; + const intervalId = setInterval(() => { + let movedCard = false; + // First check waste pile + if (waste.length > 0) { + const card = waste[waste.length - 1]; + for (const f of foundations) { + if (canMove(card, f)) { + moveSequence(waste, waste.length - 1, f); + movedCard = true; + break; + } + } + } + // Then check tableau piles + 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)) { + moveSequence(t, t.length - 1, f); + if (t.length > 0) t[t.length - 1].faceUp = true; + movedCard = true; + break; + } + } + } + if (movedCard) break; + } + } + updateBoard(); + if (!movedCard) { + clearInterval(intervalId); + isAutoCompleting = false; + } + }, 120); + } + + /** + * Checks for any available moves that make progress in the game. + * Ignores pointless, looping moves between tableau piles. + * @returns {boolean} - True if a productive move is found, otherwise false. + */ +function checkForAnyAvailableMove() { + // 1. Any move to a foundation is always productive. + 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 (t.length > 0) { + const topTableauCard = t[t.length - 1]; + for (const f of foundations) if (canMove(topTableauCard, f)) return true; + } + } + + // 2. Any move from the waste pile to the tableau is always productive. + if (topWasteCard) { + for (const t of tableau) if (canMove(topWasteCard, t)) return true; + } + + // 3. A tableau-to-tableau move is only productive if it reveals a face-down card. + for (const sourcePile of tableau) { + // Find the start index of the movable, face-up stack. + const movableStackIndex = sourcePile.findIndex(card => card.faceUp); + + // A move is productive if there is a movable stack AND a card underneath it. + if (movableStackIndex > 0) { // Index > 0 means there's a card at index 0, 1, etc. + const cardToMove = sourcePile[movableStackIndex]; + for (const destPile of tableau) { + if (sourcePile !== destPile && canMove(cardToMove, destPile)) { + // This move is guaranteed to reveal the card at [movableStackIndex - 1]. + // We can immediately say a productive move is available. + return true; + } + } + } + } + + // If we've gotten this far, no moves were found that make clear progress. + return false; +} + + + function handleStuckCheck() { + if (checkForAnyAvailableMove()) { + alert("Hint: A move is available on the board!"); + return; + } + let tempStock = JSON.parse(JSON.stringify(stock)); + let tempWaste = JSON.parse(JSON.stringify(waste)); + const originalStockSize = tempStock.length + tempWaste.length; + if (originalStockSize === 0) { + alert("No more moves available. The game appears to be unwinnable from this state."); + 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) { + for (const f of foundations) if (canMove(newTopWasteCard, f)) { + alert("Hint: A move will become available by drawing from the stock!"); + return; + } + for (const t of tableau) if (canMove(newTopWasteCard, t)) { + alert("Hint: A move will become available by drawing from the stock!"); + return; + } + } + } + alert("No more moves available. The game appears to be unwinnable from this state."); + } + + // ====== INPUT HANDLERS ====== + gameBoard.addEventListener('click', e => { + if (isAutoCompleting) return; + const pileEl = e.target.closest('.pile'); + const cardEl = e.target.closest('.card'); + if (pileEl && pileEl.id === 'stock' && !cardEl) { + handleStockClick(); + return; + } + if (cardEl) { + const { sourcePile, cardIndex, card } = findCardData(cardEl); + if (!sourcePile || !card) return; + const now = Date.now(); + if (now - lastClick.time < 300 && lastClick.cardId === card.id) { + handleCardDoubleClick(cardEl); + clearSelection(); + lastClick = { time: 0, cardId: null }; + return; + } + lastClick = { time: now, cardId: card.id }; + if (selected.card) { + if (canMove(selected.card, sourcePile)) { + pushHistory(); + moveSequence(selected.sourcePile, selected.cardIndex, sourcePile); + performMoveCleanup(sourcePile); + } else { + if (selected.card.id === card.id) { + clearSelection(); + } else { + selected = { card, sourcePile, cardIndex }; + } + updateBoard(); + } + } else { + selected = { card, sourcePile, cardIndex }; + updateBoard(); + } + return; + } + if (pileEl && selected.card) { + const destPile = getPileArrayFromElement(pileEl); + if (destPile && canMove(selected.card, destPile)) { + pushHistory(); + moveSequence(selected.sourcePile, selected.cardIndex, destPile); + performMoveCleanup(selected.sourcePile); + } else { + clearSelection(); + updateBoard(); + } + return; + } + clearSelection(); + updateBoard(); + }); + + gameBoard.addEventListener('dragstart', e => { + if (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; } + dragged = { cards: cardsToMove, sourcePile, startIndex: cardIndex }; + setTimeout(() => cardsToMove.forEach(c => cardElements[c.id]?.classList.add('dragging')), 0); + }); + gameBoard.addEventListener('dragover', e => { e.preventDefault(); document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over')); const targetEl = e.target.closest('.pile, .card'); if (targetEl) targetEl.closest('.pile')?.classList.add('drag-over'); }); + gameBoard.addEventListener('dragend', () => { if (dragged.cards?.length) { dragged.cards.forEach(c => cardElements[c.id]?.classList.remove('dragging')); } dragged = { cards: [], sourcePile: null, startIndex: -1 }; document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over')); }); + gameBoard.addEventListener('drop', e => { e.preventDefault(); if (!dragged.cards.length) return; const dropTargetEl = e.target.closest('.card, .pile'); if (!dropTargetEl) return; const destPile = getPileArrayFromElement(dropTargetEl.closest('.pile')); if (destPile && canMove(dragged.cards[0], destPile)) { pushHistory(); moveSequence(dragged.sourcePile, dragged.startIndex, destPile); performMoveCleanup(dragged.sourcePile); }}); + + document.addEventListener('keydown', (e)=>{ + if (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', 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); + + + // ====== INIT ====== + setupThemeSelector(); + if (!loadGame()) { + initState(); + } + window.addEventListener('resize', () => updateBoard(true)); +}); diff --git a/script.js.bak b/script.js.bak new file mode 100644 index 0000000..2b59c46 --- /dev/null +++ b/script.js.bak @@ -0,0 +1,475 @@ +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; + // FIX: Replaced dynamic spacing with two fixed values for better control + const SPACING_FACE_DOWN = 10; // Spacing for stacked face-down cards + const SPACING_FACE_UP = 32; // Spacing for fanned-out face-up cards + + let DRAW_COUNT = 3; + + let deck, stock, waste, foundations, tableau; + let cardElements = {}; + let dragged = { cards: [], sourcePile: null, startIndex: -1 }; + let selected = { card: null, sourcePile: null, cardIndex: -1 }; + let lastClick = { time: 0, cardId: null }; + const history = []; + + 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 gameBoard = document.querySelector('.game-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'); + + // ====== SAVE / LOAD ====== + function saveGame() { + const gameState = { stock, waste, foundations, tableau, history, drawCount: DRAW_COUNT }; + 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; + 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; + gameBoard.appendChild(el); + } + }); + + winMessage.classList.add('hidden'); + 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 }); + const restoreState = (json) => { + const s = JSON.parse(json); + stock = s.stock; waste = s.waste; foundations = s.foundations; tableau = s.tableau; + }; + const pushHistory = () => { + if (history.length > 50) history.shift(); + history.push(snapshotState()); + }; + + // ====== SETUP ====== + function initState() { + 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; + gameBoard.appendChild(el); + }); + for (let i=0; i<7; i++){ + for (let j=i; j<7; j++) tableau[j].push(deck.pop()); + } + tableau.forEach(pile => { if (pile.length) pile[pile.length-1].faceUp = true; }); + stock = deck; + winMessage.classList.add('hidden'); + history.length = 0; + pushHistory(); + updateBoard(true); + saveGame(); + } + + // ====== RENDER ====== + function updateBoard(initial=false) { + const gameBoardRect = gameBoard.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; // Used for tableau spacing calculation + + pileData.forEach((cardData, i) => { + const cardEl = cardElements[cardData.id]; + if(!cardEl) return; + + let t = baseTop; + // FIX: New iterative spacing logic for tableau piles + 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); + if (cardData.faceUp) { + cardEl.classList.add('is-flipped'); + cardEl.draggable = true; + } else { + cardEl.classList.remove('is-flipped'); + cardEl.draggable = false; + } + cardEl.classList.toggle('selected', selected.card?.id === cardData.id); + 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; + const isTopCard = i === waste.length - 1; + const visibleFanIndexStart = Math.max(0, waste.length - DRAW_COUNT); + let leftOffset = 0; + if (i >= visibleFanIndexStart) { + leftOffset = (i - visibleFanIndexStart) * 22; + } + 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 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; + } + function findCardData(cardEl) { + const id = cardEl.dataset.id; + let cardIndex; + const findIn = (pile) => pile.findIndex(c => c.id === id); + if ((cardIndex = findIn(waste)) > -1 && cardIndex === waste.length - 1) { + return { sourcePile: waste, cardIndex, card: waste[cardIndex], cardsToMove: [waste[cardIndex]] }; + } + for (const pile of tableau) { + if ((cardIndex = findIn(pile)) > -1 && pile[cardIndex].faceUp) { + return { sourcePile: pile, cardIndex, card: pile[cardIndex], cardsToMove: pile.slice(cardIndex) }; + } + } + for (const pile of foundations) { + if ((cardIndex = findIn(pile)) > -1 && cardIndex === pile.length - 1) { + return { sourcePile: pile, cardIndex, card: pile[cardIndex], cardsToMove: [pile[cardIndex]] }; + } + } + return {}; + } + function canMove(card, destPile) { + if (!card || !destPile) return false; + const destType = (foundations.includes(destPile)) ? 'foundation' : (tableau.includes(destPile)) ? 'tableau' : 'other'; + if (destType === 'other') return false; + 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 { + return card.suit === top.suit && getValueRank(card.value) === getValueRank(top.value) + 1; + } + } + function clearSelection() { + if (selected.card) cardElements[selected.card.id]?.classList.remove('selected'); + selected = { card:null, sourcePile:null, cardIndex:-1 }; + } + function moveSequence(sourcePile, startIndex, destPile) { + const seq = sourcePile.splice(startIndex); + destPile.push(...seq); + } + function performMoveCleanup(sourcePile) { + if (tableau.includes(sourcePile) && sourcePile.length > 0) { + sourcePile[sourcePile.length - 1].faceUp = true; + } + clearSelection(); + updateBoard(); + saveGame(); + } + function checkWin() { + if (foundations.every(p=>p.length===13)) { + winMessage.classList.remove('hidden'); + try { + confetti({ particleCount: 180, spread: 75, origin:{y:0.35} }); + setTimeout(()=>confetti({ particleCount: 140, spread: 65, origin:{y:.2} }), 350); + } catch(_) {} + } + } + + // ====== ACTIONS ====== + function handleStockClick() { + pushHistory(); + if (stock.length > 0) { + const cardsToMove = stock.splice(stock.length - DRAW_COUNT, DRAW_COUNT).reverse(); + cardsToMove.forEach(c => c.faceUp = true); + waste.push(...cardsToMove); + } else if (waste.length > 0) { + stock = waste.reverse(); + stock.forEach(c => c.faceUp = false); + waste = []; + } + clearSelection(); + updateBoard(); + saveGame(); + } + function handleCardDoubleClick(cardEl) { + const { sourcePile, card } = findCardData(cardEl); + if (!sourcePile || !card || sourcePile[sourcePile.length - 1].id !== card.id) return; + for (const f of foundations) { + if (canMove(card, f)) { + pushHistory(); + moveSequence(sourcePile, sourcePile.length - 1, f); + performMoveCleanup(sourcePile); + return; + } + } + for (const t of tableau) { + if (t !== sourcePile && canMove(card, t)) { + pushHistory(); + moveSequence(sourcePile, sourcePile.length - 1, t); + performMoveCleanup(sourcePile); + return; + } + } + } + function undo() { + if (history.length <= 1) return; + history.pop(); + restoreState(history[history.length-1]); + clearSelection(); + winMessage.classList.add('hidden'); + updateBoard(true); + saveGame(); + } + + // ====== INPUT HANDLERS ====== + gameBoard.addEventListener('click', e => { + const pileEl = e.target.closest('.pile'); + const cardEl = e.target.closest('.card'); + if (pileEl && pileEl.id === 'stock' && !cardEl) { + handleStockClick(); + return; + } + if (cardEl) { + const { sourcePile, cardIndex, card, cardsToMove } = findCardData(cardEl); + if (!sourcePile || !card || !card.faceUp) return; + const now = Date.now(); + if (now - lastClick.time < 300 && lastClick.cardId === card.id) { + handleCardDoubleClick(cardEl); + clearSelection(); + lastClick = { time: 0, cardId: null }; + return; + } + lastClick = { time: now, cardId: card.id }; + if (selected.card) { + if (canMove(selected.card, sourcePile)) { + pushHistory(); + moveSequence(selected.sourcePile, selected.cardIndex, sourcePile); + performMoveCleanup(selected.sourcePile); + } else { + clearSelection(); + selected = { card: cardsToMove[0], sourcePile, cardIndex }; + updateBoard(); + } + } else { + selected = { card: cardsToMove[0], sourcePile, cardIndex }; + updateBoard(); + } + return; + } + if (pileEl && selected.card) { + const destPile = getPileArrayFromElement(pileEl); + if (destPile && canMove(selected.card, destPile)) { + pushHistory(); + moveSequence(selected.sourcePile, selected.cardIndex, destPile); + performMoveCleanup(selected.sourcePile); + } else { + clearSelection(); + updateBoard(); + } + return; + } + clearSelection(); + updateBoard(); + }); + + gameBoard.addEventListener('dragstart', e => { + const cardEl = e.target.closest('.card'); + if (!cardEl) { e.preventDefault(); return; } + const { sourcePile, cardIndex, cardsToMove } = findCardData(cardEl); + if (!sourcePile || !cardsToMove.length) { e.preventDefault(); return; } + dragged = { cards: cardsToMove, sourcePile, startIndex: cardIndex }; + setTimeout(() => cardsToMove.forEach(c => cardElements[c.id]?.classList.add('dragging')), 0); + }); + gameBoard.addEventListener('dragover', e => { + e.preventDefault(); + document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over')); + const targetEl = e.target.closest('.pile, .card'); + if (targetEl) targetEl.closest('.pile')?.classList.add('drag-over'); + }); + gameBoard.addEventListener('dragend', () => { + if (dragged.cards?.length) { + dragged.cards.forEach(c => cardElements[c.id]?.classList.remove('dragging')); + } + dragged = { cards: [], sourcePile: null, startIndex: -1 }; + document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over')); + }); + gameBoard.addEventListener('drop', e => { + e.preventDefault(); + if (!dragged.cards.length) return; + const dropTargetEl = e.target.closest('.card, .pile'); + if (!dropTargetEl) return; + const destPile = getPileArrayFromElement(dropTargetEl.closest('.pile')); + if (destPile && canMove(dragged.cards[0], destPile)) { + pushHistory(); + moveSequence(dragged.sourcePile, dragged.startIndex, destPile); + performMoveCleanup(dragged.sourcePile); + } + }); + + document.addEventListener('keydown', (e)=>{ + 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', 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')); + + // ====== INIT ====== + setupThemeSelector(); + if (!loadGame()) { + initState(); + } + window.addEventListener('resize', () => updateBoard(true)); +}); \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..e691f83 --- /dev/null +++ b/style.css @@ -0,0 +1,481 @@ +/* Import a modern, clean font */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap'); + +:root { + /* UI Dimensions */ + --card-width: max(7vmin, 65px); + --card-height: calc(var(--card-width) * 1.4); + --card-radius: clamp(6px, 1.5vmin, 10px); + --gap: clamp(10px, 2vmin, 18px); + + /* New Color Palette */ + --felt: #1a5632; + --felt-dark: #113821; + --line: #ffffff2a; + --glow: #64ffda; + --shadow-color: rgba(0, 0, 0, 0.3); + + /* UI Element Colors */ + --control-bg: #ffffff11; + --control-hover-bg: #ffffff22; + --accent-color: #58cc93; + --accent-hover-color: #4ab881; + --text-color: #f0f0f0; +} + +* { box-sizing: border-box; } + +html, body { + height: 100%; + margin: 0; +} + +body { + background: radial-gradient(circle at 50% 0%, var(--felt) 0%, var(--felt-dark) 90%); + color: var(--text-color); + font-family: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + padding: 12px clamp(12px, 3vw, 28px); + gap: 12px; + background: rgba(0,0,0,0.1); + border-bottom: 1px solid var(--line); +} + +h1 { + margin: 0; + font-size: clamp(18px, 3.2vmin, 28px); + font-weight: 700; +} + +.controls { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; +} + +.btn { + background: var(--control-bg); + color: var(--text-color); + border: 1px solid var(--line); + padding: 8px 14px; + border-radius: 8px; + cursor: pointer; + font-weight: 600; + transition: transform .12s ease, background-color .15s ease, box-shadow .15s ease; +} + +.btn:hover { + background: var(--control-hover-bg); + box-shadow: 0 4px 12px rgba(0,0,0,0.2); +} + +.btn:active { + transform: translateY(1px) scale(.99); +} + +.btn-accent { + background: var(--accent-color); + border-color: transparent; + color: #113821; +} + +.btn-accent:hover { + background: var(--accent-hover-color); + border-color: transparent; +} + +.draw-toggle { + display: flex; + gap: 6px; + align-items: center; + background: var(--control-bg); + padding: 6px 10px; + border-radius: 8px; + border: 1px solid var(--line); +} + +.draw-toggle select { + appearance: none; + background: transparent; + color: var(--text-color); + border: none; + outline: none; + font-weight: 600; + cursor: pointer; +} + +.game-board { + position: relative; + display: flex; + flex-direction: column; + gap: calc(var(--gap) * 1.5); + width: min(1100px, 96vw); + margin: 0 auto; + padding: 0 clamp(10px, 2vw, 20px) clamp(10px, 2vw, 20px); + flex: 1; + min-height: 0; +} + +.top-piles, .tableau { + display: flex; + gap: var(--gap); + justify-content: space-between; +} +.tableau { flex: 1; min-height: 0; } +.stock-waste, .foundations { display: flex; gap: var(--gap); } +.foundations { margin-left: auto; } + +.pile { + width: var(--card-width); + height: var(--card-height); + border-radius: var(--card-radius); + position: relative; + border: 2px dashed var(--line); + outline: none; + transition: box-shadow 0.2s ease; +} +.pile:focus-visible { box-shadow: 0 0 0 3px #fff6; } +.tableau-pile { height: 100%; border: none; } +.tableau-pile::before { + content: ""; + position: absolute; + inset: 0 auto auto 0; + width: var(--card-width); + height: var(--card-height); + border-radius: var(--card-radius); + border: 2px dashed var(--line); +} + +.card { + width: var(--card-width); + height: var(--card-height); + position: absolute; + user-select: none; + touch-action: none; + transition: top .18s ease, left .18s ease, transform .18s ease, filter .2s ease; + will-change: top, left, transform; + filter: drop-shadow(0 2px 5px var(--shadow-color)); +} +.card.is-stacked { filter: none; } +.card-inner { + position: relative; + width: 100%; + height: 100%; + transform-style: preserve-3d; + transition: transform .24s ease; +} +.card.is-flipped .card-inner { transform: rotateY(180deg); } +.card-face { + position: absolute; + inset: 0; + border-radius: var(--card-radius); + backface-visibility: hidden; +} +.card-face--front { + background: linear-gradient(160deg, #ffffff, #f4f4f4); + transform: rotateY(180deg); + padding: 8px; + cursor: grab; + border: 1px solid #d0d0d0; +} + +.red { color: #D90429; } +.black { color: #2B2D42; } + +.card-value-display--top { + position: absolute; + top: 8px; + left: 8px; + display: flex; + flex-direction: column; + align-items: center; + line-height: .9; +} +.card-value-display--bottom { + position: absolute; + bottom: 8px; + right: 8px; + display: flex; + flex-direction: column; + align-items: center; + line-height: .9; + transform: rotate(180deg); +} + +.card-rank { font-size: 1.24em; font-weight: 800; } +.card-suit { font-size: .9em; } + +.card-watermark { + position: absolute; + inset: auto 0 0 0; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; + font-size: 3.6em; + opacity: .10; + pointer-events: none; +} +.card.dragging { + opacity: .95; + transform: rotate(-1.5deg) scale(1.06); + z-index: 900; + filter: drop-shadow(0 10px 22px rgba(0, 0, 0, .45)); + outline: 2px solid var(--glow); + border-radius: var(--card-radius); +} +.pile.drag-over::before, .pile.drag-over { + box-shadow: 0 0 14px 4px var(--glow); + border-color: var(--glow); + border-style: solid; +} +.card.selected { + outline: 3px solid #ffd54d; + border-radius: var(--card-radius); + filter: drop-shadow(0 0 0 var(--shadow-color)) drop-shadow(0 8px 16px rgba(0,0,0,0.45)); +} +#win-message.hidden { display: none; } +#win-message { + position: fixed; + inset: auto 0 14% 0; + margin: auto; + width: fit-content; + font-size: clamp(28px, 5vmin, 48px); + font-weight: 900; + text-shadow: 0 8px 20px rgba(0, 0, 0, .4); + animation: win-bounce 1.2s ease-in-out infinite; + padding: 10px 16px; + background: rgba(0, 0, 0, 0.2); + border-radius: 14px; + border: 1px solid var(--line); + backdrop-filter: blur(8px); +} +@keyframes win-bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-12px); } +} + +/* ADDED: Styles for the new modal */ +.modal { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.2s ease, visibility 0.2s ease; +} +.modal.hidden { + opacity: 0; + visibility: hidden; +} +.modal-overlay { + position: absolute; + inset: 0; + background: rgba(0,0,0,0.6); + backdrop-filter: blur(5px); +} +.modal-content { + position: relative; + background: var(--felt-dark); + border: 1px solid var(--line); + border-radius: 16px; + padding: 24px; + display: flex; + flex-direction: column; + gap: 20px; + align-items: center; + box-shadow: 0 10px 30px rgba(0,0,0,0.3); + animation: modal-enter 0.3s ease forwards; +} +.modal.hidden .modal-content { + animation: modal-exit 0.3s ease forwards; +} +@keyframes modal-enter { + from { transform: translateY(20px) scale(0.95); opacity: 0; } + to { transform: translateY(0) scale(1); opacity: 1; } +} +@keyframes modal-exit { + from { transform: translateY(0) scale(1); opacity: 1; } + to { transform: translateY(20px) scale(0.95); opacity: 0; } +} +.modal-content h2 { + margin: 0; + font-weight: 700; +} + +.card-back-options { display: flex; flex-wrap: wrap; justify-content: center; gap: 16px; } +.back-option { + width: 64px; + height: 90px; + border-radius: 8px; + cursor: pointer; + border: 3px solid transparent; + transition: border-color 0.2s ease, transform 0.2s ease; + position: relative; + overflow: hidden; +} +.back-option .card-face--back { border: none; } +.back-option.active, .back-option:hover { border-color: var(--glow); } +/* Highlight the selected card back */ + + +.back-option.active { + border: 3px solid var(--glow); + box-shadow: 0 0 14px 4px var(--glow); + transform: scale(1.08); + z-index: 2; +} + +/* Still allow hover effect, but don't override active */ +.back-option:hover { + border-color: var(--glow); + box-shadow: 0 0 10px 2px var(--glow); + transform: scale(1.05); +} + +/* --- CSS Card Backs --- */ +.card-face--back { border: 1px solid #444; } +body.card-back-waves .card .card-face--back, +.back-option.card-back-waves .card-face--back { + background-color: #a2d2ff; background-image: radial-gradient(circle at 50% 0, #bde0fe 25%, transparent 25.5%), radial-gradient(circle at 0 50%, #bde0fe 25%, transparent 25.5%), radial-gradient(circle at 100% 50%, #bde0fe 25%, transparent 25.5%), radial-gradient(circle at 50% 100%, #bde0fe 25%, transparent 25.5%); background-size: 40px 40px; border-color: #6c9dbd; +} +body.card-back-herringbone .card .card-face--back, +.back-option.card-back-herringbone .card-face--back { + background-color: #3d405b; background-image: linear-gradient(135deg, #81b29a 25%, transparent 25%), linear-gradient(225deg, #81b29a 25%, transparent 25%), linear-gradient(45deg, #81b29a 25%, transparent 25%), linear-gradient(315deg, #81b29a 25%, #3d405b 25%); background-position: 10px 0, 10px 0, 0 0, 0 0; background-size: 20px 20px; background-repeat: repeat; border-color: #2c2e40; +} +body.card-back-sunset .card .card-face--back, +.back-option.card-back-sunset .card-face--back { + background: linear-gradient(160deg, #f28482, #f6bd60, #84a59d); border-color: #6a847e; +} +body.card-back-linen .card .card-face--back, +.back-option.card-back-linen .card-face--back { + background-color: #fcf4e2; background-image: linear-gradient(45deg, #e3d5b8 25%, transparent 25%, transparent 75%, #e3d5b8 75%, #e3d5b8), linear-gradient(45deg, #e3d5b8 25%, transparent 25%, transparent 75%, #e3d5b8 75%, #e3d5b8); background-size: 20px 20px; background-position: 0 0, 10px 10px; border-color: #c9bba4; +} +body.card-back-nordic .card .card-face--back, +.back-option.card-back-nordic .card-face--back { + background-color: #2b2d42; background-image: linear-gradient(white 2px, transparent 2px), linear-gradient(90deg, white 2px, transparent 2px), linear-gradient(hsla(0,0%,100%,.3) 1px, transparent 1px), linear-gradient(90deg, hsla(0,0%,100%,.3) 1px, transparent 1px); background-size: 50px 50px, 50px 50px, 10px 10px, 10px 10px; background-position: -2px -2px, -2px -2px, -1px -1px, -1px -1px; border-color: #1e1f2e; +} + +body.card-back-midnight .card .card-face--back, +.back-option.card-back-midnight .card-face--back { + background-color: #121212; + background-image: + linear-gradient(#ffffff20 1px, transparent 1px), + linear-gradient(90deg, #ffffff20 1px, transparent 1px); + background-size: 22px 22px; + border-color: #1e1e1e; +} + +body.card-back-diamond .card .card-face--back, +.back-option.card-back-diamond .card-face--back { + background-color: #f8f8f8; + background-image: + linear-gradient(45deg, #ddd 25%, transparent 25%), + linear-gradient(-45deg, #ddd 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #ddd 75%), + linear-gradient(-45deg, transparent 75%, #ddd 75%); + background-size: 28px 28px; + background-position: 0 0, 0 14px, 14px -14px, -14px 0px; + border-color: #c2c2c2; +} + +body.card-back-royal .card .card-face--back, +.back-option.card-back-royal .card-face--back { + background: repeating-linear-gradient( + 45deg, + #224488, + #224488 12px, + #2c5ab0 12px, + #2c5ab0 24px + ); + border-color: #1b3561; +} + +body.card-back-fade .card .card-face--back, +.back-option.card-back-fade .card-face--back { + background: linear-gradient(160deg, #2c3e50, #34495e, #2c3e50); + border: 1px solid #1a242f; +} + +body.card-back-midnight .card .card-face--back, +.back-option.card-back-midnight .card-face--back { + background-color: #121212; + background-image: + linear-gradient(#ffffff20 1px, transparent 1px), + linear-gradient(90deg, #ffffff20 1px, transparent 1px); + background-size: 22px 22px; + border-color: #1e1e1e; +} + +body.card-back-diamond .card .card-face--back, +.back-option.card-back-diamond .card-face--back { + background-color: #f8f8f8; + background-image: + linear-gradient(45deg, #ddd 25%, transparent 25%), + linear-gradient(-45deg, #ddd 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #ddd 75%), + linear-gradient(-45deg, transparent 75%, #ddd 75%); + background-size: 28px 28px; + background-position: 0 0, 0 14px, 14px -14px, -14px 0px; + border-color: #c2c2c2; +} + +body.card-back-royal .card .card-face--back, +.back-option.card-back-royal .card-face--back { + background: repeating-linear-gradient( + 45deg, + #224488, + #224488 12px, + #2c5ab0 12px, + #2c5ab0 24px + ); + border-color: #1b3561; +} + +body.card-back-fade .card .card-face--back, +.back-option.card-back-fade .card-face--back { + background: linear-gradient(160deg, #2c3e50, #34495e, #2c3e50); + border: 1px solid #1a242f; +} + +/* =================================== */ +/* ======= RESPONSIVE STYLES ======= */ +/* =================================== */ + +/* For Mobile Screens (anything below 600px wide) */ +@media (max-width: 600px) { + .foundations { + /* This brings the four foundation piles closer together */ + gap: 5px; + } +} + +/* For Tablets (screens 768px and wider) */ +@media (min-width: 768px) { + :root { + /* Increase the minimum card size for tablets */ + --card-width: max(7vmin, 75px); + } +} + +/* For Laptops & Desktops (screens 1024px and wider) */ +@media (min-width: 1024px) { + :root { + /* Further increase the minimum card size for large screens */ + --card-width: max(7.5vmin, 90px); + } +} + +