<div id="app"></div>
import { makeAutoObservable, observable } from "https://esm.sh/mobx";
import { observer } from "https://esm.sh/mobx-react-lite";
import ReactDom from "https://esm.sh/react-dom/client";
import React, { useEffect, useMemo, useState } from "https://esm.sh/react";
import { shuffle } from "https://esm.sh/lodash";

type Card = { id: number; value: number; isFound?: boolean };

class Board {
  cardOne?: Card = undefined;
  cardTwo?: Card = undefined;

  pairCount = 0;

  cards = observable.array<Card>();

  size = 6;

  constructor() {
    makeAutoObservable(this);

    this.reset();
  }

  get totalPairCount() {
    return this.size ** 2 / 2;
  }

  reset() {
    const cards: Card[] = [];

    for (let cardValue = 1; cardValue <= this.totalPairCount; cardValue++) {
      cards.push({ id: cards.length, value: cardValue });
      cards.push({ id: cards.length, value: cardValue });
    }

    this.cards = shuffle(cards);
    this.pairCount = 0;
  }

  updateSize(size: number) {
    this.size = size;
    this.reset();
  }

  flipCard(card: Card) {
    if (this.cardOne === undefined) {
      this.cardOne = card;
    } else {
      this.cardTwo = card;
    }

    const { cardOne, cardTwo } = this;

    if (!(cardOne && cardTwo)) return;

    setTimeout(() => {
      // check if there is a match
      if (cardOne.value === cardTwo.value) {
        console.log("found match!");

        cardOne.isFound = true;
        cardTwo.isFound = true;

        this.pairCount += 1;
      }

      this.resetSelectedCards();
    }, 3_000);
  }

  isFlipped(card: Card) {
    return board.cardOne === card || board.cardTwo === card;
  }

  resetSelectedCards() {
    this.cardOne = undefined;
    this.cardTwo = undefined;
  }

  get isGameFinished() {
    return this.pairCount === this.totalPairCount;
  }

  get isFlipDisabled() {
    return this.cardOne && this.cardTwo;
  }
}

const board = new Board();

const CardBlock = observer(({ card }: { card: Card }) => {
  const isFlipped = card.isFound || board.isFlipped(card);
  const isFlipDisabled = isFlipped || board.isFlipDisabled;

  return (
    <label
      className={
        "swap swap-flip rounded-sm overflow-hidden transition-opacity duration-300 ease-out font-semibold text-lg  border border-black/30" +
        (isFlipped ? " swap-active" : "") +
        (isFlipDisabled ? " pointer-events-none" : "") +
        (card.isFound ? " opacity-0" : "")
      }
      onClick={() => board.flipCard(card)}
    >
      {/* Card with value */}
      <div className="swap-on text-center size-12 flex justify-center place-items-center">
        {card.value}
      </div>

      {/* Empty Card */}
      <div className="swap-off bg-base-300 size-12" />
    </label>
  );
});

const levelLabelBySize = {
  5: "Easy 5x5",
  6: "Medium 6x6",
  8: "Hard 8x8"
};

const App = observer(() => {
  return (
    <div className="rounded-lg p-6 pt-2 bg-base-200 w-fit m-4">
      <h2 className="text-xl font-semibold text-center w-full">Memory Game</h2>

      <div className="join w-full justify-center py-3">
        {[5, 6, 8].map((level) => (
          <input
            className="join-item btn"
            type="radio"
            name="options"
            aria-label={levelLabelBySize[level]}
            checked={board.size === level}
            onClick={() => board.updateSize(level)}
          />
        ))}
      </div>

      <div className="min-w-96 min-h-96 size-fit flex justify-center place-items-center">
        {board.isGameFinished ? (
          <button
            className="btn btn-lg border-black! border-2!"
            onClick={() => board.reset()}
          >
            Play again
          </button>
        ) : (
          <div className={"grid gap-4" + (" grid-cols-" + board.size)}>
            {board.cards.map((card) => (
              <CardBlock card={card} key={card.id} />
            ))}
          </div>
        )}
      </div>
    </div>
  );
});

ReactDom.createRoot(document.getElementById("app")).render(<App />);
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.