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