view react_games/src/CardMatchiing/main.tsx @ 91:19cccf6e866a

Added Epi photo reels.
author June Park <parkjune1995@gmail.com>
date Thu, 01 Jan 2026 16:34:51 -0800
parents fb9bcd3145cb
children
line wrap: on
line source

/**
 *
 * You’re tasked with building a simple memory card-matching game in React.

    Each card has a hidden value, and the player flips two cards at a time to try to find a match.

    If the cards match, they stay face-up. If not, they flip back after a short delay.

    The game ends when all pairs are matched.

   // Basic components
   // Board
   //   Cards (values 1 to 10) 
   //   There will be 20 cards, 4 x 5.
   //
   // GameState: playable or not
   // logic check if the value sames
   //
   // listOfCurrentlyTurnOpend: len(2) number[]
*/
import React, {
  createContext,
  useContext,
  useEffect,
  useReducer,
} from "react";

/* ──────────────────── Types ──────────────────── */
interface CardValue {
  value: number;
  isFacing: boolean;
  isSolved: boolean;
}

interface CardProp {
  row: number;
  col: number;
  card: CardValue;
}

interface RowProp {
  rowPos: number;
  row: CardValue[];
}

type Action =
  | { type: "move"; row: number; col: number }
  | { type: "flipBack" }
  | { type: "reset" };

interface Position {
  row: number;
  col: number;
}

interface GameState {
  board: CardValue[][];
  open: Position[]; // cards currently face-up but not yet decided (max 2)
  busy: boolean;    // UI locked while we wait to flip cards back
}

/* ──────────────────── Helpers ──────────────────── */
const shuffle = <T,>(arr: T[]): T[] =>
  [...arr].sort(() => Math.random() - 0.5);

const makeBoard = (): CardValue[][] => {
  // two of each from 1-10, then shuffle and slice into 4×5
  const values = shuffle(
    Array.from({ length: 10 }, (_, i) => i + 1).flatMap((v) => [v, v])
  );

  return Array.from({ length: 4 }, (_, r) =>
    Array.from({ length: 5 }, (_, c) => ({
      value: values[r * 5 + c],
      isFacing: false,
      isSolved: false,
    }))
  );
};

/* ──────────────────── Context ──────────────────── */
const BoardContext = createContext<React.Dispatch<Action> | null>(null);
const useBoardDispatch = () => {
  const ctx = useContext(BoardContext);
  if (!ctx) throw new Error("BoardContext missing");
  return ctx;
};

/* ──────────────────── Reducer ──────────────────── */
const initial = (): GameState => ({ board: makeBoard(), open: [], busy: false });

function reducer(state: GameState, action: Action): GameState {
  switch (action.type) {
    case "reset":
      return initial();

    case "flipBack": {
      // hide the two open cards
      const [a, b] = state.open;
      const nextBoard = state.board.map((row, r) =>
        row.map((card, c) =>
          (r === a.row && c === a.col) || (r === b.row && c === b.col)
            ? { ...card, isFacing: false }
            : card
        )
      );
      return { board: nextBoard, open: [], busy: false };
    }

    case "move": {
      if (state.busy) return state; // ignore clicks while waiting

      const { row, col } = action;
      const target = state.board[row][col];
      if (target.isFacing || target.isSolved) return state;

      // flip this one up
      const nextBoard = state.board.map((r, ri) =>
        r.map((c, ci) =>
          ri === row && ci === col ? { ...c, isFacing: true } : c
        )
      );
      const open = [...state.open, { row, col }];

      if (open.length < 2) return { ...state, board: nextBoard, open };

      // now we have two cards – decide match / mismatch
      const [a, b] = open;
      const first  = nextBoard[a.row][a.col];
      const second = nextBoard[b.row][b.col];

      if (first.value === second.value) {
        // match → mark solved, leave face-up
        const solvedBoard = nextBoard.map((r, ri) =>
          r.map((c, ci) =>
            (ri === a.row && ci === a.col) || (ri === b.row && ci === b.col)
              ? { ...c, isSolved: true }
              : c
          )
        );
        return { board: solvedBoard, open: [], busy: false };
      }

      // mismatch → leave them up temporarily, then flip back via effect
      return { board: nextBoard, open, busy: true };
    }

    default:
      return state;
  }
}

/* ──────────────────── UI Components ──────────────────── */
const Card = ({ row, col, card }: CardProp) => {
  const dispatch = useBoardDispatch();
  const style: React.CSSProperties = {
    width: 60,
    height: 80,
    margin: 4,
    fontSize: 24,
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    background: card.isSolved
      ? "#8bc34a"
      : card.isFacing
      ? "#fff"
      : "#90caf9",
    cursor: card.isSolved ? "default" : "pointer",
    borderRadius: 6,
    userSelect: "none",
  };
  return (
    <div style={style} onClick={() => dispatch({ type: "move", row, col })}>
      {card.isFacing || card.isSolved ? card.value : "🂠"}
    </div>
  );
};

const Row = ({ row, rowPos }: RowProp) => (
  <div style={{ display: "flex" }}>
    {row.map((card, idx) => (
      <Card key={idx} row={rowPos} col={idx} card={card} />
    ))}
  </div>
);

/* ──────────────────── Root component ──────────────────── */
export const MemoryGame = () => {
  const [state, dispatch] = useReducer(reducer, undefined, initial);

  /* Handle “flipBack” after 1 s for a mismatch */
  useEffect(() => {
    if (state.busy) {
      const t = setTimeout(() => dispatch({ type: "flipBack" }), 1000);
      return () => clearTimeout(t);
    }
  }, [state.busy]);

  /* Quick win detection */
  const solved = state.board.every((r) => r.every((c) => c.isSolved));

  return (
    <>
      <h3 style={{ textAlign: "center" }}>
        {solved ? "🎉 You won!" : "Memory Game"}
      </h3>
      <BoardContext.Provider value={dispatch}>
        {state.board.map((row, idx) => (
          <Row key={idx} rowPos={idx} row={row} />
        ))}
      </BoardContext.Provider>

      <div style={{ textAlign: "center", marginTop: 12 }}>
        <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
      </div>
    </>
  );
};