diff react_games/src/CardMatchiing/main.tsx @ 37:fb9bcd3145cb

[ReactGames] Few games I made using react just to practice few things.
author MrJuneJune <me@mrjunejune.com>
date Mon, 01 Dec 2025 20:22:47 -0800
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/react_games/src/CardMatchiing/main.tsx	Mon Dec 01 20:22:47 2025 -0800
@@ -0,0 +1,220 @@
+/**
+ *
+ * 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>
+    </>
+  );
+};
+