diff --git a/apps/web/src/app/games/matching/components/GamePhase.tsx b/apps/web/src/app/games/matching/components/GamePhase.tsx index 1895a05e..41e4a24a 100644 --- a/apps/web/src/app/games/matching/components/GamePhase.tsx +++ b/apps/web/src/app/games/matching/components/GamePhase.tsx @@ -16,32 +16,34 @@ export function GamePhase() { return (
- {/* Game Header */} + {/* Game Header - Compact on mobile */}
- {/* Game Type & Difficulty Info */} + {/* Game Type & Difficulty Info - Hidden on mobile */}
- {/* Current Player Indicator (Multiplayer Mode) */} + {/* Current Player Indicator (Multiplayer Mode) - Compact on mobile */} {state.gameMode === 'multiplayer' && currentPlayerData && (
- + {currentPlayerData.emoji} - {currentPlayerData.name}'s Turn - + {currentPlayerData.name}'s Turn + Turn + 🎯
@@ -184,22 +187,32 @@ export function GamePhase() {
{/* Memory Grid - The main game area */} - +
+ +
- {/* Helpful Instructions */} + {/* Helpful Instructions - Hidden on mobile */}

{state.gameType === 'abacus-numeral' ? 'Match abacus representations with their numerical values! Look for patterns and remember card positions.' @@ -209,9 +222,9 @@ export function GamePhase() { {state.gameMode === 'multiplayer' && (

Take turns finding matches. The player with the most pairs wins!

diff --git a/apps/web/src/app/games/matching/components/MemoryGrid.tsx b/apps/web/src/app/games/matching/components/MemoryGrid.tsx index dd7158c6..1c9ed0de 100644 --- a/apps/web/src/app/games/matching/components/MemoryGrid.tsx +++ b/apps/web/src/app/games/matching/components/MemoryGrid.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useMemoryPairs } from '../context/MemoryPairsContext' import { useUserProfile } from '../../../../contexts/UserProfileContext' import { GameCard } from './GameCard' @@ -8,6 +8,76 @@ import { EmojiPicker } from './EmojiPicker' import { getGridConfiguration } from '../utils/cardGeneration' import { css } from '../../../../../styled-system/css' +// Custom hook to calculate proper grid dimensions for consistent r×c layout +function useGridDimensions(gridConfig: any, totalCards: number) { + const [gridDimensions, setGridDimensions] = useState(() => { + // Calculate optimal rows and columns based on total cards and viewport + if (typeof window !== 'undefined') { + const aspectRatio = window.innerWidth / window.innerHeight + return calculateOptimalGrid(totalCards, aspectRatio, gridConfig) + } + return { columns: gridConfig.mobileColumns || 3, rows: Math.ceil(totalCards / (gridConfig.mobileColumns || 3)) } + }) + + useEffect(() => { + function calculateOptimalGrid(cards: number, aspectRatio: number, config: any) { + // For consistent grid layout, we need to ensure r×c = totalCards + // Choose columns based on viewport, then calculate exact rows needed + + let targetColumns + const width = window.innerWidth + + // Choose column count based on viewport + if (aspectRatio >= 1.6 && width >= 1200) { + // Ultra-wide: prefer wider grids + targetColumns = config.landscapeColumns || config.desktopColumns || 6 + } else if (aspectRatio >= 1.33 && width >= 768) { + // Desktop/landscape: use desktop columns + targetColumns = config.desktopColumns || config.landscapeColumns || 6 + } else if (aspectRatio >= 1.0 && width >= 600) { + // Tablet: use tablet columns + targetColumns = config.tabletColumns || config.desktopColumns || 4 + } else { + // Mobile: use mobile columns + targetColumns = config.mobileColumns || 3 + } + + // Calculate exact rows needed for this column count + const rows = Math.ceil(cards / targetColumns) + + // If we have leftover cards that would create an uneven bottom row, + // try to redistribute for a more balanced grid + const leftoverCards = cards % targetColumns + if (leftoverCards > 0 && leftoverCards < targetColumns / 2 && targetColumns > 3) { + // Try one less column for a more balanced grid + const altColumns = targetColumns - 1 + const altRows = Math.ceil(cards / altColumns) + const altLeftover = cards % altColumns + + // Use alternative if it creates a more balanced grid + if (altLeftover === 0 || altLeftover > leftoverCards) { + return { columns: altColumns, rows: altRows } + } + } + + return { columns: targetColumns, rows } + } + + const updateGrid = () => { + if (typeof window === 'undefined') return + + const aspectRatio = window.innerWidth / window.innerHeight + setGridDimensions(calculateOptimalGrid(totalCards, aspectRatio, gridConfig)) + } + + updateGrid() + window.addEventListener('resize', updateGrid) + return () => window.removeEventListener('resize', updateGrid) + }, [gridConfig, totalCards]) + + return gridDimensions +} + export function MemoryGrid() { const { state, flipCard } = useMemoryPairs() const { profile, updatePlayerEmoji } = useUserProfile() @@ -18,6 +88,8 @@ export function MemoryGrid() { } const gridConfig = getGridConfiguration(state.difficulty) + const gridDimensions = useGridDimensions(gridConfig, state.gameCards.length) + const handleCardClick = (cardId: string) => { flipCard(cardId) @@ -204,23 +276,18 @@ export function MemoryGrid() { )}
- {/* Cards Grid */} + {/* Cards Grid - Consistent r×c Layout */}
{state.gameCards.map(card => { @@ -259,29 +326,15 @@ export function MemoryGrid() { key={card.id} className={css({ aspectRatio: '3/4', - // Responsive card sizing - '@media (min-width: 1024px)': { - width: gridConfig.cardSize.width, - height: gridConfig.cardSize.height - }, - '@media (max-width: 1023px) and (min-width: 768px)': { - width: `calc(${gridConfig.cardSize.width} * 0.8)`, - height: `calc(${gridConfig.cardSize.height} * 0.8)` - }, - '@media (max-width: 767px)': { - width: `calc(${gridConfig.cardSize.width} * 0.6)`, - height: `calc(${gridConfig.cardSize.height} * 0.6)` - }, + // Fully responsive card sizing - no fixed pixel sizes + width: '100%', + minWidth: '100px', + maxWidth: '200px', // Dimming effect for invalid cards opacity: isDimmed ? 0.3 : 1, transition: 'opacity 0.3s ease', filter: isDimmed ? 'grayscale(0.7)' : 'none' - })} - style={{ - width: gridConfig.cardSize.width, - height: gridConfig.cardSize.height - }} - > + })}>

- Memory Pairs Challenge + Memory Pairs

-

- Match pairs of abacus representations with their numerical values, or find complement pairs that add up to 5 or 10! -

{state.gamePhase === 'setup' && } {state.gamePhase === 'playing' && } diff --git a/apps/web/src/app/games/matching/components/SetupPhase.tsx b/apps/web/src/app/games/matching/components/SetupPhase.tsx index 65a23004..14e76f31 100644 --- a/apps/web/src/app/games/matching/components/SetupPhase.tsx +++ b/apps/web/src/app/games/matching/components/SetupPhase.tsx @@ -149,16 +149,16 @@ export function SetupPhase() {

Configure your memory challenge. Choose your preferred mode, game type, and difficulty level.

@@ -166,29 +166,29 @@ export function SetupPhase() { {/* Current Player Setup */}

🎮 Current Setup

{activePlayerCount} player{activePlayerCount !== 1 ? 's' : ''} selected

-

+

{activePlayerCount === 1 ? 'Solo challenge mode - focus & memory' : `${activePlayerCount}-player battle mode - compete for the most pairs` @@ -451,25 +451,26 @@ export function SetupPhase() {

- {/* Game Preview */} + {/* Game Preview - Hidden on mobile */}

Game Preview

Mode: {activePlayerCount === 1 ? 'Single Player' : `${activePlayerCount} Players`}

Type: {state.gameType === 'abacus-numeral' ? 'Abacus-Numeral Matching' : 'Complement Pairs'}

diff --git a/apps/web/src/app/games/matching/utils/cardGeneration.ts b/apps/web/src/app/games/matching/utils/cardGeneration.ts index 3bd2beb7..15d64472 100644 --- a/apps/web/src/app/games/matching/utils/cardGeneration.ts +++ b/apps/web/src/app/games/matching/utils/cardGeneration.ts @@ -132,42 +132,53 @@ export function generateGameCards(gameType: GameType, difficulty: Difficulty): G } } -// Utility function to get difficulty-based grid configuration +// Utility function to get responsive grid configuration based on difficulty and screen size export function getGridConfiguration(difficulty: Difficulty) { const configs: Record = { 6: { totalCards: 12, - columns: 4, - rows: 3, + mobileColumns: 3, // 3x4 grid in portrait + tabletColumns: 4, // 4x3 grid on tablet + desktopColumns: 4, // 4x3 grid on desktop + landscapeColumns: 6, // 6x2 grid in landscape cardSize: { width: '140px', height: '180px' }, - gridTemplate: 'repeat(4, 1fr)' + gridTemplate: 'repeat(3, 1fr)' }, 8: { totalCards: 16, - columns: 4, - rows: 4, + mobileColumns: 3, // 3x6 grid in portrait (some spillover) + tabletColumns: 4, // 4x4 grid on tablet + desktopColumns: 4, // 4x4 grid on desktop + landscapeColumns: 6, // 6x3 grid in landscape (some spillover) cardSize: { width: '120px', height: '160px' }, - gridTemplate: 'repeat(4, 1fr)' + gridTemplate: 'repeat(3, 1fr)' }, 12: { totalCards: 24, - columns: 6, - rows: 4, + mobileColumns: 3, // 3x8 grid in portrait + tabletColumns: 4, // 4x6 grid on tablet + desktopColumns: 6, // 6x4 grid on desktop + landscapeColumns: 6, // 6x4 grid in landscape (changed from 8x3) cardSize: { width: '100px', height: '140px' }, - gridTemplate: 'repeat(6, 1fr)' + gridTemplate: 'repeat(3, 1fr)' }, 15: { totalCards: 30, - columns: 6, - rows: 5, + mobileColumns: 3, // 3x10 grid in portrait + tabletColumns: 5, // 5x6 grid on tablet + desktopColumns: 6, // 6x5 grid on desktop + landscapeColumns: 10, // 10x3 grid in landscape cardSize: { width: '90px', height: '120px' }, - gridTemplate: 'repeat(6, 1fr)' + gridTemplate: 'repeat(3, 1fr)' } }