Skip to main content

Overview

The Memory Game demonstrates:
  • Card flip animations with CSS
  • Array shuffling algorithms
  • Async state updates with setTimeout
  • Preventing invalid interactions
  • Win condition detection

Live Demo

View the complete source code on GitHub

What You’ll Learn

Animations

CSS transforms and transitions

Async Logic

Handling delayed state updates

Input Prevention

Blocking clicks during processing

Game Patterns

Common game development patterns

Complete Code

Game Component

game.js
import { Component, h } from "@glyphui/runtime";

const SYMBOLS = ["🚀", "🌟", "🌈", "🎮", "🔮", "🎨", "🍕", "🌮"];

function createDeck() {
  const deck = [...SYMBOLS, ...SYMBOLS];
  // Simple shuffle algorithm
  for (let i = deck.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [deck[i], deck[j]] = [deck[j], deck[i]];
  }
  return deck;
}

class MemoryGame extends Component {
  constructor() {
    super(
      {},
      {
        initialState: {
          deck: createDeck(),
          flippedIndices: [],
          matchedSymbols: [],
          moves: 0,
          isChecking: false, // Prevent clicks during match check
        },
      }
    );
    this.handleCardClick = this.handleCardClick.bind(this);
  }

  handleCardClick(index) {
    const { flippedIndices, deck, matchedSymbols, isChecking } = this.state;
    const symbol = deck[index];

    if (
      isChecking ||
      flippedIndices.includes(index) ||
      matchedSymbols.includes(symbol)
    ) {
      return; // Ignore invalid clicks
    }

    const newFlippedIndices = [...flippedIndices, index];
    this.setState({ flippedIndices: newFlippedIndices });

    if (newFlippedIndices.length === 2) {
      this.setState({ isChecking: true });
      setTimeout(() => this.checkForMatch(), 1000);
    }
  }

  checkForMatch() {
    const { flippedIndices, deck, matchedSymbols, moves } = this.state;
    const [index1, index2] = flippedIndices;
    const symbol1 = deck[index1];
    const symbol2 = deck[index2];

    const newMatchedSymbols = [...matchedSymbols];
    if (symbol1 === symbol2) {
      newMatchedSymbols.push(symbol1);
    }

    this.setState({
      flippedIndices: [],
      matchedSymbols: newMatchedSymbols,
      moves: moves + 1,
      isChecking: false,
    });
  }

  render(props, state) {
    const { deck, flippedIndices, matchedSymbols, moves } = state;
    const isGameWon = matchedSymbols.length === SYMBOLS.length;

    return h("div", { class: "game-container" }, [
      h("div", { class: "info" }, [`Moves: ${moves}`]),
      h(
        "div",
        { class: "board" },
        deck.map((symbol, index) => {
          const isFlipped = flippedIndices.includes(index);
          const isMatched = matchedSymbols.includes(symbol);

          let cardClass = "card";
          if (isFlipped || isMatched) {
            cardClass += " flipped";
          }
          if (isMatched) {
            cardClass += " matched";
          }

          return h(
            "div",
            {
              class: cardClass,
              key: index,
              on: {
                click: () => this.handleCardClick(index),
              },
            },
            [h("span", { class: "content" }, [symbol])]
          );
        })
      ),
      isGameWon &&
        h("div", { class: "win-message" }, [
          "Congratulations, You Win!",
        ]),
    ]);
  }
}

const app = new MemoryGame();
app.mount(document.getElementById("app"));

Styling

styles.css
.board {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
  width: 100%;
  max-width: 440px;
  margin: 0 auto;
}

.card {
  width: 100px;
  height: 140px;
  background-color: #7b8cde;
  border-radius: 8px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.5s, background-color 0.3s;
  transform-style: preserve-3d;
}

.card .content {
  display: none;
  font-size: 40px;
  transform: rotateY(180deg);
}

.card.flipped {
  background-color: #ffffff;
  transform: rotateY(180deg);
}

.card.flipped .content {
  display: block;
}

.card.matched {
  background-color: #a8d5ba;
  transform: rotateY(180deg);
  cursor: default;
}

.card.matched .content {
  display: block;
}

Key Concepts

1. Array Shuffling

Fisher-Yates shuffle algorithm:
function createDeck() {
  const deck = [...SYMBOLS, ...SYMBOLS]; // Duplicate symbols
  
  // Shuffle
  for (let i = deck.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [deck[i], deck[j]] = [deck[j], deck[i]]; // Swap
  }
  
  return deck;
}
Fisher-Yates produces an unbiased shuffle, ensuring every permutation is equally likely.

2. Delayed State Updates

Use setTimeout to delay checking for matches:
if (newFlippedIndices.length === 2) {
  this.setState({ isChecking: true }); // Block new clicks
  setTimeout(() => this.checkForMatch(), 1000); // Wait 1 second
}
This gives players time to see the second card before it flips back.

3. Input Prevention

Block clicks during certain states:
handleCardClick(index) {
  const { isChecking, flippedIndices, matchedSymbols, deck } = this.state;
  const symbol = deck[index];
  
  // Ignore clicks if:
  if (
    isChecking ||                        // Checking for match
    flippedIndices.includes(index) ||    // Already flipped
    matchedSymbols.includes(symbol)      // Already matched
  ) {
    return;
  }
  
  // Process click...
}

4. CSS Transform Animations

Use transform: rotateY() for 3D flip effect:
.card {
  transition: transform 0.5s;
  transform-style: preserve-3d;
}

.card.flipped {
  transform: rotateY(180deg);
}

.card .content {
  transform: rotateY(180deg); /* Pre-flip content */
}

Game Flow

1

Initialize

Create deck with pairs of symbols and shuffle
2

First Click

Player clicks card, it flips to reveal symbol
3

Second Click

Player clicks another card, both stay flipped
4

Match Check

After 1 second delay, check if symbols match
5

Update State

If match: keep flipped. If no match: flip back
6

Win Detection

When all pairs matched, show win message

Features Demonstrated

  • Multiple state properties for game status
  • Temporary state (flippedIndices)
  • Permanent state (matchedSymbols)
  • Blocking state (isChecking)
  • Click handlers with validation
  • Preventing invalid actions
  • Delayed feedback
  • Visual state indicators
  • Fisher-Yates shuffle
  • Match detection
  • Win condition checking
  • Move counting
  • 3D transforms
  • Transitions
  • Grid layout
  • Dynamic classes

Running the Example

1

Clone the repository

git clone https://github.com/x0bd/glyphui.git
cd glyphui/examples/memory-game
2

Open in browser

Open index.html in your browser:
npx serve .
3

Play the game

  • Click cards to reveal symbols
  • Try to match pairs
  • Complete all pairs to win
  • Track your moves

Enhancements to Try

Difficulty Levels

Add easy/medium/hard with different grid sizes

Timer

Track time to complete

High Scores

Save best scores to localStorage

Sound Effects

Add audio feedback

Best Practices

Always bind event handlers in the constructor to maintain correct this context:
constructor() {
  super(...);
  this.handleCardClick = this.handleCardClick.bind(this);
}
Clean up any pending timeouts when component unmounts to prevent memory leaks:
beforeUnmount() {
  if (this.matchTimeout) {
    clearTimeout(this.matchTimeout);
  }
}

Next Steps

Counter

Start with the basics

Animations

Learn animation techniques

Component Patterns

Common component patterns

Performance

Optimize your games

Build docs developers (and LLMs) love