Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 36:84672efec192 | 37:fb9bcd3145cb |
|---|---|
| 1 /** | |
| 2 * | |
| 3 * You’re tasked with building a simple memory card-matching game in React. | |
| 4 | |
| 5 Each card has a hidden value, and the player flips two cards at a time to try to find a match. | |
| 6 | |
| 7 If the cards match, they stay face-up. If not, they flip back after a short delay. | |
| 8 | |
| 9 The game ends when all pairs are matched. | |
| 10 | |
| 11 // Basic components | |
| 12 // Board | |
| 13 // Cards (values 1 to 10) | |
| 14 // There will be 20 cards, 4 x 5. | |
| 15 // | |
| 16 // GameState: playable or not | |
| 17 // logic check if the value sames | |
| 18 // | |
| 19 // listOfCurrentlyTurnOpend: len(2) number[] | |
| 20 */ | |
| 21 import React, { | |
| 22 createContext, | |
| 23 useContext, | |
| 24 useEffect, | |
| 25 useReducer, | |
| 26 } from "react"; | |
| 27 | |
| 28 /* ──────────────────── Types ──────────────────── */ | |
| 29 interface CardValue { | |
| 30 value: number; | |
| 31 isFacing: boolean; | |
| 32 isSolved: boolean; | |
| 33 } | |
| 34 | |
| 35 interface CardProp { | |
| 36 row: number; | |
| 37 col: number; | |
| 38 card: CardValue; | |
| 39 } | |
| 40 | |
| 41 interface RowProp { | |
| 42 rowPos: number; | |
| 43 row: CardValue[]; | |
| 44 } | |
| 45 | |
| 46 type Action = | |
| 47 | { type: "move"; row: number; col: number } | |
| 48 | { type: "flipBack" } | |
| 49 | { type: "reset" }; | |
| 50 | |
| 51 interface Position { | |
| 52 row: number; | |
| 53 col: number; | |
| 54 } | |
| 55 | |
| 56 interface GameState { | |
| 57 board: CardValue[][]; | |
| 58 open: Position[]; // cards currently face-up but not yet decided (max 2) | |
| 59 busy: boolean; // UI locked while we wait to flip cards back | |
| 60 } | |
| 61 | |
| 62 /* ──────────────────── Helpers ──────────────────── */ | |
| 63 const shuffle = <T,>(arr: T[]): T[] => | |
| 64 [...arr].sort(() => Math.random() - 0.5); | |
| 65 | |
| 66 const makeBoard = (): CardValue[][] => { | |
| 67 // two of each from 1-10, then shuffle and slice into 4×5 | |
| 68 const values = shuffle( | |
| 69 Array.from({ length: 10 }, (_, i) => i + 1).flatMap((v) => [v, v]) | |
| 70 ); | |
| 71 | |
| 72 return Array.from({ length: 4 }, (_, r) => | |
| 73 Array.from({ length: 5 }, (_, c) => ({ | |
| 74 value: values[r * 5 + c], | |
| 75 isFacing: false, | |
| 76 isSolved: false, | |
| 77 })) | |
| 78 ); | |
| 79 }; | |
| 80 | |
| 81 /* ──────────────────── Context ──────────────────── */ | |
| 82 const BoardContext = createContext<React.Dispatch<Action> | null>(null); | |
| 83 const useBoardDispatch = () => { | |
| 84 const ctx = useContext(BoardContext); | |
| 85 if (!ctx) throw new Error("BoardContext missing"); | |
| 86 return ctx; | |
| 87 }; | |
| 88 | |
| 89 /* ──────────────────── Reducer ──────────────────── */ | |
| 90 const initial = (): GameState => ({ board: makeBoard(), open: [], busy: false }); | |
| 91 | |
| 92 function reducer(state: GameState, action: Action): GameState { | |
| 93 switch (action.type) { | |
| 94 case "reset": | |
| 95 return initial(); | |
| 96 | |
| 97 case "flipBack": { | |
| 98 // hide the two open cards | |
| 99 const [a, b] = state.open; | |
| 100 const nextBoard = state.board.map((row, r) => | |
| 101 row.map((card, c) => | |
| 102 (r === a.row && c === a.col) || (r === b.row && c === b.col) | |
| 103 ? { ...card, isFacing: false } | |
| 104 : card | |
| 105 ) | |
| 106 ); | |
| 107 return { board: nextBoard, open: [], busy: false }; | |
| 108 } | |
| 109 | |
| 110 case "move": { | |
| 111 if (state.busy) return state; // ignore clicks while waiting | |
| 112 | |
| 113 const { row, col } = action; | |
| 114 const target = state.board[row][col]; | |
| 115 if (target.isFacing || target.isSolved) return state; | |
| 116 | |
| 117 // flip this one up | |
| 118 const nextBoard = state.board.map((r, ri) => | |
| 119 r.map((c, ci) => | |
| 120 ri === row && ci === col ? { ...c, isFacing: true } : c | |
| 121 ) | |
| 122 ); | |
| 123 const open = [...state.open, { row, col }]; | |
| 124 | |
| 125 if (open.length < 2) return { ...state, board: nextBoard, open }; | |
| 126 | |
| 127 // now we have two cards – decide match / mismatch | |
| 128 const [a, b] = open; | |
| 129 const first = nextBoard[a.row][a.col]; | |
| 130 const second = nextBoard[b.row][b.col]; | |
| 131 | |
| 132 if (first.value === second.value) { | |
| 133 // match → mark solved, leave face-up | |
| 134 const solvedBoard = nextBoard.map((r, ri) => | |
| 135 r.map((c, ci) => | |
| 136 (ri === a.row && ci === a.col) || (ri === b.row && ci === b.col) | |
| 137 ? { ...c, isSolved: true } | |
| 138 : c | |
| 139 ) | |
| 140 ); | |
| 141 return { board: solvedBoard, open: [], busy: false }; | |
| 142 } | |
| 143 | |
| 144 // mismatch → leave them up temporarily, then flip back via effect | |
| 145 return { board: nextBoard, open, busy: true }; | |
| 146 } | |
| 147 | |
| 148 default: | |
| 149 return state; | |
| 150 } | |
| 151 } | |
| 152 | |
| 153 /* ──────────────────── UI Components ──────────────────── */ | |
| 154 const Card = ({ row, col, card }: CardProp) => { | |
| 155 const dispatch = useBoardDispatch(); | |
| 156 const style: React.CSSProperties = { | |
| 157 width: 60, | |
| 158 height: 80, | |
| 159 margin: 4, | |
| 160 fontSize: 24, | |
| 161 display: "flex", | |
| 162 alignItems: "center", | |
| 163 justifyContent: "center", | |
| 164 background: card.isSolved | |
| 165 ? "#8bc34a" | |
| 166 : card.isFacing | |
| 167 ? "#fff" | |
| 168 : "#90caf9", | |
| 169 cursor: card.isSolved ? "default" : "pointer", | |
| 170 borderRadius: 6, | |
| 171 userSelect: "none", | |
| 172 }; | |
| 173 return ( | |
| 174 <div style={style} onClick={() => dispatch({ type: "move", row, col })}> | |
| 175 {card.isFacing || card.isSolved ? card.value : "🂠"} | |
| 176 </div> | |
| 177 ); | |
| 178 }; | |
| 179 | |
| 180 const Row = ({ row, rowPos }: RowProp) => ( | |
| 181 <div style={{ display: "flex" }}> | |
| 182 {row.map((card, idx) => ( | |
| 183 <Card key={idx} row={rowPos} col={idx} card={card} /> | |
| 184 ))} | |
| 185 </div> | |
| 186 ); | |
| 187 | |
| 188 /* ──────────────────── Root component ──────────────────── */ | |
| 189 export const MemoryGame = () => { | |
| 190 const [state, dispatch] = useReducer(reducer, undefined, initial); | |
| 191 | |
| 192 /* Handle “flipBack” after 1 s for a mismatch */ | |
| 193 useEffect(() => { | |
| 194 if (state.busy) { | |
| 195 const t = setTimeout(() => dispatch({ type: "flipBack" }), 1000); | |
| 196 return () => clearTimeout(t); | |
| 197 } | |
| 198 }, [state.busy]); | |
| 199 | |
| 200 /* Quick win detection */ | |
| 201 const solved = state.board.every((r) => r.every((c) => c.isSolved)); | |
| 202 | |
| 203 return ( | |
| 204 <> | |
| 205 <h3 style={{ textAlign: "center" }}> | |
| 206 {solved ? "🎉 You won!" : "Memory Game"} | |
| 207 </h3> | |
| 208 <BoardContext.Provider value={dispatch}> | |
| 209 {state.board.map((row, idx) => ( | |
| 210 <Row key={idx} rowPos={idx} row={row} /> | |
| 211 ))} | |
| 212 </BoardContext.Provider> | |
| 213 | |
| 214 <div style={{ textAlign: "center", marginTop: 12 }}> | |
| 215 <button onClick={() => dispatch({ type: "reset" })}>Reset</button> | |
| 216 </div> | |
| 217 </> | |
| 218 ); | |
| 219 }; | |
| 220 |