/** * @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'; // 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 = () => SUITS.flatMap(suit => VALUES.map(value => ({ suit, value }))); // 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; // Size of each square in the checkered pattern for (let i = 0; i < CARD_WIDTH; i += checkeredSize) { for (let j = 0; j < CARD_HEIGHT; j += checkeredSize) { // Alternate colors for the checkered pattern ctx.fillStyle = (Math.floor(i / checkeredSize) + Math.floor(j / checkeredSize)) % 2 === 0 ? '#FF9900' : '#FFCC00'; 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); // Renders cards in the order they are in the array }; // State management let gameState; const initializeGameState = () => ({ cards: [], draggingCard: null, deck: shuffle(createDeck()), stackPosition: { x: 0, y: 0 } }); const initializeGame = () => { try { gameState = initializeGameState(); gameState.cards = gameState.deck.map((cardData, index) => createCard(INITIAL_CARD_X, INITIAL_CARD_Y + index * 5, 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?")) { initializeGame(); // Reset the game state } }; const moveCard = (card, newX, newY) => ({ ...card, x: newX, y: newY }); const toggleCardFace = card => ({ ...card, isFaceUp: !card.isFaceUp }); // Start the game initializeGame(); // Clean up on window unload window.addEventListener('unload', () => { canvas.removeEventListener('mousedown', handleMouseDown); canvas.removeEventListener('contextmenu', e => e.preventDefault()); });