init commit
This commit is contained in:
commit
878c8c7125
67
index.html
Normal file
67
index.html
Normal file
@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Klondike Solitaire</title>
|
||||
<link rel="stylesheet" href="style.css"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<h1>Klondike Solitaire</h1>
|
||||
<div class="controls">
|
||||
<label class="draw-toggle">
|
||||
Draw:
|
||||
<select id="draw-select" aria-label="Draw count">
|
||||
<option value="1">1</option>
|
||||
<option value="3" selected>3</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="change-back-btn" class="btn">Card Back</button>
|
||||
<button id="undo-btn" class="btn">Undo</button>
|
||||
<button id="check-moves-btn" class="btn">Check Moves</button>
|
||||
<button id="restart-btn" class="btn btn-accent">New Game</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="game-board" aria-label="Solitaire game board">
|
||||
<div class="top-piles">
|
||||
<div class="stock-waste">
|
||||
<div class="pile stock" id="stock" aria-label="Stock" tabindex="0"></div>
|
||||
<div class="pile waste" id="waste" aria-label="Waste" tabindex="0"></div>
|
||||
</div>
|
||||
<div class="foundations">
|
||||
<div class="pile foundation" id="foundation-0" aria-label="Foundation 1" tabindex="0"></div>
|
||||
<div class="pile foundation" id="foundation-1" aria-label="Foundation 2" tabindex="0"></div>
|
||||
<div class="pile foundation" id="foundation-2" aria-label="Foundation 3" tabindex="0"></div>
|
||||
<div class="pile foundation" id="foundation-3" aria-label="Foundation 4" tabindex="0"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tableau">
|
||||
<div class="pile tableau-pile" id="tableau-0" aria-label="Tableau 1" tabindex="0"></div>
|
||||
<div class="pile tableau-pile" id="tableau-1" aria-label="Tableau 2" tabindex="0"></div>
|
||||
<div class="pile tableau-pile" id="tableau-2" aria-label="Tableau 3" tabindex="0"></div>
|
||||
<div class="pile tableau-pile" id="tableau-3" aria-label="Tableau 4" tabindex="0"></div>
|
||||
<div class="pile tableau-pile" id="tableau-4" aria-label="Tableau 5" tabindex="0"></div>
|
||||
<div class="pile tableau-pile" id="tableau-5" aria-label="Tableau 6" tabindex="0"></div>
|
||||
<div class="pile tableau-pile" id="tableau-6" aria-label="Tableau 7" tabindex="0"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="win-message" class="hidden">You Won! 🎉</div>
|
||||
|
||||
<div id="card-back-modal" class="modal hidden">
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal-content">
|
||||
<h2>Choose Card Back</h2>
|
||||
<div class="card-back-options" id="card-back-options">
|
||||
</div>
|
||||
<button id="modal-close-btn" class="btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
602
script.js
Normal file
602
script.js
Normal file
@ -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 = `
|
||||
<div class="card-value-display--top"><span class="card-rank">${cardData.value}</span><span class="card-suit">${cardData.suit}</span></div>
|
||||
<div class="card-value-display--bottom"><span class="card-rank">${cardData.value}</span><span class="card-suit">${cardData.suit}</span></div>
|
||||
<div class="card-watermark">${cardData.suit}</div>`;
|
||||
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));
|
||||
});
|
||||
475
script.js.bak
Normal file
475
script.js.bak
Normal file
@ -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 = `
|
||||
<div class="card-value-display--top"><span class="card-rank">${cardData.value}</span><span class="card-suit">${cardData.suit}</span></div>
|
||||
<div class="card-value-display--bottom"><span class="card-rank">${cardData.value}</span><span class="card-suit">${cardData.suit}</span></div>
|
||||
<div class="card-watermark">${cardData.suit}</div>`;
|
||||
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));
|
||||
});
|
||||
481
style.css
Normal file
481
style.css
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user