Add Pyramid Solitaire game mode and game switcher
This commit is contained in:
parent
f28b0fa62e
commit
84d444c0ae
96
index.html
96
index.html
@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Klondike Solitaire</title>
|
||||
<link rel="stylesheet" href="style.css"/>
|
||||
<link rel="stylesheet" href="pyramid.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
@ -14,21 +15,33 @@
|
||||
<div class="score-display">High Score: <span id="high-score">0</span></div>
|
||||
</div>
|
||||
<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>
|
||||
<div class="game-select">
|
||||
<label for="game-mode-select">Game:</label>
|
||||
<select id="game-mode-select">
|
||||
<option value="klondike">Klondike</option>
|
||||
<option value="pyramid">Pyramid</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div id="klondike-controls" class="game-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>
|
||||
</div>
|
||||
<div id="pyramid-controls" class="game-controls hidden">
|
||||
<!-- Pyramid specific controls if any -->
|
||||
</div>
|
||||
<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="check-moves-btn" class="btn">Check Moves</button>
|
||||
<button id="restart-btn" class="btn btn-accent">New Game</button>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
<main class="game-board" aria-label="Solitaire game board">
|
||||
<main id="klondike-board" class="game-board" aria-label="Klondike Solitaire board">
|
||||
<div class="top-piles">
|
||||
<div class="stock-waste">
|
||||
<div class="pile stock" id="stock" aria-label="Stock" tabindex="0"></div>
|
||||
@ -51,20 +64,63 @@
|
||||
<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>
|
||||
</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>
|
||||
<main id="pyramid-board" class="game-board hidden" aria-label="Pyramid Solitaire board">
|
||||
<div class="pyramid-layout">
|
||||
<!-- Pyramid rows will be rendered here by pyramid.js -->
|
||||
</div>
|
||||
<div class="pyramid-bottom-piles">
|
||||
<div class="stock-waste">
|
||||
<div class="pile stock" id="pyramid-stock" aria-label="Stock" tabindex="0"></div>
|
||||
<div class="pile waste" id="pyramid-waste" aria-label="Waste" tabindex="0"></div>
|
||||
</div>
|
||||
<div class="discard-pile-container">
|
||||
<div class="pile discard" id="pyramid-discard" aria-label="Discard Pile" tabindex="0"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="win-message" class="hidden">You Won! 🎉</div>
|
||||
|
||||
<div id="card-back-modal" class="modal hidden">
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script src="script.js"></script>
|
||||
<script src="pyramid.js"></script>
|
||||
<script>
|
||||
// Global switcher logic
|
||||
(function() {
|
||||
const switcher = document.getElementById('game-mode-select');
|
||||
const klondikeBoard = document.getElementById('klondike-board');
|
||||
const pyramidBoard = document.getElementById('pyramid-board');
|
||||
const klondikeControls = document.getElementById('klondike-controls');
|
||||
const pyramidControls = document.getElementById('pyramid-controls');
|
||||
|
||||
function switchMode(mode) {
|
||||
if (mode === 'pyramid') {
|
||||
klondikeBoard.classList.add('hidden');
|
||||
klondikeControls.classList.add('hidden');
|
||||
pyramidBoard.classList.remove('hidden');
|
||||
pyramidControls.classList.remove('hidden');
|
||||
} else {
|
||||
pyramidBoard.classList.add('hidden');
|
||||
pyramidControls.classList.add('hidden');
|
||||
klondikeBoard.classList.remove('hidden');
|
||||
klondikeControls.classList.remove('hidden');
|
||||
}
|
||||
localStorage.setItem('solitaire-mode', mode);
|
||||
document.dispatchEvent(new CustomEvent('gameModeChanged', { detail: mode }));
|
||||
}
|
||||
|
||||
switcher.addEventListener('change', (e) => switchMode(e.target.value));
|
||||
|
||||
const savedMode = localStorage.getItem('solitaire-mode') || 'klondike';
|
||||
switcher.value = savedMode;
|
||||
switchMode(savedMode);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
53
pyramid.css
Normal file
53
pyramid.css
Normal file
@ -0,0 +1,53 @@
|
||||
.pyramid-board {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.pyramid-layout {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
/* 7 rows * (card height * overlap) + last card height */
|
||||
height: calc(var(--card-height) + (6 * (var(--card-height) * 0.45)));
|
||||
margin-bottom: 40px;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
.pyramid-bottom-piles {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
gap: 100px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.discard-pile-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pyramid-layout .card {
|
||||
transition: all 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.card.is-selected {
|
||||
outline: 4px solid var(--glow);
|
||||
box-shadow: 0 0 20px var(--glow);
|
||||
transform: scale(1.05) translateY(-5px);
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
/* Specific to Pyramid Card layout */
|
||||
.pyramid-card {
|
||||
/* These will be overridden by inline styles from pyramid.js */
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.pyramid-bottom-piles {
|
||||
gap: 40px;
|
||||
}
|
||||
}
|
||||
373
pyramid.js
Normal file
373
pyramid.js
Normal file
@ -0,0 +1,373 @@
|
||||
(function() {
|
||||
// ====== CONSTANTS & STATE ======
|
||||
const suits = ['♥', '♦', '♣', '♠'];
|
||||
const values = ['A','2','3','4','5','6','7','8','9','10','J','Q','K'];
|
||||
const PILE_BORDER_WIDTH = 2;
|
||||
|
||||
let deck, stock, waste, pyramid, discard;
|
||||
let cardElements = {};
|
||||
let selectedCard = null;
|
||||
let score = 0;
|
||||
let highScore = 0;
|
||||
let isActive = false;
|
||||
|
||||
// DOM Elements
|
||||
const gameBoard = document.getElementById('pyramid-board');
|
||||
const pyramidLayout = gameBoard.querySelector('.pyramid-layout');
|
||||
const stockEl = document.getElementById('pyramid-stock');
|
||||
const wasteEl = document.getElementById('pyramid-waste');
|
||||
const discardEl = document.getElementById('pyramid-discard');
|
||||
const scoreEl = document.getElementById('current-score');
|
||||
const highScoreEl = document.getElementById('high-score');
|
||||
const winMessage = document.getElementById('win-message');
|
||||
|
||||
// ====== 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 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;
|
||||
}
|
||||
|
||||
function isExposed(cardId) {
|
||||
// A card in the pyramid is exposed if no cards in the row below it are still present in the slots it covers.
|
||||
// Pyramid indexing:
|
||||
// Row 0: 0
|
||||
// Row 1: 1, 2
|
||||
// Row 2: 3, 4, 5
|
||||
// Row 3: 6, 7, 8, 9
|
||||
// Row 4: 10, 11, 12, 13, 14
|
||||
// Row 5: 15, 16, 17, 18, 19, 20
|
||||
// Row 6: 21, 22, 23, 24, 25, 26, 27
|
||||
|
||||
const index = pyramid.findIndex(c => c && c.id === cardId);
|
||||
if (index === -1) return true; // Not in pyramid (waste/stock)
|
||||
|
||||
// Find which row it's in
|
||||
let row = 0;
|
||||
let count = 0;
|
||||
for (let r = 0; r < 7; r++) {
|
||||
if (index >= count && index < count + r + 1) {
|
||||
row = r;
|
||||
break;
|
||||
}
|
||||
count += r + 1;
|
||||
}
|
||||
|
||||
if (row === 6) return true; // Bottom row is always exposed if present
|
||||
|
||||
// Check the two cards below it
|
||||
// Row r, index i (within row) covers Row r+1, index i and i+1
|
||||
const indexInRow = index - count;
|
||||
const nextRowStart = count + row + 1;
|
||||
const leftBelow = nextRowStart + indexInRow;
|
||||
const rightBelow = nextRowStart + indexInRow + 1;
|
||||
|
||||
return !pyramid[leftBelow] && !pyramid[rightBelow];
|
||||
}
|
||||
|
||||
// ====== CORE LOGIC ======
|
||||
function initPyramid() {
|
||||
score = 0;
|
||||
highScore = parseInt(localStorage.getItem('pyramid-high-score'), 10) || 0;
|
||||
winMessage.classList.add('hidden');
|
||||
|
||||
// Clean up
|
||||
Object.values(cardElements).forEach(el => el.remove());
|
||||
cardElements = {};
|
||||
deck = [];
|
||||
pyramid = Array(28).fill(null);
|
||||
stock = [];
|
||||
waste = [];
|
||||
discard = [];
|
||||
selectedCard = null;
|
||||
|
||||
// Create deck
|
||||
suits.forEach(suit => values.forEach(value => {
|
||||
deck.push({ suit, value, faceUp: false, id: `p-${value}${suit}-${Math.random().toString(36).slice(2,8)}` });
|
||||
}));
|
||||
|
||||
// Shuffle
|
||||
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]];
|
||||
}
|
||||
|
||||
// Create elements
|
||||
deck.forEach(cardData => {
|
||||
const el = createCardElement(cardData);
|
||||
cardElements[cardData.id] = el;
|
||||
gameBoard.appendChild(el);
|
||||
// Start all cards at stock position
|
||||
const sRect = stockEl.getBoundingClientRect();
|
||||
const bRect = gameBoard.getBoundingClientRect();
|
||||
el.style.top = `${sRect.top - bRect.top + PILE_BORDER_WIDTH}px`;
|
||||
el.style.left = `${sRect.left - bRect.left + PILE_BORDER_WIDTH}px`;
|
||||
});
|
||||
|
||||
// Fill Pyramid
|
||||
for (let i = 0; i < 28; i++) {
|
||||
pyramid[i] = deck.pop();
|
||||
pyramid[i].faceUp = true;
|
||||
}
|
||||
|
||||
stock = deck;
|
||||
updateScoreDisplay();
|
||||
renderBoard();
|
||||
saveGame();
|
||||
}
|
||||
|
||||
function renderBoard() {
|
||||
const boardRect = gameBoard.getBoundingClientRect();
|
||||
const layoutRect = pyramidLayout.getBoundingClientRect();
|
||||
const cardWidth = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--card-width'));
|
||||
const cardHeight = cardWidth * 1.4;
|
||||
const rowOverlap = 0.45;
|
||||
|
||||
// Position Pyramid Cards
|
||||
let index = 0;
|
||||
for (let row = 0; row < 7; row++) {
|
||||
const rowWidth = (row + 1) * cardWidth + row * 10;
|
||||
const startX = (layoutRect.width - rowWidth) / 2;
|
||||
const y = row * (cardHeight * rowOverlap);
|
||||
|
||||
for (let i = 0; i <= row; i++) {
|
||||
const card = pyramid[index];
|
||||
if (card) {
|
||||
const el = cardElements[card.id];
|
||||
el.style.top = `${y + layoutRect.top - boardRect.top}px`;
|
||||
el.style.left = `${startX + i * (cardWidth + 10) + layoutRect.left - boardRect.left}px`;
|
||||
el.style.zIndex = 100 + row;
|
||||
el.classList.add('is-flipped');
|
||||
el.draggable = false; // Pyramid uses clicks
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
// Position Stock
|
||||
const sRect = stockEl.getBoundingClientRect();
|
||||
stock.forEach((card, i) => {
|
||||
const el = cardElements[card.id];
|
||||
el.style.top = `${sRect.top - boardRect.top + PILE_BORDER_WIDTH}px`;
|
||||
el.style.left = `${sRect.left - boardRect.left + PILE_BORDER_WIDTH}px`;
|
||||
el.style.zIndex = 10 + i;
|
||||
el.classList.remove('is-flipped');
|
||||
});
|
||||
|
||||
// Position Waste
|
||||
const wRect = wasteEl.getBoundingClientRect();
|
||||
waste.forEach((card, i) => {
|
||||
const el = cardElements[card.id];
|
||||
el.style.top = `${wRect.top - boardRect.top + PILE_BORDER_WIDTH}px`;
|
||||
el.style.left = `${wRect.left - boardRect.left + PILE_BORDER_WIDTH}px`;
|
||||
el.style.zIndex = 50 + i;
|
||||
el.classList.add('is-flipped');
|
||||
});
|
||||
|
||||
// Position Discard
|
||||
const dRect = discardEl.getBoundingClientRect();
|
||||
discard.forEach((card, i) => {
|
||||
const el = cardElements[card.id];
|
||||
el.style.top = `${dRect.top - boardRect.top + PILE_BORDER_WIDTH}px`;
|
||||
el.style.left = `${dRect.left - boardRect.left + PILE_BORDER_WIDTH}px`;
|
||||
el.style.zIndex = 10 + i;
|
||||
el.classList.add('is-flipped');
|
||||
});
|
||||
|
||||
checkWin();
|
||||
}
|
||||
|
||||
function handleCardClick(cardId) {
|
||||
if (!isActive) return;
|
||||
|
||||
// Find card in pyramid or waste
|
||||
let card = null;
|
||||
let source = '';
|
||||
let index = pyramid.findIndex(c => c && c.id === cardId);
|
||||
if (index !== -1) {
|
||||
card = pyramid[index];
|
||||
source = 'pyramid';
|
||||
} else if (waste.length > 0 && waste[waste.length - 1].id === cardId) {
|
||||
card = waste[waste.length - 1];
|
||||
source = 'waste';
|
||||
}
|
||||
|
||||
if (!card) return;
|
||||
if (source === 'pyramid' && !isExposed(cardId)) return;
|
||||
|
||||
const el = cardElements[card.id];
|
||||
|
||||
// King is special (13)
|
||||
if (getValueRank(card.value) === 13) {
|
||||
removeCards([card]);
|
||||
selectedCard = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedCard) {
|
||||
selectedCard = card;
|
||||
el.classList.add('is-selected');
|
||||
} else {
|
||||
if (selectedCard.id === card.id) {
|
||||
// Deselect
|
||||
el.classList.remove('is-selected');
|
||||
selectedCard = null;
|
||||
} else {
|
||||
if (getValueRank(selectedCard.value) + getValueRank(card.value) === 13) {
|
||||
removeCards([selectedCard, card]);
|
||||
selectedCard = null;
|
||||
} else {
|
||||
// Invalid pair - switch selection to new card
|
||||
cardElements[selectedCard.id].classList.remove('is-selected');
|
||||
selectedCard = card;
|
||||
el.classList.add('is-selected');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeCards(cards) {
|
||||
cards.forEach(card => {
|
||||
// Remove from pyramid
|
||||
const pIdx = pyramid.findIndex(c => c && c.id === card.id);
|
||||
if (pIdx !== -1) pyramid[pIdx] = null;
|
||||
|
||||
// Remove from waste
|
||||
const wIdx = waste.findIndex(c => c && c.id === card.id);
|
||||
if (wIdx !== -1) waste.splice(wIdx, 1);
|
||||
|
||||
discard.push(card);
|
||||
cardElements[card.id].classList.remove('is-selected');
|
||||
score += 5;
|
||||
});
|
||||
updateScoreDisplay();
|
||||
renderBoard();
|
||||
saveGame();
|
||||
}
|
||||
|
||||
function handleStockClick() {
|
||||
if (stock.length > 0) {
|
||||
const card = stock.pop();
|
||||
card.faceUp = true;
|
||||
waste.push(card);
|
||||
} else if (waste.length > 0) {
|
||||
// Recycle waste back to stock
|
||||
stock = waste.reverse();
|
||||
stock.forEach(c => c.faceUp = false);
|
||||
waste = [];
|
||||
}
|
||||
renderBoard();
|
||||
saveGame();
|
||||
}
|
||||
|
||||
function updateScoreDisplay() {
|
||||
if (!isActive) return;
|
||||
scoreEl.textContent = score;
|
||||
highScoreEl.textContent = highScore;
|
||||
}
|
||||
|
||||
function checkWin() {
|
||||
if (pyramid.every(c => c === null)) {
|
||||
if (score > highScore) {
|
||||
highScore = score;
|
||||
localStorage.setItem('pyramid-high-score', highScore);
|
||||
updateScoreDisplay();
|
||||
}
|
||||
winMessage.classList.remove('hidden');
|
||||
// Trigger bouncing cards - reuse logic if available or implement local
|
||||
if (window.triggerBouncingCards) window.triggerBouncingCards();
|
||||
}
|
||||
}
|
||||
|
||||
function saveGame() {
|
||||
const state = { pyramid, stock, waste, discard, score };
|
||||
localStorage.setItem('pyramid-save', JSON.stringify(state));
|
||||
}
|
||||
|
||||
function loadGame() {
|
||||
const saved = localStorage.getItem('pyramid-save');
|
||||
if (!saved) return false;
|
||||
try {
|
||||
const state = JSON.parse(saved);
|
||||
pyramid = state.pyramid;
|
||||
stock = state.stock;
|
||||
waste = state.waste;
|
||||
discard = state.discard;
|
||||
score = state.score || 0;
|
||||
|
||||
// Rebuild elements
|
||||
Object.values(cardElements).forEach(el => el.remove());
|
||||
cardElements = {};
|
||||
const all = [...pyramid.filter(c => c), ...stock, ...waste, ...discard];
|
||||
all.forEach(c => {
|
||||
const el = createCardElement(c);
|
||||
cardElements[c.id] = el;
|
||||
gameBoard.appendChild(el);
|
||||
});
|
||||
renderBoard();
|
||||
updateScoreDisplay();
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ====== EVENT LISTENERS ======
|
||||
gameBoard.addEventListener('click', (e) => {
|
||||
const cardEl = e.target.closest('.card');
|
||||
if (cardEl) {
|
||||
handleCardClick(cardEl.dataset.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const pileEl = e.target.closest('.pile');
|
||||
if (pileEl && pileEl.id === 'pyramid-stock') {
|
||||
handleStockClick();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('gameModeChanged', (e) => {
|
||||
isActive = (e.detail === 'pyramid');
|
||||
if (isActive) {
|
||||
if (!pyramid) {
|
||||
if (!loadGame()) initPyramid();
|
||||
} else {
|
||||
updateScoreDisplay();
|
||||
renderBoard();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle New Game button from topbar
|
||||
document.getElementById('restart-btn').addEventListener('click', () => {
|
||||
if (isActive) initPyramid();
|
||||
});
|
||||
|
||||
// Initial check
|
||||
if (localStorage.getItem('solitaire-mode') === 'pyramid') {
|
||||
isActive = true;
|
||||
if (!loadGame()) initPyramid();
|
||||
}
|
||||
})();
|
||||
31
script.js
31
script.js
@ -18,6 +18,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let isDealing = false;
|
||||
let score = 0;
|
||||
let highScore = 0;
|
||||
let isActive = false;
|
||||
|
||||
const CARD_BACKS = [
|
||||
{ id: 'waves', name: 'Blue Waves', className: 'card-back-waves' },
|
||||
@ -310,6 +311,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// ====== HELPERS ======
|
||||
function updateScoreDisplay() {
|
||||
if (!isActive) return;
|
||||
scoreEl.textContent = score;
|
||||
highScoreEl.textContent = highScore;
|
||||
}
|
||||
@ -495,6 +497,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
bounceNextCard();
|
||||
}
|
||||
window.triggerBouncingCards = triggerBouncingCards;
|
||||
|
||||
// ====== ACTIONS ======
|
||||
function handleStockClick() {
|
||||
@ -699,7 +702,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// ====== INPUT HANDLERS ======
|
||||
gameBoard.addEventListener('click', e => {
|
||||
if (isAutoCompleting) return;
|
||||
if (!isActive || isAutoCompleting) return;
|
||||
|
||||
const clickedCardEl = e.target.closest('.card');
|
||||
const clickedPileEl = e.target.closest('.pile');
|
||||
@ -721,7 +724,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Custom drag and drop functionality
|
||||
gameBoard.addEventListener('dragstart', e => {
|
||||
if (isAutoCompleting) { e.preventDefault(); return; }
|
||||
if (!isActive || isAutoCompleting) { e.preventDefault(); return; }
|
||||
const cardEl = e.target.closest('.card');
|
||||
if (!cardEl) { e.preventDefault(); return; }
|
||||
|
||||
@ -863,7 +866,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e)=>{
|
||||
if (isAutoCompleting) return;
|
||||
if (!isActive || isAutoCompleting) return;
|
||||
if (e.code === 'Space') { e.preventDefault(); handleStockClick(); }
|
||||
if (e.key.toLowerCase() === 'u' || (e.ctrlKey && e.key.toLowerCase() === 'z')) {
|
||||
e.preventDefault();
|
||||
@ -872,7 +875,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
// ====== BUTTONS & MODAL LISTENERS ======
|
||||
restartBtn?.addEventListener('click', initState);
|
||||
restartBtn?.addEventListener('click', () => {
|
||||
if (isActive) initState();
|
||||
});
|
||||
undoBtn?.addEventListener('click', undo);
|
||||
drawSelect?.addEventListener('change', () => { DRAW_COUNT = parseInt(drawSelect.value, 10) || 3; initState(); });
|
||||
openModalBtn?.addEventListener('click', () => cardBackModal.classList.remove('hidden'));
|
||||
@ -881,11 +886,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
checkMovesBtn?.addEventListener('click', handleStuckCheck);
|
||||
autoCompleteBtn?.addEventListener('click', autoComplete);
|
||||
|
||||
document.addEventListener('gameModeChanged', (e) => {
|
||||
isActive = (e.detail === 'klondike');
|
||||
if (isActive) {
|
||||
updateScoreDisplay();
|
||||
updateBoard();
|
||||
}
|
||||
});
|
||||
|
||||
// ====== INIT ======
|
||||
setupThemeSelector();
|
||||
if (!loadGame()) {
|
||||
initState();
|
||||
isActive = (localStorage.getItem('solitaire-mode') !== 'pyramid');
|
||||
if (isActive) {
|
||||
if (!loadGame()) {
|
||||
initState();
|
||||
}
|
||||
} else {
|
||||
loadGame();
|
||||
}
|
||||
window.addEventListener('resize', () => updateBoard(true));
|
||||
window.addEventListener('resize', () => { if (isActive) updateBoard(true); });
|
||||
});
|
||||
29
style.css
29
style.css
@ -75,6 +75,35 @@ h1 {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.game-select {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
background: var(--control-bg);
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--line);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.game-select select {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
color: var(--text-color);
|
||||
border: none;
|
||||
outline: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.game-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hidden { display: none !important; }
|
||||
|
||||
.btn {
|
||||
background: var(--control-bg);
|
||||
color: var(--text-color);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user