/** * @typedef {Object} Card * @property {number} x * @property {number} y * @property {CardData} card * @property {boolean} isFaceUp */ /** * @typedef {Object} CardData * @property {string} suit * @property {string} value */ /** * @typedef {Object} GameState * @property {Card[]} cards * @property {Card|null} draggingCard * @property {CardData[]} deck * @property {{x: number, y: number}} stackPosition */ // Constants const CARD_WIDTH = 100; const CARD_HEIGHT = 150; const PADDING = 10; const SUITS = ['❤️', '♦️', '♣️', '♠️']; const VALUES = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']; const PATTERN_SIZE = 10; const INITIAL_CARD_X = 20; const INITIAL_CARD_Y = 20; const FONT_SIZE = '34px "pokemon-font", monospace'; const CARD_BORDER_COLOR = '#000000'; const CARD_FACE_COLOR = '#FFFFFF'; const DECK_COUNT = 4; // Can be changed to any number const BASE_COLORS = [ { primary: '#FF9900', secondary: '#FFCC00' }, // Original orange deck { primary: '#6B8E23', secondary: '#9ACD32' }, // Olive green deck { primary: '#4169E1', secondary: '#87CEEB' }, // Royal blue deck { primary: '#8B008B', secondary: '#DA70D6' }, // Purple deck { primary: '#CD853F', secondary: '#DEB887' } // Brown deck ]; // Add new constants for pile layout const PILE_SPACING = CARD_WIDTH + PADDING * 4; // Space between piles const PILE_OFFSET = 5; // Vertical offset for stacked cards // Canvas setup const canvas = document.getElementById('cards'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Pure functions const shuffle = array => { const result = [...array]; for (let i = result.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [result[i], result[j]] = [result[j], result[i]]; } return result; }; const createDeck = (deckIndex) => SUITS.flatMap(suit => VALUES.map(value => ({ suit, value, deckId: deckIndex // Add deckId to track which deck a card belongs to })) ); // Create multiple decks const createDecks = (count) => { if (count > BASE_COLORS.length) { console.warn(`Only ${BASE_COLORS.length} unique deck colors are defined. Some decks will repeat colors.`); } return Array.from({ length: count }, (_, i) => createDeck(i)).flat(); }; // Create a more functional card factory const createCard = (x, y, cardData) => Object.freeze({ x: x + PADDING, y: y + PADDING, card: Object.freeze({ ...cardData }), isFaceUp: false }); // Function to check if a point is within a card const isPointInCard = (x, y, card) => x >= card.x && x <= card.x + CARD_WIDTH && y >= card.y && y <= card.y + CARD_HEIGHT; // Rendering functions const clearCanvas = () => { ctx.fillStyle = 'beige'; ctx.fillRect(0, 0, canvas.width, canvas.height); }; const drawCardBack = card => { ctx.fillRect(card.x, card.y, CARD_WIDTH, CARD_HEIGHT); drawRetroPattern(card); ctx.strokeStyle = CARD_BORDER_COLOR; ctx.strokeRect(card.x, card.y, CARD_WIDTH, CARD_HEIGHT); }; const drawRetroPattern = card => { const checkeredSize = 10; const deckColors = BASE_COLORS[card.card.deckId % BASE_COLORS.length]; for (let i = 0; i < CARD_WIDTH; i += checkeredSize) { for (let j = 0; j < CARD_HEIGHT; j += checkeredSize) { ctx.fillStyle = (Math.floor(i / checkeredSize) + Math.floor(j / checkeredSize)) % 2 === 0 ? deckColors.primary : deckColors.secondary; ctx.fillRect(card.x + i, card.y + j, checkeredSize, checkeredSize); } } }; const drawCardFront = card => { ctx.fillStyle = CARD_FACE_COLOR; ctx.fillRect(card.x, card.y, CARD_WIDTH, CARD_HEIGHT); ctx.fillStyle = CARD_BORDER_COLOR; ctx.font = FONT_SIZE; ctx.strokeRect(card.x, card.y, CARD_WIDTH, CARD_HEIGHT); // Draw value and suit with a retro font style drawCardValue(card.card.value, card.x + 12, card.y + 42, 'left'); drawCardSuit(card.card.suit, card.x + CARD_WIDTH / 2, card.y + CARD_HEIGHT / 2 + 20); }; const drawCardValue = (value, x, y, alignment) => { ctx.textAlign = alignment; ctx.fillStyle = CARD_BORDER_COLOR; ctx.fillText(value, x, y); }; const drawCardSuit = (suit, x, y) => { ctx.textAlign = 'center'; ctx.fillStyle = CARD_BORDER_COLOR; ctx.fillText(suit, x, y); }; const renderCard = card => { card.isFaceUp ? drawCardFront(card) : drawCardBack(card); }; const renderAllCards = cards => { clearCanvas(); cards.forEach(renderCard); // renderDeckStats(); // Add this line to show deck statistics }; // State management let gameState; const initializeGameState = () => ({ cards: [], draggingCard: null, deck: shuffle(createDecks(DECK_COUNT)), stackPosition: { x: 0, y: 0 } }); const initializeGame = () => { try { gameState = initializeGameState(); // Group cards by deck const cardsByDeck = gameState.deck.reduce((acc, cardData) => { const deckId = cardData.deckId; if (!acc[deckId]) acc[deckId] = []; acc[deckId].push(cardData); return acc; }, {}); // Calculate starting X position to center all piles const totalWidth = PILE_SPACING * DECK_COUNT; const startX = (canvas.width - totalWidth) / 2; // Create cards for each deck in its own pile gameState.cards = Object.entries(cardsByDeck).flatMap(([deckId, deckCards]) => { const pileX = startX + (parseInt(deckId) * PILE_SPACING); return deckCards.map((cardData, indexInDeck) => createCard( pileX, INITIAL_CARD_Y + (indexInDeck * PILE_OFFSET), cardData ) ); }); clearCanvas(); renderAllCards(gameState.cards); setupEventListeners(); } catch (error) { console.error('Failed to initialize game:', error); alert('Failed to initialize game. Please refresh the page.'); } }; const setupEventListeners = () => { canvas.addEventListener('mousedown', handleMouseDown); canvas.addEventListener('contextmenu', e => e.preventDefault()); document.addEventListener('keydown', e => { if (e.key === 'q') handleResetGame(); }); }; // Event handlers const handleMouseMove = e => { if (!gameState.draggingCard) return; const rect = canvas.getBoundingClientRect(); const newX = e.clientX - rect.left - dragOffset.x; const newY = e.clientY - rect.top - dragOffset.y; // Update the card's position immutably const updatedCard = moveCard(gameState.draggingCard, newX, newY); gameState.cards = gameState.cards.map(card => card === gameState.draggingCard ? updatedCard : card ); gameState.draggingCard = updatedCard; renderAllCards(gameState.cards); }; const handleMouseUp = e => { if (!gameState.draggingCard) { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Check if a card was clicked const clickedCard = gameState.cards.slice().reverse().find(card => isPointInCard(x, y, card)); if (clickedCard) { // Move the clicked card to the top of the stack gameState.cards = gameState.cards.filter(card => card !== clickedCard); gameState.cards.push(clickedCard); renderAllCards(gameState.cards); // Re-render all cards } } gameState.draggingCard = null; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; let dragOffset = { x: 0, y: 0 }; // To store the offset of the click position const findClickedCard = (x, y, cards) => cards.slice().reverse().find(card => isPointInCard(x, y, card)); const moveCardToTop = (targetCard, cards) => [ ...cards.filter(card => card !== targetCard), targetCard ]; const handleMouseDown = e => { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; if (e.button === 2) { e.preventDefault(); const clickedCard = findClickedCard(x, y, gameState.cards); if (clickedCard) { const updatedCard = toggleCardFace(clickedCard); gameState.cards = gameState.cards.map(card => card === clickedCard ? updatedCard : card ); renderAllCards(gameState.cards); } return; } const clickedCard = findClickedCard(x, y, gameState.cards); if (clickedCard) { gameState.draggingCard = clickedCard; dragOffset = { x: x - clickedCard.x, y: y - clickedCard.y }; gameState.cards = moveCardToTop(clickedCard, gameState.cards); document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); } }; // Add this function to handle the reset confirmation const handleResetGame = () => { if (confirm("Would you like to reset the cards?")) { resetCardsToOriginalPiles(); } }; const moveCard = (card, newX, newY) => ({ ...card, x: newX, y: newY }); const toggleCardFace = card => ({ ...card, isFaceUp: !card.isFaceUp }); // Add a function to get deck statistics const getDeckStats = () => { const stats = new Map(); gameState.cards.forEach(card => { const deckId = card.card.deckId; const current = stats.get(deckId) || { total: 0, faceUp: 0 }; stats.set(deckId, { total: current.total + 1, faceUp: current.faceUp + (card.isFaceUp ? 1 : 0) }); }); return stats; }; // Optional: Add a display for deck statistics const renderDeckStats = () => { const stats = getDeckStats(); ctx.font = '16px "pokemon-font", monospace'; // Calculate the same starting X position as the piles const totalWidth = PILE_SPACING * DECK_COUNT; const startX = (canvas.width - totalWidth) / 2; stats.forEach((stat, deckId) => { const colors = BASE_COLORS[deckId % BASE_COLORS.length]; const pileX = startX + (deckId * PILE_SPACING); ctx.fillStyle = colors.primary; ctx.textAlign = 'center'; ctx.fillText( `Deck ${deckId + 1}: ${stat.faceUp}/${stat.total}`, pileX + CARD_WIDTH / 2, INITIAL_CARD_Y - 10 ); }); }; // Optional: Add a function to reset cards to their original piles const resetCardsToOriginalPiles = () => { const totalWidth = PILE_SPACING * DECK_COUNT; const startX = (canvas.width - totalWidth) / 2; // Group cards by deck const cardsByDeck = gameState.cards.reduce((acc, card) => { const deckId = card.card.deckId; if (!acc[deckId]) acc[deckId] = []; acc[deckId].push(card); return acc; }, {}); // Reset position for each deck Object.entries(cardsByDeck).forEach(([deckId, deckCards]) => { const pileX = startX + (parseInt(deckId) * PILE_SPACING); deckCards.forEach((card, index) => { card.x = pileX; card.y = INITIAL_CARD_Y + (index * PILE_OFFSET); card.isFaceUp = false; }); }); renderAllCards(gameState.cards); }; // Start the game initializeGame(); // Clean up on window unload window.addEventListener('unload', () => { canvas.removeEventListener('mousedown', handleMouseDown); canvas.removeEventListener('contextmenu', e => e.preventDefault()); });