solitaire / index.html
mrwhy06's picture
Add 2 files
3200b3e verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solitaire</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
body {
font-family: 'Roboto', sans-serif;
background: linear-gradient(135deg, #1e5799, #207cca, #2989d8, #7db9e8);
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
color: #333;
overflow: hidden;
}
.game-container {
width: 100%;
max-width: 1000px;
margin: 20px auto;
display: flex;
flex-direction: column;
height: calc(100vh - 100px);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
margin-bottom: 10px;
color: white;
}
.score {
font-size: 1.2rem;
font-weight: bold;
}
.restart-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
padding: 8px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
transition: background 0.3s;
}
.restart-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.game-board {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.top-section {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.deck {
width: 70px;
height: 100px;
position: relative;
}
.stock {
cursor: pointer;
position: relative;
}
.waste {
position: relative;
margin-left: 10px;
}
.foundations {
display: flex;
gap: 10px;
}
.foundation {
width: 70px;
height: 100px;
border: 2px dashed rgba(255, 255, 255, 0.2);
border-radius: 5px;
}
.tableau-section {
display: flex;
justify-content: space-between;
flex-grow: 1;
}
.pile {
display: flex;
flex-direction: column;
gap: 3px;
min-height: 100px;
position: relative;
}
.pile.bottom {
margin-top: 30px;
}
.card {
width: 70px;
height: 100px;
background: white;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
position: absolute;
transition: transform 0.2s;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 5px;
font-weight: bold;
font-size: 1.2rem;
overflow: hidden;
}
.card.red {
color: red;
}
.card.black {
color: black;
}
.card.hidden {
background: linear-gradient(135deg, #3498db, #2980b9);
color: transparent;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2);
border: 2px solid rgba(255, 255, 255, 0.2);
}
.card.hidden .card-value,
.card.hidden .card-suit {
display: none;
}
.card-suit {
text-align: center;
font-size: 30px;
}
.card-bottom-suit {
transform: rotate(180deg);
}
.card-placeholder {
border: 2px dashed rgba(255, 255, 255, 0.3);
border-radius: 5px;
}
.card.dragging {
opacity: 0.8;
z-index: 100;
transform: rotate(2deg);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
}
.card.selected {
transform: translateY(-10px);
}
.win-message {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1000;
color: white;
opacity: 0;
pointer-events: none;
transition: opacity 0.5s;
}
.win-message.show {
opacity: 1;
pointer-events: all;
}
.win-text {
font-size: 3rem;
margin-bottom: 20px;
text-align: center;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
}
.win-details {
font-size: 1.2rem;
margin-bottom: 30px;
}
.move-counter {
font-size: 1.1rem;
margin-bottom: 5px;
color: rgba(255, 255, 255, 0.8);
}
@media (max-width: 768px) {
.game-container {
transform: scale(0.8);
transform-origin: top center;
}
}
@media (max-width: 480px) {
.game-container {
transform: scale(0.6);
}
}
</style>
</head>
<body>
<div class="game-container">
<div class="header">
<div class="score">Score: <span id="score">0</span></div>
<div class="move-counter">Moves: <span id="moves">0</span></div>
<button class="restart-btn" id="restart">New Game</button>
</div>
<div class="game-board">
<div class="top-section">
<div class="deck">
<div class="card-placeholder stock" id="stock"></div>
<div class="card-placeholder waste" id="waste"></div>
</div>
<div class="foundations">
<div class="card-placeholder foundation" id="foundation-1"></div>
<div class="card-placeholder foundation" id="foundation-2"></div>
<div class="card-placeholder foundation" id="foundation-3"></div>
<div class="card-placeholder foundation" id="foundation-4"></div>
</div>
</div>
<div class="tableau-section">
<div class="pile" id="pile-1"></div>
<div class="pile" id="pile-2"></div>
<div class="pile" id="pile-3"></div>
<div class="pile" id="pile-4"></div>
<div class="pile" id="pile-5"></div>
<div class="pile" id="pile-6"></div>
<div class="pile" id="pile-7"></div>
</div>
</div>
</div>
<div class="win-message" id="win-message">
<div class="win-text">You Won!</div>
<div class="win-details">Congratulations on completing Solitaire!</div>
<div>Final Score: <span id="final-score">0</span></div>
<div>Total Moves: <span id="final-moves">0</span></div>
<button class="restart-btn" style="margin-top: 20px;">Play Again</button>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Game state
const game = {
deck: [],
stock: [],
waste: [],
foundations: [[], [], [], []],
piles: [[], [], [], [], [], [], []],
score: 0,
moves: 0,
draggingCards: [],
dragSource: null,
gameStarted: false
};
// DOM elements
const elements = {
stock: document.getElementById('stock'),
waste: document.getElementById('waste'),
foundations: [
document.getElementById('foundation-1'),
document.getElementById('foundation-2'),
document.getElementById('foundation-3'),
document.getElementById('foundation-4')
],
piles: [
document.getElementById('pile-1'),
document.getElementById('pile-2'),
document.getElementById('pile-3'),
document.getElementById('pile-4'),
document.getElementById('pile-5'),
document.getElementById('pile-6'),
document.getElementById('pile-7')
],
scoreDisplay: document.getElementById('score'),
movesDisplay: document.getElementById('moves'),
restartBtn: document.getElementById('restart'),
winMessage: document.getElementById('win-message'),
finalScore: document.getElementById('final-score'),
finalMoves: document.getElementById('final-moves')
};
// Card suits and values
const suits = ['♥', '♦', '♠', '♣'];
const values = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
// Initialize game
function initGame() {
resetGame();
createDeck();
shuffleDeck();
dealCards();
updateUI();
game.gameStarted = true;
}
// Reset game state
function resetGame() {
game.deck = [];
game.stock = [];
game.waste = [];
game.foundations = [[], [], [], []];
game.piles = [[], [], [], [], [], [], []];
game.score = 0;
game.moves = 0;
game.draggingCards = [];
game.dragSource = null;
game.gameStarted = false;
// Clear all piles and foundations
elements.piles.forEach(pile => pile.innerHTML = '');
elements.foundations.forEach(foundation => foundation.innerHTML = '');
elements.waste.innerHTML = '';
}
// Create a standard deck of 52 cards
function createDeck() {
game.deck = [];
for (let suitIndex = 0; suitIndex < suits.length; suitIndex++) {
for (let valueIndex = 0; valueIndex < values.length; valueIndex++) {
const card = {
suit: suits[suitIndex],
value: values[valueIndex],
color: (suitIndex < 2) ? 'red' : 'black', // Hearts and diamonds are red
rank: valueIndex + 1, // A=1, K=13
hidden: true
};
game.deck.push(card);
}
}
}
// Shuffle the deck using Fisher-Yates algorithm
function shuffleDeck() {
for (let i = game.deck.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[game.deck[i], game.deck[j]] = [game.deck[j], game.deck[i]];
}
}
// Deal cards to piles
function dealCards() {
let cardIndex = 0;
// Deal to each pile
for (let pileIndex = 0; pileIndex < game.piles.length; pileIndex++) {
const pile = game.piles[pileIndex];
// Number of cards to deal to this pile is pileIndex + 1
for (let i = 0; i <= pileIndex; i++) {
const card = game.deck[cardIndex++];
// Only the last card in each pile is face up
card.hidden = i !== pileIndex;
pile.push(card);
}
}
// Remaining cards go to stock
game.stock = game.deck.slice(cardIndex);
}
// Update the UI based on game state
function updateUI() {
updateStock();
updateWaste();
updateFoundations();
updatePiles();
updateScore();
checkWin();
}
// Update stock display
function updateStock() {
elements.stock.innerHTML = '';
if (game.stock.length > 0) {
const cardBack = document.createElement('div');
cardBack.className = 'card hidden';
elements.stock.appendChild(cardBack);
} else {
elements.stock.classList.add('empty');
}
}
// Update waste display
function updateWaste() {
elements.waste.innerHTML = '';
if (game.waste.length > 0) {
const lastCard = game.waste[game.waste.length - 1];
const cardEl = createCardElement(lastCard);
cardEl.style.left = '0';
cardEl.style.top = '0';
elements.waste.appendChild(cardEl);
}
}
// Update foundations display
function updateFoundations() {
game.foundations.forEach((foundation, index) => {
elements.foundations[index].innerHTML = '';
if (foundation.length > 0) {
const lastCard = foundation[foundation.length - 1];
const cardEl = createCardElement(lastCard);
elements.foundations[index].appendChild(cardEl);
}
});
}
// Update tableaux (piles) display
function updatePiles() {
game.piles.forEach((pile, pileIndex) => {
elements.piles[pileIndex].innerHTML = '';
pile.forEach((card, cardIndex) => {
const cardEl = createCardElement(card);
cardEl.style.top = `${cardIndex * 25}px`; // Stack cards vertically
cardEl.setAttribute('data-pile-index', pileIndex);
cardEl.setAttribute('data-card-index', cardIndex);
elements.piles[pileIndex].appendChild(cardEl);
});
});
}
// Create a card DOM element
function createCardElement(card) {
const cardEl = document.createElement('div');
cardEl.className = `card ${card.color}`;
cardEl.innerHTML = `
<div class="card-value">${card.value}</div>
<div class="card-suit">${card.suit}</div>
<div class="card-suit card-bottom-suit">${card.suit}</div>
`;
if (card.hidden) {
cardEl.classList.add('hidden');
}
// Add drag event listeners
cardEl.draggable = true;
cardEl.addEventListener('dragstart', handleDragStart);
cardEl.addEventListener('mouseup', handleCardClick);
return cardEl;
}
// Update score display
function updateScore() {
elements.scoreDisplay.textContent = game.score;
elements.movesDisplay.textContent = game.moves;
}
// Draw card from stock to waste
function drawCard() {
if (!game.gameStarted) return;
if (game.stock.length === 0) {
// If stock is empty, return all waste cards to stock
game.waste.reverse().forEach(card => {
card.hidden = true;
});
game.stock = [...game.waste];
game.waste = [];
game.score = Math.max(0, game.score - 100); // Penalty for recycling waste
} else {
// Draw the top card from stock
const card = game.stock.pop();
card.hidden = false;
game.waste.push(card);
game.score += 5; // Small points for drawing
}
game.moves++;
updateUI();
}
// Handle drag start
function handleDragStart(e) {
if (!game.gameStarted) return;
const cardEl = e.target.closest('.card');
if (!cardEl || cardEl.classList.contains('hidden')) {
e.preventDefault();
return;
}
const pileIndex = parseInt(cardEl.getAttribute('data-pile-index'));
const cardIndex = parseInt(cardEl.getAttribute('data-card-index'));
// Get all cards from this one to the end of the pile
game.draggingCards = game.piles[pileIndex].slice(cardIndex);
game.dragSource = { type: 'pile', index: pileIndex };
// For foundation or waste, just move the top card
if (isNaN(pileIndex)) {
if (cardEl.parentElement === elements.waste) {
game.draggingCards = [game.waste[game.waste.length - 1]];
game.dragSource = { type: 'waste' };
} else {
const foundationIndex = elements.foundations.findIndex(f => cardEl.parentElement === f);
if (foundationIndex !== -1) {
game.draggingCards = [game.foundations[foundationIndex][game.foundations[foundationIndex].length - 1]];
game.dragSource = { type: 'foundation', index: foundationIndex };
}
}
}
// Set drag image
const dragImage = cardEl.cloneNode(true);
dragImage.style.position = 'fixed';
dragImage.style.left = '-1000px';
dragImage.style.top = '0';
dragImage.style.zIndex = '10000';
document.body.appendChild(dragImage);
e.dataTransfer.setDragImage(dragImage, 35, 50);
setTimeout(() => document.body.removeChild(dragImage), 0);
// Set effect allowed
e.dataTransfer.effectAllowed = 'move';
// Add dragging class
setTimeout(() => cardEl.classList.add('dragging'), 0);
}
// Handle drop on a pile or foundation
function handleDrop(target, e) {
e.preventDefault();
if (!game.draggingCards.length) return;
const card = game.draggingCards[0];
// Determine target type
let targetType, targetIndex;
if (target === elements.waste) {
return; // Can't drop on waste
} else if (target === elements.stock) {
return; // Can't drop on stock
} else if (elements.foundations.includes(target)) {
targetType = 'foundation';
targetIndex = elements.foundations.indexOf(target);
} else if (elements.piles.includes(target)) {
targetType = 'pile';
targetIndex = elements.piles.indexOf(target);
} else {
return;
}
// Check if move is valid
let isValid = false;
if (targetType === 'foundation') {
isValid = canMoveToFoundation(card, targetIndex);
} else if (targetType === 'pile') {
isValid = canMoveToPile(card, targetIndex);
}
if (isValid) {
// Remove cards from source
if (game.dragSource.type === 'pile') {
game.piles[game.dragSource.index].splice(
game.piles[game.dragSource.index].length - game.draggingCards.length,
game.draggingCards.length
);
// Reveal next card in source pile if needed
const sourcePile = game.piles[game.dragSource.index];
if (sourcePile.length > 0 && sourcePile[sourcePile.length - 1].hidden) {
sourcePile[sourcePile.length - 1].hidden = false;
game.score += 5; // Points for revealing a card
}
} else if (game.dragSource.type === 'waste') {
game.waste.pop();
} else if (game.dragSource.type === 'foundation') {
game.foundations[game.dragSource.index].pop();
}
// Add cards to target
if (targetType === 'foundation') {
game.foundations[targetIndex].push(card);
game.score += 10; // Points for moving to foundation
} else if (targetType === 'pile') {
game.piles[targetIndex].push(...game.draggingCards);
// Points for moving from waste or another pile
game.score += (game.draggingCards.length === 1) ? 5 : 0;
}
game.moves++;
game.draggingCards = [];
game.dragSource = null;
updateUI();
}
}
// Check if card can be moved to foundation
function canMoveToFoundation(card, foundationIndex) {
const foundation = game.foundations[foundationIndex];
if (foundation.length === 0) {
// Only Aces can start a foundation
return card.value === 'A';
} else {
const topCard = foundation[foundation.length - 1];
// Must be same suit and next in sequence
return topCard.suit === card.suit && card.rank === topCard.rank + 1;
}
}
// Check if card can be moved to a pile
function canMoveToPile(card, pileIndex) {
const pile = game.piles[pileIndex];
if (pile.length === 0) {
// Only Kings can be placed on empty pile
return card.value === 'K';
} else {
const topCard = pile[pile.length - 1];
// Must be opposite color and one rank lower
return topCard.color !== card.color && card.rank === topCard.rank - 1;
}
}
// Handle card click
function handleCardClick(e) {
if (!game.gameStarted) return;
const cardEl = e.target.closest('.card');
if (!cardEl || cardEl.classList.contains('hidden')) return;
// If clicked on waste, try to auto-move to foundation
if (cardEl.parentElement === elements.waste && game.waste.length > 0) {
const card = game.waste[game.waste.length - 1];
// Check all foundations for possible moves
for (let i = 0; i < game.foundations.length; i++) {
if (canMoveToFoundation(card, i)) {
game.foundations[i].push(game.waste.pop());
game.score += 10;
game.moves++;
updateUI();
return;
}
}
}
// If clicked on pile card, try to auto-move to foundation if it's the top card
const pileIndex = parseInt(cardEl.getAttribute('data-pile-index'));
const cardIndex = parseInt(cardEl.getAttribute('data-card-index'));
if (!isNaN(pileIndex) && cardIndex === game.piles[pileIndex].length - 1) {
const card = game.piles[pileIndex][cardIndex];
for (let i = 0; i < game.foundations.length; i++) {
if (canMoveToFoundation(card, i)) {
game.foundations[i].push(game.piles[pileIndex].pop());
game.score += 10;
game.moves++;
// Reveal next card if needed
if (game.piles[pileIndex].length > 0 && game.piles[pileIndex][game.piles[pileIndex].length - 1].hidden) {
game.piles[pileIndex][game.piles[pileIndex].length - 1].hidden = false;
game.score += 5;
}
updateUI();
return;
}
}
}
}
// Check if player has won
function checkWin() {
const allCardsInFoundations = game.foundations.every(foundation => foundation.length === 13);
if (allCardsInFoundations) {
showWinMessage();
}
}
// Show win message
function showWinMessage() {
elements.finalScore.textContent = game.score;
elements.finalMoves.textContent = game.moves;
elements.winMessage.classList.add('show');
}
// Setup event listeners
function setupEventListeners() {
// Stock click to draw card
elements.stock.addEventListener('click', drawCard);
// Restart button
elements.restartBtn.addEventListener('click', initGame);
elements.winMessage.querySelector('.restart-btn').addEventListener('click', () => {
elements.winMessage.classList.remove('show');
initGame();
});
// Drag and drop setup
document.addEventListener('dragover', e => {
e.preventDefault();
const target = document.elementFromPoint(e.clientX, e.clientY)?.closest('.pile, .foundation, .waste, .stock');
if (target) {
const rect = target.getBoundingClientRect();
const isOver = e.clientX > rect.left && e.clientX < rect.right &&
e.clientY > rect.top && e.clientY < rect.bottom;
if (isOver) {
if (target === elements.stock || target === elements.waste) return;
target.classList.add('highlight');
}
}
});
document.addEventListener('dragleave', e => {
const target = document.elementFromPoint(e.clientX, e.clientY)?.closest('.pile, .foundation');
if (!target || e.target !== target) {
elements.piles.forEach(pile => pile.classList.remove('highlight'));
elements.foundations.forEach(foundation => foundation.classList.remove('highlight'));
}
});
document.addEventListener('drop', e => {
e.preventDefault();
elements.piles.forEach(pile => pile.classList.remove('highlight'));
elements.foundations.forEach(foundation => foundation.classList.remove('highlight'));
const target = document.elementFromPoint(e.clientX, e.clientY)?.closest('.pile, .foundation, .waste, .stock');
if (target && game.draggingCards.length) {
handleDrop(target, e);
}
});
document.addEventListener('dragend', () => {
document.querySelectorAll('.card').forEach(card => card.classList.remove('dragging'));
elements.piles.forEach(pile => pile.classList.remove('highlight'));
elements.foundations.forEach(foundation => foundation.classList.remove('highlight'));
});
// Keyboard shortcuts
document.addEventListener('keydown', e => {
if (e.code === 'Space' && !e.target.matches('button, input, textarea')) {
e.preventDefault();
drawCard();
} else if (e.code === 'KeyR' && !e.target.matches('button, input, textarea')) {
e.preventDefault();
initGame();
}
});
}
// Initialize the game
setupEventListeners();
initGame();
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body>
</html>