- {/* 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)'
}
}