view react_games/src/CardMatchiing/main.tsx @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
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>
    </>
  );
};