Mercurial
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> + </> + ); +}; +