// 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 CARD_BACK_COLOR = '#0066cc'; const PATTERN_SIZE = 10; // 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 => [...array] .reduce((acc, _, i) => { const j = Math.floor(Math.random() * (i + 1)); [acc[i], acc[j]] = [acc[j], acc[i]]; return acc; }, [...array]); const createDeck = () => SUITS.flatMap(suit => VALUES.map(value => ({ suit, value }))); const createCard = (x, y, cardData) => ({ x: x + 20, y: y + 20, card: cardData, isFaceUp: false // Cards start face down }); // 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); }; // Update renderCardBack to use regular rectangles const renderCardBack = card => { ctx.fillStyle = CARD_BACK_COLOR; ctx.fillRect(card.x, card.y, CARD_WIDTH, CARD_HEIGHT); // Revert to regular rectangle // Draw checkered pattern ctx.strokeStyle = '#003366'; for (let i = 0; i < CARD_WIDTH; i += PATTERN_SIZE) { for (let j = 0; j < CARD_HEIGHT; j += PATTERN_SIZE) { if ((i + j) % (PATTERN_SIZE * 2) === 0) { ctx.fillStyle = '#0055aa'; ctx.fillRect(card.x + i, card.y + j, PATTERN_SIZE, PATTERN_SIZE); } } } // Draw border ctx.strokeStyle = 'black'; ctx.strokeRect(card.x, card.y, CARD_WIDTH, CARD_HEIGHT); // Revert to regular rectangle }; // Update renderCardFront to use regular rectangles const renderCardFront = card => { ctx.fillStyle = 'white'; ctx.fillRect(card.x, card.y, CARD_WIDTH, CARD_HEIGHT); // Regular rectangle ctx.fillStyle = 'black'; // Set a larger font size ctx.font = '30px Arial'; // Increased font size for better visibility ctx.strokeRect(card.x, card.y, CARD_WIDTH, CARD_HEIGHT); // Draw value in the top left corner ctx.textAlign = 'left'; // Ensure left alignment for the value ctx.fillText(card.card.value, card.x + 10, card.y + 40); // Adjusted y position for larger font // Draw suit in the center of the card ctx.textAlign = 'center'; // Center the text for the suit ctx.fillText(card.card.suit, card.x + CARD_WIDTH / 2, card.y + CARD_HEIGHT / 2 + 10); // Centered position // Draw value in the bottom right corner ctx.textAlign = 'right'; // Ensure right alignment for the bottom value ctx.fillText(card.card.value, card.x + CARD_WIDTH - 30, card.y + CARD_HEIGHT - 10); }; const renderCard = card => { // Always render the card based on its current state if (card.isFaceUp) { renderCardFront(card); } else { renderCardBack(card); } }; const renderAllCards = cards => { clearCanvas(); cards.forEach(renderCard); }; // State management const gameState = { cards: [], draggingCard: null, deck: shuffle(createDeck()), stackPosition: { x: 0, y: 0 } }; // Event handlers const handleMouseMove = e => { if (!gameState.draggingCard) return; const rect = canvas.getBoundingClientRect(); gameState.draggingCard.x = e.clientX - rect.left; gameState.draggingCard.y = e.clientY - rect.top; renderAllCards(gameState.cards); }; const handleMouseUp = () => { gameState.draggingCard = null; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; // Define handleMouseDown before using it const handleMouseDown = e => { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Handle right click for card flipping if (e.button === 2) { // Right click e.preventDefault(); const clickedCard = gameState.cards.find(card => isPointInCard(x, y, card) && (card.x !== gameState.stackPosition.x || card.y !== gameState.stackPosition.y) ); if (clickedCard) { clickedCard.isFaceUp = !clickedCard.isFaceUp; // Toggle card face renderAllCards(gameState.cards); // Re-render all cards } return; } // Find the top-most clicked card const clickedCard = gameState.cards.slice().reverse().find(card => isPointInCard(x, y, card)); if (clickedCard) { // If it's a card in the stack, handle drawing it if (clickedCard.x === gameState.stackPosition.x && clickedCard.y === gameState.stackPosition.y) { clickedCard.isFaceUp = true; } // Allow dragging regardless of where the card is gameState.draggingCard = clickedCard; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); } }; // Initialize game const initializeGame = () => { // Create a full deck of cards gameState.deck = shuffle(createDeck()); gameState.cards = gameState.deck.map(cardData => createCard(0, 0, cardData)); // Draw cards in a pile with their reverse side showing gameState.cards.forEach((card, index) => { card.x = 20; // Pile at 20 pixels from the left card.y = 20 + index * 5; // Stack cards with a slight offset for visibility card.isFaceUp = false; // Ensure cards start face down }); clearCanvas(); renderAllCards(gameState.cards); // Add event listeners canvas.addEventListener('mousedown', handleMouseDown); canvas.addEventListener('contextmenu', e => e.preventDefault()); }; // Start the game initializeGame(); // Clean up on window unload window.addEventListener('unload', () => { canvas.removeEventListener('mousedown', handleMouseDown); canvas.removeEventListener('contextmenu', e => e.preventDefault()); });