Memory Game: Grid Size & Theme Selection

developmentreactgameconfigextensibility

Memory Game: Grid Size & Theme Selection

A tutorial-style walkthrough of two new features: configurable grid sizes (4x3 up to 4x6) and selectable card themes (Shapes, Animals, Emoji, Vehicles). Both features are designed for easy extensibility.


Table of Contents

  1. Overview
  2. The config file — single source of truth
  3. Grid sizes
  4. Themes
  5. How to add a new grid size
  6. How to add a new theme
  7. The MemorySettings component
  8. The MemoryCardFace component
  9. Integration with solo & multiplayer
  10. Key takeaways

1. Overview

Before this change, the Memory Game had a fixed 4x3 grid (6 pairs) using 6 hardcoded SVG shape images. Both the solo and multiplayer components duplicated the same CARD_IMAGES array.

The goal was to:

  • Let players choose a grid size from 4x3 (12 cards) to 4x6 (24 cards)
  • Let players choose a theme (Shapes, Animals, Emoji, Vehicles)
  • Make both features easy to extend — adding a new grid size or theme should be a one-liner

2. The config file — single source of truth

File: src/lib/memory-config.ts

All grid sizes, themes, and helper functions live in one file. Both the solo MemoryGame and multiplayer MultiplayerMemoryGame import from here instead of defining their own constants.

// The two core data structures
export const GRID_SIZES: GridSize[] = [...]
export const THEMES: Theme[] = [...]

// Helper functions
export function getThemeById(id: string): Theme
export function getGridSizeById(id: string): GridSize
export function getItemsForGrid(theme: Theme, pairs: number): ThemeItem[]
export function getAvailableGridSizes(theme: Theme): GridSize[]
export function getCardSizeClasses(gridSize: GridSize): string
export function getEmojiSizeClasses(gridSize: GridSize): string

Why one file? When both solo and multiplayer components share the same config, changes propagate automatically. Adding a theme in one place makes it available everywhere.


3. Grid sizes

Each grid size is described by a simple object:

export interface GridSize {
  id: string       // e.g. '4x3'
  rows: number     // number of rows
  cols: number     // always 4
  pairs: number    // rows * cols / 2
  labelKey: string // i18n key for the display label
}

The grid always uses 4 columns. Rows grow from 3 to 6. Card sizes shrink automatically for larger grids via getCardSizeClasses():

GridCardsPairsCard size (mobile)Card size (desktop)
4x3126h-24 w-20sm:h-32 sm:w-28
4x4168h-24 w-20sm:h-32 sm:w-28
4x52010h-20 w-16sm:h-28 sm:w-24
4x62412h-18 w-14sm:h-24 sm:w-20

4. Themes

Each theme has a list of items. An item can be an image (SVG file path) or an emoji (rendered as text):

export type CardContentType = 'image' | 'emoji'

export interface ThemeItem {
  id: string              // unique within the theme, used for matching
  type: CardContentType   // how to render
  content: string         // SVG path or emoji character
}

export interface Theme {
  id: string
  labelKey: string        // i18n key
  items: ThemeItem[]      // must have >= 12 items for 4x6
}

The Shapes theme uses 12 SVG images (star, heart, diamond, circle, triangle, hexagon, pentagon, cross, arrow, crescent, square, oval). The other three themes use emoji characters — no image files needed.

When a grid size is selected, getItemsForGrid() picks the first N items from the theme. The settings UI automatically disables grid sizes that exceed the theme's item count.


5. How to add a new grid size

Open src/lib/memory-config.ts and add one object to the GRID_SIZES array:

export const GRID_SIZES: GridSize[] = [
  { id: '4x3', rows: 3, cols: 4, pairs: 6, labelKey: 'memory.grid.4x3' },
  { id: '4x4', rows: 4, cols: 4, pairs: 8, labelKey: 'memory.grid.4x4' },
  { id: '4x5', rows: 5, cols: 4, pairs: 10, labelKey: 'memory.grid.4x5' },
  { id: '4x6', rows: 6, cols: 4, pairs: 12, labelKey: 'memory.grid.4x6' },
  // Add your new size here:
  { id: '5x6', rows: 6, cols: 5, pairs: 15, labelKey: 'memory.grid.5x6' },
]

Then add the translation key to en.ts and de.ts:

'memory.grid.5x6': '5 x 6 (15 pairs)',

You may also want to add a card-size rule in getCardSizeClasses() if the new grid needs smaller cards.


6. How to add a new theme

Add a new entry to the THEMES array in src/lib/memory-config.ts:

{
  id: 'food',
  labelKey: 'memory.theme.food',
  items: [
    { id: 'pizza', type: 'emoji', content: '\uD83C\uDF55' },
    { id: 'burger', type: 'emoji', content: '\uD83C\uDF54' },
    // ... at least 12 items for full grid support
  ],
}

Then add the translation:

// en.ts
'memory.theme.food': 'Food',
// de.ts
'memory.theme.food': 'Essen',

That's it — the settings dropdown picks up the new theme automatically.

Image-based themes work the same way, just use type: 'image' and put the SVG/PNG path in content. Place image files in public/images/memory-game/.


7. The MemorySettings component

File: src/components/games/MemorySettings.tsx

A small, reusable component with two <select> dropdowns:

interface MemorySettingsProps {
  gridSizeId: string
  themeId: string
  onGridSizeChange: (id: string) => void
  onThemeChange: (id: string) => void
  disabled?: boolean
}

Smart grid filtering: When the user switches themes, the component checks whether the current grid size is still valid for the new theme. If not, it automatically selects the largest available grid size.

Disabled state: In multiplayer, settings are only editable in the lobby before the game starts. Once playing, disabled={true} grays out the dropdowns.


8. The MemoryCardFace component

File: src/components/games/MemoryCardFace.tsx

Renders the front face of a card. Handles both content types:

  • Image: renders <img src={content} /> with object-contain and padding
  • Emoji: renders <span>{content}</span> with responsive font sizes

This component is shared between solo and multiplayer to avoid duplicating the card rendering logic.


9. Integration with solo & multiplayer

Solo game (MemoryGame.tsx)

The component holds gridSize and theme in state. A useEffect depending on [gridSize, theme] re-creates and shuffles the deck whenever either changes. The createDeck() function uses getItemsForGrid() to pick the right number of items from the current theme.

Multiplayer game (MultiplayerMemoryGame.tsx)

Settings are chosen in the lobby phase and baked into the MemoryGameState that gets stored in Supabase:

export interface MemoryGameState {
  gridSizeId: string    // persisted so both players see the same grid
  themeId: string       // persisted so both players see the same theme
  cards: { id; itemId; type; content }[]
  // ... scores, indices, etc.
}

The joining player reads gridSizeId and themeId from the game state, so they see exactly the same configuration as the room creator.


10. Key takeaways

  • Config-driven design: A single config file (memory-config.ts) powers both features. No component logic changes are needed to add new themes or grid sizes.
  • Two content types: The CardContentType union ('image' | 'emoji') cleanly separates rendering logic. Emoji themes need zero asset files.
  • Shared components: MemoryCardFace and MemorySettings are used by both solo and multiplayer, keeping the code DRY.
  • Graceful constraints: The UI disables grid sizes that exceed the current theme's item count, preventing broken states.