// 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 with adjusted padding
ctx.textAlign = 'right'; // Ensure right alignment for the bottom value
ctx.fillText(card.card.value, card.x + CARD_WIDTH - 20, card.y + CARD_HEIGHT - 10); // Reduced padding
};
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());
});