Memory Game: Grid Size & Theme Selection
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
- Overview
- The config file — single source of truth
- Grid sizes
- Themes
- How to add a new grid size
- How to add a new theme
- The MemorySettings component
- The MemoryCardFace component
- Integration with solo & multiplayer
- 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():
| Grid | Cards | Pairs | Card size (mobile) | Card size (desktop) |
|---|---|---|---|---|
| 4x3 | 12 | 6 | h-24 w-20 | sm:h-32 sm:w-28 |
| 4x4 | 16 | 8 | h-24 w-20 | sm:h-32 sm:w-28 |
| 4x5 | 20 | 10 | h-20 w-16 | sm:h-28 sm:w-24 |
| 4x6 | 24 | 12 | h-18 w-14 | sm: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} />withobject-containand 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
CardContentTypeunion ('image' | 'emoji') cleanly separates rendering logic. Emoji themes need zero asset files. - Shared components:
MemoryCardFaceandMemorySettingsare 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.