Mercurial
view react_games/src/CardMatchiing/main.tsx @ 146:8e56f800b7e4
Fixed small issues.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Fri, 09 Jan 2026 13:45:29 -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> </> ); };