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"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>Klondike Solitaire</title>
|
<title>Klondike Solitaire</title>
|
||||||
<link rel="stylesheet" href="style.css"/>
|
<link rel="stylesheet" href="style.css"/>
|
||||||
|
<link rel="stylesheet" href="pyramid.css"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
@ -14,21 +15,33 @@
|
|||||||
<div class="score-display">High Score: <span id="high-score">0</span></div>
|
<div class="score-display">High Score: <span id="high-score">0</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<label class="draw-toggle">
|
<div class="game-select">
|
||||||
Draw:
|
<label for="game-mode-select">Game:</label>
|
||||||
<select id="draw-select" aria-label="Draw count">
|
<select id="game-mode-select">
|
||||||
<option value="1">1</option>
|
<option value="klondike">Klondike</option>
|
||||||
<option value="3" selected>3</option>
|
<option value="pyramid">Pyramid</option>
|
||||||
</select>
|
</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="change-back-btn" class="btn">Card Back</button>
|
||||||
<button id="undo-btn" class="btn">Undo</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>
|
<button id="restart-btn" class="btn btn-accent">New Game</button>
|
||||||
</div>
|
</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="top-piles">
|
||||||
<div class="stock-waste">
|
<div class="stock-waste">
|
||||||
<div class="pile stock" id="stock" aria-label="Stock" tabindex="0"></div>
|
<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-5" aria-label="Tableau 6" tabindex="0"></div>
|
||||||
<div class="pile tableau-pile" id="tableau-6" aria-label="Tableau 7" tabindex="0"></div>
|
<div class="pile tableau-pile" id="tableau-6" aria-label="Tableau 7" tabindex="0"></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div id="win-message" class="hidden">You Won! 🎉</div>
|
<main id="pyramid-board" class="game-board hidden" aria-label="Pyramid Solitaire board">
|
||||||
|
<div class="pyramid-layout">
|
||||||
<div id="card-back-modal" class="modal hidden">
|
<!-- Pyramid rows will be rendered here by pyramid.js -->
|
||||||
<div class="modal-overlay"></div>
|
</div>
|
||||||
<div class="modal-content">
|
<div class="pyramid-bottom-piles">
|
||||||
<h2>Choose Card Back</h2>
|
<div class="stock-waste">
|
||||||
<div class="card-back-options" id="card-back-options">
|
<div class="pile stock" id="pyramid-stock" aria-label="Stock" tabindex="0"></div>
|
||||||
</div>
|
<div class="pile waste" id="pyramid-waste" aria-label="Waste" tabindex="0"></div>
|
||||||
<button id="modal-close-btn" class="btn">Close</button>
|
</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>
|
||||||
</div>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
<script src="script.js"></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>
|
</body>
|
||||||
</html>
|
</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 isDealing = false;
|
||||||
let score = 0;
|
let score = 0;
|
||||||
let highScore = 0;
|
let highScore = 0;
|
||||||
|
let isActive = false;
|
||||||
|
|
||||||
const CARD_BACKS = [
|
const CARD_BACKS = [
|
||||||
{ id: 'waves', name: 'Blue Waves', className: 'card-back-waves' },
|
{ id: 'waves', name: 'Blue Waves', className: 'card-back-waves' },
|
||||||
@ -310,6 +311,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// ====== HELPERS ======
|
// ====== HELPERS ======
|
||||||
function updateScoreDisplay() {
|
function updateScoreDisplay() {
|
||||||
|
if (!isActive) return;
|
||||||
scoreEl.textContent = score;
|
scoreEl.textContent = score;
|
||||||
highScoreEl.textContent = highScore;
|
highScoreEl.textContent = highScore;
|
||||||
}
|
}
|
||||||
@ -495,6 +497,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
bounceNextCard();
|
bounceNextCard();
|
||||||
}
|
}
|
||||||
|
window.triggerBouncingCards = triggerBouncingCards;
|
||||||
|
|
||||||
// ====== ACTIONS ======
|
// ====== ACTIONS ======
|
||||||
function handleStockClick() {
|
function handleStockClick() {
|
||||||
@ -699,7 +702,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// ====== INPUT HANDLERS ======
|
// ====== INPUT HANDLERS ======
|
||||||
gameBoard.addEventListener('click', e => {
|
gameBoard.addEventListener('click', e => {
|
||||||
if (isAutoCompleting) return;
|
if (!isActive || isAutoCompleting) return;
|
||||||
|
|
||||||
const clickedCardEl = e.target.closest('.card');
|
const clickedCardEl = e.target.closest('.card');
|
||||||
const clickedPileEl = e.target.closest('.pile');
|
const clickedPileEl = e.target.closest('.pile');
|
||||||
@ -721,7 +724,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Custom drag and drop functionality
|
// Custom drag and drop functionality
|
||||||
gameBoard.addEventListener('dragstart', e => {
|
gameBoard.addEventListener('dragstart', e => {
|
||||||
if (isAutoCompleting) { e.preventDefault(); return; }
|
if (!isActive || isAutoCompleting) { e.preventDefault(); return; }
|
||||||
const cardEl = e.target.closest('.card');
|
const cardEl = e.target.closest('.card');
|
||||||
if (!cardEl) { e.preventDefault(); return; }
|
if (!cardEl) { e.preventDefault(); return; }
|
||||||
|
|
||||||
@ -863,7 +866,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('keydown', (e)=>{
|
document.addEventListener('keydown', (e)=>{
|
||||||
if (isAutoCompleting) return;
|
if (!isActive || isAutoCompleting) return;
|
||||||
if (e.code === 'Space') { e.preventDefault(); handleStockClick(); }
|
if (e.code === 'Space') { e.preventDefault(); handleStockClick(); }
|
||||||
if (e.key.toLowerCase() === 'u' || (e.ctrlKey && e.key.toLowerCase() === 'z')) {
|
if (e.key.toLowerCase() === 'u' || (e.ctrlKey && e.key.toLowerCase() === 'z')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -872,7 +875,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ====== BUTTONS & MODAL LISTENERS ======
|
// ====== BUTTONS & MODAL LISTENERS ======
|
||||||
restartBtn?.addEventListener('click', initState);
|
restartBtn?.addEventListener('click', () => {
|
||||||
|
if (isActive) initState();
|
||||||
|
});
|
||||||
undoBtn?.addEventListener('click', undo);
|
undoBtn?.addEventListener('click', undo);
|
||||||
drawSelect?.addEventListener('change', () => { DRAW_COUNT = parseInt(drawSelect.value, 10) || 3; initState(); });
|
drawSelect?.addEventListener('change', () => { DRAW_COUNT = parseInt(drawSelect.value, 10) || 3; initState(); });
|
||||||
openModalBtn?.addEventListener('click', () => cardBackModal.classList.remove('hidden'));
|
openModalBtn?.addEventListener('click', () => cardBackModal.classList.remove('hidden'));
|
||||||
@ -881,11 +886,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
checkMovesBtn?.addEventListener('click', handleStuckCheck);
|
checkMovesBtn?.addEventListener('click', handleStuckCheck);
|
||||||
autoCompleteBtn?.addEventListener('click', autoComplete);
|
autoCompleteBtn?.addEventListener('click', autoComplete);
|
||||||
|
|
||||||
|
document.addEventListener('gameModeChanged', (e) => {
|
||||||
|
isActive = (e.detail === 'klondike');
|
||||||
|
if (isActive) {
|
||||||
|
updateScoreDisplay();
|
||||||
|
updateBoard();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ====== INIT ======
|
// ====== INIT ======
|
||||||
setupThemeSelector();
|
setupThemeSelector();
|
||||||
if (!loadGame()) {
|
isActive = (localStorage.getItem('solitaire-mode') !== 'pyramid');
|
||||||
initState();
|
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;
|
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 {
|
.btn {
|
||||||
background: var(--control-bg);
|
background: var(--control-bg);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user