view react_games/src/CardMatchiing/main.tsx @ 71:75de5903355c

Giagantic changes that update Dowa library to be more align with stb style array and hashmap. Updated Seobeo to be caching on server side instead of file level caching. Deleted bunch of things I don't really use.
author June Park <parkjune1995@gmail.com>
date Sun, 28 Dec 2025 20:34:22 -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>
    </>
  );
};